Browse Source

drk: Begin rewrite for money contract interaction.

parazyd 3 years ago
parent
commit
d08bba325d
5 changed files with 377 additions and 267 deletions
  1. 46 7
      Cargo.lock
  2. 12 6
      bin/drk/Cargo.toml
  3. 53 251
      bin/drk/src/main.rs
  4. 258 0
      bin/drk/src/rpc_wallet.rs
  5. 8 3
      src/lib.rs

+ 46 - 7
Cargo.lock

@@ -645,8 +645,8 @@ checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
 dependencies = [
 dependencies = [
  "atty",
  "atty",
  "bitflags",
  "bitflags",
- "clap_derive",
- "clap_lex",
+ "clap_derive 3.2.18",
+ "clap_lex 0.2.4",
  "indexmap",
  "indexmap",
  "once_cell",
  "once_cell",
  "strsim 0.10.0",
  "strsim 0.10.0",
@@ -654,6 +654,21 @@ dependencies = [
  "textwrap 0.16.0",
  "textwrap 0.16.0",
 ]
 ]
 
 
+[[package]]
+name = "clap"
+version = "4.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca505fd2c00136e0d0cd34bcd8b6bd0b59d5779aab396054b716334230c1c"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive 4.0.21",
+ "clap_lex 0.3.0",
+ "once_cell",
+ "strsim 0.10.0",
+ "termcolor",
+]
+
 [[package]]
 [[package]]
 name = "clap_derive"
 name = "clap_derive"
 version = "3.2.18"
 version = "3.2.18"
@@ -667,6 +682,19 @@ dependencies = [
  "syn",
  "syn",
 ]
 ]
 
 
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck 0.4.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 [[package]]
 name = "clap_lex"
 name = "clap_lex"
 version = "0.2.4"
 version = "0.2.4"
@@ -676,6 +704,15 @@ dependencies = [
  "os_str_bytes",
  "os_str_bytes",
 ]
 ]
 
 
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
 [[package]]
 [[package]]
 name = "cmake"
 name = "cmake"
 version = "0.1.49"
 version = "0.1.49"
@@ -1356,6 +1393,7 @@ dependencies = [
  "fxhash",
  "fxhash",
  "incrementalmerkletree",
  "incrementalmerkletree",
  "lazy-init",
  "lazy-init",
+ "libsqlite3-sys",
  "log",
  "log",
  "pasta_curves",
  "pasta_curves",
  "rand",
  "rand",
@@ -1364,6 +1402,7 @@ dependencies = [
  "simplelog",
  "simplelog",
  "sled",
  "sled",
  "smol",
  "smol",
+ "sqlx",
  "structopt",
  "structopt",
  "structopt-toml",
  "structopt-toml",
  "url",
  "url",
@@ -1682,18 +1721,18 @@ checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0"
 name = "drk"
 name = "drk"
 version = "0.3.0"
 version = "0.3.0"
 dependencies = [
 dependencies = [
+ "anyhow",
  "async-std",
  "async-std",
- "bs58",
- "clap 3.2.23",
+ "clap 4.0.25",
  "darkfi",
  "darkfi",
  "darkfi-sdk",
  "darkfi-sdk",
- "indicatif",
- "log",
- "pasta_curves",
+ "darkfi-serial",
+ "libsqlite3-sys",
  "prettytable-rs",
  "prettytable-rs",
  "rand",
  "rand",
  "serde_json",
  "serde_json",
  "simplelog",
  "simplelog",
+ "sqlx",
  "url",
  "url",
 ]
 ]
 
 

+ 12 - 6
bin/drk/Cargo.toml

@@ -9,16 +9,22 @@ license = "AGPL-3.0-only"
 edition = "2021"
 edition = "2021"
 
 
 [dependencies]
 [dependencies]
+anyhow = "1.0.66"
 async-std = {version = "1.12.0", features = ["attributes"]}
 async-std = {version = "1.12.0", features = ["attributes"]}
-bs58 = "0.4.0"
-clap = {version = "3.2.20", features = ["derive"]}
-darkfi = {path = "../../", features = ["crypto", "util", "rpc", "wasm-runtime", "zkas"]}
+clap = {version = "4.0.25", features = ["derive"]}
+darkfi = {path = "../../", features = ["rpc", "util", "wallet"]}
 darkfi-sdk = {path = "../../src/sdk"}
 darkfi-sdk = {path = "../../src/sdk"}
-indicatif = "0.17.1"
-log = "0.4.17"
-pasta_curves = "0.4.1"
+darkfi-serial = {path = "../../src/serial", features = ["derive", "crypto"]}
 prettytable-rs = "0.9.0"
 prettytable-rs = "0.9.0"
 rand = "0.8.5"
 rand = "0.8.5"
 serde_json = "1.0.87"
 serde_json = "1.0.87"
 simplelog = "0.12.0"
 simplelog = "0.12.0"
+libsqlite3-sys = {version = "0.24.2", features = ["bundled-sqlcipher"]}
+sqlx = {version = "0.6.2", features = ["runtime-async-std-native-tls", "sqlite"]}
 url = "2.3.1"
 url = "2.3.1"
+#bs58 = "0.4.0"
+#clap = {version = "3.2.20", features = ["derive"]}
+#darkfi = {path = "../../", features = ["crypto", "util", "rpc", "wasm-runtime", "zkas"]}
+#indicatif = "0.17.1"
+#log = "0.4.17"
+#pasta_curves = "0.4.1"

+ 53 - 251
bin/drk/src/main.rs

@@ -16,16 +16,10 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
-use std::{
-    io::{stdin, Read},
-    process::exit,
-    str::FromStr,
-    time::Instant,
-};
+use std::{process::exit, time::Instant};
 
 
+use anyhow::{Context, Result};
 use clap::{Parser, Subcommand};
 use clap::{Parser, Subcommand};
-use darkfi_sdk::crypto::{Address, TokenId};
-use prettytable::{format, row, Table};
 use serde_json::json;
 use serde_json::json;
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
 use url::Url;
 use url::Url;
@@ -33,315 +27,123 @@ use url::Url;
 use darkfi::{
 use darkfi::{
     cli_desc,
     cli_desc,
     rpc::{client::RpcClient, jsonrpc::JsonRequest},
     rpc::{client::RpcClient, jsonrpc::JsonRequest},
-    util::{
-        cli::{get_log_config, get_log_level, progress_bar},
-        net_name::NetworkName,
-        parse::encode_base10,
-    },
-    Result,
+    util::cli::{get_log_config, get_log_level},
 };
 };
 
 
-//mod deploy_contract;
-//use deploy_contract::create_deploy_data;
+/// Wallet operation methods for darkfid's JSON-RPC
+mod rpc_wallet;
 
 
 #[derive(Parser)]
 #[derive(Parser)]
-#[clap(name = "drk", about = cli_desc!(), version)]
-#[clap(arg_required_else_help(true))]
+#[command(about = cli_desc!())]
 struct Args {
 struct Args {
-    #[clap(short, parse(from_occurrences))]
+    #[arg(short, action = clap::ArgAction::Count)]
     /// Increase verbosity (-vvv supported)
     /// Increase verbosity (-vvv supported)
     verbose: u8,
     verbose: u8,
 
 
-    #[clap(short, long, default_value = "tcp://127.0.0.1:8340")]
+    #[arg(short, long, default_value = "tcp://127.0.0.1:8340")]
     /// darkfid JSON-RPC endpoint
     /// darkfid JSON-RPC endpoint
     endpoint: Url,
     endpoint: Url,
 
 
-    #[clap(subcommand)]
+    #[command(subcommand)]
     command: Subcmd,
     command: Subcmd,
 }
 }
 
 
 #[derive(Subcommand)]
 #[derive(Subcommand)]
 enum Subcmd {
 enum Subcmd {
-    /// Send a ping request to the RPC
+    /// Send a ping request to the darkfid RPC endpoint
     Ping,
     Ping,
 
 
-    /// Send an airdrop request to the faucet
-    Airdrop {
-        #[clap(long, parse(try_from_str))]
-        /// Address where the airdrop should be requested
-        /// (default is darkfid's wallet default)
-        address: Option<Address>,
-
-        #[clap(long)]
-        /// JSON-RPC endpoint of the faucet
-        faucet_endpoint: Url,
-
-        /// f64 amount requested for airdrop
-        amount: f64,
-
-        /// Token ID to airdrop
-        #[clap(long)]
-        token_id: String,
-    },
-
     /// Wallet operations
     /// Wallet operations
     Wallet {
     Wallet {
-        #[clap(long)]
+        #[arg(long)]
+        /// Initialize wallet with data for Money Contract (run this first)
+        initialize: bool,
+
+        #[arg(long)]
         /// Generate a new keypair in the wallet
         /// Generate a new keypair in the wallet
         keygen: bool,
         keygen: bool,
 
 
-        #[clap(long)]
+        #[arg(long)]
         /// Query the wallet for known balances
         /// Query the wallet for known balances
         balance: bool,
         balance: bool,
 
 
-        #[clap(long)]
+        #[arg(long)]
         /// Get the default address in the wallet
         /// Get the default address in the wallet
         address: bool,
         address: bool,
-
-        #[clap(long)]
-        /// Get all addresses in the wallet
-        all_addresses: bool,
     },
     },
-
-    /// Transfer of value
-    Transfer {
-        /// Recipient address
-        #[clap(parse(try_from_str))]
-        recipient: Address,
-
-        /// Amount to transfer
-        amount: f64,
-
-        /// Coin network
-        #[clap(short, long, default_value = "darkfi", parse(try_from_str))]
-        network: NetworkName,
-
-        /// Token ID
-        #[clap(short, long)]
-        token_id: String,
-    },
-
-    /// Broadcast a given transaction from stdin
-    Broadcast,
-    // Deploy a smart contract in the current directory or a given path.
-    //DeployContract {
-    //    #[clap(long, default_value = ".")]
-    //    path: PathBuf,
-    //},
 }
 }
 
 
-struct Drk {
+pub struct Drk {
     pub rpc_client: RpcClient,
     pub rpc_client: RpcClient,
 }
 }
 
 
 impl Drk {
 impl Drk {
-    async fn _close_connection(&self) -> Result<()> {
-        self.rpc_client.close().await
-    }
-
     async fn ping(&self) -> Result<()> {
     async fn ping(&self) -> Result<()> {
-        let start = Instant::now();
+        let latency = Instant::now();
         let req = JsonRequest::new("ping", json!([]));
         let req = JsonRequest::new("ping", json!([]));
-        let rep = self.rpc_client.request(req).await?;
-        let latency = Instant::now() - start;
+        let rep = self.rpc_client.oneshot_request(req).await?;
+        let latency = latency.elapsed();
         println!("Got reply: {}", rep);
         println!("Got reply: {}", rep);
         println!("Latency: {:?}", latency);
         println!("Latency: {:?}", latency);
         Ok(())
         Ok(())
     }
     }
-
-    async fn airdrop(
-        &self,
-        address: Option<Address>,
-        endpoint: Url,
-        amount: f64,
-        token_id: String,
-    ) -> Result<()> {
-        let addr = if address.is_some() {
-            address.unwrap()
-        } else {
-            let req = JsonRequest::new("wallet.get_addrs", json!([0_i64]));
-            let rep = self.rpc_client.request(req).await?;
-            Address::from_str(rep.as_array().unwrap()[0].as_str().unwrap())?
-        };
-
-        // Check if token ID is valid base58
-        if TokenId::try_from(token_id.as_str()).is_err() {
-            eprintln!("Error: Invalid Token ID passed as argument.");
-            exit(1);
-        }
-
-        let pb = progress_bar(&format!("Requesting airdrop for {}", addr));
-
-        let req = JsonRequest::new("airdrop", json!([json!(addr.to_string()), amount, token_id]));
-        let rpc_client = RpcClient::new(endpoint).await?;
-        let rep = match rpc_client.oneshot_request(req).await {
-            Ok(v) => v,
-            Err(e) => {
-                eprintln!("{}", e);
-                exit(1);
-            }
-        };
-        pb.finish();
-
-        println!("Transaction ID: {}", rep);
-        Ok(())
-    }
-
-    async fn wallet_keygen(&self) -> Result<()> {
-        let req = JsonRequest::new("wallet.keygen", json!([]));
-        let rep = self.rpc_client.request(req).await?;
-        println!("New address: {}", rep);
-        Ok(())
-    }
-
-    async fn wallet_balance(&self) -> Result<()> {
-        let req = JsonRequest::new("wallet.get_balances", json!([]));
-        let rep = self.rpc_client.request(req).await?;
-
-        if !rep.is_object() {
-            eprintln!("Invalid balance data received from darkfid RPC endpoint.");
-            exit(1);
-        }
-
-        let mut table = Table::new();
-        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
-        table.set_titles(row!["Token ID", "Balance"]);
-
-        for i in rep.as_object().unwrap().keys() {
-            if let Some(balance) = rep[i].as_u64() {
-                table.add_row(row![i, encode_base10(balance, 8)]);
-                continue
-            }
-
-            eprintln!("Found invalid balance data for key \"{}\"", i);
-        }
-
-        if table.is_empty() {
-            println!("No balances.");
-        } else {
-            println!("{}", table);
-        }
-
-        Ok(())
-    }
-
-    async fn wallet_address(&self) -> Result<()> {
-        let req = JsonRequest::new("wallet.get_addrs", json!([0_i64]));
-        let rep = self.rpc_client.request(req).await?;
-        println!("Default wallet address: {}", rep);
-        Ok(())
-    }
-
-    async fn wallet_all_addresses(&self) -> Result<()> {
-        let req = JsonRequest::new("wallet.get_addrs", json!([-1]));
-        let rep = self.rpc_client.request(req).await?;
-        println!("Wallet addresses:\n{:#?}", rep);
-        Ok(())
-    }
-
-    async fn tx_transfer(
-        &self,
-        network: NetworkName,
-        token_id: String,
-        recipient: Address,
-        amount: f64,
-    ) -> Result<()> {
-        println!("Attempting to transfer {} tokens to {}", amount, recipient);
-
-        let req = JsonRequest::new(
-            "tx.transfer",
-            json!([network.to_string(), token_id, recipient.to_string(), amount]),
-        );
-
-        let rep = self.rpc_client.request(req).await?;
-
-        println!("Success! Transaction ID: {}", rep);
-        Ok(())
-    }
-
-    async fn tx_broadcast(&self, transaction: String) -> Result<()> {
-        println!("Attempting to broadcast transaction from stdin...");
-        let req = JsonRequest::new("tx.broadcast", json!([transaction]));
-        let rep = self.rpc_client.request(req).await?;
-        println!("Success!\nTransaction ID: {}", rep);
-        Ok(())
-    }
 }
 }
 
 
 #[async_std::main]
 #[async_std::main]
 async fn main() -> Result<()> {
 async fn main() -> Result<()> {
     let args = Args::parse();
     let args = Args::parse();
 
 
-    let log_level = get_log_level(args.verbose.into());
-    let log_config = get_log_config();
-    TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
+    if args.verbose > 0 {
+        let log_level = get_log_level(args.verbose.into());
+        let log_config = get_log_config();
+        TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
+    }
 
 
     match args.command {
     match args.command {
         Subcmd::Ping => {
         Subcmd::Ping => {
-            let rpc_client = RpcClient::new(args.endpoint).await?;
+            let rpc_client = RpcClient::new(args.endpoint)
+                .await
+                .with_context(|| "Could not connect to RPC endpoint")?;
+
             let drk = Drk { rpc_client };
             let drk = Drk { rpc_client };
-            drk.ping().await
+            drk.ping().await.with_context(|| "Failed to ping RPC endpoint")?;
+            Ok(())
         }
         }
 
 
-        Subcmd::Airdrop { address, faucet_endpoint, amount, token_id } => {
-            let rpc_client = RpcClient::new(args.endpoint).await?;
-            let drk = Drk { rpc_client };
+        Subcmd::Wallet { initialize, keygen, balance, address } => {
+            if !initialize && !keygen && !balance && !address {
+                eprintln!("Error: You must use at least one flag for this subcommand");
+                eprintln!("Run with \"wallet -h\" to see the subcommand usage.");
+                exit(2);
+            }
 
 
-            drk.airdrop(address, faucet_endpoint, amount, token_id).await
-        }
+            let rpc_client = RpcClient::new(args.endpoint)
+                .await
+                .with_context(|| "Could not connect to RPC endpoint")?;
 
 
-        Subcmd::Wallet { keygen, balance, address, all_addresses } => {
-            let rpc_client = RpcClient::new(args.endpoint).await?;
             let drk = Drk { rpc_client };
             let drk = Drk { rpc_client };
 
 
+            if initialize {
+                drk.wallet_initialize().await.with_context(|| "Failed to initialize wallet")?;
+                return Ok(())
+            }
+
             if keygen {
             if keygen {
-                return drk.wallet_keygen().await
+                drk.wallet_keygen().await.with_context(|| "Failed to generate keypair")?;
+                return Ok(())
             }
             }
 
 
             if balance {
             if balance {
-                return drk.wallet_balance().await
+                drk.wallet_balance().await.with_context(|| "Failed to fetch wallet balance")?;
+                return Ok(())
             }
             }
 
 
             if address {
             if address {
-                return drk.wallet_address().await
-            }
-
-            if all_addresses {
-                return drk.wallet_all_addresses().await
+                drk.wallet_address(0).await.with_context(|| "Failed to fetch default address")?;
+                return Ok(())
             }
             }
 
 
-            eprintln!("Run 'drk wallet -h' to see the subcommand usage.");
-            exit(2);
+            unreachable!()
         }
         }
-
-        Subcmd::Transfer { recipient, amount, network, token_id } => {
-            let rpc_client = RpcClient::new(args.endpoint).await?;
-            let drk = Drk { rpc_client };
-
-            drk.tx_transfer(network, token_id, recipient, amount).await
-        }
-
-        Subcmd::Broadcast => {
-            let rpc_client = RpcClient::new(args.endpoint).await?;
-            let drk = Drk { rpc_client };
-
-            let mut buf = String::new();
-            stdin().read_to_string(&mut buf)?;
-
-            drk.tx_broadcast(buf).await
-        } /*
-          Subcmd::DeployContract { path } => {
-              eprintln!("Trying to deploy the smart contract in {:?}", path);
-              let deploy_data = match create_deploy_data(&path) {
-                  Ok(v) => v,
-                  Err(e) => {
-                      eprintln!("{}: Failed to deploy smart contract: {}", fg_red("Error:"), e);
-                      exit(1);
-                  }
-              };
-
-              Ok(())
-          }
-          */
     }
     }
 }
 }

+ 258 - 0
bin/drk/src/rpc_wallet.rs

@@ -0,0 +1,258 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2022 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::collections::HashMap;
+
+use anyhow::{anyhow, Result};
+use darkfi::{rpc::jsonrpc::JsonRequest, util::parse::encode_base10, wallet::walletdb::QueryType};
+use darkfi_sdk::{
+    crypto::{constants::MERKLE_DEPTH, Keypair, MerkleNode, PublicKey, TokenId},
+    incrementalmerkletree::bridgetree::BridgeTree,
+};
+use darkfi_serial::{deserialize, serialize};
+use prettytable::{format, row, Table};
+use rand::rngs::OsRng;
+use serde_json::json;
+
+use super::Drk;
+
+// TODO: FIXME:
+// Find a way to have these constants be deterministic for the actual
+// contract. e.g. they could be prefixed with the contract_id in order
+// not to have collisions happen. This is because right now it's easy
+// to overwrite any table in the wallet if the developer doesn't take
+// care of it. The wallet's SQL schema comes from the money contract
+// and here we just hardcode it. There should be a nice way to parse
+// the schema and fill some map.
+const MONEY_INFO_TABLE: &str = "money_info";
+const MONEY_INFO_COL_LAST_SCANNED_SLOT: &str = "last_scanned_slot";
+
+const MONEY_TREE_TABLE: &str = "money_tree";
+const MONEY_TREE_COL_TREE: &str = "tree";
+
+const MONEY_KEYS_TABLE: &str = "money_keys";
+const MONEY_KEYS_COL_KEY_ID: &str = "key_id";
+const MONEY_KEYS_COL_IS_DEFAULT: &str = "is_default";
+const MONEY_KEYS_COL_PUBLIC: &str = "public";
+const MONEY_KEYS_COL_SECRET: &str = "secret";
+
+const MONEY_COINS_TABLE: &str = "money_coins";
+const MONEY_COINS_COL_COIN: &str = "coin";
+const MONEY_COINS_COL_IS_SPENT: &str = "is_spent";
+const MONEY_COINS_COL_SERIAL: &str = "serial";
+const MONEY_COINS_COL_VALUE: &str = "value";
+const MONEY_COINS_COL_TOKEN_ID: &str = "token_id";
+const MONEY_COINS_COL_COIN_BLIND: &str = "coin_blind";
+const MONEY_COINS_COL_VALUE_BLIND: &str = "value_blind";
+const MONEY_COINS_COL_TOKEN_BLIND: &str = "token_blind";
+const MONEY_COINS_COL_SECRET: &str = "secret";
+const MONEY_COINS_COL_NULLIFIER: &str = "nullifier";
+const MONEY_COINS_COL_LEAF_POSITION: &str = "leaf_position";
+const MONEY_COINS_COL_MEMO: &str = "memo";
+
+impl Drk {
+    /// Initialize wallet with tables for the Money contract.
+    /// This should be performed initially before doing other operations.
+    pub async fn wallet_initialize(&self) -> Result<()> {
+        let wallet_schema = include_str!("../../../src/contract/money/wallet.sql");
+
+        // We perform a request to darkfid with the schema to initialize
+        // the necessary tables in the wallet.
+        let req = JsonRequest::new("wallet.exec_sql", json!([wallet_schema]));
+        let rep = self.rpc_client.request(req).await?;
+
+        if rep == true {
+            println!("Successfully initialized wallet schema for Money Contract");
+        } else {
+            println!("Got unexpected reply from darkfid: {}", rep);
+        }
+
+        // Check if we have to initialize the Merkle tree.
+        // We check if we find a row in the tree table, and if not, we create
+        // a new tree and push it into the table.
+        let mut tree_needs_init = false;
+        let query = format!("SELECT * FROM {}", MONEY_TREE_TABLE);
+        let params = json!([query, QueryType::Blob as u8, MONEY_TREE_COL_TREE]);
+        let req = JsonRequest::new("wallet.query_row_single", params);
+
+        // For now, on success, we don't care what's returned, but maybe in
+        // the future we should actually check it?
+        // TODO: The RPC needs a better variant for errors so detailed inspection
+        //       can be done with error codes and all that.
+        if let Err(_) = self.rpc_client.request(req).await {
+            tree_needs_init = true;
+        }
+
+        if tree_needs_init {
+            println!("Initializing Merkle tree");
+            let tree = BridgeTree::<MerkleNode, MERKLE_DEPTH>::new(100);
+            let tree_bytes = serialize(&tree);
+            let query = format!(
+                "DELETE FROM {}; INSERT INTO {} ({}) VALUES (?1);",
+                MONEY_TREE_TABLE, MONEY_TREE_TABLE, MONEY_TREE_COL_TREE
+            );
+            let params = json!([query, QueryType::Blob as u8, tree_bytes]);
+            let req = JsonRequest::new("wallet.exec_sql", params);
+            let _ = self.rpc_client.oneshot_request(req).await?;
+            println!("Successfully initialized Merkle tree");
+        }
+
+        Ok(())
+    }
+
+    /// Generate a new wallet keypair and put it in the according wallet table.
+    pub async fn wallet_keygen(&self) -> Result<()> {
+        println!("Generating a new keypair");
+        // TODO: We might want to have hierarchical deterministic key derivation.
+        let keypair = Keypair::random(&mut OsRng);
+        let public = serialize(&keypair.public);
+        let secret = serialize(&keypair.secret);
+        let is_default = 0;
+
+        let query = format!(
+            "INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3)",
+            MONEY_KEYS_TABLE,
+            MONEY_KEYS_COL_IS_DEFAULT,
+            MONEY_KEYS_COL_PUBLIC,
+            MONEY_KEYS_COL_SECRET,
+        );
+
+        let params = json!([
+            query,
+            QueryType::Integer as u8,
+            is_default,
+            QueryType::Blob as u8,
+            public,
+            QueryType::Blob as u8,
+            secret,
+        ]);
+
+        let req = JsonRequest::new("wallet.exec_sql", params);
+        let rep = self.rpc_client.oneshot_request(req).await?;
+
+        if rep == true {
+            println!("Successfully added new keypair to wallet");
+        } else {
+            println!("Got unexpected reply from darkfid: {}", rep);
+        }
+
+        println!("New address: {}", keypair.public);
+        Ok(())
+    }
+
+    /// Fetch known balances from the wallet and try to print them as a table.
+    pub async fn wallet_balance(&self) -> Result<()> {
+        // This represents "false"
+        let is_spent = 0;
+
+        let query = format!(
+            "SELECT {}, {} FROM {} WHERE {} = {}",
+            MONEY_COINS_COL_VALUE,
+            MONEY_COINS_COL_TOKEN_ID,
+            MONEY_COINS_TABLE,
+            MONEY_COINS_COL_IS_SPENT,
+            is_spent,
+        );
+
+        let params = json!([
+            query,
+            QueryType::Blob as u8,
+            MONEY_COINS_COL_VALUE,
+            QueryType::Blob as u8,
+            MONEY_COINS_COL_TOKEN_ID,
+        ]);
+
+        let req = JsonRequest::new("wallet.query_row_multi", params);
+        let rep = self.rpc_client.oneshot_request(req).await?;
+
+        // The returned thing should be an array of found rows.
+        let Some(rows) = rep.as_array() else {
+            return Err(anyhow!("Unexpected response from darkfid: {}", rep))
+        };
+
+        // Fill this map with balances, and in the end we'll print it as a table.
+        let mut balmap: HashMap<String, u64> = HashMap::new();
+
+        // Let's scan through the rows and see if we got anything.
+        for row in rows {
+            let Some(row) = row.as_array() else {
+                return Err(anyhow!("Unexpected response from darkfid: {}", rep))
+            };
+
+            if row.len() != 2 {
+                eprintln!("Error: Got invalid array, row should contain two elements.");
+                eprintln!("Actual contents:\n:{:#?}", row);
+                return Err(anyhow!("Unexpected response from darkfid: {}", rep))
+            }
+
+            let value_bytes: Vec<u8> = serde_json::from_value(row[0].clone())?;
+            let mut value: u64 = deserialize(&value_bytes)?;
+
+            let token_bytes: Vec<u8> = serde_json::from_value(row[1].clone())?;
+            let token_id: TokenId = deserialize(&token_bytes)?;
+            let token_id = format!("{}", token_id);
+
+            if let Some(prev) = balmap.get(&token_id) {
+                value += prev;
+            }
+
+            balmap.insert(token_id, value);
+        }
+
+        // Create a prettytable with the new data.
+        let mut table = Table::new();
+        table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
+        table.set_titles(row!["Token ID", "Balance"]);
+
+        for (token_id, balance) in balmap.iter() {
+            // FIXME: Don't hardcode to 8 decimals
+            table.add_row(row![token_id, encode_base10(*balance, 8)]);
+        }
+
+        if table.is_empty() {
+            println!("No unspent balances found");
+        } else {
+            println!("{}", table);
+        }
+
+        Ok(())
+    }
+
+    /// Fetch pubkeys from the wallet and print the requested index.
+    pub async fn wallet_address(&self, idx: u64) -> Result<()> {
+        let query = format!("SELECT {} FROM {};", MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_TABLE);
+        let params = json!([query, QueryType::Blob as u8, MONEY_KEYS_COL_PUBLIC]);
+        let req = JsonRequest::new("wallet.query_row_single", params);
+        let rep = self.rpc_client.oneshot_request(req).await?;
+
+        let Some(arr) = rep.as_array() else {
+            return Err(anyhow!("Unexpected response from darkfid: {}", rep));
+        };
+
+        if arr.len() != 1 {
+            return Err(anyhow!("Unexpected response from darkfid: {}", rep))
+        }
+
+        let key_bytes: Vec<u8> = serde_json::from_value(arr[0].clone())?;
+        let public_key: PublicKey = deserialize(&key_bytes)?;
+
+        println!("{}", public_key);
+
+        Ok(())
+    }
+}

+ 8 - 3
src/lib.rs

@@ -74,9 +74,14 @@ pub const ANSI_LOGO: &str = include_str!("../contrib/darkfi.ansi");
 #[macro_export]
 #[macro_export]
 macro_rules! cli_desc {
 macro_rules! cli_desc {
     () => {{
     () => {{
-        let mut desc = env!("CARGO_PKG_DESCRIPTION").to_string();
-        desc.push_str("\n");
-        desc.push_str(darkfi::ANSI_LOGO);
+        let desc = format!(
+            "{} {}\n{}\n{}",
+            env!("CARGO_PKG_NAME").to_string(),
+            env!("CARGO_PKG_VERSION").to_string(),
+            env!("CARGO_PKG_DESCRIPTION").to_string(),
+            darkfi::ANSI_LOGO,
+        );
+
         Box::leak(desc.into_boxed_str()) as &'static str
         Box::leak(desc.into_boxed_str()) as &'static str
     }};
     }};
 }
 }