diff --git a/Cargo.lock b/Cargo.lock index c93e6a3e..47f19d1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9819,7 +9819,6 @@ dependencies = [ "testcontainers", "tokio", "url", - "vault_core", "wallet", ] @@ -10904,6 +10903,7 @@ dependencies = [ "token_core", "tokio", "url", + "vault_core", "zeroize", ] diff --git a/Justfile b/Justfile index 83c9dd0c..346dc84b 100644 --- a/Justfile +++ b/Justfile @@ -8,7 +8,7 @@ METHODS_PATH := "program_methods" TEST_METHODS_PATH := "test_program_methods" ARTIFACTS := "artifacts" -# Build risc0 program artifacts +# Build risc0 program artifacts. build-artifacts: @echo "๐Ÿ”จ Building artifacts" @for methods_path in {{METHODS_PATH}} {{TEST_METHODS_PATH}}; do \ @@ -18,7 +18,13 @@ build-artifacts: cp target/$methods_path/riscv32im-risc0-zkvm-elf/docker/*.bin {{ARTIFACTS}}/$methods_path; \ done -# Run tests +# Format codebase. +fmt: + @echo "๐ŸŽจ Formatting codebase" + cargo +nightly fmt + taplo fmt + +# Run tests. test: @echo "๐Ÿงช Running tests" RISC0_DEV_MODE=1 cargo nextest run --no-fail-fast @@ -29,42 +35,59 @@ bench: cargo bench -p crypto_primitives_bench --bench primitives cargo bench -p cycle_bench --features ppe --bench verify -# Run Bedrock node in docker +# Run Bedrock node in docker. [working-directory: 'bedrock'] run-bedrock: @echo "โ›“๏ธ Running bedrock" docker compose up -# Run Sequencer +# Run Sequencer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration. [working-directory: 'lez/sequencer/service'] -run-sequencer: +run-sequencer standalone="": @echo "๐Ÿง  Running sequencer" - RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p sequencer_service configs/debug/sequencer_config.json + @if [ "{{standalone}}" = "standalone" ]; then \ + echo "๐Ÿงช Running in standalone mode"; \ + RUST_LOG=info cargo run --features standalone --release -p sequencer_service configs/debug/sequencer_config.json; \ + else \ + echo "๐Ÿš€ Running in normal mode"; \ + RUST_LOG=info cargo run --release -p sequencer_service configs/debug/sequencer_config.json; \ + fi -# Run Indexer +# Run Indexer. Run with RISC0_DEV_MODE=1 to disable proof verification for faster iteration. [working-directory: 'lez/indexer/service'] run-indexer mock="": @echo "๐Ÿ” Running indexer" @if [ "{{mock}}" = "mock" ]; then \ echo "๐Ÿงช Using mock data"; \ - RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \ + RUST_LOG=info cargo run --release --features mock-responses -p indexer_service configs/indexer_config.json; \ else \ echo "๐Ÿš€ Using real data"; \ - RUST_LOG=info RISC0_DEV_MODE=1 cargo run --release -p indexer_service configs/indexer_config.json; \ + RUST_LOG=info cargo run --release -p indexer_service configs/indexer_config.json; \ fi -# Run Explorer +# Run Explorer. [working-directory: 'lez/explorer_service'] run-explorer: @echo "๐ŸŒ Running explorer" RUST_LOG=info cargo leptos serve -# Run Wallet +# Run Wallet. [working-directory: 'lez/wallet'] run-wallet +args: @echo "๐Ÿ”‘ Running wallet" LEE_WALLET_HOME_DIR=$(pwd)/configs/debug cargo run --release -p wallet -- {{args}} +# Import test accounts supplied in sequencer configuration. +wallet-import-test-accounts: + @echo "โš™๏ธ Initializing accounts" + just run-wallet account import public --private-key 7f273098f25b71e6c005a9519f2678da8d1c7f01f6a27778e2d9948abdf901fb + just run-wallet vault claim --account-id Public/CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r --amount 10000 + + just run-wallet account import public --private-key f434f8741720014586ae43356d2aec6257da086222f604ddb75d69733b86fc4c + just run-wallet vault claim --account-id Public/2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2 --amount 20000 + + just run-wallet account list + # Clean runtime data clean: @echo "๐Ÿงน Cleaning run artifacts" diff --git a/completions/bash/wallet b/completions/bash/wallet index a4d390f6..d714122c 100644 --- a/completions/bash/wallet +++ b/completions/bash/wallet @@ -46,7 +46,7 @@ _wallet() { cword=$COMP_CWORD } - local commands="auth-transfer chain-info account pinata token amm ata check-health config restore-keys deploy-program help" + local commands="auth-transfer chain-info account pinata token amm ata vault check-health config restore-keys deploy-program help" # Find the main command and subcommand by scanning words before the cursor. # Global options that take a value are skipped along with their argument. @@ -535,6 +535,38 @@ _wallet() { esac ;; + vault) + case "$subcmd" in + "") + COMPREPLY=($(compgen -W "transfer claim help" -- "$cur")) + ;; + transfer) + case "$prev" in + --from | --to) + _wallet_complete_account_id "$cur" + ;; + --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--from --to --amount" -- "$cur")) + ;; + esac + ;; + claim) + case "$prev" in + --account-id) + _wallet_complete_account_id "$cur" + ;; + --amount) + ;; # no specific completion + *) + COMPREPLY=($(compgen -W "--account-id --amount" -- "$cur")) + ;; + esac + ;; + esac + ;; + config) case "$subcmd" in "") diff --git a/completions/zsh/_wallet b/completions/zsh/_wallet index 8f573ab0..ea3de32f 100644 --- a/completions/zsh/_wallet +++ b/completions/zsh/_wallet @@ -25,6 +25,7 @@ _wallet() { 'token:Token program interaction subcommand' 'amm:AMM program interaction subcommand' 'ata:Associated Token Account program interaction subcommand' + 'vault:Vault program interaction subcommand' 'check-health:Check the wallet can connect to the node and builtin local programs match the remote versions' 'config:Command to setup config, get and set config fields' 'restore-keys:Restoring keys from given password at given depth' @@ -56,6 +57,9 @@ _wallet() { ata) _wallet_ata ;; + vault) + _wallet_vault + ;; config) _wallet_config ;; @@ -442,6 +446,41 @@ _wallet_ata() { esac } +# vault subcommand +_wallet_vault() { + local -a subcommands + + _arguments -C \ + '1: :->subcommand' \ + '*:: :->args' + + case $state in + subcommand) + subcommands=( + 'transfer:Transfer native tokens from sender to recipient vault account' + 'claim:Claim native tokens from account vault account' + 'help:Print this message or the help of the given subcommand(s)' + ) + _describe -t subcommands 'vault subcommands' subcommands + ;; + args) + case $line[1] in + transfer) + _arguments \ + '--from[Source account with privacy prefix or label]:from:_wallet_account_ids' \ + '--to[Recipient account with privacy prefix or label]:to:_wallet_account_ids' \ + '--amount[Amount of native tokens to transfer]:amount:' + ;; + claim) + _arguments \ + '--account-id[Account with privacy prefix or label; vault id is derived automatically]:account:_wallet_account_ids' \ + '--amount[Amount of native tokens to claim]:amount:' + ;; + esac + ;; + esac +} + # config subcommand _wallet_config() { local -a subcommands @@ -515,6 +554,7 @@ _wallet_help() { 'token:Token program interaction subcommand' 'amm:AMM program interaction subcommand' 'ata:Associated Token Account program interaction subcommand' + 'vault:Vault program interaction subcommand' 'check-health:Check the wallet can connect to the node' 'config:Command to setup config, get and set config fields' 'restore-keys:Restoring keys from given password at given depth' diff --git a/integration_tests/tests/vault.rs b/integration_tests/tests/vault.rs new file mode 100644 index 00000000..33d2e9a9 --- /dev/null +++ b/integration_tests/tests/vault.rs @@ -0,0 +1,208 @@ +#![expect( + clippy::tests_outside_test_module, + reason = "We don't care about these in tests" +)] + +use anyhow::{Context as _, Result}; +use integration_tests::{TestContext, private_mention, public_mention}; +use lee::program::Program; +use sequencer_service_rpc::RpcClient as _; +use tokio::test; +use wallet::cli::{Command, SubcommandReturnValue, programs::vault::VaultSubcommand}; + +#[test] +async fn public_transfer_and_public_claim() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let amount: u128 = 100; + let sender = ctx.existing_public_accounts()[0]; + let recipient = ctx.existing_public_accounts()[1]; + + let vault_program_id = Program::vault().id(); + let recipient_vault_id = vault_core::compute_vault_account_id(vault_program_id, recipient); + + let sender_balance_before = ctx.sequencer_client().get_account_balance(sender).await?; + let recipient_balance_before = ctx + .sequencer_client() + .get_account_balance(recipient) + .await?; + let recipient_vault_balance_before = ctx + .sequencer_client() + .get_account_balance(recipient_vault_id) + .await?; + + let transfer_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Vault(VaultSubcommand::Transfer { + from: public_mention(sender), + to: public_mention(recipient), + amount, + }), + ) + .await?; + assert!( + matches!(transfer_result, SubcommandReturnValue::Empty), + "Expected Empty return value for public vault transfer" + ); + + let sender_balance_after_transfer = ctx.sequencer_client().get_account_balance(sender).await?; + let recipient_balance_after_transfer = ctx + .sequencer_client() + .get_account_balance(recipient) + .await?; + let recipient_vault_balance_after_transfer = ctx + .sequencer_client() + .get_account_balance(recipient_vault_id) + .await?; + + assert_eq!( + sender_balance_after_transfer, + sender_balance_before - amount + ); + assert_eq!(recipient_balance_after_transfer, recipient_balance_before); + assert_eq!( + recipient_vault_balance_after_transfer, + recipient_vault_balance_before + amount + ); + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Vault(VaultSubcommand::Claim { + account_id: public_mention(recipient), + amount, + }), + ) + .await?; + assert!( + matches!(claim_result, SubcommandReturnValue::Empty), + "Expected Empty return value for public vault claim" + ); + + let sender_balance_after_claim = ctx.sequencer_client().get_account_balance(sender).await?; + let recipient_balance_after_claim = ctx + .sequencer_client() + .get_account_balance(recipient) + .await?; + let recipient_vault_balance_after_claim = ctx + .sequencer_client() + .get_account_balance(recipient_vault_id) + .await?; + + assert_eq!(sender_balance_after_claim, sender_balance_before - amount); + assert_eq!( + recipient_balance_after_claim, + recipient_balance_before + amount + ); + assert_eq!( + recipient_vault_balance_after_claim, + recipient_vault_balance_before + ); + + Ok(()) +} + +#[test] +async fn private_transfer_and_private_claim() -> Result<()> { + let mut ctx = TestContext::new().await?; + + let amount: u128 = 100; + let sender = ctx.existing_private_accounts()[0]; + let owner = ctx.existing_private_accounts()[1]; + + let vault_program_id = Program::vault().id(); + let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner); + + let sender_balance_before = ctx + .wallet() + .get_account_private(sender) + .context("Failed to load sender private account")? + .balance; + let owner_balance_before = ctx + .wallet() + .get_account_private(owner) + .context("Failed to load owner private account")? + .balance; + let owner_vault_balance_before = ctx + .sequencer_client() + .get_account_balance(owner_vault_id) + .await?; + + let transfer_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Vault(VaultSubcommand::Transfer { + from: private_mention(sender), + to: private_mention(owner), + amount, + }), + ) + .await?; + assert!( + matches!( + transfer_result, + SubcommandReturnValue::PrivacyPreservingTransfer { .. } + ), + "Expected PrivacyPreservingTransfer return value for private vault transfer" + ); + + let sender_balance_after_transfer = ctx + .wallet() + .get_account_private(sender) + .context("Failed to load sender private account after transfer")? + .balance; + let owner_balance_after_transfer = ctx + .wallet() + .get_account_private(owner) + .context("Failed to load owner private account after transfer")? + .balance; + let owner_vault_balance_after_transfer = ctx + .sequencer_client() + .get_account_balance(owner_vault_id) + .await?; + + assert_eq!( + sender_balance_after_transfer, + sender_balance_before - amount + ); + assert_eq!(owner_balance_after_transfer, owner_balance_before); + assert_eq!( + owner_vault_balance_after_transfer, + owner_vault_balance_before + amount + ); + + let claim_result = wallet::cli::execute_subcommand( + ctx.wallet_mut(), + Command::Vault(VaultSubcommand::Claim { + account_id: private_mention(owner), + amount, + }), + ) + .await?; + assert!( + matches!( + claim_result, + SubcommandReturnValue::PrivacyPreservingTransfer { .. } + ), + "Expected PrivacyPreservingTransfer return value for private vault claim" + ); + + let sender_balance_after_claim = ctx + .wallet() + .get_account_private(sender) + .context("Failed to load sender private account after claim")? + .balance; + let owner_balance_after_claim = ctx + .wallet() + .get_account_private(owner) + .context("Failed to load owner private account after claim")? + .balance; + let owner_vault_balance_after_claim = ctx + .sequencer_client() + .get_account_balance(owner_vault_id) + .await?; + + assert_eq!(sender_balance_after_claim, sender_balance_before - amount); + assert_eq!(owner_balance_after_claim, owner_balance_before + amount); + assert_eq!(owner_vault_balance_after_claim, owner_vault_balance_before); + + Ok(()) +} diff --git a/lez/wallet/Cargo.toml b/lez/wallet/Cargo.toml index 40222715..b6b8bdbd 100644 --- a/lez/wallet/Cargo.toml +++ b/lez/wallet/Cargo.toml @@ -18,6 +18,7 @@ token_core.workspace = true amm_core.workspace = true testnet_initial_state.workspace = true ata_core.workspace = true +vault_core.workspace = true bip39.workspace = true pyo3.workspace = true rpassword = "7" diff --git a/lez/wallet/src/cli/mod.rs b/lez/wallet/src/cli/mod.rs index 57c40b95..c30b8a56 100644 --- a/lez/wallet/src/cli/mod.rs +++ b/lez/wallet/src/cli/mod.rs @@ -22,7 +22,7 @@ use crate::{ programs::{ amm::AmmProgramAgnosticSubcommand, ata::AtaSubcommand, native_token_transfer::AuthTransferSubcommand, pinata::PinataProgramAgnosticSubcommand, - token::TokenProgramAgnosticSubcommand, + token::TokenProgramAgnosticSubcommand, vault::VaultSubcommand, }, }, storage::Storage, @@ -65,6 +65,9 @@ pub enum Command { /// Associated Token Account program interaction subcommand. #[command(subcommand)] Ata(AtaSubcommand), + /// Vault program interaction subcommand. + #[command(subcommand)] + Vault(VaultSubcommand), /// Group key management (create, invite, join, derive keys). #[command(subcommand)] Group(GroupSubcommand), @@ -254,6 +257,7 @@ pub async fn execute_subcommand( Command::Token(token_subcommand) => token_subcommand.handle_subcommand(wallet_core).await?, Command::AMM(amm_subcommand) => amm_subcommand.handle_subcommand(wallet_core).await?, Command::Ata(ata_subcommand) => ata_subcommand.handle_subcommand(wallet_core).await?, + Command::Vault(vault_subcommand) => vault_subcommand.handle_subcommand(wallet_core).await?, Command::Group(group_subcommand) => group_subcommand.handle_subcommand(wallet_core).await?, Command::Keycard(keycard_subcommand) => { keycard_subcommand.handle_subcommand(wallet_core).await? diff --git a/lez/wallet/src/cli/programs/mod.rs b/lez/wallet/src/cli/programs/mod.rs index f6e4b5dc..32133606 100644 --- a/lez/wallet/src/cli/programs/mod.rs +++ b/lez/wallet/src/cli/programs/mod.rs @@ -3,3 +3,4 @@ pub mod ata; pub mod native_token_transfer; pub mod pinata; pub mod token; +pub mod vault; diff --git a/lez/wallet/src/cli/programs/vault.rs b/lez/wallet/src/cli/programs/vault.rs new file mode 100644 index 00000000..777cc25b --- /dev/null +++ b/lez/wallet/src/cli/programs/vault.rs @@ -0,0 +1,140 @@ +use anyhow::Result; +use clap::Subcommand; +use common::transaction::LeeTransaction; +use lee::AccountId; + +use crate::{ + AccDecodeData::Decode, + WalletCore, + account::AccountIdWithPrivacy, + cli::{CliAccountMention, SubcommandReturnValue, WalletSubcommand}, + program_facades::vault::Vault, +}; + +/// Represents generic CLI subcommand for a wallet working with vault program. +#[derive(Subcommand, Debug, Clone)] +pub enum VaultSubcommand { + /// Transfer native tokens from sender to recipient's vault account. + Transfer { + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(long)] + from: CliAccountMention, + /// Either 32 byte base58 account id string with privacy prefix or a label. + #[arg(long)] + to: CliAccountMention, + /// Amount of native tokens to transfer. + #[arg(long)] + amount: u128, + }, + /// Claim native tokens from account's vault account. + Claim { + /// Either 32 byte base58 account id string with privacy prefix or a label. + /// + /// The owner's vault account id is computed from this account id. + #[arg(long)] + account_id: CliAccountMention, + /// Amount of native tokens to claim. + #[arg(long)] + amount: u128, + }, +} + +impl WalletSubcommand for VaultSubcommand { + async fn handle_subcommand( + self, + wallet_core: &mut WalletCore, + ) -> Result { + match self { + Self::Transfer { from, to, amount } => { + let from = from.resolve(wallet_core.storage())?; + let recipient = to.resolve(wallet_core.storage())?; + let recipient_id = account_id_without_privacy(recipient); + + match from { + AccountIdWithPrivacy::Public(sender_id) => { + let tx_hash = Vault(wallet_core) + .send_transfer(sender_id, recipient_id, amount) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + + println!("Transaction data is {transfer_tx:?}"); + + Ok(SubcommandReturnValue::Empty) + } + AccountIdWithPrivacy::Private(sender_id) => { + let (tx_hash, secret_sender) = Vault(wallet_core) + .send_transfer_private_sender(sender_id, recipient_id, amount) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + + println!("Transaction data is {transfer_tx:?}"); + + if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx { + wallet_core.decode_insert_privacy_preserving_transaction_results( + &tx, + &[Decode(secret_sender, sender_id)], + )?; + } + + wallet_core.store_persistent_data()?; + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + } + } + Self::Claim { account_id, amount } => { + let account_id = account_id.resolve(wallet_core.storage())?; + + match account_id { + AccountIdWithPrivacy::Public(owner_id) => { + let tx_hash = Vault(wallet_core).send_claim(owner_id, amount).await?; + + println!("Transaction hash is {tx_hash}"); + + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + + println!("Transaction data is {transfer_tx:?}"); + + Ok(SubcommandReturnValue::Empty) + } + AccountIdWithPrivacy::Private(owner_id) => { + let (tx_hash, secret_owner) = Vault(wallet_core) + .send_claim_private_owner(owner_id, amount) + .await?; + + println!("Transaction hash is {tx_hash}"); + + let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?; + + println!("Transaction data is {transfer_tx:?}"); + + if let LeeTransaction::PrivacyPreserving(tx) = transfer_tx { + wallet_core.decode_insert_privacy_preserving_transaction_results( + &tx, + &[Decode(secret_owner, owner_id)], + )?; + } + + wallet_core.store_persistent_data()?; + + Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) + } + } + } + } + } +} + +const fn account_id_without_privacy(account_id: AccountIdWithPrivacy) -> AccountId { + match account_id { + AccountIdWithPrivacy::Public(account_id) | AccountIdWithPrivacy::Private(account_id) => { + account_id + } + } +} diff --git a/lez/wallet/src/program_facades/mod.rs b/lez/wallet/src/program_facades/mod.rs index a0f8189c..cfeb54ad 100644 --- a/lez/wallet/src/program_facades/mod.rs +++ b/lez/wallet/src/program_facades/mod.rs @@ -6,3 +6,4 @@ pub mod ata; pub mod native_token_transfer; pub mod pinata; pub mod token; +pub mod vault; diff --git a/lez/wallet/src/program_facades/vault.rs b/lez/wallet/src/program_facades/vault.rs new file mode 100644 index 00000000..bccee4f2 --- /dev/null +++ b/lez/wallet/src/program_facades/vault.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; + +use common::HashType; +use lee::{ + AccountId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, +}; +use lee_core::SharedSecretKey; + +use crate::{AccountIdentity, ExecutionFailureKind, WalletCore}; + +pub struct Vault<'wallet>(pub &'wallet WalletCore); + +impl Vault<'_> { + pub async fn send_transfer( + &self, + sender_id: AccountId, + recipient_id: AccountId, + amount: u128, + ) -> Result { + let program = Program::vault(); + let vault_program_id = program.id(); + let recipient_vault_id = + vault_core::compute_vault_account_id(vault_program_id, recipient_id); + + let instruction = vault_core::Instruction::Transfer { + recipient_id, + amount, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + self.0 + .send_pub_tx( + vec![ + AccountIdentity::Public(sender_id), + AccountIdentity::PublicNoSign(recipient_vault_id), + ], + instruction_data, + &program.into(), + ) + .await + } + + pub async fn send_transfer_private_sender( + &self, + sender_id: AccountId, + recipient_id: AccountId, + amount: u128, + ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let vault_program_id = Program::vault().id(); + let recipient_vault_id = + vault_core::compute_vault_account_id(vault_program_id, recipient_id); + let instruction = vault_core::Instruction::Transfer { + recipient_id, + amount, + }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + self.0 + .send_privacy_preserving_tx( + vec![ + self.0 + .resolve_private_account(sender_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + AccountIdentity::Public(recipient_vault_id), + ], + instruction_data, + &vault_with_auth_dependency(), + ) + .await + .map(|(hash, mut secrets)| { + let secret = secrets.pop().expect("expected sender's secret"); + (hash, secret) + }) + } + + pub async fn send_claim( + &self, + owner_id: AccountId, + amount: u128, + ) -> Result { + let program = Program::vault(); + let vault_program_id = program.id(); + let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id); + + let instruction = vault_core::Instruction::Claim { amount }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + self.0 + .send_pub_tx( + vec![ + AccountIdentity::Public(owner_id), + AccountIdentity::PublicNoSign(owner_vault_id), + ], + instruction_data, + &program.into(), + ) + .await + } + + pub async fn send_claim_private_owner( + &self, + owner_id: AccountId, + amount: u128, + ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { + let vault_program_id = Program::vault().id(); + let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id); + + let instruction = vault_core::Instruction::Claim { amount }; + let instruction_data = + Program::serialize_instruction(instruction).expect("Instruction should serialize"); + + self.0 + .send_privacy_preserving_tx( + vec![ + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + AccountIdentity::Public(owner_vault_id), + ], + instruction_data, + &vault_with_auth_dependency(), + ) + .await + .map(|(hash, mut secrets)| { + let secret = secrets.pop().expect("expected owner's secret"); + (hash, secret) + }) + } +} + +fn vault_with_auth_dependency() -> ProgramWithDependencies { + let auth_transfer = Program::authenticated_transfer_program(); + let mut deps = HashMap::new(); + deps.insert(auth_transfer.id(), auth_transfer); + ProgramWithDependencies::new(Program::vault(), deps) +} diff --git a/test_fixtures/Cargo.toml b/test_fixtures/Cargo.toml index 1585cd99..54cb6733 100644 --- a/test_fixtures/Cargo.toml +++ b/test_fixtures/Cargo.toml @@ -17,7 +17,6 @@ lee_core = { workspace = true, features = ["host"] } sequencer_core = { workspace = true, features = ["default", "testnet"] } sequencer_service.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } -vault_core.workspace = true wallet.workspace = true anyhow.workspace = true diff --git a/test_fixtures/src/lib.rs b/test_fixtures/src/lib.rs index c632ba94..75394662 100644 --- a/test_fixtures/src/lib.rs +++ b/test_fixtures/src/lib.rs @@ -353,7 +353,7 @@ impl TestContextBuilder { ) .context("Failed to setup wallet")?; - setup_public_accounts_with_initial_supply(&wallet, &initial_public_accounts) + setup_public_accounts_with_initial_supply(&mut wallet, &initial_public_accounts) .await .context("Failed to initialize public accounts in wallet")?; diff --git a/test_fixtures/src/setup.rs b/test_fixtures/src/setup.rs index 597b0714..7eb0e1fd 100644 --- a/test_fixtures/src/setup.rs +++ b/test_fixtures/src/setup.rs @@ -1,19 +1,22 @@ -use std::{collections::HashMap, net::SocketAddr, path::PathBuf}; +use std::{net::SocketAddr, path::PathBuf}; use anyhow::{Context as _, Result, bail}; -use common::transaction::LeeTransaction; use indexer_service::IndexerHandle; -use lee::{AccountId, PrivateKey, PublicKey, PublicTransaction, program::Program}; +use lee::{AccountId, PrivateKey, PublicKey}; use log::{debug, warn}; use sequencer_service::{GenesisAction, SequencerHandle}; -use sequencer_service_rpc::RpcClient as _; use tempfile::TempDir; use testcontainers::compose::DockerCompose; -use wallet::{AccDecodeData::Decode, AccountIdentity, WalletCore, config::WalletConfigOverrides}; +use wallet::{ + WalletCore, + cli::{Command, SubcommandReturnValue, programs::vault::VaultSubcommand}, + config::WalletConfigOverrides, +}; use crate::{ BEDROCK_SERVICE_PORT, BEDROCK_SERVICE_WITH_OPEN_PORT, config::{self, InitialPrivateAccountForWallet}, + private_mention, public_mention, }; pub async fn setup_bedrock_node() -> Result<(DockerCompose, SocketAddr)> { @@ -185,7 +188,7 @@ pub fn setup_wallet( } pub async fn setup_public_accounts_with_initial_supply( - wallet: &WalletCore, + wallet: &mut WalletCore, initial_public_accounts: &[(PrivateKey, u128)], ) -> Result<()> { for (private_key, amount) in initial_public_accounts { @@ -219,45 +222,23 @@ pub async fn setup_private_accounts_with_initial_supply( } async fn claim_funds_from_vault( - wallet: &WalletCore, + wallet: &mut WalletCore, owner_id: AccountId, amount: u128, ) -> Result<()> { - let vault_program_id = Program::vault().id(); - let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id); - - let nonces = wallet - .get_accounts_nonces(vec![owner_id]) - .await - .context("Failed to fetch owner nonce")?; - - let signing_key = wallet - .storage() - .key_chain() - .pub_account_signing_key(owner_id) - .with_context(|| format!("Missing signing key for public account {owner_id}"))?; - - let message = lee::public_transaction::Message::try_new( - vault_program_id, - vec![owner_id, owner_vault_id], - nonces, - vault_core::Instruction::Claim { amount }, + let result = wallet::cli::execute_subcommand( + wallet, + Command::Vault(VaultSubcommand::Claim { + account_id: public_mention(owner_id), + amount, + }), ) - .context("Failed to build vault claim message")?; + .await + .context("Failed to execute public vault claim command")?; - let witness_set = lee::public_transaction::WitnessSet::for_message(&message, &[signing_key]); - let tx = PublicTransaction::new(message, witness_set); - - let tx_hash = wallet - .sequencer_client - .send_transaction(LeeTransaction::Public(tx)) - .await - .context("Failed to submit vault claim transaction")?; - - wallet - .poll_native_token_transfer(tx_hash) - .await - .context("Failed to confirm vault claim transaction")?; + let SubcommandReturnValue::Empty = result else { + bail!("Expected Empty return value for public vault claim"); + }; Ok(()) } @@ -271,55 +252,19 @@ async fn claim_funds_from_vault_to_private( bail!("Missing private account in wallet key chain for account {owner_id}"); }; - let vault_program = Program::vault(); - let vault_program_id = vault_program.id(); - let owner_vault_id = vault_core::compute_vault_account_id(vault_program_id, owner_id); + let result = wallet::cli::execute_subcommand( + wallet, + Command::Vault(VaultSubcommand::Claim { + account_id: private_mention(owner_id), + amount, + }), + ) + .await + .context("Failed to execute private vault claim command")?; - let instruction_data = - Program::serialize_instruction(vault_core::Instruction::Claim { amount }) - .context("Failed to serialize vault private claim instruction")?; - - let program_with_dependencies = - lee::privacy_preserving_transaction::circuit::ProgramWithDependencies::new( - vault_program, - HashMap::from([( - Program::authenticated_transfer_program().id(), - Program::authenticated_transfer_program(), - )]), - ); - - let (tx_hash, mut secrets) = wallet - .send_privacy_preserving_tx( - vec![ - AccountIdentity::PrivateOwned(owner_id), - AccountIdentity::Public(owner_vault_id), - ], - instruction_data, - &program_with_dependencies, - ) - .await - .context("Failed to submit private vault claim transaction")?; - - let secret = secrets - .pop() - .context("Expected one private output secret for vault claim")?; - - let transfer_tx = wallet - .poll_native_token_transfer(tx_hash) - .await - .context("Failed to confirm private vault claim transaction")?; - - let LeeTransaction::PrivacyPreserving(tx) = transfer_tx else { - bail!("Expected privacy preserving transaction result for private vault claim"); + let SubcommandReturnValue::PrivacyPreservingTransfer { .. } = result else { + bail!("Expected PrivacyPreservingTransfer return value for private vault claim"); }; - wallet - .decode_insert_privacy_preserving_transaction_results(&tx, &[Decode(secret, owner_id)]) - .context("Failed to decode private vault claim transaction")?; - - wallet - .store_persistent_data() - .context("Failed to store wallet data after private vault claim")?; - Ok(()) }