diff --git a/Cargo.lock b/Cargo.lock index 9d23467..3429cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,7 +806,7 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "equihash" version = "0.2.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "blake2b_simd", "byteorder", @@ -858,7 +858,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "blake2b_simd", ] @@ -1680,6 +1680,9 @@ name = "nonempty" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +dependencies = [ + "serde", +] [[package]] name = "nu-ansi-term" @@ -3648,11 +3651,13 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.5.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "bech32", "bs58", "f4jumble", + "serde", + "serde_with", "zcash_encoding", "zcash_protocol", ] @@ -3660,7 +3665,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.13.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "async-trait", "base64", @@ -3686,6 +3691,8 @@ dependencies = [ "rayon", "sapling-crypto", "secrecy", + "serde", + "serde_with", "shardtree", "subtle", "time", @@ -3706,7 +3713,7 @@ dependencies = [ [[package]] name = "zcash_client_memory" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "async-trait", "bs58", @@ -3741,7 +3748,7 @@ dependencies = [ [[package]] name = "zcash_client_sqlite" version = "0.11.2" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "bs58", "byteorder", @@ -3777,7 +3784,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "byteorder", "nonempty", @@ -3786,7 +3793,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "bech32", "bip32", @@ -3827,7 +3834,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.17.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "aes", "bip32", @@ -3851,6 +3858,7 @@ dependencies = [ "ripemd", "sapling-crypto", "secp256k1", + "serde", "sha2", "subtle", "tracing", @@ -3865,7 +3873,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.17.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "bellman", "blake2b_simd", @@ -3885,10 +3893,12 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.3.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "document-features", "memuse", + "serde", + "serde_with", ] [[package]] @@ -3954,11 +3964,13 @@ dependencies = [ [[package]] name = "zip321" version = "0.1.0" -source = "git+https://github.com/ChainSafe/librustzcash?rev=d1fa3e846c5e61de3f1df23dd9f4d5416915631a#d1fa3e846c5e61de3f1df23dd9f4d5416915631a" +source = "git+https://github.com/ChainSafe/librustzcash?rev=9673cc2859e8a2528d1efd3c74795363f87ddf8f#9673cc2859e8a2528d1efd3c74795363f87ddf8f" dependencies = [ "base64", "nom", "percent-encoding", + "serde", + "serde_with", "zcash_address", "zcash_protocol", ] diff --git a/Cargo.toml b/Cargo.toml index 69da9c9..5af0dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,12 +61,12 @@ tokio_with_wasm = { version = "0.7.1", features = ["rt", "rt-multi-thread", "syn ## Zcash dependencies -zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } -zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a", default-features = false, features = ["sync", "lightwalletd-tonic", "wasm-bindgen", "orchard"] } -zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a", features = ["orchard"] } -zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a" } -zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a" } -zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a", default-features = false, features = ["bundled-prover"] } +zcash_keys = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f", features = ["transparent-inputs", "orchard", "sapling", "unstable"] } +zcash_client_backend = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f", default-features = false, features = ["sync", "lightwalletd-tonic", "wasm-bindgen", "orchard"] } +zcash_client_memory = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f", features = ["orchard"] } +zcash_primitives = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f" } +zcash_address = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f" } +zcash_proofs = { git = "https://github.com/ChainSafe/librustzcash", rev = "9673cc2859e8a2528d1efd3c74795363f87ddf8f", default-features = false, features = ["bundled-prover"] } ## gRPC Web dependencies prost = { version = "0.12", default-features = false } @@ -77,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 = "d1fa3e846c5e61de3f1df23dd9f4d5416915631a", 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/packages/demo-wallet/src/App/Actions.tsx b/packages/demo-wallet/src/App/Actions.tsx index 8d82e8b..afc0c74 100644 --- a/packages/demo-wallet/src/App/Actions.tsx +++ b/packages/demo-wallet/src/App/Actions.tsx @@ -61,6 +61,12 @@ export async function triggerTransfer( } let activeAccountSeedPhrase = state.accountSeeds.get(state.activeAccount) || ""; - await state.webWallet?.transfer(activeAccountSeedPhrase, state.activeAccount, toAddress, amount); - await syncStateWithWallet(state, dispatch); + + 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/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 f974f97..0750884 100644 --- a/src/bindgen/wallet.rs +++ b/src/bindgen/wallet.rs @@ -1,25 +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, 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::WalletRead; +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::transaction::TxId; pub type MemoryWallet = Wallet, T>; pub type AccountId = as WalletRead>::AccountId; +pub type NoteRef = as InputSource>::NoteRef; /// # A Zcash wallet /// @@ -168,31 +172,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 + /// Create a transaction proposal to send funds from the wallet to a given address. /// - /// 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( + pub async fn propose_transfer( &self, - seed_phrase: &str, - from_account_id: u32, + 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, - AccountId::from(from_account_id), - 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/wallet.rs b/src/wallet.rs index cccdc0d..548dde6 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -381,7 +381,7 @@ where /// /// 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_id: AccountId, to_address: ZcashAddress, @@ -426,7 +426,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, @@ -448,18 +448,42 @@ 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, @@ -468,7 +492,6 @@ where 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_id, to_address, value) @@ -477,37 +500,12 @@ where 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_id: u32, network: &consensus::Network,