use std::time::SystemTime; use std::io::{self, Read, Write}; use std::cmp; use std::collections::HashMap; use std::sync::{Arc, RwLock}; use log::{info, error}; use protobuf::parse_from_bytes; use bip39::{Mnemonic, Language}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use pairing::bls12_381::{Bls12}; use zcash_client_backend::{ encoding::encode_payment_address, proto::compact_formats::CompactBlock, welding_rig::scan_block, }; use zcash_primitives::{ block::BlockHash, merkle_tree::{CommitmentTree}, serialize::{Vector}, transaction::{ builder::{Builder}, components::{Amount, OutPoint, TxOut}, components::amount::DEFAULT_FEE, TxId, Transaction, }, legacy::{Script, TransparentAddress}, note_encryption::{Memo, try_sapling_note_decryption, try_sapling_output_recovery}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey, ChildIndex}, JUBJUB, primitives::{PaymentAddress}, }; use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote}; use crate::address; use crate::prover; use crate::LightClientConfig; use sha2::{Sha256, Digest}; pub mod data; pub mod extended_key; const ANCHOR_OFFSET: u32 = 1; fn now() -> f64 { SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64 } /// Sha256(Sha256(value)) pub fn double_sha256(payload: &[u8]) -> Vec { let h1 = Sha256::digest(&payload); let h2 = Sha256::digest(&h1); h2.to_vec() } use base58::{ToBase58, FromBase58}; /// A trait for converting a [u8] to base58 encoded string. pub trait ToBase58Check { /// Converts a value of `self` to a base58 value, returning the owned string. /// The version is a coin-specific prefix that is added. /// The suffix is any bytes that we want to add at the end (like the "iscompressed" flag for /// Secret key encoding) fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String; } impl ToBase58Check for [u8] { fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String { let mut payload: Vec = Vec::new(); payload.extend_from_slice(version); payload.extend_from_slice(self); payload.extend_from_slice(suffix); let mut checksum = double_sha256(&payload); payload.append(&mut checksum[..4].to_vec()); payload.to_base58() } } pub trait FromBase58Check { fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec; } impl FromBase58Check for str { fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec { let mut payload: Vec = Vec::new(); let bytes = self.from_base58().unwrap(); let start = version.len(); let end = bytes.len() - (4 + suffix.len()); payload.extend(&bytes[start..end]); payload } } pub struct LightWallet { seed: [u8; 32], // Seed phrase for this wallet. // List of keys, actually in this wallet. This may include more // than keys derived from the seed, for example, if user imports // a private key extsks: Vec, extfvks: Vec, pub address: Vec>, // Transparent keys. TODO: Make it not pubic pub tkeys: Vec, blocks: Arc>>, pub txs: Arc>>, // Non-serialized fields config: LightClientConfig, } impl LightWallet { pub fn serialized_version() -> u64 { return 1; } fn get_pk_from_seed(seed: &[u8; 32]) -> (ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress) { let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(seed), &[ ChildIndex::Hardened(32), ChildIndex::Hardened(1), // TODO: Cointype should be 133 for mainnet ChildIndex::Hardened(0) ], ); let extfvk = ExtendedFullViewingKey::from(&extsk); let address = extfvk.default_address().unwrap().1; (extsk, extfvk, address) } pub fn new(seed_phrase: Option, config: &LightClientConfig) -> io::Result { use rand::{FromEntropy, ChaChaRng, Rng}; let mut seed_bytes = [0u8; 32]; if seed_phrase.is_none() { // Create a random seed. let mut system_rng = ChaChaRng::from_entropy(); system_rng.fill(&mut seed_bytes); } else { seed_bytes.copy_from_slice(&Mnemonic::from_phrase(seed_phrase.expect("should have a seed phrase"), Language::English).unwrap().entropy()); } // TODO: HD-derive the address instead straight from the seed. // TODO: This only reads one key for now let tpk = secp256k1::SecretKey::from_slice(&seed_bytes).unwrap(); // Derive only the first address // TODO: We need to monitor addresses, and always keep 1 "free" address, so // users can import a seed phrase and automatically get all used addresses let (extsk, extfvk, address) = LightWallet::get_pk_from_seed(&seed_bytes); Ok(LightWallet { seed: seed_bytes, extsks: vec![extsk], extfvks: vec![extfvk], address: vec![address], tkeys: vec![tpk], blocks: Arc::new(RwLock::new(vec![])), txs: Arc::new(RwLock::new(HashMap::new())), config: config.clone(), }) } pub fn read(mut reader: R, config: &LightClientConfig) -> io::Result { let version = reader.read_u64::()?; assert!(version <= LightWallet::serialized_version()); info!("Reading wallet version {}", version); // Seed let mut seed_bytes = [0u8; 32]; reader.read_exact(&mut seed_bytes)?; // Read the spending keys let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?; // Calculate the viewing keys let extfvks = extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk)) .collect::>(); // Calculate the addresses let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 ) .collect::>>(); let mut tpk_bytes = [0u8; 32]; reader.read_exact(&mut tpk_bytes)?; let tpk = secp256k1::SecretKey::from_slice(&tpk_bytes).unwrap(); let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?; let txs_tuples = Vector::read(&mut reader, |r| { let mut txid_bytes = [0u8; 32]; r.read_exact(&mut txid_bytes)?; Ok((TxId{0: txid_bytes}, WalletTx::read(r).unwrap())) })?; let txs = txs_tuples.into_iter().collect::>(); Ok(LightWallet{ seed: seed_bytes, extsks: extsks, extfvks: extfvks, address: addresses, tkeys: vec![tpk], blocks: Arc::new(RwLock::new(blocks)), txs: Arc::new(RwLock::new(txs)), config: config.clone(), }) } pub fn write(&self, mut writer: W) -> io::Result<()> { // Write the version writer.write_u64::(LightWallet::serialized_version())?; // Write the seed writer.write_all(&self.seed)?; // Write all the spending keys Vector::write(&mut writer, &self.extsks, |w, sk| sk.write(w) )?; // Write the transparent private key // TODO: This only writes the first key for now writer.write_all(&self.tkeys[0][..])?; Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?; // The hashmap, write as a set of tuples Vector::write(&mut writer, &self.txs.read().unwrap().iter().collect::>(), |w, (k, v)| { w.write_all(&k.0)?; v.write(w) })?; Ok(()) } pub fn note_address(&self, note: &SaplingNoteData) -> Option { match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) { Some(pa) => Some(encode_payment_address(self.config.hrp_sapling_address(), &pa)), None => None } } // Clears all the downloaded blocks and resets the state back to the inital block. // After this, the wallet's initial state will need to be set // and the wallet will need to be rescanned pub fn clear_blocks(&self) { self.blocks.write().unwrap().clear(); self.txs.write().unwrap().clear(); } pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { let mut blocks = self.blocks.write().unwrap(); if !blocks.is_empty() { return false; } let hash = match hex::decode(hash) { Ok(hash) => { let mut r = hash; r.reverse(); BlockHash::from_slice(&r) }, Err(e) => { eprintln!("{}", e); return false; } }; let sapling_tree = match hex::decode(sapling_tree) { Ok(tree) => tree, Err(e) => { eprintln!("{}", e); return false; } }; if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { blocks.push(BlockData { height, hash, tree }); true } else { false } } // Get the latest sapling commitment tree. It will return the height and the hex-encoded sapling commitment tree at that height pub fn get_sapling_tree(&self) -> Result<(i32, String, String), String> { let blocks = self.blocks.read().unwrap(); let block = match blocks.last() { Some(block) => block, None => return Err("Couldn't get a block height!".to_string()) }; let mut write_buf = vec![]; block.tree.write(&mut write_buf).map_err(|e| format!("Error writing commitment tree {}", e))?; let mut blockhash = vec![]; blockhash.extend_from_slice(&block.hash.0); blockhash.reverse(); Ok((block.height, hex::encode(blockhash), hex::encode(write_buf))) } pub fn last_scanned_height(&self) -> i32 { self.blocks.read().unwrap() .last() .map(|block| block.height) .unwrap_or(self.config.sapling_activation_height as i32 - 1) } /// Determines the target height for a transaction, and the offset from which to /// select anchors, based on the current synchronised block chain. fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { match { let blocks = self.blocks.read().unwrap(); ( blocks.first().map(|block| block.height as u32), blocks.last().map(|block| block.height as u32), ) } { (Some(min_height), Some(max_height)) => { let target_height = max_height + 1; // Select an anchor ANCHOR_OFFSET back from the target block, // unless that would be before the earliest block we have. let anchor_height = cmp::max(target_height.saturating_sub(ANCHOR_OFFSET), min_height); Some((target_height, (target_height - anchor_height) as usize)) } _ => None, } } pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String { let secp = secp256k1::Secp256k1::new(); let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); // Encode into t address let mut hash160 = ripemd160::Ripemd160::new(); hash160.input(Sha256::digest(&pk.serialize()[..].to_vec())); // TODO: The taddr version prefix needs to be different for testnet and mainnet hash160.result().to_base58check(&self.config.base58_pubkey_address(), &[]) } pub fn address_from_pubkeyhash(&self, ta: Option) -> Option { match ta { Some(TransparentAddress::PublicKey(hash)) => { Some(hash.to_base58check(&self.config.base58_pubkey_address(), &[])) }, _ => None } } pub fn get_seed_phrase(&self) -> String { Mnemonic::from_entropy(&self.seed, Language::English, ).unwrap().phrase().to_string() } pub fn zbalance(&self, addr: Option) -> u64 { self.txs.read().unwrap() .values() .map(|tx| { tx.notes.iter() .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { Some(a) => a == encode_payment_address( self.config.hrp_sapling_address(), &nd.extfvk.fvk.vk .into_payment_address(nd.diversifier, &JUBJUB).unwrap() ), None => true } }) .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) .sum::() }) .sum::() } // Get all (unspent) utxos. Unconfirmed spent utxos are included pub fn get_utxos(&self) -> Vec { let txs = self.txs.read().unwrap(); txs.values() .flat_map(|tx| { tx.utxos.iter().filter(|utxo| utxo.spent.is_none()) }) .map(|utxo| utxo.clone()) .collect::>() } pub fn tbalance(&self, addr: Option) -> u64 { self.get_utxos().iter() .filter(|utxo| { match addr.clone() { Some(a) => utxo.address == a, None => true, } }) .map(|utxo| utxo.value ) .sum::() } pub fn verified_zbalance(&self, addr: Option) -> u64 { let anchor_height = match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32, None => return 0, }; self.txs .read() .unwrap() .values() .map(|tx| { if tx.block as u32 <= anchor_height { tx.notes .iter() .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { Some(a) => a == encode_payment_address( self.config.hrp_sapling_address(), &nd.extfvk.fvk.vk .into_payment_address(nd.diversifier, &JUBJUB).unwrap() ), None => true } }) .map(|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.note.value } else { 0 }) .sum::() } else { 0 } }) .sum::() } fn add_toutput_to_wtx(&self, height: i32, txid: &TxId, vout: &TxOut, n: u64) { let mut txs = self.txs.write().unwrap(); // Find the existing transaction entry, or create a new one. if !txs.contains_key(&txid) { let tx_entry = WalletTx::new(height, &txid); txs.insert(txid.clone(), tx_entry); } let tx_entry = txs.get_mut(&txid).unwrap(); // Make sure the vout isn't already there. match tx_entry.utxos.iter().find(|utxo| { utxo.txid == *txid && utxo.output_index == n && Amount::from_u64(utxo.value).unwrap() == vout.value }) { Some(utxo) => { info!("Already have {}:{}", utxo.txid, utxo.output_index); } None => { let address = self.address_from_pubkeyhash(vout.script_pubkey.address()); if address.is_none() { println!("Couldn't determine address for output!"); } info!("Added to wallet {}:{}", txid, n); // Add the utxo tx_entry.utxos.push(Utxo{ address: address.unwrap(), txid: txid.clone(), output_index: n, script: vout.script_pubkey.0.clone(), value: vout.value.into(), height, spent: None, unconfirmed_spent: None, }); } } } // Scan the full Tx and update memos for incoming shielded transactions pub fn scan_full_tx(&self, tx: &Transaction, height: i32) { // Scan all the inputs to see if we spent any transparent funds in this tx // TODO: Save this object let secp = secp256k1::Secp256k1::new(); // TODO: Iterate over all transparent addresses. This is currently looking only at // the first one. let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys[0]).serialize(); let mut total_transparent_spend: u64 = 0; for vin in tx.vin.iter() { // Find the txid in the list of utxos that we have. let txid = TxId {0: vin.prevout.hash}; match self.txs.write().unwrap().get_mut(&txid) { Some(wtx) => { //println!("Looking for {}, {}", txid, vin.prevout.n); // One of the tx outputs is a match let spent_utxo = wtx.utxos.iter_mut() .find(|u| u.txid == txid && u.output_index == (vin.prevout.n as u64)); match spent_utxo { Some(su) => { info!("Spent utxo from {} was spent in {}", txid, tx.txid()); su.spent = Some(txid.clone()); su.unconfirmed_spent = None; total_transparent_spend += su.value; }, _ => {} } }, _ => {} }; } if total_transparent_spend > 0 { // Update the WalletTx. Do it in a short scope because of the write lock. let mut txs = self.txs.write().unwrap(); if !txs.contains_key(&tx.txid()) { let tx_entry = WalletTx::new(height, &tx.txid()); txs.insert(tx.txid().clone(), tx_entry); } txs.get_mut(&tx.txid()).unwrap() .total_transparent_value_spent = total_transparent_spend; } // Scan for t outputs for (n, vout) in tx.vout.iter().enumerate() { match vout.script_pubkey.address() { Some(TransparentAddress::PublicKey(hash)) => { if hash[..] == ripemd160::Ripemd160::digest(&Sha256::digest(&pubkey))[..] { // This is out address. Add this as an output to the txid self.add_toutput_to_wtx(height, &tx.txid(), &vout, n as u64); } }, _ => {} } } // Scan shielded sapling outputs to see if anyone of them is us, and if it is, extract the memo for output in tx.shielded_outputs.iter() { let ivks: Vec<_> = self.extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); let cmu = output.cmu; let ct = output.enc_ciphertext; // Search all of our keys for (_account, ivk) in ivks.iter().enumerate() { let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(); let (note, _to, memo) = match try_sapling_note_decryption(ivk, &epk_prime, &cmu, &ct) { Some(ret) => ret, None => continue, }; { info!("A sapling note was spent in {}", tx.txid()); // Update the WalletTx // Do it in a short scope because of the write lock. let mut txs = self.txs.write().unwrap(); txs.get_mut(&tx.txid()).unwrap() .notes.iter_mut() .find(|nd| nd.note == note).unwrap() .memo = Some(memo); } } // Also scan the output to see if it can be decoded with our OutgoingViewKey // If it can, then we sent this transaction, so we should be able to get // the memo and value for our records let ovks: Vec<_> = self.extfvks.iter().map(|extfvk| extfvk.fvk.ovk).collect(); for (_account, ovk) in ovks.iter().enumerate() { match try_sapling_output_recovery(ovk, &output.cv, &output.cmu, &output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(), &output.enc_ciphertext, &output.out_ciphertext) { Some((note, address, memo)) => { // This could be a chane or an outgoing transaction println!("Recovered outgoing for {} to {} :{:?}", note.value, encode_payment_address(self.config.hrp_sapling_address(), &address), memo.to_utf8()) }, None => {} }; } } } pub fn scan_block(&self, block: &[u8]) -> bool { let block: CompactBlock = match parse_from_bytes(block) { Ok(block) => block, Err(e) => { eprintln!("Could not parse CompactBlock from bytes: {}", e); return false; } }; // Scanned blocks MUST be height-sequential. let height = block.get_height() as i32; if height == self.last_scanned_height() { // If the last scanned block is rescanned, check it still matches. if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { if block.hash() != hash { eprintln!("Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); return false; } } return true; } else if height != (self.last_scanned_height() + 1) { eprintln!( "Block is not height-sequential (expected {}, found {})", self.last_scanned_height() + 1, height ); return false; } // Get the most recent scanned data. let mut block_data = BlockData { height, hash: block.hash(), tree: self .blocks .read() .unwrap() .last() .map(|block| block.tree.clone()) .unwrap_or(CommitmentTree::new()), }; let mut txs = self.txs.write().unwrap(); // Create a Vec containing all unspent nullifiers. // Include only the confirmed spent nullifiers, since unconfirmed ones still need to be included // during scan_block below. let nfs: Vec<_> = txs .iter() .map(|(txid, tx)| { let txid = *txid; tx.notes.iter().filter_map(move |nd| { if nd.spent.is_none() { Some((nd.nullifier, nd.account, txid)) } else { None } }) }) .flatten() .collect(); // Prepare the note witnesses for updating for tx in txs.values_mut() { for nd in tx.notes.iter_mut() { // Duplicate the most recent witness if let Some(witness) = nd.witnesses.last() { let clone = witness.clone(); nd.witnesses.push(clone); } // Trim the oldest witnesses nd.witnesses = nd .witnesses .split_off(nd.witnesses.len().saturating_sub(100)); } } let new_txs = { let nf_refs: Vec<_> = nfs.iter().map(|(nf, acc, _)| (&nf[..], *acc)).collect(); // Create a single mutable slice of all the newly-added witnesses. let mut witness_refs: Vec<_> = txs .values_mut() .map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut())) .flatten() .collect(); scan_block( block, &self.extfvks, &nf_refs[..], &mut block_data.tree, &mut witness_refs[..], ) }; for tx in new_txs { // Mark notes as spent. let mut total_shielded_value_spent: u64 = 0; info!("Txid {} belongs to wallet", tx.txid); for spend in &tx.shielded_spends { // TODO: Add up the spent value here and add it to the WalletTx as a Spent let txid = nfs .iter() .find(|(nf, _, _)| &nf[..] == &spend.nf[..]) .unwrap() .2; let mut spent_note = txs .get_mut(&txid) .unwrap() .notes .iter_mut() .find(|nd| &nd.nullifier[..] == &spend.nf[..]) .unwrap(); // Mark the note as spent, and remove the unconfirmed part of it info!("Marked a note as spent"); spent_note.spent = Some(tx.txid); spent_note.unconfirmed_spent = None::; total_shielded_value_spent += spent_note.note.value; } // Find the existing transaction entry, or create a new one. if !txs.contains_key(&tx.txid) { let tx_entry = WalletTx::new(block_data.height as i32, &tx.txid); txs.insert(tx.txid, tx_entry); } let tx_entry = txs.get_mut(&tx.txid).unwrap(); tx_entry.total_shielded_value_spent = total_shielded_value_spent; // Save notes. for output in tx .shielded_outputs .into_iter() { info!("Received sapling output"); tx_entry.notes.push(SaplingNoteData::new( &self.extfvks[output.account], output )); } } // Store scanned data for this block. self.blocks.write().unwrap().push(block_data); // Print info about the block every 10,000 blocks if height % 10_000 == 0 { match self.get_sapling_tree() { Ok((h, hash, stree)) => info!("Sapling tree at height {}/{} - {}", h, hash, stree), Err(e) => error!("Couldn't determine sapling tree: {}", e) } } true } pub fn send_to_address( &self, consensus_branch_id: u32, spend_params: &[u8], output_params: &[u8], to: &str, value: u64, memo: Option, ) -> Option> { let start_time = now(); println!( "0: Creating transaction sending {} tazoshis to {}", value, to ); // TODO: This only spends from the first address right now. let extsk = &self.extsks[0]; let extfvk = &self.extfvks[0]; let ovk = extfvk.fvk.ovk; let to = match address::RecipientAddress::from_str(to, self.config.hrp_sapling_address(), self.config.base58_pubkey_address(), self.config.base58_script_address()) { Some(to) => to, None => { eprintln!("Invalid recipient address"); return None; } }; let value = Amount::from_u64(value).unwrap(); // 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 => { eprintln!("Cannot send funds before scanning any blocks"); return None; } }; // Select notes to cover the target value println!("{}: Selecting notes", now() - start_time); let target_value = value + DEFAULT_FEE ; let notes: Vec<_> = self.txs.read().unwrap().iter() .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) .flatten() .filter_map(|(txid, note)| SpendableNote::from(txid, note, anchor_offset)) .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); // A note on t addresses // Funds recieved by t-addresses can't be explicitly spent in ZecWallet. // ZecWallet will lazily consolidate all t address funds into your shielded addresses. // Specifically, if you send an outgoing transaction that is sent to a shielded address, // ZecWallet will add all your t-address funds into that transaction, and send them to your shielded // address as change. let tinputs = self.get_utxos().iter() .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends .map(|utxo| utxo.clone()) .collect::>(); if let Err(e) = match to { address::RecipientAddress::Shielded(_) => { // The destination is a sapling address, so add all transparent inputs // TODO: This only spends from the first address right now. let sk = self.tkeys[0]; // Add all tinputs tinputs.iter() .map(|utxo| { let outpoint: OutPoint = utxo.to_outpoint(); let coin = TxOut { value: Amount::from_u64(utxo.value).unwrap(), script_pubkey: Script { 0: utxo.script.clone() }, }; builder.add_transparent_input(sk, outpoint.clone(), coin.clone()) }) .collect::, _>>() }, _ => Ok(vec![]) } { eprintln!("Error adding transparent inputs: {:?}", e); return None; } // 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) { eprintln!( "Insufficient funds (have {}, need {:?})", selected_value, target_value ); return None; } // Create the transaction println!("{}: Adding {} notes and {} utxos", now() - start_time, notes.len(), tinputs.len()); for selected in notes.iter() { if let Err(e) = builder.add_sapling_spend( extsk.clone(), selected.diversifier, selected.note.clone(), selected.witness.clone(), ) { eprintln!("Error adding note: {:?}", e); return None; } } // Compute memo if it exists let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap() ); println!("{}: Adding output", 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) } } { eprintln!("Error adding output: {:?}", e); return None; } 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) => { eprintln!("Error creating transaction: {:?}", e); return None; } }; println!("{}: Transaction created", now() - start_time); println!("Transaction ID: {}", tx.txid()); // 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()); } // 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()); } } // Return the encoded transaction, so the caller can send it. let mut raw_tx = vec![]; tx.write(&mut raw_tx).unwrap(); Some(raw_tx.into_boxed_slice()) } }