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 diff --git a/Cargo.lock b/Cargo.lock index 62c96f2..deadc5d 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=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=c30f614ce2d78afebb0ef2587f71851e740ef28d#c30f614ce2d78afebb0ef2587f71851e740ef28d" +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", @@ -1992,6 +1993,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" @@ -2120,8 +2171,10 @@ dependencies = [ "console_error_panic_hook", "futures-util", "getrandom", + "hex", "indexed_db_futures", "js-sys", + "nonempty", "prost 0.12.6", "ripemd", "secrecy", @@ -2136,10 +2189,12 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "zcash_address", "zcash_client_backend", "zcash_client_memory", "zcash_keys", "zcash_primitives", + "zcash_proofs", ] [[package]] @@ -2270,7 +2325,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bech32", "bs58", @@ -2282,7 +2337,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "base64 0.21.7", "bech32", @@ -2299,6 +2354,7 @@ dependencies = [ "nom", "nonempty", "orchard", + "pasta_curves", "percent-encoding", "prost 0.13.2", "rand_core", @@ -2325,7 +2381,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bs58", "byteorder", @@ -2355,7 +2411,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "byteorder", "nonempty", @@ -2364,7 +2420,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "bech32", "bip32", @@ -2405,7 +2461,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "aes", "bip32", @@ -2440,10 +2496,30 @@ dependencies = [ "zip32", ] +[[package]] +name = "zcash_proofs" +version = "0.17.0" +source = "git+https://github.com/ChainSafe/librustzcash?rev=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" +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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "document-features", "memuse", @@ -2512,7 +2588,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=a77e8a0204dab421fdbf5822e585716339567b96#a77e8a0204dab421fdbf5822e585716339567b96" dependencies = [ "base64 0.21.7", "nom", diff --git a/Cargo.toml b/Cargo.toml index 02de2bd..3d4bd16 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" @@ -27,10 +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_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 } @@ -51,6 +57,8 @@ futures-util = "0.3.30" 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 0bc8c0b..21cb6fe 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -1,21 +1,46 @@ use std::collections::HashMap; +use std::num::NonZeroU32; +use nonempty::NonEmpty; 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_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::{create_proposed_transactions, propose_transfer}; +use zcash_client_backend::data_api::{ + 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::wallet::OvkPolicy; +use zcash_client_backend::zip321::{Payment, TransactionRequest}; +use zcash_client_backend::ShieldedProtocol; use zcash_client_memory::MemoryWalletDb; -use zcash_primitives::consensus; +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; + +/// The maximum number of checkpoints to store in each shard-tree +const PRUNING_DEPTH: usize = 100; + +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. @@ -42,7 +67,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] @@ -52,7 +77,6 @@ impl Wallet { pub fn new( network: &str, lightwalletd_url: &str, - max_checkpoints: usize, min_confirmations: u32, ) -> Result { let network = match network { @@ -62,9 +86,11 @@ 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), + db: MemoryWalletDb::new(network, PRUNING_DEPTH), client: CompactTxStreamerClient::new(Client::new(lightwalletd_url.to_string())), network, min_confirmations, @@ -75,21 +101,18 @@ 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 - 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) - }; + // decode the mnemonic and derive the first account + 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 { Some(height) => height, @@ -117,8 +140,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> { @@ -135,19 +161,59 @@ 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() - }), + /// Synchronize the wallet with the blockchain up to the tip + /// 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()?; + 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 + + 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| { + // 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?; + callback(scan_range.block_range().end); + } + + Ok(()) + } + + /// Download and process all blocks in the given range + 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 @@ -156,7 +222,6 @@ impl Wallet { ..Default::default() }) .await?; - let chainstate = tree_state.into_inner().to_chain_state()?; // Get the scanning keys from the DB @@ -169,9 +234,19 @@ 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 + let range = service::BlockRange { + start: Some(service::BlockId { + height: start.into(), + ..Default::default() + }), + end: Some(service::BlockId { + height: (end - 1).into(), + ..Default::default() + }), + }; + + tracing::info!("Scanning block range: {:?} to {:?}", start, end); + let scanned_blocks = self .client .get_block_range(range) @@ -190,15 +265,162 @@ impl Wallet { .await?; self.db.put_blocks(&chainstate, scanned_blocks)?; + Ok(()) } 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)) } + + 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()) + .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) + } + + /// + /// 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 { + 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)?, + NonNegativeAmount::from_u64(value)?, + )]) + .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::< + _, + _, + _, + as InputSource>::Error, + >( + &mut self.db, + &self.network, + account_id, + &input_selector, + request, + 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 async 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)?; + + // 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(); + + 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 { + Err(Error::SendFailed { + code: response.error_code, + reason: response.error_message, + }) + } else { + tracing::info!("Transaction {} send successfully :)", txid); + Ok(()) + } + } } #[wasm_bindgen] @@ -255,3 +477,20 @@ 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) +} diff --git a/src/error.rs b/src/error.rs index 2f6a0d5..283a6ef 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")] @@ -21,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}")] @@ -33,6 +36,14 @@ 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), + #[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 d213706..c3eb1da 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,12 +27,26 @@ 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", 1).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(); + tracing::info!("Syncing wallet"); + 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.transfer(SEED, 0, "utest1z00xn09t4eyeqw9zmjss75sf460423dymgyfjn8rtlj26cffy0yad3eea82xekk24s00wnm38cvyrm2c6x7fxlc0ns4a5j7utgl6lchvglfvl9g9p56fqwzvzvj9d3z6r6ft88j654d7dj0ep6myq5duz9s8x78fdzmtx04d2qn8ydkxr4lfdhlkx9ktrw98gd97dateegrr68vl8xu".to_string(), 1000).await.unwrap(); + tracing::info!("Transaction proposed"); let summary = w.get_wallet_summary().unwrap(); tracing::info!("Wallet summary: {:?}", summary);