216 lines
6 KiB
Rust
216 lines
6 KiB
Rust
#![feature(try_blocks)]
|
|
|
|
mod blockchain;
|
|
mod indexer;
|
|
mod response;
|
|
|
|
use argp::FromArgs;
|
|
use log::error;
|
|
use serde::{Deserialize, Serialize};
|
|
use sp_core::{
|
|
Pair as _,
|
|
crypto::{Ss58AddressFormat, Ss58Codec, set_default_ss58_version},
|
|
};
|
|
use std::{
|
|
collections::{HashMap, hash_map::Entry},
|
|
io::{BufRead, Read},
|
|
str::FromStr,
|
|
sync::Arc,
|
|
};
|
|
use subxt::{ext::sp_core::ed25519::Pair, utils::AccountId32};
|
|
use tiny_http::{Response, ResponseBox, Server};
|
|
use tokio::sync::mpsc::{Sender, channel};
|
|
use typed_sled::Tree;
|
|
|
|
/// Game to Blockchain
|
|
const TREE_DEBT_G2B: &'static str = "debt_g2b";
|
|
|
|
/// Blob
|
|
#[derive(FromArgs)]
|
|
struct Opt {
|
|
/// path to the database
|
|
#[argp(option, short = 'd')]
|
|
db: String,
|
|
|
|
/// Duniter indexer URL
|
|
#[argp(
|
|
option,
|
|
short = 'i',
|
|
default = "String::from(\"https://gt-squid.axiom-team.fr/v1/graphql\")"
|
|
)]
|
|
indexer: String,
|
|
|
|
/// secret key is legacy (two passwords on the two first lines)
|
|
#[argp(switch, short = 'l')]
|
|
legacy: bool,
|
|
|
|
/// path to the file containing the secret key in Substrate format
|
|
#[argp(option, short = 's')]
|
|
secret_key: String,
|
|
|
|
/// duniter node URL (default: ws://127.0.0.1:9944)
|
|
#[argp(option, short = 'u', default = "String::from(\"ws://127.0.0.1:9944\")")]
|
|
url: String,
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
struct DebtG2B {
|
|
address: AccountId32,
|
|
amount: u64,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let opt: Opt = argp::parse_args_or_exit(argp::DEFAULT);
|
|
|
|
let mut sk_file = std::fs::File::open(opt.secret_key).expect("Cannot open secret key file");
|
|
let pair: Pair = if opt.legacy {
|
|
let sk_reader = std::io::BufReader::new(sk_file);
|
|
let mut sk_lines = sk_reader.lines();
|
|
let psw1 = sk_lines
|
|
.next()
|
|
.expect("Secret key file should contain at least 2 lines for the 2 passwords")
|
|
.unwrap();
|
|
let psw2 = sk_lines
|
|
.next()
|
|
.expect("Secret key file should contain at least 2 lines for the 2 passwords")
|
|
.unwrap();
|
|
let params = scrypt::Params::new(12, 16, 1, 32).unwrap();
|
|
let mut seed = [0u8; 32];
|
|
scrypt::scrypt(
|
|
psw2.trim().as_bytes(),
|
|
psw1.trim().as_bytes(),
|
|
¶ms,
|
|
&mut seed,
|
|
)
|
|
.unwrap();
|
|
Pair::from_seed(&seed)
|
|
} else {
|
|
let mut sk_content = String::new();
|
|
sk_file
|
|
.read_to_string(&mut sk_content)
|
|
.expect("Cannot read secret key file");
|
|
Pair::from_string(sk_content.trim(), None).expect("Cannot decode secret key")
|
|
};
|
|
|
|
set_default_ss58_version(Ss58AddressFormat::custom(4450));
|
|
let pk: AccountId32 = pair.public().into();
|
|
let pk: sp_core::crypto::AccountId32 = pk.0.into();
|
|
let pk_str = dbg!(pk.to_ss58check_with_version(Ss58AddressFormat::custom(4450)));
|
|
|
|
simplelog::SimpleLogger::init(log::LevelFilter::Debug, simplelog::Config::default()).unwrap();
|
|
|
|
let db = typed_sled::open(opt.db).expect("Cannot open database");
|
|
let tree_debt_g2b = Tree::<DebtG2B, ()>::open(&db, TREE_DEBT_G2B);
|
|
|
|
let client = reqwest::Client::new();
|
|
|
|
let (bc_send, bc_recv) = channel(128);
|
|
|
|
tokio::spawn(blockchain::run(
|
|
opt.url,
|
|
pair,
|
|
bc_recv,
|
|
tree_debt_g2b.clone(),
|
|
));
|
|
|
|
let server = Server::http("127.0.0.1:30061").expect("Cannot start server");
|
|
|
|
struct Locals {
|
|
tree_debt_g2b: Tree<DebtG2B, ()>,
|
|
bc_send: Sender<DebtG2B>,
|
|
indexer: String,
|
|
pk_str: String,
|
|
}
|
|
|
|
tokio::task_local! {
|
|
static LOCALS: Arc<Locals>;
|
|
};
|
|
|
|
LOCALS.scope(
|
|
Arc::new(Locals {
|
|
tree_debt_g2b,
|
|
bc_send,
|
|
indexer: opt.indexer,
|
|
pk_str: pk_str,
|
|
}),
|
|
async move {
|
|
for request in server.incoming_requests() {
|
|
let locals = LOCALS.get();
|
|
let client = client.clone();
|
|
tokio::spawn(async move {
|
|
let url = request.url();
|
|
|
|
let mut url_items = url.split('/').filter(|item| !item.is_empty());
|
|
|
|
let resp: Option<ResponseBox> = try {
|
|
match url_items.next() {
|
|
Some("send") => {
|
|
let address = AccountId32::from_str(url_items.next()?).ok()?;
|
|
let amount = url_items.next()?.parse::<u64>().ok()?;
|
|
|
|
let debt = DebtG2B { address, amount };
|
|
locals.tree_debt_g2b.insert(&debt, &()).ok()?;
|
|
locals.bc_send.send(debt).await.ok()?;
|
|
|
|
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?,
|
|
}
|
|
};
|
|
if let Some(resp) = resp {
|
|
request.respond(resp).ok();
|
|
} else {
|
|
request.respond(Response::empty(400)).ok();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
).await;
|
|
}
|