main.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. use std::{path::PathBuf, str::FromStr};
  2. use clap::{IntoApp, Parser, Subcommand};
  3. use log::{debug, error};
  4. use prettytable::{cell, format, row, Table};
  5. use serde::{Deserialize, Serialize};
  6. use serde_json::{json, Value};
  7. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  8. use url::Url;
  9. use darkfi::{
  10. rpc::{jsonrpc, jsonrpc::JsonResult},
  11. util::{
  12. cli::{log_config, spawn_config, Config, UrlConfig},
  13. join_config_path,
  14. path::expand_path,
  15. NetworkName,
  16. },
  17. Error, Result,
  18. };
  19. /// The configuration for drk
  20. #[derive(Clone, Serialize, Deserialize, Debug)]
  21. pub struct DrkConfig {
  22. /// The URL where darkfid RPC is listening on
  23. pub darkfid_rpc_url: UrlConfig,
  24. /// Socks5 server url. eg. `socks5://127.0.0.1:9050` used for tor and nym protocols
  25. pub socks_url: UrlConfig,
  26. }
  27. #[derive(Subcommand)]
  28. pub enum CliDrkSubCommands {
  29. /// Say hello to the RPC
  30. Hello {},
  31. /// Show what features the cashier supports
  32. Features {},
  33. /// Wallet operations
  34. Wallet {
  35. /// Initialize a new wallet
  36. #[clap(long)]
  37. create: bool,
  38. /// Generate wallet keypair
  39. #[clap(long)]
  40. keygen: bool,
  41. /// Get default wallet address
  42. #[clap(long)]
  43. address: bool,
  44. /// Get wallet addresses
  45. #[clap(long)]
  46. addresses: bool,
  47. /// Set default address
  48. #[clap(long, value_name = "ADDRESS")]
  49. set_default_address: Option<String>,
  50. /// Export default address
  51. #[clap(long, value_name = "PATH")]
  52. export_keypair: Option<String>,
  53. /// Import address
  54. #[clap(long, value_name = "PATH")]
  55. import_keypair: Option<String>,
  56. /// Get wallet balances
  57. #[clap(long)]
  58. balances: bool,
  59. },
  60. /// Get hexidecimal ID for token symbol
  61. Id {
  62. /// Which network to use (bitcoin/solana/...)
  63. #[clap(long)]
  64. network: String,
  65. /// Which token to query (btc/sol/usdc/...)
  66. #[clap(parse(try_from_str))]
  67. token: String,
  68. },
  69. /// Withdraw Dark tokens for clear tokens
  70. Withdraw {
  71. /// Which network to use (bitcoin/solana/...)
  72. #[clap(long)]
  73. network: String,
  74. /// Which token to receive (btc/sol/usdc/...)
  75. #[clap(parse(try_from_str))]
  76. token_sym: String,
  77. /// Recipient address
  78. #[clap(parse(try_from_str))]
  79. address: String,
  80. /// Amount to withdraw
  81. #[clap(parse(try_from_str))]
  82. amount: u64,
  83. },
  84. /// Transfer Dark tokens to address
  85. Transfer {
  86. /// Which network to use (bitcoin/solana/...)
  87. #[clap(long)]
  88. network: String,
  89. /// Which token to transfer (btc/sol/usdc/...)
  90. #[clap(parse(try_from_str))]
  91. token_sym: String,
  92. /// Recipient address
  93. #[clap(parse(try_from_str))]
  94. address: String,
  95. /// Amount to transfer
  96. #[clap(parse(try_from_str))]
  97. amount: f64,
  98. },
  99. /// Deposit clear tokens for Dark tokens
  100. Deposit {
  101. /// Which network to use (bitcoin/solana/...)
  102. #[clap(long)]
  103. network: String,
  104. /// Which token to deposit (btc/sol/usdc/...)
  105. #[clap(parse(try_from_str))]
  106. token_sym: String,
  107. },
  108. }
  109. /// Drk cli
  110. #[derive(Parser)]
  111. #[clap(name = "drk")]
  112. #[clap(author, version, about)]
  113. pub struct CliDrk {
  114. /// Sets a custom config file
  115. #[clap(short, long)]
  116. pub config: Option<String>,
  117. /// Increase verbosity
  118. #[clap(short, parse(from_occurrences))]
  119. pub verbose: u8,
  120. #[clap(subcommand)]
  121. pub command: Option<CliDrkSubCommands>,
  122. }
  123. const CONFIG_FILE_CONTENTS: &[u8] = include_bytes!("../drk_config.toml");
  124. struct Drk {
  125. url: Url,
  126. socks_url: Url,
  127. }
  128. impl Drk {
  129. pub fn new(url: Url, socks_url: Url) -> Self {
  130. Self { url, socks_url }
  131. }
  132. // Retrieve cashier features and error if they
  133. // don't support the network
  134. async fn check_network(&self, network: &NetworkName) -> Result<()> {
  135. let features = self.features().await?;
  136. if features.as_object().is_none() &&
  137. features.as_object().unwrap()["networks"].as_array().is_none() &&
  138. features.as_object().unwrap()["networks"].as_array().unwrap().is_empty()
  139. {
  140. return Err(Error::NotSupportedNetwork)
  141. }
  142. for nets in features.as_object().unwrap()["networks"].as_array().unwrap() {
  143. for (net, _) in nets.as_object().unwrap() {
  144. if network == &NetworkName::from_str(net.as_str())? {
  145. return Ok(())
  146. }
  147. }
  148. }
  149. Err(Error::NotSupportedNetwork)
  150. }
  151. async fn request(&self, r: jsonrpc::JsonRequest) -> Result<Value> {
  152. let reply: JsonResult =
  153. match jsonrpc::send_request(&self.url, json!(r), Some(self.socks_url.clone())).await {
  154. Ok(v) => v,
  155. Err(e) => return Err(e),
  156. };
  157. match reply {
  158. JsonResult::Resp(r) => {
  159. debug!(target: "RPC", "<-- {}", serde_json::to_string(&r)?);
  160. Ok(r.result)
  161. }
  162. JsonResult::Err(e) => {
  163. debug!(target: "RPC", "<-- {}", serde_json::to_string(&e)?);
  164. Err(Error::JsonRpcError(e.error.message.to_string()))
  165. }
  166. JsonResult::Notif(n) => {
  167. debug!(target: "RPC", "<-- {}", serde_json::to_string(&n)?);
  168. Err(Error::JsonRpcError("Unexpected reply".to_string()))
  169. }
  170. }
  171. }
  172. // --> {"jsonrpc": "2.0", "method": "say_hello", "params": [], "id": 42}
  173. // <-- {"jsonrpc": "2.0", "result": "hello world", "id": 42}
  174. async fn say_hello(&self) -> Result<Value> {
  175. let req = jsonrpc::request(json!("say_hello"), json!([]));
  176. Ok(self.request(req).await?)
  177. }
  178. // --> {"jsonrpc": "2.0", "method": "create_wallet", "params": [], "id": 42}
  179. // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
  180. async fn create_wallet(&self) -> Result<Value> {
  181. let req = jsonrpc::request(json!("create_wallet"), json!([]));
  182. Ok(self.request(req).await?)
  183. }
  184. // --> {"jsonrpc": "2.0", "method": "key_gen", "params": [], "id": 42}
  185. // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
  186. async fn key_gen(&self) -> Result<Value> {
  187. let req = jsonrpc::request(json!("key_gen"), json!([]));
  188. Ok(self.request(req).await?)
  189. }
  190. // --> {"jsonrpc": "2.0", "method": "get_key", "params": [], "id": 42}
  191. // <-- {"jsonrpc": "2.0", "result": "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC", "id": 42}
  192. async fn get_key(&self) -> Result<Value> {
  193. let req = jsonrpc::request(json!("get_key"), json!([]));
  194. Ok(self.request(req).await?)
  195. }
  196. // --> {"jsonrpc": "2.0", "method": "get_keys", "params": [], "id": 42}
  197. // <-- {"jsonrpc": "2.0", "result": "[vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC, ...]", "id":
  198. // 42}
  199. async fn get_keys(&self) -> Result<Value> {
  200. let req = jsonrpc::request(json!("get_keys"), json!([]));
  201. Ok(self.request(req).await?)
  202. }
  203. // --> {"jsonrpc": "2.0", "method": "set_default_address", "params":
  204. // "[vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC]", "id": 42}
  205. // <-- {"jsonrpc": "2.0", "result":
  206. // true, "id": 42}
  207. async fn set_default_address(&self, address: &str) -> Result<Value> {
  208. let req = jsonrpc::request(json!("set_default_address"), json!([address]));
  209. Ok(self.request(req).await?)
  210. }
  211. // --> {"jsonrpc": "2.0", "method": "export_keypair", "params": "[path/]", "id": 42}
  212. // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
  213. async fn export_keypair(&self, path: &str) -> Result<Value> {
  214. let req = jsonrpc::request(json!("export_keypair"), json!([path]));
  215. Ok(self.request(req).await?)
  216. }
  217. // --> {"jsonrpc": "2.0", "method": "import_keypair", "params": "[path/]", "id": 42}
  218. // <-- {"jsonrpc": "2.0", "result": true, "id": 42}
  219. async fn import_keypair(&self, path: &str) -> Result<Value> {
  220. let req = jsonrpc::request(json!("import_keypair"), json!([path]));
  221. Ok(self.request(req).await?)
  222. }
  223. // --> {"jsonrpc": "2.0", "method": "get_key", "params": ["solana", "usdc"], "id": 42}
  224. // <-- {"jsonrpc": "2.0", "result": "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC", "id": 42}
  225. async fn get_token_id(&self, network: &str, token: &str) -> Result<Value> {
  226. let req = jsonrpc::request(json!("get_token_id"), json!([network, token]));
  227. Ok(self.request(req).await?)
  228. }
  229. // --> {"method": "get_balances", "params": []}
  230. // <-- {"result": "get_balances": "[ {"btc": (value, network)}, .. ]"}
  231. async fn get_balances(&self) -> Result<Value> {
  232. let req = jsonrpc::request(json!("get_balances"), json!([]));
  233. Ok(self.request(req).await?)
  234. }
  235. // --> {"jsonrpc": "2.0", "method": "features", "params": [], "id": 42}
  236. // <-- {"jsonrpc": "2.0", "result": ["network": "btc", "sol"], "id": 42}
  237. async fn features(&self) -> Result<Value> {
  238. let req = jsonrpc::request(json!("features"), json!([]));
  239. Ok(self.request(req).await?)
  240. }
  241. // --> {"jsonrpc": "2.0", "method": "deposit", "params": ["solana", "usdc"], "id": 42}
  242. // <-- {"jsonrpc": "2.0", "result": "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL", "id": 42}
  243. async fn deposit(&self, network: &str, token: &str) -> Result<Value> {
  244. let req = jsonrpc::request(json!("deposit"), json!([network, token]));
  245. Ok(self.request(req).await?)
  246. }
  247. // --> {"jsonrpc": "2.0", "method": "withdraw",
  248. // "params": ["solana", "usdc", "Ht5G1RhkcKnpLVLMhqJc5aqZ4wYUEbxbtZwGCVbgU7DL", 13.37"],
  249. // "id": 42} <-- {"jsonrpc": "2.0", "result": "txID", "id": 42}
  250. async fn withdraw(
  251. &self,
  252. network: &str,
  253. token: &str,
  254. address: &str,
  255. amount: &str,
  256. ) -> Result<Value> {
  257. let req = jsonrpc::request(json!("withdraw"), json!([network, token, address, amount]));
  258. Ok(self.request(req).await?)
  259. }
  260. // --> {"jsonrpc": "2.0", "method": "transfer",
  261. // "params": ["dusdc", "vdNS7oBj7KvsMWWmo9r96SV4SqATLrGsH2a3PGpCfJC", 13.37], "id": 42}
  262. // <-- {"jsonrpc": "2.0", "result": "txID", "id": 42}
  263. async fn transfer(
  264. &self,
  265. network: &str,
  266. token: &str,
  267. address: &str,
  268. amount: &str,
  269. ) -> Result<Value> {
  270. let req = jsonrpc::request(json!("transfer"), json!([network, token, address, amount]));
  271. Ok(self.request(req).await?)
  272. }
  273. }
  274. async fn start(config: &DrkConfig, options: CliDrk) -> Result<()> {
  275. let client = Drk::new(
  276. Url::try_from(config.darkfid_rpc_url.clone())?,
  277. Url::try_from(config.socks_url.clone())?,
  278. );
  279. match options.command {
  280. Some(CliDrkSubCommands::Hello {}) => {
  281. let reply = client.say_hello().await?;
  282. println!("Server replied: {}", &reply.to_string());
  283. return Ok(())
  284. }
  285. Some(CliDrkSubCommands::Features {}) => {
  286. let reply = client.features().await?;
  287. println!("Features: {}", &reply.to_string());
  288. return Ok(())
  289. }
  290. Some(CliDrkSubCommands::Wallet {
  291. create,
  292. keygen,
  293. address,
  294. balances,
  295. addresses,
  296. export_keypair,
  297. import_keypair,
  298. set_default_address,
  299. }) => {
  300. if create {
  301. let reply = client.create_wallet().await?;
  302. if reply.as_bool().unwrap() {
  303. println!("Wallet created successfully.")
  304. } else {
  305. println!("Server replied: {}", &reply.to_string());
  306. }
  307. return Ok(())
  308. }
  309. if keygen {
  310. let reply = client.key_gen().await?;
  311. if reply.as_bool().unwrap() {
  312. println!("Key generation successful.")
  313. } else {
  314. println!("Server replied: {}", &reply.to_string());
  315. }
  316. return Ok(())
  317. }
  318. if address {
  319. let reply = client.get_key().await?;
  320. println!("Wallet address: {}", &reply.to_string());
  321. return Ok(())
  322. }
  323. if addresses {
  324. let reply = client.get_keys().await?;
  325. println!("Wallet addresses: ");
  326. if reply.as_array().is_some() {
  327. for (i, address) in reply.as_array().unwrap().iter().enumerate() {
  328. if i == 0 {
  329. println!("- [X] {}", address);
  330. } else {
  331. println!("- [ ] {}", address);
  332. }
  333. }
  334. } else {
  335. println!("Empty!!",);
  336. }
  337. return Ok(())
  338. }
  339. if balances {
  340. let reply = client.get_balances().await?;
  341. if reply.as_object().is_some() && !reply.as_object().unwrap().is_empty() {
  342. let mut table = Table::new();
  343. table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
  344. table.set_titles(row!["token", "amount", "network"]);
  345. for (tkn, data) in reply.as_object().unwrap() {
  346. table.add_row(row![
  347. tkn,
  348. data[0].as_str().unwrap(),
  349. data[1].as_str().unwrap()
  350. ]);
  351. }
  352. table.printstd();
  353. } else {
  354. println!("Balances: {}", 0);
  355. }
  356. return Ok(())
  357. }
  358. if set_default_address.is_some() {
  359. let default_address = set_default_address.unwrap();
  360. client.set_default_address(&default_address).await?;
  361. return Ok(())
  362. }
  363. if export_keypair.is_some() {
  364. let path = export_keypair.unwrap();
  365. client.export_keypair(&path).await?;
  366. return Ok(())
  367. }
  368. if import_keypair.is_some() {
  369. let path = import_keypair.unwrap();
  370. client.import_keypair(&path).await?;
  371. return Ok(())
  372. }
  373. }
  374. Some(CliDrkSubCommands::Id { network, token }) => {
  375. let network = network.to_lowercase();
  376. client.check_network(&NetworkName::from_str(&network)?).await?;
  377. let reply = client.get_token_id(&network, &token).await?;
  378. println!("Token ID: {}", &reply.to_string());
  379. return Ok(())
  380. }
  381. Some(CliDrkSubCommands::Deposit { network, token_sym }) => {
  382. let network = network.to_lowercase();
  383. client.check_network(&NetworkName::from_str(&network)?).await?;
  384. let reply = client.deposit(&network, &token_sym).await?;
  385. println!("Deposit your coins to the following address: {}", &reply.to_string());
  386. return Ok(())
  387. }
  388. Some(CliDrkSubCommands::Transfer { network, token_sym, address, amount }) => {
  389. let network = network.to_lowercase();
  390. client.check_network(&NetworkName::from_str(&network)?).await?;
  391. client.transfer(&network, &token_sym, &address, &amount.to_string()).await?;
  392. println!("{} {} Transfered successfully", amount, token_sym.to_uppercase(),);
  393. return Ok(())
  394. }
  395. Some(CliDrkSubCommands::Withdraw { network, token_sym, address, amount }) => {
  396. let network = network.to_lowercase();
  397. client.check_network(&NetworkName::from_str(&network)?).await?;
  398. let reply =
  399. client.withdraw(&network, &token_sym, &address, &amount.to_string()).await?;
  400. println!("{}", &reply.to_string());
  401. return Ok(())
  402. }
  403. None => {}
  404. }
  405. error!("Please run 'drk help' to see usage.");
  406. Err(Error::MissingParams)
  407. }
  408. #[async_std::main]
  409. async fn main() -> Result<()> {
  410. let args = CliDrk::parse();
  411. let matches =
  412. CliDrk::command().propagate_version(true).arg_required_else_help(true).get_matches();
  413. let config_path = if args.config.is_some() {
  414. expand_path(&args.config.clone().unwrap())?
  415. } else {
  416. join_config_path(&PathBuf::from("drk.toml"))?
  417. };
  418. // Spawn config file if it's not in place already.
  419. spawn_config(&config_path, CONFIG_FILE_CONTENTS)?;
  420. let verbosity_level = matches.occurrences_of("verbose");
  421. let (lvl, conf) = log_config(verbosity_level)?;
  422. TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
  423. let config = Config::<DrkConfig>::load(config_path)?;
  424. start(&config, args).await
  425. }