From b0d5e5e84b0ee092624f4376126fa38fa2745a2a Mon Sep 17 00:00:00 2001
From: librelois <c@elo.tf>
Date: Sat, 20 Mar 2021 03:38:33 +0100
Subject: [PATCH] feat (wallet): add pay sub-command

---
 Cargo.lock             |  73 +++++++++++-------
 Cargo.toml             |   4 +-
 src/commands/wallet.rs | 168 ++++++++++++++++++++++++++++++++++-------
 src/main.rs            |   4 +-
 4 files changed, 192 insertions(+), 57 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 97e06d3..099be32 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -101,17 +101,6 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "async-trait"
-version = "0.1.48"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -155,6 +144,16 @@ version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409"
 
+[[package]]
+name = "bincode"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d175dfa69e619905c4c3cdb7c3c203fa3bdd5d51184e3afdb2742c0280493772"
+dependencies = [
+ "byteorder",
+ "serde",
+]
+
 [[package]]
 name = "bitflags"
 version = "1.2.1"
@@ -217,9 +216,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
 
 [[package]]
 name = "byteorder"
-version = "1.4.3"
+version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 
 [[package]]
 name = "bytes"
@@ -351,9 +350,9 @@ checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d"
 
 [[package]]
 name = "dubp"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3e3fe6a250aaa17de9d86c77028ae2b22f4f7d3b9716659d89d82fb72b2db68"
+checksum = "1b9617091ba0d0a0cc95392655b0b7136fee3b63101d64c0fd0a5c2741983f18"
 dependencies = [
  "dubp-common",
  "dubp-documents",
@@ -365,23 +364,25 @@ dependencies = [
 [[package]]
 name = "dubp-client"
 version = "0.1.0"
-source = "git+https://git.duniter.org/libs/dubp-rs-client-lib?branch=master#787229ae4a94fdb4278e86f8f47af15388345f90"
+source = "git+https://git.duniter.org/libs/dubp-rs-client-lib?branch=master#398b44b8b61e6cf631c1842ca44c9f3916c1c66f"
 dependencies = [
- "async-trait",
+ "bincode",
  "dubp",
+ "duniter-bca-types",
  "graphql_client",
  "maybe-async",
  "mockall 0.9.1",
  "reqwest",
  "serde",
+ "static_assertions",
  "thiserror",
 ]
 
 [[package]]
 name = "dubp-common"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c83386c1914b8f3d4a1fb2895b6e31899019b05de3fb6c7ec45d0f13238e6c64"
+checksum = "da764b34a61ecb52fa90dc11f5a44d6bc9599b043d37592b5102b95d993fd677"
 dependencies = [
  "dup-crypto",
  "serde",
@@ -392,9 +393,9 @@ dependencies = [
 
 [[package]]
 name = "dubp-documents"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeabf1395e31c3e2a1dfabc1f97a933c361312e65f65ca843092b94a32e4ce19"
+checksum = "8810039b6dd6a101109aebc9836ddec58366052803f824c56016889d9444f5cf"
 dependencies = [
  "beef",
  "dubp-wallet",
@@ -406,9 +407,9 @@ dependencies = [
 
 [[package]]
 name = "dubp-documents-parser"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f441e42514531d4b04001ff95e20d9b681198bc8f13872af24281c350d2e96f2"
+checksum = "c791c77a3f9c670666dc41f861481cc80eb1ad193584727adfee792350fd3129"
 dependencies = [
  "dubp-documents",
  "json-pest-parser",
@@ -420,9 +421,9 @@ dependencies = [
 
 [[package]]
 name = "dubp-wallet"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b89b1d3262a26156de5edcd97e20c2cd747a24ee1244e929b5ac3aada5519b94"
+checksum = "2ef8e7c5334ddbc7899f83911026ebc324ba7fc7232c7ca5265e611490dbd282"
 dependencies = [
  "byteorder",
  "dubp-common",
@@ -432,11 +433,23 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "duniter-bca-types"
+version = "0.1.0"
+source = "git+https://git.duniter.org/nodes/typescript/duniter?branch=bca#fd6d221857461f75d059c5fc2afc24daab4d5545"
+dependencies = [
+ "bincode",
+ "dubp",
+ "serde",
+ "smallvec",
+ "thiserror",
+]
+
 [[package]]
 name = "dup-crypto"
-version = "0.48.0"
+version = "0.49.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093aa174cc3792e029e5deabf83986842595279840e5eab58e845efe33d75b48"
+checksum = "0be04829b31b18bacf5317001366d807e5fbd02085ee6348508c1299b5bcaf6c"
 dependencies = [
  "aes",
  "arrayvec",
@@ -1458,6 +1471,12 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "strsim"
 version = "0.8.0"
diff --git a/Cargo.toml b/Cargo.toml
index a2bd143..0a29130 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,14 +9,14 @@ description = "A command line client written in Rust that use Duniter GVA API."
 
 [dependencies]
 anyhow = "1.0.32"
-dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking"] }
+dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking"], default-features = false }
 #dubp-client= { path = "../dubp-rs-client-lib", features = ["blocking"], default-features = false }
 rpassword = "5.0.1"
 serde = { version = "1.0.105", features = ["derive"] }
 structopt = "0.3.18"
 
 [dev-dependencies]
-dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking", "mock"] }
+dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking", "mock"], default-features = false }
 #dubp-client= { path = "../dubp-rs-client-lib", features = ["blocking", "mock"], default-features = false }
 mockall = "0.8.0"
 
diff --git a/src/commands/wallet.rs b/src/commands/wallet.rs
index c2f4009..a4c596c 100644
--- a/src/commands/wallet.rs
+++ b/src/commands/wallet.rs
@@ -14,17 +14,21 @@
 // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 use crate::*;
-use dubp_client::crypto::{
-    bases::b58::ToBase58,
-    dewif::{
-        read_dewif_file_content, write_dewif_v4_content, Currency, ExpectedCurrency, G1_CURRENCY,
+use dubp_client::{
+    crypto::{
+        bases::b58::ToBase58,
+        dewif::{
+            read_dewif_file_content, write_dewif_v4_content, Currency, ExpectedCurrency,
+            G1_CURRENCY,
+        },
+        keys::{
+            ed25519::bip32::{KeyPair, PrivateDerivationPath},
+            KeyPair as _, KeyPairEnum, PublicKey,
+        },
+        mnemonic::{mnemonic_to_seed, Language, Mnemonic, MnemonicType},
+        utils::U31,
     },
-    keys::{
-        ed25519::bip32::{KeyPair, PrivateDerivationPath},
-        KeyPair as _, KeyPairEnum,
-    },
-    mnemonic::{mnemonic_to_seed, Language, Mnemonic, MnemonicType},
-    utils::U31,
+    wallet::prelude::SourceAmount,
 };
 
 #[derive(StructOpt)]
@@ -44,7 +48,7 @@ pub enum WalletCommand {
         word_count: MnemonicType,
     },
     /// Get a public key
-    GetPubkey {
+    Pubkey {
         account_index: u32,
         address_index: Option<u32>,
         #[structopt(long)]
@@ -53,6 +57,8 @@ pub enum WalletCommand {
         dewif_file: Option<PathBuf>,
         #[structopt(short, long)]
         external: bool,
+        #[structopt(short, long, default_value = "en")]
+        lang: Language,
         #[structopt(long)]
         password_stdin: bool,
     },
@@ -65,9 +71,33 @@ pub enum WalletCommand {
         #[structopt(long)]
         password_stdin: bool,
     },
+    /// send money
+    Pay {
+        account_index: u32,
+        #[structopt(short, long)]
+        amount: u64,
+        #[structopt(short, long)]
+        base: Option<u64>,
+        #[structopt(short, long)]
+        comment: Option<String>,
+        #[structopt(long)]
+        dewif: Option<String>,
+        #[structopt(long, parse(from_os_str))]
+        dewif_file: Option<PathBuf>,
+        #[structopt(short, long, default_value = "en")]
+        lang: Language,
+        #[structopt(long)]
+        password_stdin: bool,
+        #[structopt(short, long)]
+        recipient: String,
+    },
 }
 
-pub(crate) fn wallet<W: Write>(out: &mut W, command: WalletCommand) -> anyhow::Result<()> {
+pub(crate) fn wallet<C: GvaClient, W: Write>(
+    gva_client: &C,
+    out: &mut W,
+    command: WalletCommand,
+) -> anyhow::Result<()> {
     match command {
         WalletCommand::Gen {
             lang,
@@ -104,26 +134,31 @@ pub(crate) fn wallet<W: Write>(out: &mut W, command: WalletCommand) -> anyhow::R
             gen_dewif(out, log_n, mnemonic, password_stdin)?;
             Ok(())
         }
-        WalletCommand::GetPubkey {
+        WalletCommand::Pubkey {
             account_index,
             address_index,
             dewif,
             dewif_file,
             external,
+            lang,
             password_stdin,
         } => {
-            let dewif_content = get_dewif_content(dewif, dewif_file)?;
-            let keypair = get_keypair_from_dewif(dewif_content, password_stdin)?;
-
-            let derivation_path = PrivateDerivationPath::opaque(
-                U31::new(account_index)?,
-                external,
-                if let Some(address_index) = address_index {
-                    Some(U31::new(address_index)?)
-                } else {
-                    None
-                },
-            )?;
+            let keypair = get_master_keypair(dewif, dewif_file, lang, password_stdin)?;
+
+            let derivation_path = match account_index % 3 {
+                0 => PrivateDerivationPath::transparent(U31::new(account_index)?)?,
+                1 => todo!(),
+                2 => PrivateDerivationPath::opaque(
+                    U31::new(account_index)?,
+                    external,
+                    if let Some(address_index) = address_index {
+                        Some(U31::new(address_index)?)
+                    } else {
+                        None
+                    },
+                )?,
+                _ => unreachable!(),
+            };
 
             let derived_keypair = keypair.derive(derivation_path);
 
@@ -131,6 +166,68 @@ pub(crate) fn wallet<W: Write>(out: &mut W, command: WalletCommand) -> anyhow::R
 
             Ok(())
         }
+        WalletCommand::Pay {
+            account_index,
+            amount,
+            base,
+            comment,
+            dewif,
+            dewif_file,
+            lang,
+            password_stdin,
+            recipient,
+        } => {
+            let recipient = PublicKey::from_base58(&recipient)?;
+
+            let keypair = get_master_keypair(dewif, dewif_file, lang, password_stdin)?;
+
+            match account_index % 3 {
+                0 => {
+                    let trasparent_keypair = keypair.derive(PrivateDerivationPath::transparent(
+                        U31::new(account_index)?,
+                    )?);
+
+                    writeln!(
+                        out,
+                        "Sending {}.{} Ğ1 to {} from transparent account {} …",
+                        amount / 100,
+                        amount % 100,
+                        recipient,
+                        trasparent_keypair.public_key().to_base58()
+                    )?;
+
+                    let req_time = Instant::now();
+                    let payment_result = gva_client.simple_payment(
+                        SourceAmount::new(amount as i64, base.unwrap_or_default() as i64),
+                        &trasparent_keypair.generate_signator(),
+                        recipient,
+                        comment,
+                        None,
+                    )?;
+
+                    if let PaymentResult::Errors(errors) = payment_result {
+                        writeln!(out, "All or part of the payment failed, errors: \n")?;
+                        for error in errors {
+                            writeln!(out, "- {:?}", error)?;
+                        }
+                        todo!()
+                    } else {
+                        writeln!(out, "Payment succesfully sent.")?;
+                        let duration = req_time.elapsed();
+                        println!(
+                            "Payment processed in {}.{} ms.",
+                            duration.as_millis(),
+                            duration.subsec_micros() % 1_000
+                        );
+                    }
+
+                    Ok(())
+                }
+                1 => todo!(),
+                2 => todo!(),
+                _ => unreachable!(),
+            }
+        }
     }
 }
 
@@ -166,6 +263,22 @@ fn gen_dewif<W: Write>(
     Ok(())
 }
 
+fn get_master_keypair(
+    dewif_opt: Option<String>,
+    dewif_file_opt: Option<PathBuf>,
+    lang: Language,
+    password_stdin: bool,
+) -> anyhow::Result<KeyPair> {
+    if dewif_opt.is_some() || dewif_file_opt.is_some() {
+        let dewif_content = get_dewif_content(dewif_opt, dewif_file_opt)?;
+        get_master_keypair_from_dewif(dewif_content, password_stdin)
+    } else {
+        let mnemonic_phrase = rpassword::prompt_password_stdout("Mnemonic: ")?;
+        let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, lang)?;
+        Ok(KeyPair::from_seed(mnemonic_to_seed(&mnemonic)))
+    }
+}
+
 fn get_dewif_content(
     dewif_opt: Option<String>,
     dewif_file_opt: Option<PathBuf>,
@@ -191,7 +304,10 @@ fn get_dewif_content(
     }
 }
 
-fn get_keypair_from_dewif(dewif_content: String, password_stdin: bool) -> anyhow::Result<KeyPair> {
+fn get_master_keypair_from_dewif(
+    dewif_content: String,
+    password_stdin: bool,
+) -> anyhow::Result<KeyPair> {
     let password = if password_stdin {
         rpassword::read_password()?
     } else {
diff --git a/src/main.rs b/src/main.rs
index f192aa4..cebd558 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -35,7 +35,7 @@ use commands::{
 use dubp_client::MockGvaClient;
 use dubp_client::{
     crypto::keys::{ed25519::PublicKey, PublicKey as _},
-    AccountBalance, GvaClient, Idty, NaiveGvaClient, PubkeyOrScript,
+    AccountBalance, GvaClient, Idty, NaiveGvaClient, PaymentResult, PubkeyOrScript,
 };
 use std::{
     env::var_os,
@@ -73,7 +73,7 @@ fn main() -> anyhow::Result<()> {
         Command::CurrentUd => current_ud(&gva_client, &mut out)?,
         Command::Idty { pubkey } => idty(&gva_client, &mut out, &pubkey)?,
         Command::MembersCount => members_count(&gva_client, &mut out)?,
-        Command::Wallet { command } => wallet(&mut out, command)?,
+        Command::Wallet { command } => wallet(&gva_client, &mut out, command)?,
     }
     Ok(())
 }
-- 
GitLab