|
|
@@ -0,0 +1,323 @@
|
|
|
+use log::*;
|
|
|
+use std::fs;
|
|
|
+use std::path::PathBuf;
|
|
|
+
|
|
|
+use clap::clap_app;
|
|
|
+use serde_json::{json, Value};
|
|
|
+use simplelog::{
|
|
|
+ CombinedLogger, Config as SimLogConfig, ConfigBuilder, LevelFilter, TermLogger, TerminalMode,
|
|
|
+ WriteLogger,
|
|
|
+};
|
|
|
+
|
|
|
+use async_std::sync::Arc;
|
|
|
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
+use tokio::net::TcpListener;
|
|
|
+
|
|
|
+use drk::{
|
|
|
+ cli::{Config, DarkfidConfig},
|
|
|
+ rpc::{
|
|
|
+ jsonrpc::{error as jsonerr, request as jsonreq, response as jsonresp, send_request},
|
|
|
+ jsonrpc::{ErrorCode::*, JsonRequest, JsonResult},
|
|
|
+ },
|
|
|
+ serial::serialize,
|
|
|
+ util::join_config_path,
|
|
|
+ wallet::WalletDb,
|
|
|
+ Error,
|
|
|
+};
|
|
|
+
|
|
|
+#[derive(Clone)]
|
|
|
+struct Darkfid {
|
|
|
+ verbose: bool,
|
|
|
+ config: DarkfidConfig,
|
|
|
+ wallet: Arc<WalletDb>,
|
|
|
+ // clientdb:
|
|
|
+ // mint_params:
|
|
|
+ // spend_params:
|
|
|
+}
|
|
|
+
|
|
|
+impl Darkfid {
|
|
|
+ fn new(verbose: bool, config_path: PathBuf) -> Result<Self, Error> {
|
|
|
+ let config: DarkfidConfig = Config::<DarkfidConfig>::load(config_path)?;
|
|
|
+ let wallet = WalletDb::new(
|
|
|
+ &PathBuf::from(config.walletdb_path.clone()),
|
|
|
+ config.password.clone(),
|
|
|
+ )?;
|
|
|
+
|
|
|
+ Ok(Self {
|
|
|
+ verbose,
|
|
|
+ config,
|
|
|
+ wallet,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: ServerError codes should be part of the lib.
|
|
|
+ async fn handle_request(self, req: JsonRequest) -> JsonResult {
|
|
|
+ if req.params.as_array().is_none() {
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, req.id));
|
|
|
+ }
|
|
|
+
|
|
|
+ debug!(target: "RPC", "--> {:#?}", serde_json::to_string(&req).unwrap());
|
|
|
+
|
|
|
+ match req.method.as_str() {
|
|
|
+ Some("say_hello") => return self.say_hello(req.id, req.params).await,
|
|
|
+ Some("create_wallet") => return self.create_wallet(req.id, req.params).await,
|
|
|
+ Some("key_gen") => return self.key_gen(req.id, req.params).await,
|
|
|
+ Some("get_key") => return self.get_key(req.id, req.params).await,
|
|
|
+ Some("get_token_id") => return self.get_token_id(req.id, req.params).await,
|
|
|
+ Some("deposit") => return self.deposit(req.id, req.params).await,
|
|
|
+ Some("withdraw") => return self.withdraw(req.id, req.params).await,
|
|
|
+ Some("transfer") => return self.transfer(req.id, req.params).await,
|
|
|
+ Some(_) => {}
|
|
|
+ None => {}
|
|
|
+ };
|
|
|
+
|
|
|
+ return JsonResult::Err(jsonerr(MethodNotFound, None, req.id));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "say_hello", "params": []}
|
|
|
+ // <-- {"result": "hello world"}
|
|
|
+ async fn say_hello(self, id: Value, _params: Value) -> JsonResult {
|
|
|
+ JsonResult::Resp(jsonresp(json!("hello world"), id))
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "create_wallet", "params": []}
|
|
|
+ // <-- {"result": true}
|
|
|
+ async fn create_wallet(self, id: Value, _params: Value) -> JsonResult {
|
|
|
+ match self.wallet.init_db() {
|
|
|
+ Ok(()) => return JsonResult::Resp(jsonresp(json!(true), id)),
|
|
|
+ Err(e) => {
|
|
|
+ return JsonResult::Err(jsonerr(ServerError(-32001), Some(e.to_string()), id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "key_gen", "params": []}
|
|
|
+ // <-- {"result": true}
|
|
|
+ async fn key_gen(self, id: Value, _params: Value) -> JsonResult {
|
|
|
+ match self.wallet.key_gen() {
|
|
|
+ Ok((_, _)) => return JsonResult::Resp(jsonresp(json!(true), id)),
|
|
|
+ Err(e) => {
|
|
|
+ return JsonResult::Err(jsonerr(ServerError(-32002), Some(e.to_string()), id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "get_key", "params": []}
|
|
|
+ // <-- {"result": "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC"}
|
|
|
+ async fn get_key(self, id: Value, _params: Value) -> JsonResult {
|
|
|
+ match self.wallet.get_keypairs() {
|
|
|
+ Ok(v) => {
|
|
|
+ let pk = v[0].public;
|
|
|
+ let b58 = bs58::encode(serialize(&pk)).into_string();
|
|
|
+ return JsonResult::Resp(jsonresp(json!(b58), id));
|
|
|
+ }
|
|
|
+ Err(e) => {
|
|
|
+ return JsonResult::Err(jsonerr(ServerError(-32003), Some(e.to_string()), id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"jsonrpc": "2.0", "method": "get_token_id",
|
|
|
+ // "params": [token],
|
|
|
+ // "id": 42}
|
|
|
+ // <-- {"result": "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL"}
|
|
|
+ async fn get_token_id(self, id: Value, params: Value) -> JsonResult {
|
|
|
+ let args = params.as_array().unwrap();
|
|
|
+ let symbol = &args[0];
|
|
|
+
|
|
|
+ if symbol.as_str().is_none() {
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, id));
|
|
|
+ };
|
|
|
+
|
|
|
+ let symbol = symbol.as_str().unwrap().to_uppercase();
|
|
|
+
|
|
|
+ let file_contents =
|
|
|
+ fs::read_to_string("token/solanatokenlist.json").expect("Can't find tokenlist file");
|
|
|
+ let root: Value = serde_json::from_str(&file_contents).unwrap();
|
|
|
+ let tokens = root["tokens"].as_array().unwrap();
|
|
|
+
|
|
|
+ for item in tokens {
|
|
|
+ if item["symbol"] == symbol {
|
|
|
+ let address = &item["address"];
|
|
|
+ return JsonResult::Resp(jsonresp(json!(address), id));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, id));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"jsonrpc": "2.0", "method": "deposit",
|
|
|
+ // "params": [network, token, publickey],
|
|
|
+ // "id": 42}
|
|
|
+ // The publickey sent here is used so the cashier can know where to send
|
|
|
+ // assets once the deposit is received.
|
|
|
+ // <-- {"result": "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL"}
|
|
|
+ async fn deposit(self, id: Value, params: Value) -> JsonResult {
|
|
|
+ let args = params.as_array().unwrap();
|
|
|
+ if args.len() != 2 {
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, id));
|
|
|
+ }
|
|
|
+
|
|
|
+ let network = &args[0];
|
|
|
+ let token = &args[1];
|
|
|
+
|
|
|
+ if token.as_str().is_none() {
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, id));
|
|
|
+ };
|
|
|
+
|
|
|
+ // TODO: Optional sanity checking here, but cashier *must* do so too.
|
|
|
+
|
|
|
+ let pubkey: String;
|
|
|
+ match self.wallet.get_keypairs() {
|
|
|
+ Ok(v) => {
|
|
|
+ let pk = v[0].public;
|
|
|
+ pubkey = bs58::encode(serialize(&pk)).into_string();
|
|
|
+ }
|
|
|
+ Err(e) => {
|
|
|
+ return JsonResult::Err(jsonerr(ServerError(-32003), Some(e.to_string()), id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Send request to cashier. If the cashier supports the requested network
|
|
|
+ // (and token), it shall return a valid address where assets can be deposited.
|
|
|
+ // If not, an error is returned, and forwarded to the method caller.
|
|
|
+ let req = jsonreq(json!("deposit"), json!([network, token, pubkey]));
|
|
|
+ let rep: JsonResult;
|
|
|
+ match send_request(self.config.cashier_url, json!(req)).await {
|
|
|
+ Ok(v) => rep = v,
|
|
|
+ Err(e) => {
|
|
|
+ return JsonResult::Err(jsonerr(ServerError(-32004), Some(e.to_string()), id))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ match rep {
|
|
|
+ JsonResult::Resp(r) => return JsonResult::Resp(r),
|
|
|
+ JsonResult::Err(e) => return JsonResult::Err(e),
|
|
|
+ JsonResult::Notif(_n) => return JsonResult::Err(jsonerr(InternalError, None, id)),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "withdraw", "params": [network, token, publickey, amount]}
|
|
|
+ // The publickey sent here is the address where the caller wants to receive
|
|
|
+ // the tokens they plan to withdraw.
|
|
|
+ // On request, send request to cashier to get deposit address, and then transfer
|
|
|
+ // dark assets to the cashier's wallet. Following that, the cashier should return
|
|
|
+ // a transaction ID of them sending the funds that are requested for withdrawal.
|
|
|
+ // <-- {"result": "txID"}
|
|
|
+ async fn withdraw(self, id: Value, params: Value) -> JsonResult {
|
|
|
+ let args = params.as_array().unwrap();
|
|
|
+ if args.len() != 4 {
|
|
|
+ return JsonResult::Err(jsonerr(InvalidParams, None, id));
|
|
|
+ }
|
|
|
+
|
|
|
+ let network = &args[0];
|
|
|
+ let token = &args[1];
|
|
|
+ let address = &args[2];
|
|
|
+ let amount = &args[3];
|
|
|
+
|
|
|
+ // 1. Send request to cashier.
|
|
|
+ // 2. Cashier checks if they support the network, and if so,
|
|
|
+ // return adeposit address.
|
|
|
+ // 3. We issue a transfer of $amount to the given address.
|
|
|
+
|
|
|
+ return JsonResult::Err(jsonerr(
|
|
|
+ ServerError(-32005),
|
|
|
+ Some("failed to withdraw".to_string()),
|
|
|
+ id,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --> {"method": "transfer", [dToken, address, amount]}
|
|
|
+ // <-- {"result": "txID"}
|
|
|
+ async fn transfer(self, id: Value, _params: Value) -> JsonResult {
|
|
|
+ return JsonResult::Err(jsonerr(
|
|
|
+ ServerError(-32006),
|
|
|
+ Some("failed to transfer".to_string()),
|
|
|
+ id,
|
|
|
+ ));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[tokio::main]
|
|
|
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
+ let args = clap_app!(darkfid =>
|
|
|
+ (@arg CONFIG: -c --config +takes_value "Sets a custom config file")
|
|
|
+ (@arg verbose: -v --verbose "Increase verbosity")
|
|
|
+ )
|
|
|
+ .get_matches();
|
|
|
+
|
|
|
+ let config_path: PathBuf;
|
|
|
+ if args.is_present("CONFIG") {
|
|
|
+ config_path = PathBuf::from(args.value_of("CONFIG").unwrap());
|
|
|
+ } else {
|
|
|
+ config_path = join_config_path(&PathBuf::from("darkfid.toml"))?;
|
|
|
+ }
|
|
|
+
|
|
|
+ let darkfid = Darkfid::new(args.clone().is_present("verbose"), config_path)?;
|
|
|
+ // TODO: TLS
|
|
|
+ let listener = TcpListener::bind(darkfid.clone().config.rpc_url).await?;
|
|
|
+ debug!(target: "RPC SERVER", "Listening on {}", darkfid.clone().config.rpc_url);
|
|
|
+
|
|
|
+ let logger_config = ConfigBuilder::new().set_time_format_str("%T%.6f").build();
|
|
|
+ let debug_level = if args.is_present("verbose") {
|
|
|
+ LevelFilter::Debug
|
|
|
+ } else {
|
|
|
+ LevelFilter::Off
|
|
|
+ };
|
|
|
+
|
|
|
+ let log_path = darkfid.clone().config.log_path;
|
|
|
+ CombinedLogger::init(vec![
|
|
|
+ TermLogger::new(debug_level, logger_config, TerminalMode::Mixed).unwrap(),
|
|
|
+ WriteLogger::new(
|
|
|
+ LevelFilter::Debug,
|
|
|
+ SimLogConfig::default(),
|
|
|
+ std::fs::File::create(log_path).unwrap(),
|
|
|
+ ),
|
|
|
+ ])
|
|
|
+ .unwrap();
|
|
|
+
|
|
|
+ loop {
|
|
|
+ debug!(target: "RPC SERVER", "waiting for client");
|
|
|
+
|
|
|
+ let (mut socket, _) = listener.accept().await?;
|
|
|
+ let darkfid = darkfid.clone();
|
|
|
+
|
|
|
+ debug!(target: "RPC SERVER", "accepted client");
|
|
|
+
|
|
|
+ tokio::spawn(async move {
|
|
|
+ let mut buf = [0; 2048];
|
|
|
+
|
|
|
+ loop {
|
|
|
+ let n = match socket.read(&mut buf).await {
|
|
|
+ Ok(n) if n == 0 => {
|
|
|
+ debug!(target: "RPC SERVER", "closed connection");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Ok(n) => n,
|
|
|
+ Err(e) => {
|
|
|
+ debug!(target: "RPC SERVER", "failed to read from socket; err = {:?}", e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let r: JsonRequest = match serde_json::from_slice(&buf[0..n]) {
|
|
|
+ Ok(r) => r,
|
|
|
+ Err(e) => {
|
|
|
+ debug!(target: "RPC SERVER", "received invalid json; err = {:?}", e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let reply = darkfid.clone().handle_request(r).await;
|
|
|
+ let j = serde_json::to_string(&reply).unwrap();
|
|
|
+
|
|
|
+ debug!(target: "RPC", "<-- {:#?}", j);
|
|
|
+
|
|
|
+ // Write the data back
|
|
|
+ if let Err(e) = socket.write_all(j.as_bytes()).await {
|
|
|
+ debug!(target: "RPC SERVER", "failed to write to socket; err = {:?}", e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|