diff --git a/lib/Cargo.toml b/lib/Cargo.toml index d29c9c7..f94c614 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -48,33 +48,33 @@ reqwest = { version = "0.10.8", features = ["blocking", "json"] } [dependencies.bellman] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" default-features = false features = ["groth16", "multicore"] [dependencies.pairing] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" [dependencies.zcash_client_backend] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" default-features = false [dependencies.zcash_primitives] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" default-features = false features = ["transparent-inputs"] [dependencies.zcash_proofs] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" default-features = false [dependencies.ff] git = "https://github.com/CalDescent1/librustzcash.git" -rev = "3bc31b9cceaef79865f3a4c85e31f76b9755e15c" +rev = "5a4fd01f351259694c31ca161842dfd7078ef2f1" features = ["ff_derive"] [build-dependencies] diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 899fb3c..03bc2f0 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -668,6 +668,163 @@ impl Command for SendP2shCommand { } } +struct RedeemP2shCommand {} +impl Command for RedeemP2shCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Redeem ARRR from an HTLC"); + h.push("Usage:"); + h.push("send '{'input':
, 'output': [{'address':
, 'amount': , 'memo': , 'script': , 'txid': , 'secret': , 'privkey': }, ...]}"); + 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 '{\"input\":\"ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d\", \"output\": [{ \"address\": \"ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d\", \"amount\": 200000, \"memo\": \"Hello from the command line\", \"script\": \"acbdef\", \"secret\": \"acbdef\", \"privkey\": \"acbdef\"}]}'"); + h.push(""); + h.join("\n") + } + + fn short_help(&self) -> String { + "Redeem ARRR from a P2SH address, using redeem script, secret, and private key".to_string() + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + // 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 { + return self.help(); + } + + use std::convert::TryInto; + use zcash_primitives::transaction::components::amount::DEFAULT_FEE; + + // Check for a single argument that can be parsed as JSON + 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()); + } + }; + + //Check for a fee key and convert to u64 + let fee: u64 = if json_args.has_key("fee") { + match json_args["fee"].as_u64() { + Some(f) => f.clone(), + None => DEFAULT_FEE.try_into().unwrap() + } + } else { + DEFAULT_FEE.try_into().unwrap() + }; + + //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 for output script and convert to a string + let script58 = if json_args.has_key("script") { + json_args["script"].as_str().unwrap().to_string().clone() + } else { + return format!("Error: {}\n{}", "Need script", self.help()); + }; + + // Decode base58 encoded string + let script_vec = script58.from_base58().unwrap(); + let script_bytes = &script_vec[..]; + + + //Check for funding txid and convert to a string + let txid58 = if json_args.has_key("txid") { + json_args["txid"].as_str().unwrap().to_string().clone() + } else { + return format!("Error: {}\n{}", "Need funding txid", self.help()); + }; + + // Decode base58 encoded string + let txid_vec = txid58.from_base58().unwrap(); + let txid_bytes = &txid_vec[..]; + + + //Check for secret and convert to a string + let secret58 = if json_args.has_key("secret") { + json_args["secret"].as_str().unwrap().to_string().clone() + } else { + return format!("Error: {}\n{}", "Need secret", self.help()); + }; + + // Decode base58 encoded string + let secret_vec = secret58.from_base58().unwrap(); + let secret_bytes = &secret_vec[..]; + + + //Check for privkey and convert to a string + let privkey58 = if json_args.has_key("privkey") { + json_args["privkey"].as_str().unwrap().to_string().clone() + } else { + return format!("Error: {}\n{}", "Need privkey", self.help()); + }; + + // Decode base58 encoded string + let privkey_vec = privkey58.from_base58().unwrap(); + let privkey_bytes = &privkey_vec[..]; + + + //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_redeem_p2sh(from, tos, &fee, script_bytes, txid_bytes, secret_bytes, privkey_bytes) { + Ok(txid) => { object!{ "txid" => txid } }, + Err(e) => { object!{ "error" => e } } + }.pretty(2) + }, + Err(e) => e + } + } +} + struct SaveCommand {} impl Command for SaveCommand { fn help(&self) -> String { @@ -980,6 +1137,7 @@ pub fn get_commands() -> Box>> { map.insert("info".to_string(), Box::new(InfoCommand{})); map.insert("send".to_string(), Box::new(SendCommand{})); map.insert("sendp2sh".to_string(), Box::new(SendP2shCommand{})); + map.insert("redeemp2sh".to_string(), Box::new(RedeemP2shCommand{})); map.insert("save".to_string(), Box::new(SaveCommand{})); map.insert("quit".to_string(), Box::new(QuitCommand{})); map.insert("list".to_string(), Box::new(TransactionsCommand{})); diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index b445a62..fe52820 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -1746,6 +1746,30 @@ impl LightClient { result.map(|(txid, _)| txid) } + + pub fn do_redeem_p2sh(&self, from: &str, addrs: Vec<(&str, u64, Option)>, fee: &u64, script: &[u8], txid: &[u8], secret: &[u8], privkey: &[u8]) -> Result { + if !self.wallet.read().unwrap().is_unlocked_for_spending() { + error!("Wallet is locked"); + return Err("Wallet is locked".to_string()); + } + + info!("Creating transaction to redeem funds from P2SH"); + + let result = { + let _lock = self.sync_lock.lock().unwrap(); + + self.wallet.write().unwrap().redeem_p2sh( + u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), + &self.sapling_spend, &self.sapling_output, + from, addrs, script, txid, secret, privkey, fee, + |txbytes| broadcast_raw_tx(&self.get_server_uri(), txbytes) + ) + }; + + info!("Transaction Complete"); + + result.map(|(txid, _)| txid) + } } #[cfg(test)] diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 64e8ff2..8462a1b 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -2,6 +2,7 @@ use std::time::{SystemTime, Duration}; use std::io::{self, Read, Write}; use std::cmp; use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; use std::sync::{Arc, RwLock}; use std::io::{Error, ErrorKind}; @@ -1565,7 +1566,7 @@ impl LightWallet { nd.spent = None; } - if nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) { + if nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) { // TODO: fix bug when nd.spent already set to None nd.unconfirmed_spent = None; } }) @@ -2773,6 +2774,340 @@ impl LightWallet { Ok((txid, raw_tx)) } + pub fn redeem_p2sh ( + &self, + consensus_branch_id: u32, + spend_params: &[u8], + output_params: &[u8], + from: &str, + tos: Vec<(&str, u64, Option)>, + redeem_script_pubkey: &[u8], + outpoint_txid: &[u8], + secret: &[u8], + privkey: &[u8], + fee: &u64, + broadcast_fn: F + ) -> Result<(String, Vec), String> + where F: Fn(Box<[u8]>) -> Result + { + if !self.unlocked { + return Err("Cannot spend while wallet is locked".to_string()); + } + + let start_time = now(); + if tos.len() == 0 { + return Err("Need at least one destination address".to_string()); + } + + let total_value = tos.iter().map(|to| to.1).sum::(); + println!( + "0: Creating transaction sending {} zatoshis to {} addresses", + total_value, tos.len() + ); + + // Convert address (str) to RecepientAddress and value to Amount + let recepients = tos.iter().map(|to| { + let ra = match address::RecipientAddress::from_str(to.0, + self.config.hrp_sapling_address(), + self.config.base58_pubkey_address(), + self.config.base58_script_address()) { + Some(to) => to, + None => { + let e = format!("Invalid recipient address: '{}'", to.0); + error!("{}", e); + return Err(e); + } + }; + + let value = Amount::from_u64(to.1).unwrap(); + + Ok((ra, value, to.2.clone())) + }).collect::)>, String>>()?; + + // Target the next block, assuming we are up-to-date. + let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { + Some(res) => res, + None => { + let e = format!("Cannot send funds before scanning any blocks"); + error!("{}", e); + return Err(e); + } + }; + + // Select notes to cover the target value + println!("{}: Selecting notes", now() - start_time); + let target_value = Amount::from_u64(total_value).unwrap() + Amount::from_u64(*fee).unwrap(); + + // Select the candidate notes that are eligible to be spent + let mut candidate_notes: Vec<_> = self.txs.read().unwrap().iter() + .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) + .flatten() + .filter_map(|(txid, note)| { + // Filter out notes that are already spent + if note.spent.is_some() || note.unconfirmed_spent.is_some() { + None + } else { + // Get the spending key for the selected fvk, if we have it + let extsk = self.zkeys.read().unwrap().iter() + .find(|zk| zk.extfvk == note.extfvk) + .and_then(|zk| zk.extsk.clone()); + //filter only on Notes with a matching from address + if from == LightWallet::note_address(self.config.hrp_sapling_address(), note).unwrap() { + SpendableNote::from(txid, note, anchor_offset, &extsk) + } else { + None + } + } + }).collect(); + + // Sort by highest value-notes first. + candidate_notes.sort_by(|a, b| b.note.value.cmp(&a.note.value)); + + // Select the minimum number of notes required to satisfy the target value + let notes: Vec<_> = candidate_notes.iter() + .scan(0, |running_total, spendable| { + let value = spendable.note.value; + let ret = if *running_total < u64::from(target_value) { + Some(spendable) + } else { + None + }; + *running_total = *running_total + value; + ret + }) + .collect(); + + let mut builder = Builder::new(height); + + //set fre + builder.set_fee(Amount::from_u64(*fee).unwrap()); + + + println!("{}: Adding P2SH transaction as transparent input", now() - start_time); + + // Add P2SH transaction as transparent input, including secret and redeem script + + let outpoint: OutPoint = OutPoint { + hash: <[u8; 32]>::try_from(outpoint_txid).unwrap(), + n: 0 + }; + + let coin = TxOut { + value: Amount::from_u64(total_value).unwrap(), + script_pubkey: Script { 0: redeem_script_pubkey.to_vec() }, + }; + + let sk = SecretKey::from_slice(privkey).unwrap(); + + if let Err(e) = builder.add_transparent_input_with_secret(sk, outpoint.clone(), coin.clone(), secret.to_vec(), redeem_script_pubkey.to_vec() + ) { + let e = format!("Error adding transparent input: {:?}", e); + error!("{}", e); + return Err(e); + } + + + // "Sufficient value check" disabled since the uxto input details are supplied manually (and are not in this wallet). + // It is down to the caller to verify the amount; we would generally expect to spend the full value held in the P2SH. + + // // Confirm we were able to select sufficient value + // let selected_value = notes.iter().map(|selected| selected.note.value).sum::() + // + tinputs.iter().map::(|utxo| utxo.value.into()).sum::(); + + // if selected_value < u64::from(target_value) { + // let e = format!( + // "Insufficient verified funds (have {}, need {:?}). NOTE: funds need {} confirmations before they can be spent.", + // selected_value, target_value, self.config.anchor_offset + 1 + // ); + // error!("{}", e); + // return Err(e); + // } + + // Create the transaction + println!("{}: Adding {} notes and {} utxos", now() - start_time, notes.len(), 1); + + for selected in notes.iter() { + if let Err(e) = builder.add_sapling_spend( + selected.extsk.clone(), + selected.diversifier, + selected.note.clone(), + selected.witness.path().unwrap(), + ) { + let e = format!("Error adding note: {:?}", e); + error!("{}", e); + return Err(e); + } + } + + + // 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.zkeys.read().unwrap()[0].extfvk.fvk.ovk + } else { + ExtendedFullViewingKey::from(¬es[0].extsk).fvk.ovk + }; + + + // Change disabled, since this function is only designed to be used when redeeming the full amount held in the P2SH. + // This will need some attention if used for anything more complex than the originally intended use case. + + // // If no Sapling notes were added, add the change address manually. That is, + // // 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); + // } + // } + + + + + for (to, value, memo) in recepients { + // Compute memo if it exists + let encoded_memo = match memo { + None => None, + Some(s) => { + // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then + // interpret it as a hex + match utils::interpret_memo_string(&s) { + Ok(m) => Some(m), + Err(e) => { + error!("{}", e); + return Err(e); + } + } + } + }; + + println!("{}: Adding outputs", now() - start_time); + + if let Err(e) = match to { + address::RecipientAddress::Shielded(to) => { + builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) + } + address::RecipientAddress::Transparent(to) => { + builder.add_transparent_output(&to, value) + } + } { + let e = format!("Error adding output: {:?}", e); + error!("{}", e); + return Err(e); + } + } + + + println!("{}: Building transaction", now() - start_time); + let (tx, _) = match builder.build( + consensus_branch_id, + &prover::InMemTxProver::new(spend_params, output_params), + ) { + Ok(res) => res, + Err(e) => { + let e = format!("Error creating transaction: {:?}", e); + error!("{}", e); + return Err(e); + } + }; + println!("{}: Transaction created", now() - start_time); + println!("Transaction ID: {}", tx.txid()); + + // Create the TX bytes + let mut raw_tx = vec![]; + tx.write(&mut raw_tx).unwrap(); + + let txid = broadcast_fn(raw_tx.clone().into_boxed_slice())?; + + // Mark notes as spent. + { + // Mark sapling notes as unconfirmed spent + let mut txs = self.txs.write().unwrap(); + for selected in notes { + let mut spent_note = txs.get_mut(&selected.txid).unwrap() + .notes.iter_mut() + .find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) + .unwrap(); + spent_note.unconfirmed_spent = Some(tx.txid()); + } + + + // Disabled below code since the uxto didn't derive from this wallet + + // // Mark this utxo as unconfirmed spent + // for utxo in tinputs { + // let mut spent_utxo = txs.get_mut(&utxo.txid).unwrap().utxos.iter_mut() + // .find(|u| utxo.txid == u.txid && utxo.output_index == u.output_index) + // .unwrap(); + // spent_utxo.unconfirmed_spent = Some(tx.txid()); + // } + } + + // Add this Tx to the mempool structure + { + let mut mempool_txs = self.mempool_txs.write().unwrap(); + + match mempool_txs.get_mut(&tx.txid()) { + None => { + // Collect the outgoing metadata + let outgoing_metadata = tos.iter().map(|(addr, amt, maybe_memo)| { + OutgoingTxMetadata { + address: addr.to_string(), + value: *amt, + memo: match maybe_memo { + None => Memo::default(), + Some(s) => { + // If the address is not a z-address, then drop the memo + if !LightWallet::is_shielded_address(&addr.to_string(), &self.config) { + Memo::default() + } else { + match utils::interpret_memo_string(s) { + Ok(m) => m, + Err(e) => { + error!("{}", e); + Memo::default() + } + } + } + } + }, + } + }).collect::>(); + + // Create a new WalletTx + let mut wtx = WalletTx::new(height as i32, now() as u64, &tx.txid()); + wtx.outgoing_metadata = outgoing_metadata; + wtx.total_shielded_value_spent = total_value + fee; + + // Add it into the mempool + mempool_txs.insert(tx.txid(), wtx); + }, + Some(_) => { + warn!("A newly created Tx was already in the mempool! How's that possible? Txid: {}", tx.txid()); + } + } + } + + Ok((txid, raw_tx)) + } + // After some blocks have been mined, we need to remove the Txns from the mempool_tx structure // if they : // 1. Have expired