From 6f8aa9f39e4168cdba10cc322a603cdc19d80593 Mon Sep 17 00:00:00 2001 From: Cryptoforge Date: Thu, 23 Jul 2020 20:28:37 -0700 Subject: [PATCH] Require spending from specified address 1. Add from address, t or z 2. Use ovk of the address being spent from 3. disable lazy consolidation of t addresses 4. In a t->z transaction use the ovk of the fist z-address in the wallet. --- lib/src/commands.rs | 126 ++++++++++++++++-------------------- lib/src/lightclient.rs | 9 ++- lib/src/lightwallet.rs | 51 +++++++++++---- lib/src/lightwallet/bugs.rs | 2 +- 4 files changed, 102 insertions(+), 86 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 9a248d3..1da339f 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -457,15 +457,12 @@ impl Command for SendCommand { let mut h = vec![]; h.push("Send ZEC to a given address(es)"); h.push("Usage:"); - h.push("send
\"optional_memo\""); - h.push("OR"); - h.push("send '[{'address':
, 'amount': , 'memo': }, ...]'"); + h.push("send '{'input':
, 'output': [{'address':
, 'amount': , 'memo': }, ...]}"); h.push(""); h.push("NOTE: The fee required to send this transaction (currently ZEC 0.0001) is additionally detected from your balance."); h.push("Example:"); - h.push("send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 \"Hello from the command line\""); + h.push("send '{\"input\":\"ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d\", \"output\": [{ \"address\": \"ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d\", \"amount\": 200000, \"memo\": \"Hello from the command line\"}]}'"); h.push(""); - h.join("\n") } @@ -474,12 +471,11 @@ impl Command for SendCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - // Parse the args. There are two argument types. - // 1 - A set of 2(+1 optional) arguments for a single address send representing address, value, memo? - // 2 - A single argument in the form of a JSON string that is "[{address: address, value: value, memo: memo},...]" + // Parse the args. + // A single argument in the form of a JSON string that is "{input: address, output: [{address: address, value: value, memo: memo},...], fee: fee}" // 1 - Destination address. T or Z address - if args.len() < 1 || args.len() > 3 { + if args.len() != 1 { return self.help(); } @@ -487,77 +483,65 @@ impl Command for SendCommand { use zcash_primitives::transaction::components::amount::DEFAULT_FEE; let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + // Check for a single argument that can be parsed as JSON - let send_args = if args.len() == 1 { - let arg_list = args[0]; + let arg_list = args[0]; - let json_args = match json::parse(&arg_list) { - Ok(j) => j, - Err(e) => { - let es = format!("Couldn't understand JSON: {}", e); - return format!("{}\n{}", es, self.help()); - } - }; - - if !json_args.is_array() { - return format!("Couldn't parse argument as array\n{}", self.help()); + let json_args = match json::parse(&arg_list) { + Ok(j) => j, + Err(e) => { + let es = format!("Couldn't understand JSON: {}", e); + return format!("{}\n{}", es, self.help()); } - - let maybe_send_args = json_args.members().map( |j| { - if !j.has_key("address") || !j.has_key("amount") { - Err(format!("Need 'address' and 'amount'\n")) - } else { - let amount = match j["amount"].as_str() { - Some("entire-verified-zbalance") => lightclient.wallet.read().unwrap().verified_zbalance(None).checked_sub(fee), - _ => Some(j["amount"].as_u64().unwrap()) - }; - - match amount { - Some(amt) => Ok((j["address"].as_str().unwrap().to_string().clone(), amt, j["memo"].as_str().map(|s| s.to_string().clone()))), - None => Err(format!("Not enough in wallet to pay transaction fee")) - } - } - }).collect::)>, String>>(); - - match maybe_send_args { - Ok(a) => a.clone(), - Err(s) => { return format!("Error: {}\n{}", s, self.help()); } - } - } else if args.len() == 2 || args.len() == 3 { - let address = args[0].to_string(); - - // Make sure we can parse the amount - let value = match args[1].parse::() { - Ok(amt) => amt, - Err(e) => { - if args[1] == "entire-verified-zbalance" { - match lightclient.wallet.read().unwrap().verified_zbalance(None).checked_sub(fee) { - Some(amt) => amt, - None => { return format!("Not enough in wallet to pay transaction fee") } - } - } else { - return format!("Couldn't parse amount: {}", e); - } - } - }; - - let memo = if args.len() == 3 { Some(args[2].to_string()) } else { None }; - - // Memo has to be None if not sending to a shileded address - if memo.is_some() && !LightWallet::is_shielded_address(&address, &lightclient.config) { - return format!("Can't send a memo to the non-shielded address {}", address); - } - - vec![(args[0].to_string(), value, memo)] - } else { - return self.help() }; + //Check for a input key and convert to str + let from = if json_args.has_key("input") { + json_args["input"].as_str().unwrap().clone() + } else { + return format!("Error: {}\n{}", "Need input address", self.help()); + }; + + //Check for output key + let json_tos = if json_args.has_key("output") { + &json_args["output"] + } else { + return format!("Error: {}\n{}", "Need output address", self.help()); + }; + + //Check output is in the form of an array + if !json_tos.is_array() { + return format!("Couldn't parse argument as array\n{}", self.help()); + } + + //Check array for manadantory address and amount keys + let maybe_send_args = json_tos.members().map( |j| { + if !j.has_key("address") || !j.has_key("amount") { + Err(format!("Need 'address' and 'amount'\n")) + } else { + let amount = match j["amount"].as_str() { + Some("entire-verified-zbalance") => lightclient.wallet.read().unwrap().verified_zbalance(None).checked_sub(fee), + _ => Some(j["amount"].as_u64().unwrap()) + }; + + match amount { + Some(amt) => Ok((j["address"].as_str().unwrap().to_string().clone(), amt, j["memo"].as_str().map(|s| s.to_string().clone()))), + None => Err(format!("Not enough in wallet to pay transaction fee")) + } + } + }).collect::)>, String>>(); + + let send_args = match maybe_send_args { + Ok(a) => a.clone(), + Err(s) => { return format!("Error: {}\n{}", s, self.help()); } + }; + + match lightclient.do_sync(true) { Ok(_) => { // Convert to the right format. String -> &str. let tos = send_args.iter().map(|(a, v, m)| (a.as_str(), *v, m.clone()) ).collect::>(); - match lightclient.do_send(tos) { + match lightclient.do_send(from, tos) { Ok(txid) => { object!{ "txid" => txid } }, Err(e) => { object!{ "error" => e } } }.pretty(2) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 4b1134c..76bdd8d 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -1334,7 +1334,7 @@ impl LightClient { } } - pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> Result { + pub fn do_send(&self, from: &str, addrs: Vec<(&str, u64, Option)>) -> Result { if !self.wallet.read().unwrap().is_unlocked_for_spending() { error!("Wallet is locked"); return Err("Wallet is locked".to_string()); @@ -1343,10 +1343,13 @@ impl LightClient { info!("Creating transaction"); let rawtx = self.wallet.write().unwrap().send_to_address( - u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), + u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), &self.sapling_spend, &self.sapling_output, - addrs + from, addrs ); + + info!("Transaction Complete"); + match rawtx { Ok(txbytes) => broadcast_raw_tx(&self.get_server_uri(), txbytes), diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index d2244d0..853e3a7 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1679,6 +1679,7 @@ impl LightWallet { consensus_branch_id: u32, spend_params: &[u8], output_params: &[u8], + from: &str, tos: Vec<(&str, u64, Option)> ) -> Result, String> { if !self.unlocked { @@ -1692,7 +1693,7 @@ impl LightWallet { let total_value = tos.iter().map(|to| to.1).sum::(); println!( - "0: Creating transaction sending {} ztoshis to {} addresses", + "0: Creating transaction sending {} zatoshis to {} addresses", total_value, tos.len() ); @@ -1731,6 +1732,7 @@ impl LightWallet { let notes: Vec<_> = self.txs.read().unwrap().iter() .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) .flatten() + .filter(|(_txid, note)|LightWallet::note_address(self.config.hrp_sapling_address(), note).unwrap() == from) .filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset, &self.extsks.read().unwrap()[note.account]) ) @@ -1755,6 +1757,7 @@ impl LightWallet { // ZecWallet will add all your t-address funds into that transaction, and send them to your shielded // address as change. let tinputs: Vec<_> = self.get_utxos().iter() + .filter(|utxo| utxo.address == from) .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends .map(|utxo| utxo.clone()) .collect(); @@ -1820,17 +1823,43 @@ impl LightWallet { } } + + // Use the ovk belonging to the address being sent from, if not using any notes + // use the first address in the wallet for the ovk. + let ovk = if notes.len() == 0 { + self.extfvks.read().unwrap()[0].fvk.ovk + } else { + ExtendedFullViewingKey::from(¬es[0].extsk).fvk.ovk + }; + // If no Sapling notes were added, add the change address manually. That is, - // send the change to our sapling address manually. Note that if a sapling note was spent, - // the builder will automatically send change to that address - if notes.len() == 0 { - builder.send_change_to( - ExtendedFullViewingKey::from(&self.extsks.read().unwrap()[0]).fvk.ovk, - self.extsks.read().unwrap()[0].default_address().unwrap().1); + // send the change back to the transparent address being used, + // the builder will automatically send change back to the sapling address if notes are used. + if notes.len() == 0 && selected_value - u64::from(target_value) > 0 { + + println!("{}: Adding change output", now() - start_time); + + let from_addr = address::RecipientAddress::from_str(from, + self.config.hrp_sapling_address(), + self.config.base58_pubkey_address(), + self.config.base58_script_address()).unwrap(); + + if let Err(e) = match from_addr { + address::RecipientAddress::Shielded(from_addr) => { + builder.add_sapling_output(ovk, from_addr.clone(), Amount::from_u64(selected_value - u64::from(target_value)).unwrap(), None) + } + address::RecipientAddress::Transparent(from_addr) => { + builder.add_transparent_output(&from_addr, Amount::from_u64(selected_value - u64::from(target_value)).unwrap()) + } + } { + let e = format!("Error adding transparent change output: {:?}", e); + error!("{}", e); + return Err(e); + } } - // TODO: We're using the first ovk to encrypt outgoing Txns. Is that Ok? - let ovk = self.extfvks.read().unwrap()[0].fvk.ovk; + + for (to, value, memo) in recepients { // Compute memo if it exists @@ -1845,8 +1874,8 @@ impl LightWallet { Some(m) => Some(m) } }; - - println!("{}: Adding output", now() - start_time); + + println!("{}: Adding outputs", now() - start_time); if let Err(e) = match to { address::RecipientAddress::Shielded(to) => { diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs index 8a88170..a641b13 100644 --- a/lib/src/lightwallet/bugs.rs +++ b/lib/src/lightwallet/bugs.rs @@ -82,7 +82,7 @@ impl BugBip39Derivation { let txid = if amount > 0 { println!("Sending funds to ourself."); let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - match client.do_send(vec![(&zaddr, amount-fee, None)]) { + match client.do_send(client.do_address()["z_addresses"][0].as_str().unwrap(), vec![(&zaddr, amount-fee, None)]) { Ok(txid) => txid, Err(e) => { let r = object!{