diff --git a/js/pivx_shield.ts b/js/pivx_shield.ts index b07a64d..82e3157 100644 --- a/js/pivx_shield.ts +++ b/js/pivx_shield.ts @@ -22,6 +22,7 @@ interface TransactionResult { decrypted_notes: [Note, string][]; commitment_tree: string; nullifiers: string[]; + memos: string[]; } interface Transaction { @@ -31,6 +32,7 @@ interface Transaction { useShieldInputs: boolean; utxos: UTXO[]; transparentChangeAddress: string; + memo: string; } interface CreateTransactionReturnValue { @@ -312,12 +314,15 @@ export class PIVXShield { await this.removeSpentNotes(res.nullifiers); } } - return res.decrypted_notes.filter( - (note) => - !this.unspentNotes.some( - (note2) => JSON.stringify(note2[0]) === JSON.stringify(note[0]), - ), - ); + return { + notes: res.decrypted_notes.filter( + (note) => + !this.unspentNotes.some( + (note2) => JSON.stringify(note2[0]) === JSON.stringify(note[0]), + ), + ), + memos: res.memos, + }; } /** @@ -359,6 +364,7 @@ export class PIVXShield { useShieldInputs = true, utxos, transparentChangeAddress, + memo = "", }: Transaction) { if (!this.extsk) { throw new Error("You cannot create a transaction in view only mode!"); @@ -380,6 +386,7 @@ export class PIVXShield { amount, block_height: blockHeight, is_testnet: this.isTestnet, + memo, }, ); @@ -388,7 +395,7 @@ export class PIVXShield { } this.pendingUnspentNotes.set( txid, - (await this.addTransaction(txhex, true)).map((n) => n[0]), + (await this.addTransaction(txhex, true)).notes.map((n) => n[0]), ); return { hex: txhex, diff --git a/src/transaction.rs b/src/transaction.rs index cdf56f4..f1be348 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -6,7 +6,9 @@ use pivx_primitives::consensus::Network; pub use pivx_primitives::consensus::Parameters; pub use pivx_primitives::consensus::{BlockHeight, MAIN_NETWORK, TEST_NETWORK}; use pivx_primitives::legacy::Script; +use pivx_primitives::memo::Memo; pub use pivx_primitives::memo::MemoBytes; +use pivx_primitives::memo::TextMemo; pub use pivx_primitives::merkle_tree::{CommitmentTree, IncrementalWitness, MerklePath}; pub use pivx_primitives::sapling::PaymentAddress; pub use pivx_primitives::transaction::builder::Progress; @@ -32,6 +34,7 @@ use secp256k1::SecretKey; pub use serde::{Deserialize, Serialize}; use std::convert::TryInto; pub use std::path::Path; +use std::str::FromStr; #[cfg(feature = "multicore")] use std::sync::Mutex; pub use std::{collections::HashMap, error::Error, io::Cursor}; @@ -131,6 +134,7 @@ pub struct JSTxSaplingData { pub decrypted_notes: Vec<(Note, String)>, pub nullifiers: Vec, pub commitment_tree: String, + pub memos: Vec, } //Input a tx and return: the updated commitment merkletree, all the nullifier found in the tx and all the node decoded with the corresponding witness @@ -159,8 +163,9 @@ pub fn handle_transaction( (note, IncrementalWitness::read(wit).unwrap()) }) .collect::>(); - let nullifiers = handle_transaction_internal(&mut tree, tx, &key, true, &mut comp_note) - .map_err(|_| "Cannot decode tx")?; + let (nullifiers, memos) = + handle_transaction_internal(&mut tree, tx, &key, true, &mut comp_note) + .map_err(|_| "Cannot decode tx")?; let mut ser_comp_note: Vec<(Note, String)> = vec![]; let mut ser_nullifiers: Vec = vec![]; for (note, witness) in comp_note.iter() { @@ -183,6 +188,7 @@ pub fn handle_transaction( decrypted_notes: ser_comp_note, nullifiers: ser_nullifiers, commitment_tree: hex::encode(buff), + memos, }; Ok(serde_wasm_bindgen::to_value(&res).map_err(|_| "Cannot serialize tx output")?) } @@ -194,7 +200,7 @@ pub fn handle_transaction_internal( key: &UnifiedFullViewingKey, is_testnet: bool, witnesses: &mut Vec<(Note, IncrementalWitness)>, -) -> Result, Box> { +) -> Result<(Vec, Vec), Box> { let tx = Transaction::read( Cursor::new(hex::decode(tx)?), pivx_primitives::consensus::BranchId::Sapling, @@ -207,13 +213,13 @@ pub fn handle_transaction_internal( decrypt_transaction(&MAIN_NETWORK, BlockHeight::from_u32(320), &tx, &hash) }; let mut nullifiers: Vec = vec![]; + let mut memos = vec![]; if let Some(sapling) = tx.sapling_bundle() { for x in sapling.shielded_spends() { nullifiers.push(*x.nullifier()); } for (i, out) in sapling.shielded_outputs().iter().enumerate() { - println!("note found!"); tree.append(Node::from_cmu(out.cmu())) .map_err(|_| "Failed to add cmu to tree")?; for (_, witness) in witnesses.iter_mut() { @@ -226,11 +232,22 @@ pub fn handle_transaction_internal( // Save witness let witness = IncrementalWitness::from_tree(tree); witnesses.push((note.note.clone(), witness)); + // Save Memo + let memo = Memo::from_bytes(note.memo.as_slice()) + .and_then(|m| { + if let Memo::Text(e) = m { + Ok((&*e).to_owned()) + } else { + Ok(String::new()) + } + }) + .unwrap_or_default(); + memos.push(memo); } } } } - Ok(nullifiers) + Ok((nullifiers, memos)) } #[wasm_bindgen] @@ -289,6 +306,7 @@ pub struct JSTxOptions { notes: Option>, utxos: Option>, extsk: String, + memo: String, to_address: String, change_address: String, amount: u64, @@ -307,6 +325,7 @@ pub async fn create_transaction(options: JsValue) -> Result { block_height, is_testnet, utxos, + memo, } = serde_wasm_bindgen::from_value::(options)?; assert!( !(notes.is_some() && utxos.is_some()), @@ -318,6 +337,7 @@ pub async fn create_transaction(options: JsValue) -> Result { } else { Network::MainNetwork }; + let memo = Memo::from_str(&memo).map_err(|_| "Can't convert string to memo")?; let input = if let Some(mut notes) = notes { notes.sort_by_key(|(note, _)| note.value().inner()); Either::Left(notes) @@ -333,6 +353,7 @@ pub async fn create_transaction(options: JsValue) -> Result { &to_address, &change_address, amount, + &memo, BlockHeight::from_u32(block_height), network, ) @@ -351,6 +372,7 @@ pub async fn create_transaction_internal( to_address: &str, change_address: &str, mut amount: u64, + memo: &Memo, block_height: BlockHeight, network: Network, ) -> Result> { @@ -385,7 +407,7 @@ pub async fn create_transaction_internal( .add_transparent_output(&x, amount) .map_err(|_| "Failed to add output")?, GenericAddress::Shield(x) => builder - .add_sapling_output(None, x, amount, MemoBytes::empty()) + .add_sapling_output(None, x, amount, memo.encode()) .map_err(|_| "Failed to add output")?, } diff --git a/src/transaction/test.rs b/src/transaction/test.rs index 3965e8d..aef23e4 100644 --- a/src/transaction/test.rs +++ b/src/transaction/test.rs @@ -10,6 +10,7 @@ use pivx_client_backend::encoding; use pivx_client_backend::encoding::decode_extended_spending_key; use pivx_client_backend::keys::UnifiedFullViewingKey; use pivx_primitives::consensus::{BlockHeight, Network, Parameters, TEST_NETWORK}; +use pivx_primitives::memo::Memo; use pivx_primitives::merkle_tree::CommitmentTree; use pivx_primitives::sapling::value::NoteValue; use pivx_primitives::sapling::Node; @@ -17,6 +18,7 @@ use pivx_primitives::sapling::Note; use pivx_primitives::sapling::Rseed::BeforeZip212; use std::error::Error; use std::io::Cursor; +use std::str::FromStr; #[test] fn check_tx_decryption() { @@ -28,8 +30,10 @@ fn check_tx_decryption() { let key = UnifiedFullViewingKey::new(Some(skey.to_diversifiable_full_viewing_key()), None) .expect("Failed to create key"); let mut comp_note = vec![]; - let nullifiers = + let (nullifiers, memos) = handle_transaction_internal(&mut tree, tx, &key, true, &mut comp_note).unwrap(); + assert_eq!(memos.len(), 1); + assert_eq!(memos[0], ""); //This was a t-s tx assert_eq!(nullifiers.len(), 0); //Successfully decrypt exactly 1 note @@ -91,12 +95,14 @@ pub async fn test_create_transaction() -> Result<(), Box> { let mut path_vec = vec![]; path.write(&mut path_vec)?; let path = hex::encode(path_vec); + let memo = Memo::from_str("Hello").map_err(|_| "Failed to encode memo")?; let tx = create_transaction_internal( Either::Left(vec![(note.clone(), path)]), &extended_spending_key, output, address, 5 * 10e6 as u64, + &memo, BlockHeight::from_u32(317), Network::TestNetwork, )