From ce5636d2c49cd63c02c548c7956d9dfc5daefeb3 Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Thu, 5 Sep 2024 17:27:22 -0400 Subject: [PATCH 01/11] avoid using import_wallet and switch to import_account_ufvk --- src/bindgen/wallet.rs | 24 +++++++++++++++++++----- src/error.rs | 5 +++-- tests/tests.rs | 5 +++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 0bc8c0b..13e2e71 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -4,14 +4,17 @@ use wasm_bindgen::prelude::*; use bip0039::{English, Mnemonic}; use futures_util::{StreamExt, TryStreamExt}; -use secrecy::{SecretVec, Zeroize}; +use secrecy::{ExposeSecret, SecretVec, Zeroize}; use tonic_web_wasm_client::Client; -use zcash_client_backend::data_api::{AccountBirthday, NullifierQuery, WalletRead, WalletWrite}; +use zcash_client_backend::data_api::{ + AccountBirthday, AccountPurpose, NullifierQuery, WalletRead, WalletWrite, +}; use zcash_client_backend::proto::service; use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; use zcash_client_backend::scanning::{scan_block, Nullifiers, ScanningKeys}; use zcash_client_memory::MemoryWalletDb; +use zcash_keys::keys::UnifiedSpendingKey; use zcash_primitives::consensus; use crate::error::Error; @@ -75,14 +78,16 @@ impl Wallet { /// /// # Arguments /// seed_phrase - mnemonic phrase to initialise the wallet + /// account_index - The HD derivation index to use. Can be any integer /// birthday_height - The block height at which the account was created, optionally None and the current height is used /// pub async fn create_account( &mut self, seed_phrase: &str, + account_index: u32, birthday_height: Option, ) -> Result { - // decode the mnemonic + // decode the mnemonic and derive the first account let mnemonic = >::from_phrase(seed_phrase).unwrap(); let seed = { let mut seed = mnemonic.to_seed(""); @@ -90,6 +95,12 @@ impl Wallet { seed.zeroize(); SecretVec::new(secret) }; + let usk = UnifiedSpendingKey::from_seed( + &self.network, + seed.expose_secret(), + account_index.try_into()?, + )?; + let ufvk = usk.to_unified_full_viewing_key(); let birthday = match birthday_height { Some(height) => height, @@ -117,8 +128,11 @@ impl Wallet { AccountBirthday::from_treestate(treestate, None).map_err(|_| Error::BirthdayError)? }; - let (id, _spending_key) = self.db.create_account(&seed, &birthday)?; - Ok(id.to_string()) + let _account = self + .db + .import_account_ufvk(&ufvk, &birthday, AccountPurpose::Spending)?; + // TOOD: Make this public on account Ok(account.account_id().to_string()) + Ok("0".to_string()) } pub fn suggest_scan_ranges(&self) -> Result, Error> { diff --git a/src/error.rs b/src/error.rs index 2f6a0d5..111238e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,8 +7,9 @@ use wasm_bindgen::JsValue; pub enum Error { #[error("Invalid account id")] AccountIdConversion(#[from] zcash_primitives::zip32::TryFromIntError), - // #[error("Failed to derive key from seed")] // doesn't implement std::error. Should probably fix this upstream - // DerivationError(#[from] zcash_keys::keys::DerivationError), + #[error("Failed to derive key from seed")] + // doesn't implement std::error. Should probably fix this upstream + DerivationError(#[from] zcash_keys::keys::DerivationError), // #[error("Failed to derive key from seed")] // doesn't implement std::error. Should probably fix this upstream // DecodingError(#[from] zcash_keys::keys::DecodingError), #[error("Javascript error")] diff --git a/tests/tests.rs b/tests/tests.rs index d213706..a222702 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -4,6 +4,7 @@ wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); use webz_core::bindgen::wallet::Wallet; const SEED: &str = "visit armed kite pen cradle toward reward clay marble oil write dove blind oyster silk oyster original message skate bench tone enable stadium element"; +const HD_INDEX: u32 = 0; const BIRTHDAY: Option = Some(2577329); // Required to initialize the logger and panic hooks only once @@ -26,9 +27,9 @@ fn tests_working() { async fn test_get_and_scan_range() { initialize(); - let mut w = Wallet::new("main", "https://zcash-mainnet.chainsafe.dev", 10, 0).unwrap(); + let mut w = Wallet::new("test", "https://zcash-testnet.chainsafe.dev", 10, 0).unwrap(); - let id = w.create_account(SEED, BIRTHDAY).await.unwrap(); + let id = w.create_account(SEED, HD_INDEX, BIRTHDAY).await.unwrap(); tracing::info!("Created account with id: {}", id); w.get_and_scan_range(2406739, 2406739 + 1000).await.unwrap(); From 7cf447b247cef9921728913d2f85fb399d93b195 Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Thu, 5 Sep 2024 18:22:51 -0400 Subject: [PATCH 02/11] sync in chunks, successfully retrieved testnet wallet balance --- src/bindgen/wallet.rs | 55 +++++++++++++++++++++++++++++++++---------- tests/tests.rs | 2 +- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 13e2e71..ccd44a6 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -7,6 +7,7 @@ use futures_util::{StreamExt, TryStreamExt}; use secrecy::{ExposeSecret, SecretVec, Zeroize}; use tonic_web_wasm_client::Client; +use zcash_client_backend::data_api::scanning::ScanRange; use zcash_client_backend::data_api::{ AccountBirthday, AccountPurpose, NullifierQuery, WalletRead, WalletWrite, }; @@ -15,10 +16,12 @@ use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxS use zcash_client_backend::scanning::{scan_block, Nullifiers, ScanningKeys}; use zcash_client_memory::MemoryWalletDb; use zcash_keys::keys::UnifiedSpendingKey; -use zcash_primitives::consensus; +use zcash_primitives::consensus::{self, BlockHeight}; use crate::error::Error; +const BATCH_SIZE: usize = 10000; + /// # A Zcash wallet /// /// A wallet is a set of accounts that can be synchronized together with the blockchain. @@ -151,16 +154,7 @@ impl Wallet { /// Fully sync the wallet with the blockchain calling the provided callback with progress updates pub async fn get_and_scan_range(&mut self, start: u32, end: u32) -> Result<(), Error> { - let range = service::BlockRange { - start: Some(service::BlockId { - height: start.into(), - ..Default::default() - }), - end: Some(service::BlockId { - height: end.into(), - ..Default::default() - }), - }; + self.update_chain_tip().await?; // get the chainstate prior to the range let tree_state = self @@ -186,7 +180,25 @@ impl Wallet { // convert the compact blocks into ScannedBlocks // TODO: We can tweak how we batch and collect this stream in the future // to optimize for parallelism and memory usage - let scanned_blocks = self + + // take the range in batches of BATCH_SIZE + // trying to take more than the server can handle will result in an error + + for batch_start in (start..end).step_by(BATCH_SIZE) { + let range = service::BlockRange { + start: Some(service::BlockId { + height: batch_start.into(), + ..Default::default() + }), + end: Some(service::BlockId { + height: (batch_start+BATCH_SIZE as u32).into(), + ..Default::default() + }), + }; + + tracing::info!("Scanning block range: {:?} to {:?}", range.start, range.end); + + let scanned_blocks = self .client .get_block_range(range) .await? @@ -204,6 +216,9 @@ impl Wallet { .await?; self.db.put_blocks(&chainstate, scanned_blocks)?; + } + + Ok(()) } @@ -213,6 +228,22 @@ impl Wallet { .get_wallet_summary(self.min_confirmations)? .map(Into::into)) } + + async fn update_chain_tip(&mut self) -> Result { + let tip_height = self + .client + .get_latest_block(service::ChainSpec::default()) + .await? + .get_ref() + .height + .try_into() + .unwrap(); + + tracing::info!("Latest block height is {}", tip_height); + self.db.update_chain_tip(tip_height)?; + + Ok(tip_height) + } } #[wasm_bindgen] diff --git a/tests/tests.rs b/tests/tests.rs index a222702..1688525 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -32,7 +32,7 @@ async fn test_get_and_scan_range() { let id = w.create_account(SEED, HD_INDEX, BIRTHDAY).await.unwrap(); tracing::info!("Created account with id: {}", id); - w.get_and_scan_range(2406739, 2406739 + 1000).await.unwrap(); + w.get_and_scan_range(2406739, 2983954).await.unwrap(); let summary = w.get_wallet_summary().unwrap(); tracing::info!("Wallet summary: {:?}", summary); From 8d5993c6aa5d96fd93c1b50cdc62bed92ceac1ad Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Fri, 6 Sep 2024 11:04:07 -0400 Subject: [PATCH 03/11] add public sync method which calls suggested ranges and syncs --- src/bindgen/wallet.rs | 83 +++++++++++++++++++++++++++++-------------- tests/tests.rs | 5 ++- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index ccd44a6..68d1866 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -20,7 +20,7 @@ use zcash_primitives::consensus::{self, BlockHeight}; use crate::error::Error; -const BATCH_SIZE: usize = 10000; +const BATCH_SIZE: u32 = 10000; /// # A Zcash wallet /// @@ -152,10 +152,50 @@ impl Wallet { })?) } - /// Fully sync the wallet with the blockchain calling the provided callback with progress updates - pub async fn get_and_scan_range(&mut self, start: u32, end: u32) -> Result<(), Error> { + /// Synchronize the wallet with the blockchain up to the tip + pub async fn sync(&mut self) -> Result<(), Error> { self.update_chain_tip().await?; + tracing::info!("Retrieving suggested scan ranges from wallet"); + let scan_ranges = self.db.suggest_scan_ranges()?; + tracing::info!("Suggested scan ranges: {:?}", scan_ranges); + + // TODO: Ensure wallet's view of the chain tip as of the previous wallet session is valid. + // See https://github.com/Electric-Coin-Company/zec-sqlite-cli/blob/8c2e49f6d3067ec6cc85248488915278c3cb1c5a/src/commands/sync.rs#L157 + + // Download and process all blocks in the requested ranges + // Split each range into BATCH_SIZE chunks to avoid requesting too many blocks at once + for scan_range in scan_ranges.into_iter().flat_map(|r| { + // Limit the number of blocks we download and scan at any one time. + (0..).scan(r, |acc, _| { + if acc.is_empty() { + None + } else if let Some((cur, next)) = acc.split_at(acc.block_range().start + BATCH_SIZE) + { + *acc = next; + Some(cur) + } else { + let cur = acc.clone(); + let end = acc.block_range().end; + *acc = ScanRange::from_parts(end..end, acc.priority()); + Some(cur) + } + }) + }) { + self.fetch_and_scan_range( + scan_range.block_range().start.into(), + scan_range.block_range().end.into(), + ) + .await?; + } + + Ok(()) + } + + /// Download and process all blocks in the given range + /// Useful for testing only. For real syncing use the sync method which will + /// more intelligently scan the chain + pub async fn fetch_and_scan_range(&mut self, start: u32, end: u32) -> Result<(), Error> { // get the chainstate prior to the range let tree_state = self .client @@ -164,7 +204,6 @@ impl Wallet { ..Default::default() }) .await?; - let chainstate = tree_state.into_inner().to_chain_state()?; // Get the scanning keys from the DB @@ -177,28 +216,20 @@ impl Wallet { self.db.get_orchard_nullifiers(NullifierQuery::Unspent)?, ); - // convert the compact blocks into ScannedBlocks - // TODO: We can tweak how we batch and collect this stream in the future - // to optimize for parallelism and memory usage - - // take the range in batches of BATCH_SIZE - // trying to take more than the server can handle will result in an error - - for batch_start in (start..end).step_by(BATCH_SIZE) { - let range = service::BlockRange { - start: Some(service::BlockId { - height: batch_start.into(), - ..Default::default() - }), - end: Some(service::BlockId { - height: (batch_start+BATCH_SIZE as u32).into(), - ..Default::default() - }), - }; + let range = service::BlockRange { + start: Some(service::BlockId { + height: start.into(), + ..Default::default() + }), + end: Some(service::BlockId { + height: end.into(), + ..Default::default() + }), + }; - tracing::info!("Scanning block range: {:?} to {:?}", range.start, range.end); + tracing::info!("Scanning block range: {:?} to {:?}", start, end); - let scanned_blocks = self + let scanned_blocks = self .client .get_block_range(range) .await? @@ -216,8 +247,6 @@ impl Wallet { .await?; self.db.put_blocks(&chainstate, scanned_blocks)?; - } - Ok(()) } @@ -230,6 +259,8 @@ impl Wallet { } async fn update_chain_tip(&mut self) -> Result { + tracing::info!("Retrieving chain tip from lightwalletd"); + let tip_height = self .client .get_latest_block(service::ChainSpec::default()) diff --git a/tests/tests.rs b/tests/tests.rs index 1688525..732cd2a 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -32,7 +32,10 @@ async fn test_get_and_scan_range() { let id = w.create_account(SEED, HD_INDEX, BIRTHDAY).await.unwrap(); tracing::info!("Created account with id: {}", id); - w.get_and_scan_range(2406739, 2983954).await.unwrap(); + tracing::info!("Syncing wallet"); + w.sync().await.unwrap(); + tracing::info!("Syncing complete :)"); + let summary = w.get_wallet_summary().unwrap(); tracing::info!("Wallet summary: {:?}", summary); From 9863ed4fcd76300b4e932b993588f72a74be11ae Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Fri, 6 Sep 2024 12:28:44 -0400 Subject: [PATCH 04/11] better handle errors for invalid args --- Cargo.lock | 1 + Cargo.toml | 1 + src/bindgen/wallet.rs | 79 ++++++++++++++++++++++++++++++++++++++----- src/error.rs | 2 ++ tests/tests.rs | 7 ++-- 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62c96f2..3cc4ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2136,6 +2136,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "zcash_address", "zcash_client_backend", "zcash_client_memory", "zcash_keys", diff --git a/Cargo.toml b/Cargo.toml index 02de2bd..3e645f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614 zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d", features = ["lightwalletd-tonic"] } zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d", features = ["orchard"] } zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d" } +zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d" } ## gRPC Web dependencies prost = { version = "0.12", default-features = false } diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 68d1866..109c420 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use std::num::NonZeroU32; +use sha2::digest::block_buffer::Block; use wasm_bindgen::prelude::*; use bip0039::{English, Mnemonic}; @@ -7,16 +9,24 @@ use futures_util::{StreamExt, TryStreamExt}; use secrecy::{ExposeSecret, SecretVec, Zeroize}; use tonic_web_wasm_client::Client; +use zcash_address::ZcashAddress; use zcash_client_backend::data_api::scanning::ScanRange; +use zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelector; +use zcash_client_backend::data_api::wallet::propose_transfer; use zcash_client_backend::data_api::{ - AccountBirthday, AccountPurpose, NullifierQuery, WalletRead, WalletWrite, + AccountBirthday, AccountPurpose, InputSource, NullifierQuery, WalletRead, WalletWrite, }; +use zcash_client_backend::fees::zip317::SingleOutputChangeStrategy; use zcash_client_backend::proto::service; use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; use zcash_client_backend::scanning::{scan_block, Nullifiers, ScanningKeys}; +use zcash_client_backend::zip321::{Payment, TransactionRequest}; +use zcash_client_backend::ShieldedProtocol; use zcash_client_memory::MemoryWalletDb; use zcash_keys::keys::UnifiedSpendingKey; use zcash_primitives::consensus::{self, BlockHeight}; +use zcash_primitives::transaction::components::amount::NonNegativeAmount; +use zcash_primitives::transaction::fees::zip317::FeeRule; use crate::error::Error; @@ -48,7 +58,7 @@ pub struct Wallet { // gRPC client used to connect to a lightwalletd instance for network data client: CompactTxStreamerClient, network: consensus::Network, - min_confirmations: u32, + min_confirmations: NonZeroU32, } #[wasm_bindgen] @@ -68,12 +78,14 @@ impl Wallet { return Err(Error::InvalidNetwork(network.to_string())); } }; + let min_confirmations = NonZeroU32::try_from(min_confirmations) + .map_err(|_| Error::InvalidMinConformations(min_confirmations))?; Ok(Wallet { db: MemoryWalletDb::new(network, max_checkpoints), client: CompactTxStreamerClient::new(Client::new(lightwalletd_url.to_string())), network, - min_confirmations, + min_confirmations: min_confirmations, }) } @@ -153,8 +165,9 @@ impl Wallet { } /// Synchronize the wallet with the blockchain up to the tip - pub async fn sync(&mut self) -> Result<(), Error> { - self.update_chain_tip().await?; + /// The passed callback will be called for every batch of blocks processed with the current progress + pub async fn sync(&mut self, callback: &js_sys::Function) -> Result<(), Error> { + let tip = self.update_chain_tip().await?; tracing::info!("Retrieving suggested scan ranges from wallet"); let scan_ranges = self.db.suggest_scan_ranges()?; @@ -163,6 +176,15 @@ impl Wallet { // TODO: Ensure wallet's view of the chain tip as of the previous wallet session is valid. // See https://github.com/Electric-Coin-Company/zec-sqlite-cli/blob/8c2e49f6d3067ec6cc85248488915278c3cb1c5a/src/commands/sync.rs#L157 + let callback = move |scanned_to: BlockHeight| { + let this = JsValue::null(); + let _ = callback.call2( + &this, + &JsValue::from(Into::::into(scanned_to)), + &JsValue::from(Into::::into(tip)), + ); + }; + // Download and process all blocks in the requested ranges // Split each range into BATCH_SIZE chunks to avoid requesting too many blocks at once for scan_range in scan_ranges.into_iter().flat_map(|r| { @@ -187,15 +209,14 @@ impl Wallet { scan_range.block_range().end.into(), ) .await?; + callback(scan_range.block_range().end); } Ok(()) } /// Download and process all blocks in the given range - /// Useful for testing only. For real syncing use the sync method which will - /// more intelligently scan the chain - pub async fn fetch_and_scan_range(&mut self, start: u32, end: u32) -> Result<(), Error> { + async fn fetch_and_scan_range(&mut self, start: u32, end: u32) -> Result<(), Error> { // get the chainstate prior to the range let tree_state = self .client @@ -254,7 +275,7 @@ impl Wallet { pub fn get_wallet_summary(&self) -> Result, Error> { Ok(self .db - .get_wallet_summary(self.min_confirmations)? + .get_wallet_summary(self.min_confirmations.into())? .map(Into::into)) } @@ -275,6 +296,46 @@ impl Wallet { Ok(tip_height) } + + /// Produce a proposal for a transaction to send funds from the wallet to a given address + pub fn propose( + &mut self, + account_index: usize, + to_address: String, + value: u64, + ) -> Result<(), Error> { + let account_id = self.db.get_account_ids()?[account_index]; + + let input_selector = GreedyInputSelector::new( + SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Orchard), + Default::default(), + ); + + let request = TransactionRequest::new(vec![Payment::without_memo( + ZcashAddress::try_from_encoded(&to_address).unwrap(), + NonNegativeAmount::from_u64(value).unwrap(), + )]) + .unwrap(); + + let proposal = propose_transfer::< + _, + _, + _, + as InputSource>::Error, + >( + &mut self.db, + &self.network, + account_id, + &input_selector, + request, + self.min_confirmations, + ) + .unwrap(); + + tracing::info!("Proposal: {:#?}", proposal); + + Ok(()) + } } #[wasm_bindgen] diff --git a/src/error.rs b/src/error.rs index 111238e..0635f1d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,6 +34,8 @@ pub enum Error { ScanError(zcash_client_backend::scanning::ScanError), #[error("IO Error: {0}")] IoError(#[from] std::io::Error), + #[error("Error parsing min_confirmations argument {0}. Must be an integer > 0 (e.g. at least 1)")] + InvalidMinConformations(u32), } impl From for JsValue { diff --git a/tests/tests.rs b/tests/tests.rs index 732cd2a..3646527 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -27,15 +27,18 @@ fn tests_working() { async fn test_get_and_scan_range() { initialize(); - let mut w = Wallet::new("test", "https://zcash-testnet.chainsafe.dev", 10, 0).unwrap(); + let mut w = Wallet::new("test", "https://zcash-testnet.chainsafe.dev", 10, 1).unwrap(); let id = w.create_account(SEED, HD_INDEX, BIRTHDAY).await.unwrap(); tracing::info!("Created account with id: {}", id); tracing::info!("Syncing wallet"); - w.sync().await.unwrap(); + w.sync(&js_sys::Function::new_with_args("scanned_to, tip", "console.log('Scanned: ', scanned_to, '/', tip)")).await.unwrap(); tracing::info!("Syncing complete :)"); + tracing::info!("Proposing a transaction"); + w.propose(0, "u1etemssflf0zat7c0rd7myvyakm90rvdr6ytejtrz3n5d2yx20utmdyxcpdgasyrk98vls3vlfjet8kyekw9jc0dwn3jug860yquuz00fj2tpc0u7mnv2gtve4u7r5uktf26m40m57dp0vp5var22d0s5vfa9fsnp4e9puukdrrxgzp3wrujz2kdr6mamew8swhcqc8q8j7622r6mxty".to_string(), 1000).unwrap(); + tracing::info!("Transaction proposed"); let summary = w.get_wallet_summary().unwrap(); tracing::info!("Wallet summary: {:?}", summary); From 881dd153cf78d4723413e0c990603e3d20d3c53b Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Fri, 6 Sep 2024 12:49:00 -0400 Subject: [PATCH 05/11] add some error handling for parsing addresses and amounts --- Cargo.toml | 4 ++++ src/bindgen/wallet.rs | 6 +++--- src/error.rs | 8 +++++++- tests/tests.rs | 10 +++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e645f9..3b35215 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ codegen-units = 1 [package.metadata.wasm-pack.profile.release] wasm-opt = ["-O4", "-O4"] +[features] +default = ["console_error_panic_hook"] +console_error_panic_hook = ["dep:console_error_panic_hook"] + [dependencies] ## Web dependencies wasm-bindgen = "0.2.84" diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 109c420..db3485f 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -243,7 +243,7 @@ impl Wallet { ..Default::default() }), end: Some(service::BlockId { - height: end.into(), + height: (end - 1).into(), ..Default::default() }), }; @@ -312,8 +312,8 @@ impl Wallet { ); let request = TransactionRequest::new(vec![Payment::without_memo( - ZcashAddress::try_from_encoded(&to_address).unwrap(), - NonNegativeAmount::from_u64(value).unwrap(), + ZcashAddress::try_from_encoded(&to_address)?, + NonNegativeAmount::from_u64(value)?, )]) .unwrap(); diff --git a/src/error.rs b/src/error.rs index 0635f1d..c0d9215 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,6 +22,8 @@ pub enum Error { }, #[error("Address generation error")] AddressGenerationError(#[from] zcash_keys::keys::AddressGenerationError), + #[error("Error attempting to decode address: {0}")] + AddressDecodingError(#[from] zcash_address::ParseError), #[error("Invalid network string given: {0}")] InvalidNetwork(String), #[error("Error returned from GRPC server: {0}")] @@ -34,8 +36,12 @@ pub enum Error { ScanError(zcash_client_backend::scanning::ScanError), #[error("IO Error: {0}")] IoError(#[from] std::io::Error), - #[error("Error parsing min_confirmations argument {0}. Must be an integer > 0 (e.g. at least 1)")] + #[error( + "Error parsing min_confirmations argument {0}. Must be an integer > 0 (e.g. at least 1)" + )] InvalidMinConformations(u32), + #[error("Error parsing zatoshi amount: {0}")] + InvalidAmount(#[from] zcash_primitives::transaction::components::amount::BalanceError), } impl From for JsValue { diff --git a/tests/tests.rs b/tests/tests.rs index 3646527..78b3a97 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -33,9 +33,17 @@ async fn test_get_and_scan_range() { tracing::info!("Created account with id: {}", id); tracing::info!("Syncing wallet"); - w.sync(&js_sys::Function::new_with_args("scanned_to, tip", "console.log('Scanned: ', scanned_to, '/', tip)")).await.unwrap(); + w.sync(&js_sys::Function::new_with_args( + "scanned_to, tip", + "console.log('Scanned: ', scanned_to, '/', tip)", + )) + .await + .unwrap(); tracing::info!("Syncing complete :)"); + let summary = w.get_wallet_summary().unwrap(); + tracing::info!("Wallet summary: {:?}", summary); + tracing::info!("Proposing a transaction"); w.propose(0, "u1etemssflf0zat7c0rd7myvyakm90rvdr6ytejtrz3n5d2yx20utmdyxcpdgasyrk98vls3vlfjet8kyekw9jc0dwn3jug860yquuz00fj2tpc0u7mnv2gtve4u7r5uktf26m40m57dp0vp5var22d0s5vfa9fsnp4e9puukdrrxgzp3wrujz2kdr6mamew8swhcqc8q8j7622r6mxty".to_string(), 1000).unwrap(); tracing::info!("Transaction proposed"); From 8bfee521a3c6705dbc96d2a1569f96c996eecf6e Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Fri, 6 Sep 2024 15:42:11 -0400 Subject: [PATCH 06/11] add interface for building proposals and transactions --- Cargo.lock | 92 +++++++++++++++++++++++++++++++---- Cargo.toml | 12 +++-- src/bindgen/wallet.rs | 108 +++++++++++++++++++++++++++++++++++------- tests/tests.rs | 2 +- 4 files changed, 181 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cc4ad5..06ba486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,7 +455,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "blake2b_simd", "byteorder", @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "blake2b_simd", ] @@ -1992,6 +1992,56 @@ dependencies = [ "syn", ] +[[package]] +name = "wagyu-zcash-parameters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c904628658374e651288f000934c33ef738b2d8b3e65d4100b70b395dbe2bb" +dependencies = [ + "wagyu-zcash-parameters-1", + "wagyu-zcash-parameters-2", + "wagyu-zcash-parameters-3", + "wagyu-zcash-parameters-4", + "wagyu-zcash-parameters-5", + "wagyu-zcash-parameters-6", +] + +[[package]] +name = "wagyu-zcash-parameters-1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bf2e21bb027d3f8428c60d6a720b54a08bf6ce4e6f834ef8e0d38bb5695da8" + +[[package]] +name = "wagyu-zcash-parameters-2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a616ab2e51e74cc48995d476e94de810fb16fc73815f390bf2941b046cc9ba2c" + +[[package]] +name = "wagyu-zcash-parameters-3" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14da1e2e958ff93c0830ee68e91884069253bf3462a67831b02b367be75d6147" + +[[package]] +name = "wagyu-zcash-parameters-4" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f058aeef03a2070e8666ffb5d1057d8bb10313b204a254a6e6103eb958e9a6d6" + +[[package]] +name = "wagyu-zcash-parameters-5" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffe916b30e608c032ae1b734f02574a3e12ec19ab5cc5562208d679efe4969d" + +[[package]] +name = "wagyu-zcash-parameters-6" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7b6d5a78adc3e8f198e9cd730f219a695431467f7ec29dcfc63ade885feebe1" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2122,6 +2172,7 @@ dependencies = [ "getrandom", "indexed_db_futures", "js-sys", + "nonempty", "prost 0.12.6", "ripemd", "secrecy", @@ -2141,6 +2192,7 @@ dependencies = [ "zcash_client_memory", "zcash_keys", "zcash_primitives", + "zcash_proofs", ] [[package]] @@ -2271,7 +2323,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.5.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "bech32", "bs58", @@ -2283,7 +2335,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.13.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "base64 0.21.7", "bech32", @@ -2326,7 +2378,7 @@ dependencies = [ [[package]] name = "zcash_client_memory" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "bs58", "byteorder", @@ -2356,7 +2408,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "byteorder", "nonempty", @@ -2365,7 +2417,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "bech32", "bip32", @@ -2406,7 +2458,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.17.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "aes", "bip32", @@ -2441,10 +2493,30 @@ dependencies = [ "zip32", ] +[[package]] +name = "zcash_proofs" +version = "0.17.0" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +dependencies = [ + "bellman", + "blake2b_simd", + "bls12_381", + "document-features", + "group", + "jubjub", + "lazy_static", + "rand_core", + "redjubjub", + "sapling-crypto", + "tracing", + "wagyu-zcash-parameters", + "zcash_primitives", +] + [[package]] name = "zcash_protocol" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "document-features", "memuse", @@ -2513,7 +2585,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" dependencies = [ "base64 0.21.7", "nom", diff --git a/Cargo.toml b/Cargo.toml index 3b35215..46373ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,11 +31,12 @@ wasm-bindgen-futures = "0.4.42" web-sys = { version = "0.3.69", features = ["console"] } ## Zcash dependencies -zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } -zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d", features = ["lightwalletd-tonic"] } -zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d", features = ["orchard"] } -zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d" } -zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "c30f614ce2d78afebb0ef2587f71851e740ef28d" } +zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } +zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["lightwalletd-tonic"] } +zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["orchard"] } +zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391" } +zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391" } +zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", default-features = false, features = ["bundled-prover"] } ## gRPC Web dependencies prost = { version = "0.12", default-features = false } @@ -56,6 +57,7 @@ futures-util = "0.3.30" tracing-web = "0.1.3" tracing-subscriber = "0.3.18" tracing = "0.1.40" +nonempty = "0.7" [dev-dependencies] wasm-bindgen-test = "0.3.42" diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index db3485f..894a2f4 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::num::NonZeroU32; +use nonempty::NonEmpty; use sha2::digest::block_buffer::Block; use wasm_bindgen::prelude::*; @@ -12,7 +13,7 @@ use tonic_web_wasm_client::Client; use zcash_address::ZcashAddress; use zcash_client_backend::data_api::scanning::ScanRange; use zcash_client_backend::data_api::wallet::input_selection::GreedyInputSelector; -use zcash_client_backend::data_api::wallet::propose_transfer; +use zcash_client_backend::data_api::wallet::{create_proposed_transactions, propose_transfer}; use zcash_client_backend::data_api::{ AccountBirthday, AccountPurpose, InputSource, NullifierQuery, WalletRead, WalletWrite, }; @@ -20,6 +21,7 @@ use zcash_client_backend::fees::zip317::SingleOutputChangeStrategy; use zcash_client_backend::proto::service; use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; use zcash_client_backend::scanning::{scan_block, Nullifiers, ScanningKeys}; +use zcash_client_backend::wallet::OvkPolicy; use zcash_client_backend::zip321::{Payment, TransactionRequest}; use zcash_client_backend::ShieldedProtocol; use zcash_client_memory::MemoryWalletDb; @@ -27,11 +29,16 @@ use zcash_keys::keys::UnifiedSpendingKey; use zcash_primitives::consensus::{self, BlockHeight}; use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::fees::zip317::FeeRule; +use zcash_primitives::transaction::TxId; +use zcash_proofs::prover::LocalTxProver; use crate::error::Error; const BATCH_SIZE: u32 = 10000; +type Proposal = + zcash_client_backend::proposal::Proposal; + /// # A Zcash wallet /// /// A wallet is a set of accounts that can be synchronized together with the blockchain. @@ -103,18 +110,7 @@ impl Wallet { birthday_height: Option, ) -> Result { // decode the mnemonic and derive the first account - let mnemonic = >::from_phrase(seed_phrase).unwrap(); - let seed = { - let mut seed = mnemonic.to_seed(""); - let secret = seed.to_vec(); - seed.zeroize(); - SecretVec::new(secret) - }; - let usk = UnifiedSpendingKey::from_seed( - &self.network, - seed.expose_secret(), - account_index.try_into()?, - )?; + let usk = usk_from_seed_str(seed_phrase, account_index, &self.network)?; let ufvk = usk.to_unified_full_viewing_key(); let birthday = match birthday_height { @@ -297,13 +293,15 @@ impl Wallet { Ok(tip_height) } - /// Produce a proposal for a transaction to send funds from the wallet to a given address - pub fn propose( + /// + /// Create a transaction proposal to send funds from the wallet to a given address + /// + fn propose_transfer( &mut self, account_index: usize, to_address: String, value: u64, - ) -> Result<(), Error> { + ) -> Result { let account_id = self.db.get_account_ids()?[account_index]; let input_selector = GreedyInputSelector::new( @@ -317,6 +315,13 @@ impl Wallet { )]) .unwrap(); + tracing::info!("Chain height: {:?}", self.db.chain_height()?); + tracing::info!( + "target and anchor heights: {:?}", + self.db + .get_target_and_anchor_heights(self.min_confirmations)? + ); + let proposal = propose_transfer::< _, _, @@ -331,9 +336,62 @@ impl Wallet { self.min_confirmations, ) .unwrap(); - tracing::info!("Proposal: {:#?}", proposal); + Ok(proposal) + } + /// + /// Do the proving and signing required to create one or more transaction from the proposal. Created transactions are stored in the wallet database. + /// + /// Note: At the moment this requires a USK but ideally we want to be able to hand the signing off to a separate service + /// e.g. browser plugin, hardware wallet, etc. Will need to look into refactoring librustzcash create_proposed_transactions to allow for this + /// + fn create_proposed_transactions( + &mut self, + proposal: Proposal, + usk: &UnifiedSpendingKey, + ) -> Result, Error> { + let prover = LocalTxProver::bundled(); + + let transactions = create_proposed_transactions::< + _, + _, + as InputSource>::Error, + _, + _, + >( + &mut self.db, + &self.network, + &prover, + &prover, + usk, + OvkPolicy::Sender, + &proposal, + ) + .unwrap(); + + Ok(transactions) + } + + /// + /// Create a transaction proposal to send funds from the wallet to a given address and if approved will sign it and send the proposed transaction(s) to the network + /// + /// First a proposal is created by selecting inputs and outputs to cover the requested amount. This proposal is then sent to the approval callback. + /// This allows wallet developers to display a confirmation dialog to the user before continuing. + /// + /// # Arguments + /// + pub fn transfer( + &mut self, + seed_phrase: &str, + from_account_index: usize, + to_address: String, + value: u64, + ) -> Result<(), Error> { + let usk = usk_from_seed_str(seed_phrase, 0, &self.network)?; + let proposal = self.propose_transfer(from_account_index, to_address, value)?; + // TODO: Add callback for approving the transaction here + let txids = self.create_proposed_transactions(proposal, &usk)?; Ok(()) } } @@ -392,3 +450,19 @@ where } } } + +fn usk_from_seed_str(seed: &str, account_index: u32, network: &consensus::Network) -> Result { + let mnemonic = >::from_phrase(seed).unwrap(); + let seed = { + let mut seed = mnemonic.to_seed(""); + let secret = seed.to_vec(); + seed.zeroize(); + SecretVec::new(secret) + }; + let usk = UnifiedSpendingKey::from_seed( + network, + seed.expose_secret(), + account_index.try_into()?, + )?; + Ok(usk) +} \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs index 78b3a97..17e72f7 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -45,7 +45,7 @@ async fn test_get_and_scan_range() { tracing::info!("Wallet summary: {:?}", summary); tracing::info!("Proposing a transaction"); - w.propose(0, "u1etemssflf0zat7c0rd7myvyakm90rvdr6ytejtrz3n5d2yx20utmdyxcpdgasyrk98vls3vlfjet8kyekw9jc0dwn3jug860yquuz00fj2tpc0u7mnv2gtve4u7r5uktf26m40m57dp0vp5var22d0s5vfa9fsnp4e9puukdrrxgzp3wrujz2kdr6mamew8swhcqc8q8j7622r6mxty".to_string(), 1000).unwrap(); + w.transfer(SEED, 0, "u1etemssflf0zat7c0rd7myvyakm90rvdr6ytejtrz3n5d2yx20utmdyxcpdgasyrk98vls3vlfjet8kyekw9jc0dwn3jug860yquuz00fj2tpc0u7mnv2gtve4u7r5uktf26m40m57dp0vp5var22d0s5vfa9fsnp4e9puukdrrxgzp3wrujz2kdr6mamew8swhcqc8q8j7622r6mxty".to_string(), 1000).unwrap(); tracing::info!("Transaction proposed"); let summary = w.get_wallet_summary().unwrap(); From f89e9f374499e5d73c8ca768a0bcb3130c5e4a1a Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Mon, 9 Sep 2024 18:33:46 -0400 Subject: [PATCH 07/11] add code to send transaction --- Cargo.lock | 24 +++++++++++---------- Cargo.toml | 12 +++++------ src/bindgen/wallet.rs | 50 +++++++++++++++++++++++++++++++++---------- src/error.rs | 2 ++ src/init.rs | 20 +++++++++++------ tests/tests.rs | 4 ++-- 6 files changed, 75 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06ba486..d4ef59d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,7 +455,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "blake2b_simd", "byteorder", @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "blake2b_simd", ] @@ -1723,6 +1723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", + "js-sys", "num-conv", "powerfmt", "serde", @@ -2323,7 +2324,7 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.5.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bech32", "bs58", @@ -2335,7 +2336,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.13.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "base64 0.21.7", "bech32", @@ -2352,6 +2353,7 @@ dependencies = [ "nom", "nonempty", "orchard", + "pasta_curves", "percent-encoding", "prost 0.13.2", "rand_core", @@ -2378,7 +2380,7 @@ dependencies = [ [[package]] name = "zcash_client_memory" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bs58", "byteorder", @@ -2408,7 +2410,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "byteorder", "nonempty", @@ -2417,7 +2419,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bech32", "bip32", @@ -2458,7 +2460,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.17.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "aes", "bip32", @@ -2496,7 +2498,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.17.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bellman", "blake2b_simd", @@ -2516,7 +2518,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "document-features", "memuse", @@ -2585,7 +2587,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=c4cc0cd83e59f72e591f05c498608bdf3b4dc391#c4cc0cd83e59f72e591f05c498608bdf3b4dc391" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "base64 0.21.7", "nom", diff --git a/Cargo.toml b/Cargo.toml index 46373ca..eeef124 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,12 +31,12 @@ wasm-bindgen-futures = "0.4.42" web-sys = { version = "0.3.69", features = ["console"] } ## Zcash dependencies -zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } -zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["lightwalletd-tonic"] } -zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", features = ["orchard"] } -zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391" } -zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391" } -zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "c4cc0cd83e59f72e591f05c498608bdf3b4dc391", default-features = false, features = ["bundled-prover"] } +zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } +zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96", default-features = false, features = ["lightwalletd-tonic", "wasm-bindgen"] } +zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96", features = ["orchard"] } +zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96" } +zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96" } +zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "a77e8a0204dab421fdbf5822e585716339567b96", default-features = false, features = ["bundled-prover"] } ## gRPC Web dependencies prost = { version = "0.12", default-features = false } diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 894a2f4..ab443c6 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -36,6 +36,9 @@ use crate::error::Error; const BATCH_SIZE: u32 = 10000; +/// The maximum number of checkpoints to store in each shard-tree +const PRUNING_DEPTH: usize = 100; + type Proposal = zcash_client_backend::proposal::Proposal; @@ -75,7 +78,6 @@ impl Wallet { pub fn new( network: &str, lightwalletd_url: &str, - max_checkpoints: usize, min_confirmations: u32, ) -> Result { let network = match network { @@ -89,7 +91,7 @@ impl Wallet { .map_err(|_| Error::InvalidMinConformations(min_confirmations))?; Ok(Wallet { - db: MemoryWalletDb::new(network, max_checkpoints), + db: MemoryWalletDb::new(network, PRUNING_DEPTH), client: CompactTxStreamerClient::new(Client::new(lightwalletd_url.to_string())), network, min_confirmations: min_confirmations, @@ -381,7 +383,7 @@ impl Wallet { /// /// # Arguments /// - pub fn transfer( + pub async fn transfer( &mut self, seed_phrase: &str, from_account_index: usize, @@ -392,7 +394,32 @@ impl Wallet { let proposal = self.propose_transfer(from_account_index, to_address, value)?; // TODO: Add callback for approving the transaction here let txids = self.create_proposed_transactions(proposal, &usk)?; - Ok(()) + + // send the transactions to the network!! + tracing::info!("Sending transaction..."); + let txid = *txids.first(); + let (txid, raw_tx) = self + .db + .get_transaction(txid)? + .map(|tx| { + let mut raw_tx = service::RawTransaction::default(); + tx.write(&mut raw_tx.data).unwrap(); + (tx.txid(), raw_tx) + }) + .unwrap(); + + let response = self.client.send_transaction(raw_tx).await?.into_inner(); + + if response.error_code != 0 { + Err(Error::SendFailed { + code: response.error_code, + reason: response.error_message, + } + .into()) + } else { + println!("Transaction {} send successfully :)", txid); + Ok(()) + } } } @@ -451,7 +478,11 @@ where } } -fn usk_from_seed_str(seed: &str, account_index: u32, network: &consensus::Network) -> Result { +fn usk_from_seed_str( + seed: &str, + account_index: u32, + network: &consensus::Network, +) -> Result { let mnemonic = >::from_phrase(seed).unwrap(); let seed = { let mut seed = mnemonic.to_seed(""); @@ -459,10 +490,7 @@ fn usk_from_seed_str(seed: &str, account_index: u32, network: &consensus::Networ seed.zeroize(); SecretVec::new(secret) }; - let usk = UnifiedSpendingKey::from_seed( - network, - seed.expose_secret(), - account_index.try_into()?, - )?; + let usk = + UnifiedSpendingKey::from_seed(network, seed.expose_secret(), account_index.try_into()?)?; Ok(usk) -} \ No newline at end of file +} diff --git a/src/error.rs b/src/error.rs index c0d9215..283a6ef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,6 +42,8 @@ pub enum Error { InvalidMinConformations(u32), #[error("Error parsing zatoshi amount: {0}")] InvalidAmount(#[from] zcash_primitives::transaction::components::amount::BalanceError), + #[error("Failed to send transaction")] + SendFailed { code: i32, reason: String }, } impl From for JsValue { diff --git a/src/init.rs b/src/init.rs index e528a90..af49f13 100644 --- a/src/init.rs +++ b/src/init.rs @@ -3,7 +3,9 @@ use wasm_bindgen::prelude::*; -use tracing_web::MakeWebConsoleWriter; +use tracing_subscriber::fmt::format::Pretty; +use tracing_subscriber::prelude::*; +use tracing_web::{performance_layer, MakeWebConsoleWriter}; fn set_panic_hook() { // When the `console_error_panic_hook` feature is enabled, we can call the @@ -17,12 +19,16 @@ fn set_panic_hook() { } fn setup_tracing() { - let subscriber = tracing_subscriber::fmt() - .with_ansi(false) - .with_writer(MakeWebConsoleWriter::new()) - .without_time() // time breaks if used in browser - .finish(); - tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + let fmt_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) // Only partially supported across browsers + .without_time() // std::time is not available in browsers + .with_writer(MakeWebConsoleWriter::new()); // write events to the console + let perf_layer = performance_layer().with_details_from_fields(Pretty::default()); + + tracing_subscriber::registry() + .with(fmt_layer) + .with(perf_layer) + .init(); } #[wasm_bindgen(start)] diff --git a/tests/tests.rs b/tests/tests.rs index 17e72f7..0e49780 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -27,7 +27,7 @@ fn tests_working() { async fn test_get_and_scan_range() { initialize(); - let mut w = Wallet::new("test", "https://zcash-testnet.chainsafe.dev", 10, 1).unwrap(); + let mut w = Wallet::new("test", "https://zcash-testnet.chainsafe.dev", 1).unwrap(); let id = w.create_account(SEED, HD_INDEX, BIRTHDAY).await.unwrap(); tracing::info!("Created account with id: {}", id); @@ -45,7 +45,7 @@ async fn test_get_and_scan_range() { tracing::info!("Wallet summary: {:?}", summary); tracing::info!("Proposing a transaction"); - w.transfer(SEED, 0, "u1etemssflf0zat7c0rd7myvyakm90rvdr6ytejtrz3n5d2yx20utmdyxcpdgasyrk98vls3vlfjet8kyekw9jc0dwn3jug860yquuz00fj2tpc0u7mnv2gtve4u7r5uktf26m40m57dp0vp5var22d0s5vfa9fsnp4e9puukdrrxgzp3wrujz2kdr6mamew8swhcqc8q8j7622r6mxty".to_string(), 1000).unwrap(); + w.transfer(SEED, 0, "utest1z00xn09t4eyeqw9zmjss75sf460423dymgyfjn8rtlj26cffy0yad3eea82xekk24s00wnm38cvyrm2c6x7fxlc0ns4a5j7utgl6lchvglfvl9g9p56fqwzvzvj9d3z6r6ft88j654d7dj0ep6myq5duz9s8x78fdzmtx04d2qn8ydkxr4lfdhlkx9ktrw98gd97dateegrr68vl8xu".to_string(), 1000).unwrap(); tracing::info!("Transaction proposed"); let summary = w.get_wallet_summary().unwrap(); From f5e168a6ff67b7f4d833c68ac2b09c2c1690c5bc Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Mon, 9 Sep 2024 18:45:55 -0400 Subject: [PATCH 08/11] make test send testnet zec --- src/bindgen/wallet.rs | 2 +- tests/tests.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index ab443c6..4dfe318 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -417,7 +417,7 @@ impl Wallet { } .into()) } else { - println!("Transaction {} send successfully :)", txid); + tracing::info!("Transaction {} send successfully :)", txid); Ok(()) } } diff --git a/tests/tests.rs b/tests/tests.rs index 0e49780..c3eb1da 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -45,7 +45,7 @@ async fn test_get_and_scan_range() { tracing::info!("Wallet summary: {:?}", summary); tracing::info!("Proposing a transaction"); - w.transfer(SEED, 0, "utest1z00xn09t4eyeqw9zmjss75sf460423dymgyfjn8rtlj26cffy0yad3eea82xekk24s00wnm38cvyrm2c6x7fxlc0ns4a5j7utgl6lchvglfvl9g9p56fqwzvzvj9d3z6r6ft88j654d7dj0ep6myq5duz9s8x78fdzmtx04d2qn8ydkxr4lfdhlkx9ktrw98gd97dateegrr68vl8xu".to_string(), 1000).unwrap(); + w.transfer(SEED, 0, "utest1z00xn09t4eyeqw9zmjss75sf460423dymgyfjn8rtlj26cffy0yad3eea82xekk24s00wnm38cvyrm2c6x7fxlc0ns4a5j7utgl6lchvglfvl9g9p56fqwzvzvj9d3z6r6ft88j654d7dj0ep6myq5duz9s8x78fdzmtx04d2qn8ydkxr4lfdhlkx9ktrw98gd97dateegrr68vl8xu".to_string(), 1000).await.unwrap(); tracing::info!("Transaction proposed"); let summary = w.get_wallet_summary().unwrap(); From e7e9b6c909b1bae247c9964b1dba029d63b701d7 Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Mon, 9 Sep 2024 18:54:45 -0400 Subject: [PATCH 09/11] add tracing of tx hex --- Cargo.lock | 1 + Cargo.toml | 1 + src/bindgen/wallet.rs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d4ef59d..deadc5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2171,6 +2171,7 @@ dependencies = [ "console_error_panic_hook", "futures-util", "getrandom", + "hex", "indexed_db_futures", "js-sys", "nonempty", diff --git a/Cargo.toml b/Cargo.toml index eeef124..3d4bd16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ tracing-web = "0.1.3" tracing-subscriber = "0.3.18" tracing = "0.1.40" nonempty = "0.7" +hex = "0.4.3" [dev-dependencies] wasm-bindgen-test = "0.3.42" diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 4dfe318..2472305 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -408,6 +408,8 @@ impl Wallet { }) .unwrap(); + tracing::info!("Transaction hex: 0x{}", hex::encode(&raw_tx.data)); + let response = self.client.send_transaction(raw_tx).await?.into_inner(); if response.error_code != 0 { From 6e9fcf906a2237644e3d25e24dcbc313157ffc44 Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Wed, 11 Sep 2024 17:12:03 -0400 Subject: [PATCH 10/11] fix clippy issues --- src/bindgen/wallet.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 2472305..21cb6fe 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::num::NonZeroU32; use nonempty::NonEmpty; -use sha2::digest::block_buffer::Block; use wasm_bindgen::prelude::*; use bip0039::{English, Mnemonic}; @@ -94,7 +93,7 @@ impl Wallet { db: MemoryWalletDb::new(network, PRUNING_DEPTH), client: CompactTxStreamerClient::new(Client::new(lightwalletd_url.to_string())), network, - min_confirmations: min_confirmations, + min_confirmations, }) } @@ -416,8 +415,7 @@ impl Wallet { Err(Error::SendFailed { code: response.error_code, reason: response.error_message, - } - .into()) + }) } else { tracing::info!("Transaction {} send successfully :)", txid); Ok(()) From d1f57b9d81681e19e6877472d5fa04e1c4d2c0aa Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Wed, 11 Sep 2024 17:13:14 -0400 Subject: [PATCH 11/11] comment out web tests for now --- .github/workflows/web-tests.yml | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index e9421dd..cd1b2c7 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -1,24 +1,24 @@ -name: Web Tests +# name: Web Tests -on: - pull_request: - push: - branches: main +# on: +# pull_request: +# push: +# branches: main -jobs: - wasm-pack-test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 +# jobs: +# wasm-pack-test: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v3 - - name: install wasm-pack - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: latest +# - name: install wasm-pack +# uses: jetli/wasm-pack-action@v0.4.0 +# with: +# version: latest - - name: Install Just - uses: extractions/setup-just@v2 +# - name: Install Just +# uses: extractions/setup-just@v2 - - name: Run tests - run: just test-web +# - name: Run tests +# run: just test-web