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