diff --git a/src/lightclient.rs b/src/lightclient.rs index a5e76a7..23197f3 100644 --- a/src/lightclient.rs +++ b/src/lightclient.rs @@ -441,14 +441,23 @@ impl LightClient { // TODO: What happens if change is > than sent ? + // Collect outgoing metadata + let outgoing_json = v.outgoing_metadata.iter() + .map(|om| + object!{ + "address" => om.address.clone(), + "value" => om.value, + "memo" => LightWallet::memo_str(&Some(om.memo.clone())), + }) + .collect::>(); + txns.push(object! { "block_height" => v.block, "txid" => format!("{}", v.txid), "amount" => total_change as i64 - v.total_shielded_value_spent as i64 - v.total_transparent_value_spent as i64, - "address" => None::, // TODO: For send, we don't have an address - "memo" => None:: + "outgoing_metadata" => outgoing_json, }); } @@ -461,15 +470,7 @@ impl LightClient { "txid" => format!("{}", v.txid), "amount" => nd.note.value as i64, "address" => self.wallet.note_address(nd), - "memo" => match &nd.memo { - Some(memo) => { - match memo.to_utf8() { - Some(Ok(memo_str)) => Some(memo_str), - _ => None - } - } - _ => None - } + "memo" => LightWallet::memo_str(&nd.memo), }) ); diff --git a/src/lightwallet/data.rs b/src/lightwallet/data.rs index 877345e..81da0d7 100644 --- a/src/lightwallet/data.rs +++ b/src/lightwallet/data.rs @@ -324,6 +324,42 @@ impl Utxo { } } +pub struct OutgoingTxMetadata { + pub address: String, + pub value : u64, + pub memo : Memo, +} + +impl OutgoingTxMetadata { + pub fn read(mut reader: R) -> io::Result { + let address_len = reader.read_u64::()?; + let mut address_bytes = vec![0; address_len as usize]; + reader.read_exact(&mut address_bytes)?; + let address = String::from_utf8(address_bytes).unwrap(); + + let value = reader.read_u64::()?; + + let mut memo_bytes = [0u8; 512]; + reader.read_exact(&mut memo_bytes)?; + let memo = Memo::from_bytes(&memo_bytes).unwrap(); + + Ok(OutgoingTxMetadata{ + address, + value, + memo, + }) + } + + pub fn write(&self, mut writer: W) -> io::Result<()> { + // Strings are written as len + utf8 + writer.write_u64::(self.address.as_bytes().len() as u64)?; + writer.write_all(self.address.as_bytes())?; + + writer.write_u64::(self.value)?; + writer.write_all(self.memo.as_bytes()) + } +} + pub struct WalletTx { pub block: i32, @@ -345,11 +381,14 @@ pub struct WalletTx { // Total amount of transparent funds that belong to us that were spent in this Tx. pub total_transparent_value_spent : u64, + + // All outgoing sapling sends to addresses outside this wallet + pub outgoing_metadata: Vec, } impl WalletTx { pub fn serialized_version() -> u64 { - return 1; + return 2; } pub fn new(height: i32, txid: &TxId) -> Self { @@ -359,13 +398,14 @@ impl WalletTx { notes: vec![], utxos: vec![], total_shielded_value_spent: 0, - total_transparent_value_spent: 0 + total_transparent_value_spent: 0, + outgoing_metadata: vec![], } } pub fn read(mut reader: R) -> io::Result { let version = reader.read_u64::()?; - assert_eq!(version, WalletTx::serialized_version()); + assert!(version <= WalletTx::serialized_version()); let block = reader.read_i32::()?; @@ -380,13 +420,21 @@ impl WalletTx { let total_shielded_value_spent = reader.read_u64::()?; let total_transparent_value_spent = reader.read_u64::()?; + + // Outgoing metadata was only added in version 2 + let outgoing_metadata = match version { + 1 => vec![], + _ => Vector::read(&mut reader, |r| OutgoingTxMetadata::read(r))? + }; + Ok(WalletTx{ block, txid, notes, utxos, total_shielded_value_spent, - total_transparent_value_spent + total_transparent_value_spent, + outgoing_metadata, }) } @@ -403,6 +451,9 @@ impl WalletTx { writer.write_u64::(self.total_shielded_value_spent)?; writer.write_u64::(self.total_transparent_value_spent)?; + // Write the outgoing metadata + Vector::write(&mut writer, &self.outgoing_metadata, |w, om| om.write(w))?; + Ok(()) } } diff --git a/src/lightwallet/mod.rs b/src/lightwallet/mod.rs index 8c55dcd..9948acc 100644 --- a/src/lightwallet/mod.rs +++ b/src/lightwallet/mod.rs @@ -34,7 +34,7 @@ use zcash_primitives::{ primitives::{PaymentAddress}, }; -use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote}; +use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata}; use crate::address; use crate::prover; @@ -354,6 +354,18 @@ impl LightWallet { } } + pub fn memo_str(memo: &Option) -> Option { + match memo { + Some(memo) => { + match memo.to_utf8() { + Some(Ok(memo_str)) => Some(memo_str), + _ => None + } + } + _ => 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); @@ -593,6 +605,14 @@ impl LightWallet { // 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 + + // First, collect all our z addresses, to check for change + // Collect z addresses + let z_addresses = self.address.iter().map( |ad| { + encode_payment_address(self.config.hrp_sapling_address(), &ad) + }).collect::>(); + + // Search all ovks that we have 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, @@ -601,12 +621,35 @@ impl LightWallet { &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()) + Some((note, payment_address, memo)) => { + let address = encode_payment_address(self.config.hrp_sapling_address(), + &payment_address); + + // Check if this is a change address + if z_addresses.contains(&address) { + continue; + } + + { + // Update the WalletTx + // Do it in a short scope because of the write lock. + info!("A sapling output was sent in {}", tx.txid()); + + let mut txs = self.txs.write().unwrap(); + if txs.get(&tx.txid()).unwrap().outgoing_metadata.iter() + .find(|om| om.address == address && om.value == note.value) + .is_some() { + warn!("Duplicate outgoing metadata"); + continue; + } + + // Write the outgoing metadata + txs.get_mut(&tx.txid()).unwrap() + .outgoing_metadata + .push(OutgoingTxMetadata{ + address, value: note.value, memo, + }); + } }, None => {} };