Browse Source

cli: Implement drk2 for JSON-RPC API reference.

parazyd 4 years ago
parent
commit
47b0c211ff
3 changed files with 265 additions and 1 deletions
  1. 256 0
      src/bin/drk2.rs
  2. 1 0
      src/cli/cli_config.rs
  3. 8 1
      src/error.rs

+ 256 - 0
src/bin/drk2.rs

@@ -0,0 +1,256 @@
+#[macro_use]
+extern crate clap;
+use clap::ArgMatches;
+use drk::cli::{Config, DrkConfig};
+use drk::util::join_config_path;
+use drk::{rpc::jsonrpc, rpc::jsonrpc::JsonResult, Error, Result};
+use log::debug;
+use serde_json::{json, Value};
+use simplelog::{
+    CombinedLogger, Config as SimplelogConfig, ConfigBuilder, LevelFilter, TermLogger,
+    TerminalMode, WriteLogger,
+};
+use std::path::PathBuf;
+
+struct Drk {
+    url: String,
+}
+
+impl Drk {
+    pub fn new(url: String) -> Self {
+        Self { url }
+    }
+
+    async fn request(&self, r: jsonrpc::JsonRequest) -> Result<Value> {
+        let data = surf::Body::from_json(&r)?;
+        debug!(target: "DRK", "--> {:?}", r);
+        let mut req = surf::post(&self.url).body(data).await?;
+
+        let resp = req.take_body();
+        let json = resp.into_string().await?;
+
+        let v: JsonResult = serde_json::from_str(&json)?;
+        match v {
+            JsonResult::Resp(r) => {
+                debug!(target: "DRK", "<-- {:?}", r);
+                return Ok(r.result);
+            }
+
+            JsonResult::Err(e) => {
+                debug!(target: "DRK", "<-- {:?}", e);
+                return Err(Error::JsonRpcError(e.error.message.to_string()));
+            }
+
+            JsonResult::Notif(n) => {
+                debug!(target: "DRK", "<-- {:?}", n);
+                return Err(Error::JsonRpcError(
+                    "Unexpected reply from server".to_string(),
+                ));
+            }
+        };
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "say_hello", "params": [], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": "hello world", "id": 42}
+    async fn say_hello(&self) -> Result<Value> {
+        let req = jsonrpc::request(json!("say_hello"), json!([]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "create_wallet", "params": [], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
+    async fn create_wallet(&self) -> Result<Value> {
+        let req = jsonrpc::request(json!("create_wallet"), json!([]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "key_gen", "params": [], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
+    async fn key_gen(&self) -> Result<Value> {
+        let req = jsonrpc::request(json!("key_gen"), json!([]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "get_key", "params": [], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC", "id": 42}
+    async fn get_key(&self) -> Result<Value> {
+        let req = jsonrpc::request(json!("get_key"), json!([]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "deposit", "params": ["solana", "usdc"], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL", "id": 42}
+    async fn deposit(&self, network: &str, asset: &str) -> Result<Value> {
+        let req = jsonrpc::request(json!("deposit"), json!([network, asset]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "withdraw",
+    //      "params": ["solana", "usdc", "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL", 13.37"], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": "txID", "id": 42}
+    async fn withdraw(
+        &self,
+        network: &str,
+        asset: &str,
+        address: &str,
+        amount: f64,
+    ) -> Result<Value> {
+        let req = jsonrpc::request(json!("withdraw"), json!([network, asset, address, amount]));
+        Ok(self.request(req).await?)
+    }
+
+    // --> {"jsonrpc": "2.0", "method": "transfer",
+    //      "params": ["dusdc", "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC", 13.37], "id": 42}
+    // <-- {"jsonrpc": "2.0", "result": "txID", "id": 42}
+    async fn transfer(&self, asset: &str, address: &str, amount: f64) -> Result<Value> {
+        let req = jsonrpc::request(json!("transfer"), json!([asset, address, amount]));
+        Ok(self.request(req).await?)
+    }
+}
+
+async fn start(config: &DrkConfig, options: ArgMatches<'_>) -> Result<()> {
+    let client = Drk::new(config.rpc_url.clone());
+
+    if options.is_present("hello") {
+        let reply = client.say_hello().await?;
+        println!("Server replied: {}", &reply.to_string());
+        return Ok(());
+    }
+
+    if let Some(matches) = options.subcommand_matches("wallet") {
+        if matches.is_present("create") {
+            let reply = client.create_wallet().await?;
+            println!("Server replied: {}", &reply.to_string());
+            return Ok(());
+        }
+
+        if matches.is_present("keygen") {
+            let reply = client.key_gen().await?;
+            println!("Server replied: {}", &reply.to_string());
+            return Ok(());
+        }
+
+        if matches.is_present("address") {
+            let reply = client.get_key().await?;
+            println!("Server replied: {}", &reply.to_string());
+            return Ok(());
+        }
+    }
+
+    if let Some(matches) = options.subcommand_matches("deposit") {
+        let network = matches.value_of("network").unwrap().to_lowercase();
+        let token = matches.value_of("TOKEN").unwrap();
+
+        // TODO: Retrieve cashier features and error if they
+        // don't support the network.
+
+        let reply = client.deposit(&network, &token).await?;
+
+        println!(
+            "Deposit your coins to the following address: {}",
+            &reply.to_string()
+        );
+
+        return Ok(());
+    }
+
+    if let Some(matches) = options.subcommand_matches("withdraw") {
+        let network = matches.value_of("network").unwrap().to_lowercase();
+        let token = matches.value_of("TOKEN").unwrap();
+        let address = matches.value_of("ADDRESS").unwrap();
+        let amount = matches.value_of("AMOUNT").unwrap().parse::<f64>()?;
+
+        // TODO: Retrieve cashier features and error if they
+        // don't support the network.
+
+        let reply = client.withdraw(&network, &token, &address, amount).await?;
+
+        println!("Transaction ID: {}", &reply.to_string());
+
+        return Ok(());
+    }
+
+    if let Some(matches) = options.subcommand_matches("transfer") {
+        let asset_type = matches.value_of("ASSET_TYPE").unwrap();
+        let address = matches.value_of("ADDRESS").unwrap();
+        let amount = matches.value_of("AMOUNT").unwrap().parse::<f64>()?;
+
+        let reply = client.transfer(&asset_type, &address, amount).await?;
+
+        println!("Transaction ID: {}", &reply.to_string());
+
+        return Ok(());
+    }
+
+    println!("Please run 'drk help' to see usage.");
+    Err(Error::MissingParams)
+}
+
+fn main() -> Result<()> {
+    let args = clap_app!(drk =>
+        (@arg CONFIG: -c --config +takes_value "Sets a custom config file")
+        (@arg verbose: -v --verbose "Increase verbosity")
+        (@subcommand hello =>
+            (about: "Say hello to the RPC")
+        )
+        (@subcommand wallet =>
+            (about: "Wallet operations")
+            (@arg create: --create "Initialize a new wallet")
+            (@arg keygen: --keygen "Generate wallet keypair")
+            (@arg address: --address "Get wallet address")
+        )
+        (@subcommand deposit =>
+            (about: "Deposit clear assets for Dark assets")
+            (@arg network: +required +takes_value --network
+                    "Which network to use (bitcoin/solana/...)")
+            (@arg TOKEN: +required
+                    "Which token to deposit (BTC/SOL/USDC/...)")
+        )
+        (@subcommand transfer =>
+            (about: "Transfer Dark assets to address")
+            (@arg ASSET_TYPE: +required "Desired asset")
+            (@arg ADDRESS: +required "Recipient address")
+            (@arg AMOUNT: +required "Amount to send")
+        )
+        (@subcommand withdraw =>
+            (about: "Withdraw Dark assets for clear assets")
+            (@arg network: +required +takes_value --network
+                "Which network to use (bitcoin/solana/...)")
+            (@arg TOKEN: +required "Desired asset")
+            (@arg ADDRESS: +required "Recipient address")
+            (@arg AMOUNT: +required "Amount to send")
+        )
+    )
+    .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("drk.toml"))?;
+    }
+
+    let config = Config::<DrkConfig>::load(config_path)?;
+
+    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 = config.log_path.clone();
+
+    CombinedLogger::init(vec![
+        TermLogger::new(debug_level, logger_config, TerminalMode::Mixed).unwrap(),
+        WriteLogger::new(
+            LevelFilter::Debug,
+            SimplelogConfig::default(),
+            std::fs::File::create(log_path).unwrap(),
+        ),
+    ])
+    .unwrap();
+
+    futures::executor::block_on(start(&config, args))
+}

+ 1 - 0
src/cli/cli_config.rs

@@ -9,6 +9,7 @@ use std::{
     str,
 };
 
+#[derive(Default)]
 pub struct Config<T> {
     config: PhantomData<T>,
 }

+ 8 - 1
src/error.rs

@@ -16,6 +16,7 @@ pub enum Error {
     /// Parsing error
     ParseFailed(&'static str),
     ParseIntError,
+    ParseFloatError,
     AsyncChannelSenderError,
     AsyncChannelReceiverError,
     MalformedPacket,
@@ -71,6 +72,7 @@ impl fmt::Display for Error {
             Error::NonMinimalVarInt => f.write_str("non-minimal varint"),
             Error::ParseFailed(ref err) => write!(f, "parse failed: {}", err),
             Error::ParseIntError => f.write_str("Parse int error"),
+            Error::ParseFloatError => f.write_str("Parse float error"),
             Error::AsyncChannelSenderError => f.write_str("Async_channel sender error"),
             Error::AsyncChannelReceiverError => f.write_str("Async_channel receiver error"),
             Error::MalformedPacket => f.write_str("Malformed packet"),
@@ -199,6 +201,12 @@ impl From<std::num::ParseIntError> for Error {
     }
 }
 
+impl From<std::num::ParseFloatError> for Error {
+    fn from(_err: std::num::ParseFloatError) -> Error {
+        Error::ParseFloatError
+    }
+}
+
 impl From<std::string::FromUtf8Error> for Error {
     fn from(_err: std::string::FromUtf8Error) -> Error {
         Error::Utf8Error
@@ -266,4 +274,3 @@ impl From<bs58::decode::Error> for Error {
         Error::Base58DecodeError(err.to_string())
     }
 }
-