Compare commits
No commits in common. "2bab960f23b994ed443f445ce1fd68b496be0d40" and "0f27dc05f4ee32c59cc4755c273bc568ff7762f1" have entirely different histories.
2bab960f23
...
0f27dc05f4
10 changed files with 936 additions and 62527 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
Luanti mod to exchange in-game [currency](https://github.com/mt-mods/currency) with [libre currency](https://duniter.org) (Ğ1 blockchain).
|
Luanti mod to exchange in-game [currency](https://github.com/mt-mods/currency) with [libre currency](https://duniter.org) (Ğ1 blockchain).
|
||||||
|
|
||||||
Luanti server has a dedicated account.
|
Luanti server has a dedicated account.
|
||||||
Players transfer money on it and the server gives them a corresponding amount of in-game banknotes.
|
Player transfer money on it and the server gives them a corresponding amount of in-game banknotes.
|
||||||
They can then do whatever they want with the banknotes (trade, decorate walls, burn...).
|
They can then do whatever they want with the banknotes (trade, decorate walls, burn...).
|
||||||
Players can ask the server to transfer their banknotes to an account of their choice using a chat command.
|
Players can ask the server to transfer their banknotes to an account of their choice using a chat command.
|
||||||
|
|
||||||
|
|
@ -13,8 +13,6 @@ It works with Ğ1v2 which is still in development/testing phase.
|
||||||
|
|
||||||
Put this repo in the `mods` directory.
|
Put this repo in the `mods` directory.
|
||||||
|
|
||||||
Add `g1_bridge` to `secure.http_mods`.
|
|
||||||
|
|
||||||
Have a local Duniter node.
|
Have a local Duniter node.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -33,7 +31,6 @@ Have a local Duniter node.
|
||||||
* Ensure all transfers are failproof (any communication, storage medium, server or whatever should be able to fail or rollback to a previous state, without causing double spend or money loss)
|
* Ensure all transfers are failproof (any communication, storage medium, server or whatever should be able to fail or rollback to a previous state, without causing double spend or money loss)
|
||||||
* Force reception account to exist
|
* Force reception account to exist
|
||||||
* Nice ATM-like interface
|
* Nice ATM-like interface
|
||||||
* Accept mixed values (other banknotes values than 1)
|
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
|
|
|
||||||
2344
g1bridge/Cargo.lock
generated
2344
g1bridge/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,16 +6,13 @@ edition = "2024"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argp = "0.4.0"
|
argp = "0.4.0"
|
||||||
codec = { package = "parity-scale-codec", version = "3.6.12" }
|
codec = { package = "parity-scale-codec", version = "3.6.12" }
|
||||||
hex = "0.4.3"
|
|
||||||
log = "0.4.27"
|
log = "0.4.27"
|
||||||
reqwest = { version = "0.11.27", features = ["json"] }
|
|
||||||
scrypt = { version = "0.11.0", default-features = false }
|
scrypt = { version = "0.11.0", default-features = false }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.140"
|
|
||||||
simplelog = "0.12.2"
|
simplelog = "0.12.2"
|
||||||
sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.14.0" }
|
sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.11.0" }
|
||||||
sp-runtime = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.14.0" }
|
sp-runtime = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.11.0" }
|
||||||
subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.37.0-duniter-substrate-v1.14.0', default-features = false, features = [
|
subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.35.3-duniter-substrate-v1.11.0', default-features = false, features = [
|
||||||
"substrate-compat",
|
"substrate-compat",
|
||||||
"native",
|
"native",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
|
|
@ -24,10 +21,5 @@ tiny_http = "0.12.0"
|
||||||
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
|
||||||
typed-sled = "0.2.3"
|
typed-sled = "0.2.3"
|
||||||
|
|
||||||
# some dependency forgot to specify revision of another git dep...
|
|
||||||
[patch.'https://github.com/w3f/fflonk']
|
|
||||||
# https://github.com/rust-lang/cargo/issues/5478
|
|
||||||
fflonk = { git = "https://github.com//w3f/fflonk.git", rev = "1e854f35e9a65d08b11a86291405cdc95baa0a35" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "fat"
|
lto = "fat"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
query ReceivedTransactions($address: String!, $block: Int!) {
|
|
||||||
tx: transfer(where: { blockNumber: { _gt: 1 }, to: { id: { _eq: "" } } }) {
|
|
||||||
amount
|
|
||||||
comment {
|
|
||||||
remark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b: block(limit: 1, orderBy: { height: DESC }) {
|
|
||||||
height
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,51 +0,0 @@
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
/*
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"transfer": [
|
|
||||||
{
|
|
||||||
"amount": 42,
|
|
||||||
"comment": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"amount": 30,
|
|
||||||
"comment": {"remark": "tuxmain"}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"block": [
|
|
||||||
{
|
|
||||||
"height": 7040132,
|
|
||||||
"hash": "\\xe669ff0d26dc9c48ec905916d39b0f11447cf63a37c0d1094f19cdfbbb199c67"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
*/
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Comment {
|
|
||||||
pub remark: String,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Transfer {
|
|
||||||
pub amount: u64,
|
|
||||||
pub comment: Option<Comment>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Block {
|
|
||||||
pub height: u64,
|
|
||||||
pub hash: String,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct Data {
|
|
||||||
pub transfer: Vec<Transfer>,
|
|
||||||
pub block: Vec<Block>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct DataWrapper {
|
|
||||||
pub data: Data,
|
|
||||||
}
|
|
||||||
pub type Response = Vec<DataWrapper>;
|
|
||||||
|
|
@ -1,23 +1,22 @@
|
||||||
#![feature(try_blocks)]
|
#![feature(try_blocks)]
|
||||||
|
|
||||||
mod blockchain;
|
mod blockchain;
|
||||||
mod indexer;
|
|
||||||
mod response;
|
|
||||||
|
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize, de};
|
||||||
use sp_core::Pair as _;
|
use sp_core::Pair as _;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{hash_map::Entry, HashMap}, io::{BufRead, Read}, str::FromStr, sync::Arc
|
io::{BufRead, Read},
|
||||||
|
str::FromStr,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use subxt::{
|
use subxt::{
|
||||||
ext::sp_core::ed25519::Pair,
|
ext::sp_core::ed25519::{Pair, Public},
|
||||||
utils::AccountId32,
|
utils::{AccountId32, MultiAddress},
|
||||||
};
|
};
|
||||||
use tiny_http::{Response, ResponseBox, Server};
|
use tiny_http::{Response, ResponseBox, Server, ServerConfig};
|
||||||
use tokio::sync::mpsc::{Sender, channel};
|
use tokio::sync::mpsc::{Sender, channel};
|
||||||
use typed_sled::Tree;
|
use typed_sled::Tree;
|
||||||
use log::error;
|
|
||||||
|
|
||||||
/// Game to Blockchain
|
/// Game to Blockchain
|
||||||
const TREE_DEBT_G2B: &'static str = "debt_g2b";
|
const TREE_DEBT_G2B: &'static str = "debt_g2b";
|
||||||
|
|
@ -29,10 +28,6 @@ struct Opt {
|
||||||
#[argp(option, short = 'd')]
|
#[argp(option, short = 'd')]
|
||||||
db: String,
|
db: String,
|
||||||
|
|
||||||
/// Duniter indexer URL
|
|
||||||
#[argp(option, short = 'i', default = "String::from(\"https://squid.gdev.gyroi.de/v1/graphql\")")]
|
|
||||||
indexer: String,
|
|
||||||
|
|
||||||
/// secret key is legacy (two passwords on the two first lines)
|
/// secret key is legacy (two passwords on the two first lines)
|
||||||
#[argp(switch, short = 'l')]
|
#[argp(switch, short = 'l')]
|
||||||
legacy: bool,
|
legacy: bool,
|
||||||
|
|
@ -85,16 +80,12 @@ async fn main() {
|
||||||
.expect("Cannot read secret key file");
|
.expect("Cannot read secret key file");
|
||||||
Pair::from_string(sk_content.trim(), None).expect("Cannot decode secret key")
|
Pair::from_string(sk_content.trim(), None).expect("Cannot decode secret key")
|
||||||
};
|
};
|
||||||
let pk: AccountId32 = pair.public().into();
|
|
||||||
let pk_str = pk.to_string();
|
|
||||||
|
|
||||||
simplelog::SimpleLogger::init(log::LevelFilter::Debug, simplelog::Config::default()).unwrap();
|
simplelog::SimpleLogger::init(log::LevelFilter::Debug, simplelog::Config::default()).unwrap();
|
||||||
|
|
||||||
let db = typed_sled::open(opt.db).expect("Cannot open database");
|
let db = typed_sled::open(opt.db).expect("Cannot open database");
|
||||||
let tree_debt_g2b = Tree::<DebtG2B, ()>::open(&db, TREE_DEBT_G2B);
|
let tree_debt_g2b = Tree::<DebtG2B, ()>::open(&db, TREE_DEBT_G2B);
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
|
|
||||||
let (bc_send, bc_recv) = channel(128);
|
let (bc_send, bc_recv) = channel(128);
|
||||||
|
|
||||||
tokio::spawn(blockchain::run(
|
tokio::spawn(blockchain::run(
|
||||||
|
|
@ -109,8 +100,6 @@ async fn main() {
|
||||||
struct Locals {
|
struct Locals {
|
||||||
tree_debt_g2b: Tree<DebtG2B, ()>,
|
tree_debt_g2b: Tree<DebtG2B, ()>,
|
||||||
bc_send: Sender<DebtG2B>,
|
bc_send: Sender<DebtG2B>,
|
||||||
indexer: String,
|
|
||||||
pk_str: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::task_local! {
|
tokio::task_local! {
|
||||||
|
|
@ -121,13 +110,10 @@ async fn main() {
|
||||||
Arc::new(Locals {
|
Arc::new(Locals {
|
||||||
tree_debt_g2b,
|
tree_debt_g2b,
|
||||||
bc_send,
|
bc_send,
|
||||||
indexer: opt.indexer,
|
|
||||||
pk_str: pk_str,
|
|
||||||
}),
|
}),
|
||||||
async move {
|
async move {
|
||||||
for request in server.incoming_requests() {
|
for request in server.incoming_requests() {
|
||||||
let locals = LOCALS.get();
|
let locals = LOCALS.get();
|
||||||
let client = client.clone();
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let url = request.url();
|
let url = request.url();
|
||||||
|
|
||||||
|
|
@ -138,6 +124,7 @@ async fn main() {
|
||||||
Some("send") => {
|
Some("send") => {
|
||||||
let address = AccountId32::from_str(url_items.next()?).ok()?;
|
let address = AccountId32::from_str(url_items.next()?).ok()?;
|
||||||
let amount = url_items.next()?.parse::<u64>().ok()?;
|
let amount = url_items.next()?.parse::<u64>().ok()?;
|
||||||
|
println!("{address} {amount}");
|
||||||
|
|
||||||
let debt = DebtG2B { address, amount };
|
let debt = DebtG2B { address, amount };
|
||||||
locals.tree_debt_g2b.insert(&debt, &()).ok()?;
|
locals.tree_debt_g2b.insert(&debt, &()).ok()?;
|
||||||
|
|
@ -145,52 +132,6 @@ async fn main() {
|
||||||
|
|
||||||
Response::empty(200).boxed()
|
Response::empty(200).boxed()
|
||||||
}
|
}
|
||||||
Some("recv") => {
|
|
||||||
let block_number = url_items.next()?.parse::<u64>().ok()?;
|
|
||||||
let req = client.post(&locals.indexer).body(dbg!(format!(
|
|
||||||
r#"[{{"query":"{{transfer(where:{{blockNumber:{{_gt:{}}},to:{{id:{{_eq:\"{}\"}}}}}}){{amount comment{{remark}}}}block(limit:1,orderBy:{{height:DESC}}){{height hash}}}}"}}]"#,
|
|
||||||
block_number, locals.pk_str))).build().unwrap();
|
|
||||||
let Ok(res) = client.execute(req).await else {
|
|
||||||
request.respond(Response::empty(500)).ok();
|
|
||||||
error!("Error from indexer");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Ok(res) = res.json::<indexer::Response>().await else {
|
|
||||||
request.respond(Response::empty(500)).ok();
|
|
||||||
error!("Bad JSON from indexer");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(data) = res.first() else {
|
|
||||||
request.respond(Response::empty(500)).ok();
|
|
||||||
error!("No data from indexer");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(block) = data.data.block.first() else {
|
|
||||||
request.respond(Response::empty(500)).ok();
|
|
||||||
error!("No block from indexer");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut debts = HashMap::<String, u64>::new();
|
|
||||||
for transfer in data.data.transfer.iter() {
|
|
||||||
if let Some(comment) = &transfer.comment {
|
|
||||||
match debts.entry(comment.remark.clone()) {
|
|
||||||
Entry::Occupied(mut debt) => {
|
|
||||||
*debt.get_mut() = debt.get().saturating_add(transfer.amount);
|
|
||||||
}
|
|
||||||
Entry::Vacant(debt) => {
|
|
||||||
debt.insert(transfer.amount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Response::from_string(serde_json::to_string(&response::Response {
|
|
||||||
block: block.height,
|
|
||||||
debts: debts.into_iter().map(|(name, amount)| response::Debt {
|
|
||||||
name,
|
|
||||||
amount,
|
|
||||||
}).collect(),
|
|
||||||
}).unwrap()).with_status_code(200).boxed()
|
|
||||||
}
|
|
||||||
_ => None?,
|
_ => None?,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Debt {
|
|
||||||
pub name: String,
|
|
||||||
pub amount: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct Response {
|
|
||||||
pub block: u64,
|
|
||||||
pub debts: Vec<Debt>,
|
|
||||||
}
|
|
||||||
112
init.lua
112
init.lua
|
|
@ -1,97 +1,49 @@
|
||||||
local MG_RATE = 10
|
|
||||||
|
|
||||||
local http = minetest.request_http_api()
|
local http = minetest.request_http_api()
|
||||||
if http == nil then
|
if http == nil then
|
||||||
error("Please add g1_bridge to secure.http_mods")
|
error("Please add g1_bridge to secure.http_mods")
|
||||||
end
|
end
|
||||||
|
|
||||||
local mod_storage = core.get_mod_storage()
|
minetest.register_chatcommand("g1", {
|
||||||
if mod_storage == nil then
|
|
||||||
error("Cannot get mod storage")
|
|
||||||
end
|
|
||||||
|
|
||||||
function receive_debts()
|
|
||||||
local block = mod_storage:get_int("block")
|
|
||||||
http.fetch({url="http://127.0.0.1:30061/recv/"..block, method="GET"},
|
|
||||||
function(res)
|
|
||||||
if res.code == 200 then
|
|
||||||
local data = minetest.parse_json(res.data)
|
|
||||||
if data == nil or type(data.block) ~= "number" or data.block <= block or type(data.debts) ~= "table" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
for i, debt in ipairs(data.debts) do
|
|
||||||
if type(debt.name) == "string" and type(debt.amount) == "number" and core.player_exists(debt.name) then
|
|
||||||
local player_debt = mod_storage:get_int("debt_"..debt.name)
|
|
||||||
player_debt = player_debt + debt.amount
|
|
||||||
mod_storage:set_int("debt_"..debt.name, player_debt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
mod_storage:set_int("block", data.block + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
minetest.register_chatcommand("g1_send", {
|
|
||||||
params = "<address> <amount>",
|
|
||||||
description = "Send MineGeld from your inventory to a G1 address. Fixed rate: 1G1=10MG.",
|
|
||||||
func = function(name, param)
|
func = function(name, param)
|
||||||
local args = {}
|
local args = {}
|
||||||
for arg in string.gmatch(param,"%S+") do
|
for arg in string.gmatch(param,"%S+") do
|
||||||
if #args >= 2 then
|
if #args >= 3 then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
table.insert(args, arg)
|
table.insert(args, arg)
|
||||||
end
|
end
|
||||||
if #args == 2 and string.find(args[1],"^[%da-zA-Z]+$") and string.find(args[2],"^%d+$") then
|
if #args >= 1 then
|
||||||
local address = args[1]
|
core.chat_send_player(name, "args >= 1")
|
||||||
local amount_mg = tonumber(args[2])
|
if args[1] == "send" and #args == 3 and string.find(args[2],"^[%da-zA-Z]+$") and string.find(args[3],"^%d+$") then
|
||||||
-- This version Lua does not have any way to check if a number is represented as an integer or a float,
|
local address = args[2]
|
||||||
-- because it explicitely doesn't care.
|
local amount = tonumber(args[3])
|
||||||
-- Hence, this program won't care if the player writes an invalid number,
|
-- This version Lua does not have any way to check if a number is represented as an integer or a float,
|
||||||
-- as there is no need to ensure arithmetic rules are respected. (as Lua's manual pretends)
|
-- because it explicitely doesn't care.
|
||||||
-- As the pattern excludes "." and nobody should have billions of items,
|
-- Hence, this program won't care if the player writes an invalid number,
|
||||||
-- it should still be safe. I hope.
|
-- as there is no need to ensure arithmetic rules are respected. (as Lua's manual pretends)
|
||||||
if type(amount_mg) ~= "number" then
|
-- As the pattern excludes "." and nobody should have billions of items,
|
||||||
return
|
-- it should still be safe. I hope.
|
||||||
end
|
if type(amount) ~= "number" then
|
||||||
local inv = core.get_inventory({type="player", name=name})
|
return
|
||||||
local wanted_stack = "currency:minegeld "..amount_mg
|
end
|
||||||
local balanced = inv:contains_item("main", ItemStack(wanted_stack))
|
local inv = core.get_inventory({type="player", name=name})
|
||||||
if balanced then
|
local wanted_stack = "currency:minegeld "..amount
|
||||||
local amount = amount_mg * MG_RATE
|
local balanced = inv:contains_item("main", ItemStack(wanted_stack))
|
||||||
inv:remove_item("main", ItemStack(wanted_stack))
|
if balanced then
|
||||||
http.fetch({url="http://127.0.0.1:30061/send/"..address.."/"..amount, method="GET"},
|
inv:remove_item("main", wanted_stack)
|
||||||
function(res)
|
http.fetch({url="http://127.0.0.1:30061/send/"..address.."/"..amount, method="GET"},
|
||||||
if res ~= nil and res.code == 200 then
|
function(res)
|
||||||
core.chat_send_player(name, "Transfer sent")
|
if res.code == 200 then
|
||||||
else
|
core.chat_send_player(name, "Transfer sent")
|
||||||
core.chat_send_player(name, "Error during transfer, giving your money back")
|
else
|
||||||
inv:add_item("main", ItemStack(wanted_stack))
|
core.chat_send_player(name, "Error during transfer")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
)
|
||||||
)
|
else
|
||||||
else
|
core.chat_send_player(name, "Not enough currency")
|
||||||
core.chat_send_player(name, "Not enough money (please ensure it is available in 1MG banknotes)")
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
minetest.register_chatcommand("g1_claim", {
|
|
||||||
description = "Claim MineGeld from G1 sent to the server",
|
|
||||||
func = function(name, param)
|
|
||||||
receive_debts()
|
|
||||||
local amount = mod_storage:get_int("debt_"..name)
|
|
||||||
local amount_mg = amount / MG_RATE
|
|
||||||
local inv = core.get_inventory({type="player", name=name})
|
|
||||||
local stack = "currency:minegeld "..amount_mg
|
|
||||||
if inv:room_for_item("main", ItemStack(stack)) then
|
|
||||||
inv:add_item("main", ItemStack(stack))
|
|
||||||
core.chat_send_player(name, "Claimed "..amount_mg.."MG")
|
|
||||||
mod_storage:set_int("debt_"..name, 0)
|
|
||||||
else
|
|
||||||
core.chat_send_player(name, "No room in your inventory! Available: "..amount_mg.."MG")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue