diff --git a/Cargo.toml b/Cargo.toml index 1cbe248..b31d826 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,8 +41,6 @@ native = ["tonic/channel", "tonic/gzip", "tonic/tls-webpki-roots", "tokio/macros sqlite-db = ["dep:zcash_client_sqlite"] console_error_panic_hook = ["dep:console_error_panic_hook"] no-bundler = ["wasm-bindgen-rayon?/no-bundler", "wasm_thread/no-bundler"] -sync2 = [] -sync3 = [] [dependencies] ## Web dependencies @@ -62,12 +60,13 @@ tokio_with_wasm = { version = "0.7.1", features = ["rt", "rt-multi-thread", "syn ## Zcash dependencies -zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } -zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4", default-features = false, features = ["transparent-inputs", "sync", "lightwalletd-tonic", "wasm-bindgen", "orchard"] } -zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4", features = ["orchard", "transparent-inputs"] } -zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4" } -zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4" } -zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4", default-features = false, features = ["bundled-prover"] } +zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } +zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8", default-features = false, features = ["transparent-inputs", "sync", "lightwalletd-tonic", "wasm-bindgen", "orchard"] } +zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8", features = ["orchard", "transparent-inputs"] } +zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8" } +zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8" } +zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "e9f82490ac370f221a2650ae1b2563c8ccadf4c8", default-features = false, features = ["bundled-prover"] } + ## gRPC Web dependencies prost = { version = "0.12", default-features = false } @@ -78,7 +77,7 @@ tonic = { version = "0.12", default-features = false, features = [ # Used in Native tests tokio = { version = "1.0" } -zcash_client_sqlite = { git = "https://github.com/ChainSafe/librustzcash", rev = "b6d32dd9a57165fb1508e9c1c8ab1a3aba09c7f4", default-features = false, features = ["unstable", "orchard"], optional = true } +zcash_client_sqlite = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f", default-features = false, features = ["unstable", "orchard"], optional = true } getrandom = { version = "0.2", features = ["js"] } thiserror = "1.0.63" diff --git a/justfile b/justfile index 4d7723d..1a1b854 100644 --- a/justfile +++ b/justfile @@ -2,26 +2,26 @@ default: just --list build *features: - wasm-pack build --no-opt -t web --scope webzjs --release --out-dir ./packages/webz-core --no-default-features --features="wasm wasm-parallel sync2 {{features}}" -Z build-std="panic_abort,std" + wasm-pack build --no-opt -t web --scope webzjs --release --out-dir ./packages/webz-core --no-default-features --features="wasm wasm-parallel {{features}}" -Z build-std="panic_abort,std" # All Wasm Tests test-web *features: WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --firefox --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" -# sync message board in the web: addigional args: sync2 +# sync message board in the web: addigional args: test-message-board-web *features: WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --chrome --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" --test message-board-sync -# simple example in the web: additional args: sync2 +# simple example in the web: additional args: test-simple-web *features: WASM_BINDGEN_TEST_TIMEOUT=99999 wasm-pack test --release --chrome --no-default-features --features "wasm no-bundler {{features}}" -Z build-std="panic_abort,std" --test simple-sync-and-send -# simple example: additional args: sync2, sqlite-db +# simple example: additional args:, sqlite-db example-simple *features: RUST_LOG="info,zcash_client_backend::sync=debug" cargo run -r --example simple-sync --features "native {{features}}" -# sync the message board: additional args: sync2, sqlite-db +# sync the message board: additional args:, sqlite-db example-message-board *features: RUST_LOG=info,zcash_client_backend::sync=debug cargo run -r --example message-board-sync --features "native {{features}}" diff --git a/packages/demo-wallet/src/App/Actions.tsx b/packages/demo-wallet/src/App/Actions.tsx index 3cb6786..afc0c74 100644 --- a/packages/demo-wallet/src/App/Actions.tsx +++ b/packages/demo-wallet/src/App/Actions.tsx @@ -1,7 +1,7 @@ import initWasm, { initThreadPool, WebWallet } from "@webzjs/webz-core"; import { State, Action } from "./App"; -import { MAINNET_LIGHTWALLETD_PROXY } from "./constants"; +import { MAINNET_LIGHTWALLETD_PROXY } from "./Constants"; export async function init(dispatch: React.Dispatch) { await initWasm(); @@ -13,8 +13,8 @@ export async function init(dispatch: React.Dispatch) { } export async function addNewAccount(state: State, dispatch: React.Dispatch, seedPhrase: string, birthdayHeight: number) { - await state.webWallet?.create_account(seedPhrase, 0, birthdayHeight); - dispatch({ type: "append-account-seed", payload: seedPhrase }); + let account_id = await state.webWallet?.create_account(seedPhrase, 0, birthdayHeight) || 0; + dispatch({ type: "add-account-seed", payload: [account_id, seedPhrase] }); await syncStateWithWallet(state, dispatch); } @@ -60,6 +60,13 @@ export async function triggerTransfer( throw new Error("No active account"); } - await state.webWallet?.transfer(state.accountSeeds[state.activeAccount], state.activeAccount, toAddress, amount); - await syncStateWithWallet(state, dispatch); + let activeAccountSeedPhrase = state.accountSeeds.get(state.activeAccount) || ""; + + let proposal = await state.webWallet?.propose_transfer(state.activeAccount, toAddress, amount); + console.log(JSON.stringify(proposal.describe(), null, 2)); + + let txids = await state.webWallet.create_proposed_transactions(proposal, activeAccountSeedPhrase); + console.log(JSON.stringify(txids, null, 2)); + + await state.webWallet.send_authorized_transactions(txids); } diff --git a/packages/demo-wallet/src/App/App.tsx b/packages/demo-wallet/src/App/App.tsx index d650a94..16ae026 100644 --- a/packages/demo-wallet/src/App/App.tsx +++ b/packages/demo-wallet/src/App/App.tsx @@ -23,19 +23,19 @@ export type State = { activeAccount?: number; summary?: WalletSummary; chainHeight?: bigint; - accountSeeds: string[]; + accountSeeds: Map; }; const initialState: State = { activeAccount: undefined, summary: undefined, chainHeight: undefined, - accountSeeds: [], + accountSeeds: new Map(), }; export type Action = | { type: "set-active-account"; payload: number } - | { type: "append-account-seed"; payload: string } + | { type: "add-account-seed"; payload: [number, string] } | { type: "set-web-wallet"; payload: WebWallet } | { type: "set-summary"; payload: WalletSummary } | { type: "set-chain-height"; payload: bigint }; @@ -45,8 +45,8 @@ const reducer = (state: State, action: Action): State => { case "set-active-account": { return { ...state, activeAccount: action.payload }; } - case "append-account-seed": { - return { ...state, accountSeeds: [...state.accountSeeds, action.payload] }; + case "add-account-seed": { + return { ...state, accountSeeds: state.accountSeeds.set(action.payload[0], action.payload[1]) }; } case "set-web-wallet": { return { ...state, webWallet: action.payload }; diff --git a/packages/demo-wallet/src/App/components/ImportAccount.tsx b/packages/demo-wallet/src/App/components/ImportAccount.tsx index 8588efc..aec337c 100644 --- a/packages/demo-wallet/src/App/components/ImportAccount.tsx +++ b/packages/demo-wallet/src/App/components/ImportAccount.tsx @@ -18,6 +18,7 @@ export function ImportAccount() { await addNewAccount(state, dispatch, seedPhrase, birthdayHeight); toast.success("Account imported successfully", { position: "top-center", + autoClose: 2000, }); setBirthdayHeight(0); setSeedPhrase(""); diff --git a/src/bindgen/mod.rs b/src/bindgen/mod.rs index 2fff25c..8aa035b 100644 --- a/src/bindgen/mod.rs +++ b/src/bindgen/mod.rs @@ -1 +1,2 @@ +pub mod proposal; pub mod wallet; diff --git a/src/bindgen/proposal.rs b/src/bindgen/proposal.rs new file mode 100644 index 0000000..db6bdae --- /dev/null +++ b/src/bindgen/proposal.rs @@ -0,0 +1,32 @@ +use wasm_bindgen::prelude::*; + +use super::wallet::NoteRef; +use zcash_primitives::transaction::fees::zip317::FeeRule; + +/// A handler to an immutable proposal. This can be passed to `create_proposed_transactions` to prove/authorize the transactions +/// before they are sent to the network. +/// +/// The proposal can be reviewed by calling `describe` which will return a JSON object with the details of the proposal. +#[wasm_bindgen] +pub struct Proposal { + inner: zcash_client_backend::proposal::Proposal, +} + +impl From> for Proposal { + fn from(inner: zcash_client_backend::proposal::Proposal) -> Self { + Self { inner } + } +} + +impl From for zcash_client_backend::proposal::Proposal { + fn from(proposal: Proposal) -> Self { + proposal.inner + } +} + +#[wasm_bindgen] +impl Proposal { + pub fn describe(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.inner).unwrap() + } +} diff --git a/src/bindgen/wallet.rs b/src/bindgen/wallet.rs index 02ad00e..7166f89 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -1,20 +1,29 @@ use std::num::NonZeroU32; +use nonempty::NonEmpty; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use tonic_web_wasm_client::Client; use crate::error::Error; -use crate::{BlockRange, MemoryWallet, Wallet, PRUNING_DEPTH}; +use crate::wallet::usk_from_seed_str; +use crate::{bindgen::proposal::Proposal, BlockRange, Wallet, PRUNING_DEPTH}; use wasm_thread as thread; use zcash_address::ZcashAddress; +use zcash_client_backend::data_api::{InputSource, WalletRead}; use zcash_client_backend::proto::service::{ compact_tx_streamer_client::CompactTxStreamerClient, ChainSpec, }; use zcash_client_memory::MemoryWalletDb; use zcash_keys::keys::UnifiedFullViewingKey; -use zcash_primitives::consensus::{self, BlockHeight}; +use zcash_primitives::consensus; +use zcash_primitives::transaction::TxId; + +pub type MemoryWallet = Wallet, T>; +pub type AccountId = + as WalletRead>::AccountId; +pub type NoteRef = as InputSource>::NoteRef; /// # A Zcash wallet /// @@ -87,61 +96,44 @@ impl WebWallet { /// /// # Arguments /// seed_phrase - mnemonic phrase to initialise the wallet - /// account_index - The HD derivation index to use. Can be any integer + /// account_hd_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( &self, seed_phrase: &str, - account_index: u32, + account_hd_index: u32, birthday_height: Option, - ) -> Result { + ) -> Result { tracing::info!("Create account called"); self.inner - .create_account(seed_phrase, account_index, birthday_height) + .create_account(seed_phrase, account_hd_index, birthday_height) .await + .map(|id| *id) } - pub async fn import_ufvk( - &self, - key: &str, - birthday_height: Option, - ) -> Result { + pub async fn import_ufvk(&self, key: &str, birthday_height: Option) -> Result { let ufvk = UnifiedFullViewingKey::decode(&self.inner.network, key) .map_err(Error::KeyParseError)?; - self.inner.import_ufvk(&ufvk, birthday_height).await + self.inner + .import_ufvk(&ufvk, birthday_height) + .await + .map(|id| *id) } pub async fn suggest_scan_ranges(&self) -> Result, Error> { self.inner.suggest_scan_ranges().await } - /// 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(&self, callback: &js_sys::Function) -> Result<(), Error> { - let callback = move |scanned_to: BlockHeight, tip: BlockHeight| { - let this = JsValue::null(); - let _ = callback.call2( - &this, - &JsValue::from(Into::::into(scanned_to)), - &JsValue::from(Into::::into(tip)), - ); - }; - - self.inner.sync(callback).await?; - - Ok(()) - } - /// Synchronize the wallet with the blockchain up to the tip using zcash_client_backend's algo - pub async fn sync2(&self) -> Result<(), Error> { + pub async fn sync(&self) -> Result<(), Error> { assert!(!thread::is_web_worker_thread()); let db = self.inner.clone(); let sync_handler = thread::Builder::new() - .name("sync2".to_string()) + .name("sync".to_string()) .spawn_async(|| async { assert!(thread::is_web_worker_thread()); tracing::debug!( @@ -150,7 +142,7 @@ impl WebWallet { ); let db = db; - db.sync2().await.unwrap_throw(); + db.sync().await.unwrap_throw(); }) .unwrap_throw() .join_async(); @@ -187,26 +179,51 @@ impl WebWallet { } /// - /// 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. + /// Create a transaction proposal to send funds from the wallet to a given address. /// - /// # Arguments - /// - pub async fn transfer( + pub async fn propose_transfer( &self, - seed_phrase: &str, - from_account_index: usize, + account_id: u32, to_address: String, value: u64, - ) -> Result<(), Error> { + ) -> Result { let to_address = ZcashAddress::try_from_encoded(&to_address)?; - self.inner - .transfer(seed_phrase, from_account_index, to_address, value) - .await + let proposal = self + .inner + .propose_transfer(AccountId::from(account_id), to_address, value) + .await?; + Ok(proposal.into()) } + /// + /// Perform the proving and signing required to create one or more transaction from the proposal. + /// Created transactions are stored in the wallet database and a list of the IDs is returned + /// + pub async fn create_proposed_transactions( + &self, + proposal: Proposal, + seed_phrase: &str, + ) -> Result { + let usk = usk_from_seed_str(seed_phrase, 0, &self.inner.network)?; + let txids = self + .inner + .create_proposed_transactions(proposal.into(), &usk) + .await?; + Ok(serde_wasm_bindgen::to_value(&txids).unwrap()) + } + + /// + /// Send a list of transactions to the network via the lightwalletd instance this wallet is connected to + /// + pub async fn send_authorized_transactions(&self, txids: JsValue) -> Result<(), Error> { + let txids: NonEmpty = serde_wasm_bindgen::from_value(txids).unwrap(); + self.inner.send_authorized_transactions(&txids).await + } + + /////////////////////////////////////////////////////////////////////////////////////// + // lightwalletd gRPC methods + /////////////////////////////////////////////////////////////////////////////////////// + /// Forwards a call to lightwalletd to retrieve the height of the latest block in the chain pub async fn get_latest_block(&self) -> Result { self.client() diff --git a/src/error.rs b/src/error.rs index 5dfb8c3..b097f3e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,8 @@ pub enum Error { SqliteError(#[from] zcash_client_sqlite::error::SqliteClientError), #[error("Invalid seed phrase")] InvalidSeedPhrase, + #[error("Failed when creating transaction")] + FailedToCreateTransaction, } impl From for JsValue { diff --git a/src/lib.rs b/src/lib.rs index e67c368..5c30930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,14 +15,17 @@ pub mod wallet; pub use wallet::Wallet; use wasm_bindgen::prelude::*; -use zcash_client_memory::MemoryWalletDb; -use zcash_primitives::consensus; /// The maximum number of checkpoints to store in each shard-tree pub const PRUNING_DEPTH: usize = 100; + +use zcash_client_memory::MemoryWalletDb; +use zcash_primitives::consensus; + #[cfg(feature = "wasm-parallel")] pub use wasm_bindgen_rayon::init_thread_pool; + // dummy NO-OP init_thread pool to maintain the same API between features #[cfg(not(feature = "wasm-parallel"))] #[wasm_bindgen(js_name = initThreadPool)] @@ -31,6 +34,5 @@ pub fn init_thread_pool(_threads: usize) {} #[wasm_bindgen] pub struct BlockRange(pub u32, pub u32); -pub type MemoryWallet = Wallet, T>; pub mod sync3; diff --git a/src/wallet.rs b/src/wallet.rs index ed044fb..c07b8e1 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -1,7 +1,6 @@ use std::num::NonZeroU32; use bip0039::{English, Mnemonic}; -use futures_util::{StreamExt, TryStreamExt}; use nonempty::NonEmpty; use secrecy::{ExposeSecret, SecretVec, Zeroize}; use tonic::{ @@ -22,23 +21,21 @@ use zcash_address::ZcashAddress; use zcash_client_backend::data_api::wallet::{ create_proposed_transactions, input_selection::GreedyInputSelector, propose_transfer, }; -use zcash_client_backend::data_api::{scanning::ScanRange, WalletCommitmentTrees}; +use zcash_client_backend::data_api::WalletCommitmentTrees; use zcash_client_backend::data_api::{ - AccountBirthday, AccountPurpose, InputSource, NullifierQuery, WalletRead, WalletSummary, - WalletWrite, + Account, AccountBirthday, AccountPurpose, InputSource, WalletRead, WalletSummary, WalletWrite, }; use zcash_client_backend::fees::zip317::SingleOutputChangeStrategy; use zcash_client_backend::proposal::Proposal; use zcash_client_backend::proto::service::{ self, 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::{MemBlockCache, MemoryWalletDb}; use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; -use zcash_primitives::consensus::{self, BlockHeight, Network}; +use zcash_primitives::consensus::{self, Network}; use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::fees::zip317::FeeRule; use zcash_primitives::transaction::TxId; @@ -80,8 +77,8 @@ impl Clone for Wallet { Self { db: self.db.clone(), client: self.client.clone(), - network: self.network.clone(), - min_confirmations: self.min_confirmations.clone(), + network: self.network, + min_confirmations: self.min_confirmations, } } } @@ -143,17 +140,17 @@ where /// /// # Arguments /// seed_phrase - mnemonic phrase to initialise the wallet - /// account_index - The HD derivation index to use. Can be any integer + /// account_id - 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( &self, seed_phrase: &str, - account_index: u32, + account_hd_index: u32, birthday_height: Option, - ) -> Result { + ) -> Result { // decode the mnemonic and derive the first account - let usk = usk_from_seed_str(seed_phrase, account_index, &self.network)?; + let usk = usk_from_seed_str(seed_phrase, account_hd_index, &self.network)?; let ufvk = usk.to_unified_full_viewing_key(); tracing::info!("Key successfully decoded. Importing into wallet"); @@ -166,7 +163,7 @@ where &self, ufvk: &UnifiedFullViewingKey, birthday_height: Option, - ) -> Result { + ) -> Result { self.import_account_ufvk(ufvk, birthday_height, AccountPurpose::ViewOnly) .await } @@ -177,7 +174,7 @@ where ufvk: &UnifiedFullViewingKey, birthday_height: Option, purpose: AccountPurpose, - ) -> Result { + ) -> Result { tracing::info!("Importing account with Ufvk: {:?}", ufvk); let mut client = self.client.clone(); let birthday = match birthday_height { @@ -205,13 +202,12 @@ where AccountBirthday::from_treestate(treestate, None).map_err(|_| Error::BirthdayError)? }; - let _account = self + Ok(self .db .write() .await - .import_account_ufvk(ufvk, &birthday, purpose)?; - - Ok("0".to_string()) + .import_account_ufvk(ufvk, &birthday, purpose)? + .id()) } pub async fn suggest_scan_ranges(&self) -> Result, Error> { @@ -244,7 +240,7 @@ where .map_err(Into::into) } - pub async fn sync2(&self) -> Result<(), Error> { + pub async fn sync(&self) -> Result<(), Error> { let mut client = self.client.clone(); // TODO: This should be held in the Wallet struct so we can download in parallel let db_cache = MemBlockCache::new(); @@ -261,113 +257,6 @@ where .map_err(Into::into) } - /// 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(&self, callback: impl Fn(BlockHeight, BlockHeight)) -> Result<(), Error> { - let tip = self.update_chain_tip().await?; - - tracing::info!("Retrieving suggested scan ranges from wallet"); - let scan_ranges = self.db.read().await.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?; - callback(scan_range.block_range().end, tip); - } - - Ok(()) - } - - /// Download and process all blocks in the given range - async fn fetch_and_scan_range(&self, start: u32, end: u32) -> Result<(), Error> { - let mut client = self.client.clone(); - // get the chainstate prior to the range - let tree_state = client - .get_tree_state(service::BlockId { - height: (start - 1).into(), - ..Default::default() - }) - .await?; - let chainstate = tree_state.into_inner().to_chain_state()?; - - // Get the scanning keys from the DB - let account_ufvks = self.db.read().await.get_unified_full_viewing_keys()?; - let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks); - - // Get the nullifiers for the unspent notes we are tracking - let nullifiers = Nullifiers::new( - self.db - .read() - .await - .get_sapling_nullifiers(NullifierQuery::Unspent)?, - self.db - .read() - .await - .get_orchard_nullifiers(NullifierQuery::Unspent)?, - ); - - 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 = client - .get_block_range(range) - .await? - .into_inner() - .map(|compact_block| { - scan_block( - &self.network, - compact_block.unwrap(), - &scanning_keys, - &nullifiers, - None, - ) - }) - .try_collect() - .await?; - - self.db - .write() - .await - .put_blocks(&chainstate, scanned_blocks)?; - - Ok(()) - } - pub async fn get_wallet_summary(&self) -> Result>, Error> { Ok(self .db @@ -376,36 +265,15 @@ where .get_wallet_summary(self.min_confirmations.into())?) } - pub(crate) async fn update_chain_tip(&self) -> Result { - tracing::info!("Retrieving chain tip from lightwalletd"); - - let tip_height = self - .client - .clone() - .get_latest_block(service::ChainSpec::default()) - .await? - .get_ref() - .height - .try_into() - .unwrap(); - - tracing::info!("Latest block height is {}", tip_height); - self.db.write().await.update_chain_tip(tip_height)?; - - Ok(tip_height) - } - /// /// Create a transaction proposal to send funds from the wallet to a given address /// - async fn propose_transfer( + pub async fn propose_transfer( &self, - account_index: usize, + account_id: AccountId, to_address: ZcashAddress, value: u64, ) -> Result, Error> { - let account_id = self.db.read().await.get_account_ids()?[account_index]; - let input_selector = GreedyInputSelector::new( SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Orchard), Default::default(), @@ -445,7 +313,7 @@ where /// 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 /// - pub(crate) async fn create_proposed_transactions( + pub async fn create_proposed_transactions( &self, proposal: Proposal, usk: &UnifiedSpendingKey, @@ -467,68 +335,66 @@ where OvkPolicy::Sender, &proposal, ) - .unwrap(); + .map_err(|_| Error::FailedToCreateTransaction)?; Ok(transactions) } + pub async fn send_authorized_transactions(&self, txids: &NonEmpty) -> Result<(), Error> { + let mut client = self.client.clone(); + for txid in txids.iter() { + let (txid, raw_tx) = self + .db + .read() + .await + .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 = client.send_transaction(raw_tx).await?.into_inner(); + + if response.error_code != 0 { + return Err(Error::SendFailed { + code: response.error_code, + reason: response.error_message, + }); + } else { + tracing::info!("Transaction {} send successfully :)", txid); + } + } + Ok(()) + } + /// - /// 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 + /// A helper function that creates a proposal, creates a transation from the proposal and then submits it /// pub async fn transfer( &self, seed_phrase: &str, - from_account_index: usize, + from_account_id: AccountId, to_address: ZcashAddress, value: u64, ) -> Result<(), Error> { - let mut client = self.client.clone(); let usk = usk_from_seed_str(seed_phrase, 0, &self.network)?; let proposal = self - .propose_transfer(from_account_index, to_address, value) + .propose_transfer(from_account_id, to_address, value) .await?; // TODO: Add callback for approving the transaction here let txids = self.create_proposed_transactions(proposal, &usk).await?; // send the transactions to the network!! - tracing::info!("Sending transaction..."); - let txid = *txids.first(); - let (txid, raw_tx) = self - .db - .read() - .await - .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 = 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(()) - } + tracing::info!("Sending transactions"); + self.send_authorized_transactions(&txids).await } } -fn usk_from_seed_str( +pub(crate) fn usk_from_seed_str( seed: &str, - account_index: u32, + account_id: u32, network: &consensus::Network, ) -> Result { let mnemonic = >::from_phrase(seed).map_err(|_| Error::InvalidSeedPhrase)?; @@ -538,7 +404,6 @@ fn usk_from_seed_str( 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_id.try_into()?)?; Ok(usk) } diff --git a/tests/message-board-sync.rs b/tests/message-board-sync.rs index 829bb7a..8163e54 100644 --- a/tests/message-board-sync.rs +++ b/tests/message-board-sync.rs @@ -39,21 +39,9 @@ async fn test_message_board() { let id = w.import_ufvk(&ufvk_str, Some(2477329)).await.unwrap(); tracing::info!("Created account with id: {}", id); - #[cfg(not(feature = "sync2"))] - { - tracing::info!("Syncing wallet with our sync impl"); - w.sync(&js_sys::Function::new_with_args( - "scanned_to, tip", - "console.log('Scanned: ', scanned_to, '/', tip)", - )) - .await - .unwrap(); - } - #[cfg(feature = "sync2")] - { - tracing::info!("Syncing wallet with sync2"); - w.sync2().await.unwrap(); - } + tracing::info!("Syncing wallet with our sync impl"); + w.sync().await.unwrap(); + tracing::info!("Syncing complete :)"); let summary = w.get_wallet_summary().await.unwrap(); diff --git a/tests/simple-sync-and-send.rs b/tests/simple-sync-and-send.rs index dcc2768..b5f2e1f 100644 --- a/tests/simple-sync-and-send.rs +++ b/tests/simple-sync-and-send.rs @@ -33,26 +33,8 @@ async fn test_get_and_scan_range() { let w_clone = w.clone(); let w = w_clone; - #[cfg(not(feature = "sync2"))] - { - w.sync(&js_sys::Function::new_with_args( - "scanned_to, tip", - "console.log('Scanned: ', scanned_to, '/', tip)", - )) - .await - .unwrap(); - } - #[cfg(feature = "sync2")] - { - tracing::info!("Syncing wallet with sync2"); - w.sync2().await.unwrap(); - } + w.sync().await.unwrap(); - #[cfg(feature = "sync3")] - { - tracing::info!("Syncing wallet with sync2"); - w.sync3().await.unwrap(); - } tracing::info!("Syncing complete :)"); let summary = w.get_wallet_summary().await.unwrap();