diff --git a/integration_tests/tests/private_pda.rs b/integration_tests/tests/private_pda.rs index f9969d98..208c6c15 100644 --- a/integration_tests/tests/private_pda.rs +++ b/integration_tests/tests/private_pda.rs @@ -57,6 +57,7 @@ async fn fund_private_pda( Program::serialize_instruction((seed, amount, auth_transfer_id, true)) .context("failed to serialize pda_fund_spend_proxy fund instruction")?, proxy_program, + &None, ) .await .map_err(|e| anyhow::anyhow!("{e}"))?; @@ -93,6 +94,7 @@ async fn spend_private_pda( Program::serialize_instruction((seed, amount, auth_transfer_id, false)) .context("failed to serialize pda_fund_spend_proxy instruction")?, spend_program, + &None, ) .await .map_err(|e| anyhow::anyhow!("{e}"))?; diff --git a/keycard_tests_2.sh b/keycard_tests_2.sh index bb15588c..1c38f571 100644 --- a/keycard_tests_2.sh +++ b/keycard_tests_2.sh @@ -100,29 +100,38 @@ wallet token mint \ --amount 0 echo "LEZ holding initialized for keycard path 6" -# Keycard path 7: LEE holding +# Keycard path 7: LEE holding (different definition — safe to submit immediately) wallet token mint \ --definition "m/44'/60'/0'/0/4" \ --holder "m/44'/60'/0'/0/7" \ --amount 0 echo "LEE holding initialized for keycard path 7" -# pub-receiver: public LEZ holding (for token transfer test) +# Wait for path2 (LEZ def) and path4 (LEE def) nonces to be confirmed before reusing them +sleep 15 + +# pub-receiver: public LEZ holding wallet token mint \ --definition "m/44'/60'/0'/0/2" \ --holder pub-receiver \ --amount 0 echo "LEZ holding initialized for pub-receiver" -# AMM seed accounts -wallet token mint \ - --definition "m/44'/60'/0'/0/2" \ - --holder amm-lez-fund \ - --amount 0 +# amm-lee-fund: LEE holding (different definition — safe to submit with pub-receiver) wallet token mint \ --definition "m/44'/60'/0'/0/4" \ --holder amm-lee-fund \ --amount 0 +echo "LEE holding initialized for amm-lee-fund" + +# Wait for path2 nonce to be confirmed before the third LEZ mint +sleep 15 + +# amm-lez-fund: LEZ holding +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder amm-lez-fund \ + --amount 0 echo "AMM seed holdings initialized" # ============================================================================= @@ -143,6 +152,9 @@ wallet token send \ --amount 20000 echo "Transferred 20000 LEE → keycard path 7" +# Wait for path3 and path5 nonces to be confirmed before reusing them +sleep 15 + wallet token send \ --from "m/44'/60'/0'/0/3" \ --to amm-lez-fund \ @@ -292,19 +304,6 @@ wallet account get --account-id "m/44'/60'/0'/0/7" # ============================================================================= # (9) Add liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8) # ============================================================================= -echo "" -echo "=== (9) Initialize LP holding (keycard path 8) before add-liquidity ===" -wallet token mint \ - --definition "Public/$LP_DEF_ID" \ - --holder "m/44'/60'/0'/0/8" \ - --amount 0 -echo "Keycard path 8 (LP holding) initialized" - -sleep 15 - -echo "Keycard path 8 (LP holding) state (after init):" -wallet account get --account-id "m/44'/60'/0'/0/8" - echo "" echo "=== (9) Add liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ===" wallet amm add-liquidity \ diff --git a/keycard_wallet/src/lib.rs b/keycard_wallet/src/lib.rs index efa77812..9d75b575 100644 --- a/keycard_wallet/src/lib.rs +++ b/keycard_wallet/src/lib.rs @@ -3,11 +3,14 @@ use std::path::PathBuf; use nssa::{AccountId, PublicKey, Signature}; use nssa_core::NullifierPublicKey; use pyo3::{prelude::*, types::PyAny}; -use zeroize::Zeroizing; use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; pub mod python_path; +/// NSK and VSK as fixed-length zeroizing byte arrays. +type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>); + // TODO: encrypt at rest alongside broader wallet storage encryption work. #[derive(Serialize, Deserialize)] pub struct KeycardPairingData { @@ -140,6 +143,10 @@ impl KeycardWallet { }) } + #[expect( + clippy::arithmetic_side_effects, + reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]" + )] pub fn sign_message_for_path( &self, py: Python, @@ -152,12 +159,19 @@ impl KeycardWallet { .call_method1("sign_message_for_path", (message, path))? .extract()?; - // The keycard Python library strips the leading zero from the S component when - // S < 2^248. Re-insert it so the slice is always the expected 64 bytes (R || S). - let py_signature = if py_signature.len() == 63 { + // The keycard Python library strips leading zeros from S when S < 2^(8k) for some k. + // Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S). + let py_signature = if py_signature.len() < 64 { + if py_signature.len() < 32 { + return Err(PyErr::new::(format!( + "signature from keycard too short: {} bytes", + py_signature.len() + ))); + } + let s_stripped = &py_signature[32..]; let mut padded = [0_u8; 64]; padded[..32].copy_from_slice(&py_signature[..32]); - padded[33..].copy_from_slice(&py_signature[32..]); + padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped); padded.to_vec() } else { py_signature @@ -212,11 +226,7 @@ impl KeycardWallet { Ok(format!("Public/{}", AccountId::from(&public_key))) } - pub fn get_private_keys_for_path( - &self, - py: Python, - path: &str, - ) -> PyResult<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>)> { + pub fn get_private_keys_for_path(&self, py: Python, path: &str) -> PyResult { let (raw_nsk, raw_vsk): (Vec, Vec) = self .instance .bind(py) @@ -233,7 +243,7 @@ impl KeycardWallet { raw_nsk.len() ))); } - let mut arr = Zeroizing::new([0u8; 32]); + let mut arr = Zeroizing::new([0_u8; 32]); arr.copy_from_slice(&raw_nsk); arr }; @@ -245,7 +255,7 @@ impl KeycardWallet { raw_vsk.len() ))); } - let mut arr = Zeroizing::new([0u8; 32]); + let mut arr = Zeroizing::new([0_u8; 32]); arr.copy_from_slice(&raw_vsk); arr }; @@ -256,7 +266,7 @@ impl KeycardWallet { pub fn get_private_keys_for_path_with_connect( pin: &str, path: &str, - ) -> PyResult<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>)> { + ) -> PyResult { Python::with_gil(|py| { python_path::add_python_path(py)?; diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 27ad9b8b..5802a40c 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -89,7 +89,7 @@ impl PrivateAccountKind { /// Borsh layout (all integers little-endian, variant index is u8): /// /// ```text - /// Regular(ident): 0x00 || ident (16 LE) || [0u8; 64] + /// Regular(ident): 0x00 || ident (16 LE) || [0_u8; 64] /// Pda { program_id, seed, ident }: 0x01 || program_id (32) || seed (32) || ident (16 LE) /// ``` /// diff --git a/nssa/src/privacy_preserving_transaction/message.rs b/nssa/src/privacy_preserving_transaction/message.rs index 3a968bfb..34c4b47b 100644 --- a/nssa/src/privacy_preserving_transaction/message.rs +++ b/nssa/src/privacy_preserving_transaction/message.rs @@ -208,7 +208,7 @@ pub mod tests { let nonces_bytes: &[u8] = &[1, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // all remaining vec fields are empty: u32 len=0 let empty_vec_bytes: &[u8] = &[0_u8; 4]; - // validity windows: unbounded = {from: None (0u8), to: None (0u8)} + // validity windows: unbounded = {from: None (0_u8), to: None (0_u8)} let unbounded_window_bytes: &[u8] = &[0_u8; 2]; let expected_borsh_vec: Vec = [ diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 546997b0..1c532e1b 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -188,7 +188,10 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded( }; let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data }); let amount = u128::from_le_bytes(unsafe { *amount }); - let key_path = optional_c_str(key_path); + let from_mention = optional_c_str(key_path).map_or_else( + || CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)), + CliAccountMention::KeyPath, + ); let transfer = NativeTokenTransfer(&wallet); @@ -198,7 +201,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded( to_vpk, to_identifier, amount, - &key_path, + &from_mention, )) { Ok((tx_hash, _shared_key)) => { let tx_hash = CString::new(tx_hash.to_string()) @@ -466,11 +469,14 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned( let from_id = AccountId::new(unsafe { (*from).data }); let to_id = AccountId::new(unsafe { (*to).data }); let amount = u128::from_le_bytes(unsafe { *amount }); - let key_path = optional_c_str(key_path); + let from_mention = optional_c_str(key_path).map_or_else( + || CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)), + CliAccountMention::KeyPath, + ); let transfer = NativeTokenTransfer(&wallet); - match block_on(transfer.send_shielded_transfer(from_id, to_id, amount, &key_path)) { + match block_on(transfer.send_shielded_transfer(from_id, to_id, amount, &from_mention)) { Ok((tx_hash, _shared_key)) => { let tx_hash = CString::new(tx_hash.to_string()) .map_or(ptr::null_mut(), std::ffi::CString::into_raw); diff --git a/wallet/src/cli/keycard.rs b/wallet/src/cli/keycard.rs index 0234f6ff..7355afbc 100644 --- a/wallet/src/cli/keycard.rs +++ b/wallet/src/cli/keycard.rs @@ -159,8 +159,8 @@ impl WalletSubcommand for KeycardSubcommand { let (nsk, vsk) = KeycardWallet::get_private_keys_for_path_with_connect(&pin, &key_path) .map_err(anyhow::Error::from)?; - println!("NSK: {}", hex::encode(&*nsk)); - println!("VSK: {}", hex::encode(&*vsk)); + println!("NSK: {}", hex::encode(*nsk)); + println!("VSK: {}", hex::encode(*vsk)); Ok(SubcommandReturnValue::Empty) } } diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 4b3fabb8..572745b4 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -138,8 +138,10 @@ impl CliAccountMention { Self::KeyPath(path) => { let pin = read_pin()?; let id_str = - keycard_wallet::KeycardWallet::get_public_account_id_for_path_with_connect(&pin, path) - .map_err(anyhow::Error::from)?; + keycard_wallet::KeycardWallet::get_public_account_id_for_path_with_connect( + &pin, path, + ) + .map_err(anyhow::Error::from)?; AccountIdWithPrivacy::from_str(&id_str) .map_err(|e| anyhow::anyhow!("Invalid account id from keycard: {e}")) } @@ -158,7 +160,6 @@ impl CliAccountMention { Self::Id(_) | Self::Label(_) => None, } } - } impl FromStr for CliAccountMention { diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index c1c0de08..47af6f4b 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -171,10 +171,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { let a_id = user_holding_a.resolve(wallet_core.storage())?; let b_id = user_holding_b.resolve(wallet_core.storage())?; match (a_id, b_id) { - ( - AccountIdWithPrivacy::Public(a), - AccountIdWithPrivacy::Public(b), - ) => { + (AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => { Amm(wallet_core) .send_swap_exact_input( a, @@ -205,10 +202,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { let a_id = user_holding_a.resolve(wallet_core.storage())?; let b_id = user_holding_b.resolve(wallet_core.storage())?; match (a_id, b_id) { - ( - AccountIdWithPrivacy::Public(a), - AccountIdWithPrivacy::Public(b), - ) => { + (AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => { Amm(wallet_core) .send_swap_exact_output( a, @@ -256,6 +250,7 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { max_amount_b, &user_holding_a, &user_holding_b, + &user_holding_lp, ) .await?; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 90caaffb..ee7962b0 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -400,7 +400,12 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { wallet_core: &mut WalletCore, ) -> Result { match self { - Self::ShieldedOwned { from, to, amount, from_mention } => { + Self::ShieldedOwned { + from, + to, + amount, + from_mention, + } => { let (tx_hash, secret) = NativeTokenTransfer(wallet_core) .send_shielded_transfer(from, to, amount, &from_mention) .await?; diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 1844a628..4f202695 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -119,17 +119,16 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { let definition_account_id = definition_account_id.resolve(wallet_core.storage())?; let supply_account_id = supply_account_id.resolve(wallet_core.storage())?; let underlying_subcommand = match (definition_account_id, supply_account_id) { - ( - AccountIdWithPrivacy::Public(_), - AccountIdWithPrivacy::Public(_), - ) => TokenProgramSubcommand::Create( - CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { - definition_account_id: def_mention, - supply_account_id: sup_mention, - name, - total_supply, - }, - ), + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Create( + CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { + definition_account_id: def_mention, + supply_account_id: sup_mention, + name, + total_supply, + }, + ) + } ( AccountIdWithPrivacy::Public(definition_account_id), AccountIdWithPrivacy::Private(supply_account_id), @@ -230,6 +229,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { sender_account_id: from, recipient_account_id: to, balance_to_move: amount, + sender_mention: from_mention, }, ) } @@ -251,6 +251,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { recipient_vpk: to_vpk, recipient_identifier: to_identifier, balance_to_move: amount, + sender_mention: from_mention, }, ), }, @@ -267,14 +268,13 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { let definition = definition.resolve(wallet_core.storage())?; let holder = holder.resolve(wallet_core.storage())?; let underlying_subcommand = match (definition, holder) { - ( - AccountIdWithPrivacy::Public(definition), - AccountIdWithPrivacy::Public(_), - ) => TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken { - definition_account_id: definition, - holder_account_id: holder_mention, - amount, - }), + (AccountIdWithPrivacy::Public(definition), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken { + definition_account_id: definition, + holder_account_id: holder_mention, + amount, + }) + } ( AccountIdWithPrivacy::Private(definition), AccountIdWithPrivacy::Private(holder), @@ -338,16 +338,15 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { anyhow::bail!("List of public keys is uncomplete"); } (Some(holder), None, None) => match (definition, holder) { - ( - AccountIdWithPrivacy::Public(_), - AccountIdWithPrivacy::Public(_), - ) => TokenProgramSubcommand::Public( - TokenProgramSubcommandPublic::MintToken { - definition_account_id: def_mention, - holder_account_id: hol_mention.expect("matched Some branch"), - amount, - }, - ), + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { + TokenProgramSubcommand::Public( + TokenProgramSubcommandPublic::MintToken { + definition_account_id: def_mention, + holder_account_id: hol_mention.expect("matched Some branch"), + amount, + }, + ) + } ( AccountIdWithPrivacy::Private(definition), AccountIdWithPrivacy::Private(holder), @@ -568,6 +567,8 @@ pub enum TokenProgramSubcommandShielded { recipient_account_id: AccountId, #[arg(short, long)] balance_to_move: u128, + #[arg(skip)] + sender_mention: CliAccountMention, }, // Transfer tokens using the token program TransferTokenShieldedForeign { @@ -584,6 +585,8 @@ pub enum TokenProgramSubcommandShielded { recipient_identifier: Option, #[arg(short, long)] balance_to_move: u128, + #[arg(skip)] + sender_mention: CliAccountMention, }, // Burn tokens using the token program BurnTokenShielded { @@ -689,7 +692,11 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { } => { let sender = sender_account_id.resolve(wallet_core.storage())?; let recipient = recipient_account_id.resolve(wallet_core.storage())?; - let (AccountIdWithPrivacy::Public(sender_id), AccountIdWithPrivacy::Public(recipient_id)) = (sender, recipient) else { + let ( + AccountIdWithPrivacy::Public(sender_id), + AccountIdWithPrivacy::Public(recipient_id), + ) = (sender, recipient) + else { anyhow::bail!("Only public accounts supported for token transfer"); }; Token(wallet_core) @@ -713,7 +720,12 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { anyhow::bail!("Only public holder account supported for token burn"); }; Token(wallet_core) - .send_burn_transaction(definition_account_id, holder_id, amount, &holder_account_id) + .send_burn_transaction( + definition_account_id, + holder_id, + amount, + &holder_account_id, + ) .await?; Ok(SubcommandReturnValue::Empty) } @@ -724,11 +736,19 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { } => { let definition = definition_account_id.resolve(wallet_core.storage())?; let holder = holder_account_id.resolve(wallet_core.storage())?; - let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(holder_id)) = (definition, holder) else { + let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(holder_id)) = + (definition, holder) + else { anyhow::bail!("Only public accounts supported for token mint"); }; Token(wallet_core) - .send_mint_transaction(def_id, holder_id, amount, &definition_account_id, &holder_account_id) + .send_mint_transaction( + def_id, + holder_id, + amount, + &definition_account_id, + &holder_account_id, + ) .await?; Ok(SubcommandReturnValue::Empty) } @@ -1049,6 +1069,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_vpk, recipient_identifier, balance_to_move, + sender_mention, } => { let recipient_npk_res = hex::decode(recipient_npk)?; let mut recipient_npk = [0; 32]; @@ -1069,6 +1090,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { recipient_vpk, recipient_identifier.unwrap_or_else(rand::random), balance_to_move, + &sender_mention, ) .await?; @@ -1088,12 +1110,14 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { sender_account_id, recipient_account_id, balance_to_move, + sender_mention, } => { let (tx_hash, secret_recipient) = Token(wallet_core) .send_transfer_transaction_shielded_owned_account( sender_account_id, recipient_account_id, balance_to_move, + &sender_mention, ) .await?; @@ -1332,7 +1356,9 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { } => { let definition = definition_account_id.resolve(wallet_core.storage())?; let supply = supply_account_id.resolve(wallet_core.storage())?; - let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(sup_id)) = (definition, supply) else { + let (AccountIdWithPrivacy::Public(def_id), AccountIdWithPrivacy::Public(sup_id)) = + (definition, supply) + else { anyhow::bail!("Only public accounts supported for new token definition"); }; Token(wallet_core) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index d66bccfa..36492c31 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1,5 +1,6 @@ #![expect( clippy::print_stdout, + clippy::print_stderr, reason = "This is a CLI application, printing to stdout and stderr is expected and convenient" )] #![expect( @@ -17,10 +18,12 @@ use key_protocol::key_management::key_tree::chain_index::ChainIndex; use keycard_wallet::KeycardWallet; use log::info; use nssa::{ - Account, AccountId, PrivacyPreservingTransaction, PublicKey, Signature, + Account, AccountId, PrivacyPreservingTransaction, PublicKey, PublicTransaction, Signature, privacy_preserving_transaction::{ circuit::ProgramWithDependencies, message::EncryptedAccountData, }, + program::Program, + public_transaction::WitnessSet as PublicWitnessSet, }; use nssa_core::{ Commitment, MembershipProof, SharedSecretKey, @@ -36,6 +39,7 @@ use crate::{ account::{AccountIdWithPrivacy, Label}, config::WalletConfigOverrides, poller::TxPoller, + signing::SigningGroups, storage::key_chain::SharedAccountEntry, }; @@ -83,6 +87,20 @@ pub enum ExecutionFailureKind { KeycardError(#[from] pyo3::PyErr), } +impl ExecutionFailureKind { + /// Convert an [`anyhow::Error`] (e.g. from [`SigningGroups`]) into a keycard error. + #[must_use] + #[expect( + clippy::needless_pass_by_value, + reason = "used as a method reference in map_err" + )] + pub fn from_anyhow(e: anyhow::Error) -> Self { + Self::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + } +} + #[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")] pub struct WalletCore { config_path: PathBuf, @@ -545,6 +563,62 @@ impl WalletCore { Ok(()) } + /// Send a public transaction, fetching nonces automatically from + /// [`SigningGroups::signing_ids`]. + pub async fn send_public_tx( + &self, + program: &Program, + account_ids: Vec, + instruction: T, + groups: SigningGroups, + ) -> Result { + let nonces = self + .get_accounts_nonces(groups.signing_ids()) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + self.send_public_tx_with_nonces(program, account_ids, nonces, instruction, groups) + .await + } + + /// Send a public transaction with caller-supplied nonces. + /// + /// Use this when nonce fetching requires special handling (e.g. the AMM LP account + /// may not yet exist on-chain and needs a `Nonce(0)` fallback). + pub async fn send_public_tx_with_nonces( + &self, + program: &Program, + account_ids: Vec, + nonces: Vec, + instruction: T, + groups: SigningGroups, + ) -> Result { + let message = nssa::public_transaction::Message::try_new( + program.id(), + account_ids, + nonces, + instruction, + )?; + + let pin = if groups.needs_pin() { + crate::helperfunctions::read_pin() + .map_err(ExecutionFailureKind::from_anyhow)? + .as_str() + .to_owned() + } else { + String::new() + }; + + let sigs = groups + .sign_all(&message.hash(), &pin) + .map_err(ExecutionFailureKind::from_anyhow)?; + + let tx = PublicTransaction::new(message, PublicWitnessSet::from_raw_parts(sigs)); + Ok(self + .sequencer_client + .send_transaction(NSSATransaction::Public(tx)) + .await?) + } + pub async fn send_privacy_preserving_tx( &self, accounts: Vec, @@ -574,36 +648,39 @@ impl WalletCore { let mut pre_states = acc_manager.pre_states(); - let (keycard_account, keycard_pin, keycard_path) = if let Some(key_path_str) = key_path.as_deref() { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let account_id_str = - KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)?; - let account_id: AccountId = - match account_id_str.parse::().expect("Valid parsing of account id") { + let (keycard_account, keycard_pin, keycard_path) = + if let Some(key_path_str) = key_path.as_deref() { + let pin = crate::helperfunctions::read_pin().map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< + pyo3::exceptions::PyRuntimeError, + _, + >(e.to_string())) + })?; + let account_id_str = + KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)?; + let account_id: AccountId = match account_id_str + .parse::() + .expect("Valid parsing of account id") + { AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id, }; - let account = self - .get_account_public(account_id) - .await - .expect("Expect valid account"); - let pin_str = pin.as_str().to_owned(); - ( - Some(AccountWithMetadata { - account, - is_authorized: true, - account_id, - }), - Some(pin_str), - Some(key_path_str.to_owned()), - ) - } else { - (None, None, None) - }; + let account = self + .get_account_public(account_id) + .await + .expect("Expect valid account"); + let pin_str = pin.as_str().to_owned(); + ( + Some(AccountWithMetadata { + account, + is_authorized: true, + account_id, + }), + Some(pin_str), + Some(key_path_str.to_owned()), + ) + } else { + (None, None, None) + }; let mut nonces: Vec = acc_manager.public_account_nonces().into_iter().collect(); @@ -653,35 +730,39 @@ impl WalletCore { ) .unwrap(); - let witness_set = if let (Some(pin), Some(path)) = - (keycard_pin.as_deref(), keycard_path.as_deref()) - { - let hash = message.hash(); - let local_auth = acc_manager.public_account_auth(); - let mut sigs: Vec<(Signature, PublicKey)> = local_auth - .iter() - .map(|&key| (Signature::new(key, &hash), PublicKey::new_from_private_key(key))) - .collect(); - let keycard_sig = pyo3::Python::with_gil(|py| { - let mut ctx = crate::signing::KeycardSessionContext::new(pin); - let result = ctx - .get_or_connect(py) - .and_then(|w| w.sign_message_for_path(py, path, &hash)); - ctx.close(py); - result - }) - .map_err(ExecutionFailureKind::KeycardError)?; - sigs.push(keycard_sig); - nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts( - sigs, proof, - ) - } else { - nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( - &message, - proof, - &acc_manager.public_account_auth(), - ) - }; + let witness_set = + if let (Some(pin), Some(path)) = (keycard_pin.as_deref(), keycard_path.as_deref()) { + let hash = message.hash(); + let local_auth = acc_manager.public_account_auth(); + let mut sigs: Vec<(Signature, PublicKey)> = local_auth + .iter() + .map(|&key| { + ( + Signature::new(key, &hash), + PublicKey::new_from_private_key(key), + ) + }) + .collect(); + let keycard_sig = pyo3::Python::with_gil(|py| { + let mut ctx = crate::signing::KeycardSessionContext::new(pin); + let result = ctx + .get_or_connect(py) + .and_then(|w| w.sign_message_for_path(py, path, &hash)); + ctx.close(py); + result + }) + .map_err(ExecutionFailureKind::KeycardError)?; + sigs.push(keycard_sig); + nssa::privacy_preserving_transaction::witness_set::WitnessSet::from_raw_parts( + sigs, proof, + ) + } else { + nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message( + &message, + proof, + &acc_manager.public_account_auth(), + ) + }; let tx = PrivacyPreservingTransaction::new(message, witness_set); let shared_secrets: Vec<_> = private_account_keys diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index 10355c33..797169ac 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -1,16 +1,9 @@ use amm_core::{compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda}; -use common::{HashType, transaction::NSSATransaction}; -use nssa::{AccountId, program::Program, public_transaction::WitnessSet}; -use pyo3::exceptions::PyRuntimeError; -use sequencer_service_rpc::RpcClient as _; +use common::HashType; +use nssa::{AccountId, program::Program}; use token_core::TokenHolding; -use crate::{ - ExecutionFailureKind, WalletCore, - cli::CliAccountMention, - helperfunctions::read_pin, - signing::SigningGroups, -}; +use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroups}; pub struct Amm<'wallet>(pub &'wallet WalletCore); impl Amm<'_> { @@ -73,41 +66,35 @@ impl Amm<'_> { .add_sender(a_mention, user_holding_a, self.0) .and_then(|()| groups.add_sender(b_mention, user_holding_b, self.0)) .and_then(|()| groups.add_recipient(lp_mention, user_holding_lp, self.0)) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let mut nonces = self.0.get_accounts_nonces(vec![user_holding_a, user_holding_b]).await + let mut nonces = self + .0 + .get_accounts_nonces(vec![user_holding_a, user_holding_b]) + .await .map_err(ExecutionFailureKind::SequencerError)?; if groups.signing_ids().contains(&user_holding_lp) { - let lp_nonces = self.0.get_accounts_nonces(vec![user_holding_lp]).await + let lp_nonces = self + .0 + .get_accounts_nonces(vec![user_holding_lp]) + .await .map_err(ExecutionFailureKind::SequencerError)?; - nonces.push(lp_nonces.into_iter().next().unwrap_or(nssa_core::account::Nonce(0))); + nonces.push( + lp_nonces + .into_iter() + .next() + .unwrap_or(nssa_core::account::Nonce(0)), + ); } else { println!( "Liquidity pool tokens receiver's account ({user_holding_lp}) private key not found in wallet. Proceeding with only liquidity provider's keys." ); } - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx_with_nonces(&program, account_ids, nonces, instruction, groups) + .await } #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] @@ -165,37 +152,18 @@ impl Amm<'_> { } else if definition_token_b_id == token_definition_id_in { (user_holding_b, b_mention) } else { - return Err(ExecutionFailureKind::AccountDataError(token_definition_id_in)); + return Err(ExecutionFailureKind::AccountDataError( + token_definition_id_in, + )); }; let mut groups = SigningGroups::new(); groups .add_sender(seller_mention, account_id_auth, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] @@ -253,37 +221,18 @@ impl Amm<'_> { } else if definition_token_b_id == token_definition_id_in { (user_holding_b, b_mention) } else { - return Err(ExecutionFailureKind::AccountDataError(token_definition_id_in)); + return Err(ExecutionFailureKind::AccountDataError( + token_definition_id_in, + )); }; let mut groups = SigningGroups::new(); groups .add_sender(seller_mention, account_id_auth, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] @@ -297,6 +246,7 @@ impl Amm<'_> { max_amount_to_add_token_b: u128, a_mention: &CliAccountMention, b_mention: &CliAccountMention, + lp_mention: &CliAccountMention, ) -> Result { let instruction = amm_core::Instruction::AddLiquidity { min_amount_liquidity, @@ -344,31 +294,36 @@ impl Amm<'_> { groups .add_sender(a_mention, user_holding_a, self.0) .and_then(|()| groups.add_sender(b_mention, user_holding_b, self.0)) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .and_then(|()| groups.add_recipient(lp_mention, user_holding_lp, self.0)) + .map_err(ExecutionFailureKind::from_anyhow)?; - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await + let mut nonces = self + .0 + .get_accounts_nonces(vec![user_holding_a, user_holding_b]) + .await .map_err(ExecutionFailureKind::SequencerError)?; - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() + if groups.signing_ids().contains(&user_holding_lp) { + let lp_nonces = self + .0 + .get_accounts_nonces(vec![user_holding_lp]) + .await + .map_err(ExecutionFailureKind::SequencerError)?; + nonces.push( + lp_nonces + .into_iter() + .next() + .unwrap_or(nssa_core::account::Nonce(0)), + ); } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + println!( + "LP holder's account ({user_holding_lp}) private key not found in wallet. Proceeding with only liquidity providers' keys." + ); + } - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx_with_nonces(&program, account_ids, nonces, instruction, groups) + .await } #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] @@ -427,30 +382,9 @@ impl Amm<'_> { let mut groups = SigningGroups::new(); groups .add_sender(lp_mention, user_holding_lp, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } } diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs index 13add7c8..7fc1f145 100644 --- a/wallet/src/program_facades/ata.rs +++ b/wallet/src/program_facades/ata.rs @@ -1,19 +1,14 @@ use std::collections::HashMap; use ata_core::{compute_ata_seed, get_associated_token_account_id}; -use common::{HashType, transaction::NSSATransaction}; +use common::HashType; use nssa::{ AccountId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program, - public_transaction::WitnessSet, }; use nssa_core::SharedSecretKey; -use pyo3::exceptions::PyRuntimeError; -use sequencer_service_rpc::RpcClient as _; use crate::{ - ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, - cli::CliAccountMention, - helperfunctions::read_pin, + ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention, signing::SigningGroups, }; @@ -39,26 +34,10 @@ impl Ata<'_> { let mut groups = SigningGroups::new(); groups .add_sender(owner_mention, owner_id, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction)?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } pub async fn send_transfer( @@ -77,31 +56,18 @@ impl Ata<'_> { ); let account_ids = vec![owner_id, sender_ata_id, recipient_id]; - let instruction = ata_core::Instruction::Transfer { ata_program_id, amount }; + let instruction = ata_core::Instruction::Transfer { + ata_program_id, + amount, + }; let mut groups = SigningGroups::new(); groups .add_sender(owner_mention, owner_id, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction)?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } pub async fn send_burn( @@ -119,31 +85,18 @@ impl Ata<'_> { ); let account_ids = vec![owner_id, holder_ata_id, definition_id]; - let instruction = ata_core::Instruction::Burn { ata_program_id, amount }; + let instruction = ata_core::Instruction::Burn { + ata_program_id, + amount, + }; let mut groups = SigningGroups::new(); groups .add_sender(owner_mention, owner_id, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction)?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) + .map_err(ExecutionFailureKind::from_anyhow)?; + self.0 + .send_public_tx(&program, account_ids, instruction, groups) + .await } pub async fn send_create_private_owner( @@ -170,7 +123,12 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency(), &None) + .send_privacy_preserving_tx( + accounts, + instruction_data, + &ata_with_token_dependency(), + &None, + ) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -207,7 +165,12 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency(), &None) + .send_privacy_preserving_tx( + accounts, + instruction_data, + &ata_with_token_dependency(), + &None, + ) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); @@ -243,7 +206,12 @@ impl Ata<'_> { ]; self.0 - .send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency(), &None) + .send_privacy_preserving_tx( + accounts, + instruction_data, + &ata_with_token_dependency(), + &None, + ) .await .map(|(hash, mut secrets)| { let secret = secrets.pop().expect("expected owner's secret"); diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index 60c42189..1f37c2d9 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -1,18 +1,9 @@ use authenticated_transfer_core::Instruction as AuthTransferInstruction; -use common::{HashType, transaction::NSSATransaction}; -use nssa::{ - AccountId, PublicTransaction, - program::Program, - public_transaction::{Message, WitnessSet}, -}; -use pyo3::exceptions::PyRuntimeError; -use sequencer_service_rpc::RpcClient as _; +use common::HashType; +use nssa::{AccountId, program::Program}; use super::NativeTokenTransfer; -use crate::{ - ExecutionFailureKind, cli::CliAccountMention, helperfunctions::read_pin, - signing::SigningGroups, -}; +use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroups}; impl NativeTokenTransfer<'_> { pub async fn send_public_transfer( @@ -27,52 +18,18 @@ impl NativeTokenTransfer<'_> { groups .add_sender(from_mention, from, self.0) .and_then(|()| groups.add_recipient(to_mention, to, self.0)) - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let program_id = Program::authenticated_transfer_program().id(); - let nonces = self - .0 - .get_accounts_nonces(groups.signing_ids()) + self.0 + .send_public_tx( + &Program::authenticated_transfer_program(), + vec![from, to], + AuthTransferInstruction::Transfer { + amount: balance_to_move, + }, + groups, + ) .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = Message::try_new( - program_id, - vec![from, to], - nonces, - AuthTransferInstruction::Transfer { - amount: balance_to_move, - }, - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })? - .as_str() - .to_owned() - } else { - String::new() - }; - - let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) - })?; - - let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) } pub async fn register_account( @@ -80,53 +37,18 @@ impl NativeTokenTransfer<'_> { from: AccountId, account_mention: &CliAccountMention, ) -> Result { - let nonces = self - .0 - .get_accounts_nonces(vec![from]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - - let account_ids = vec![from]; - let program_id = Program::authenticated_transfer_program().id(); - let message = Message::try_new( - program_id, - account_ids, - nonces, - AuthTransferInstruction::Initialize, - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - let mut groups = SigningGroups::new(); groups .add_sender(account_mention, from, self.0) - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })? - .as_str() - .to_owned() - } else { - String::new() - }; - - let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) - })?; - - let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx( + &Program::authenticated_transfer_program(), + vec![from], + AuthTransferInstruction::Initialize, + groups, + ) + .await } } diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index d1ccf37a..2cbd9f73 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,21 +1,16 @@ -use common::{HashType, transaction::NSSATransaction}; -use nssa::{AccountId, program::Program, public_transaction::WitnessSet}; +use common::HashType; +use nssa::{AccountId, program::Program}; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; -use pyo3::exceptions::PyRuntimeError; -use sequencer_service_rpc::RpcClient as _; use token_core::Instruction; use crate::{ - ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, - cli::CliAccountMention, - helperfunctions::read_pin, + ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention, signing::SigningGroups, }; pub struct Token<'wallet>(pub &'wallet WalletCore); impl Token<'_> { - #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_new_definition( &self, definition_account_id: AccountId, @@ -26,37 +21,17 @@ impl Token<'_> { supply_mention: &CliAccountMention, ) -> Result { let account_ids = vec![definition_account_id, supply_account_id]; - let program_id = nssa::program::Program::token().id(); let instruction = Instruction::NewFungibleDefinition { name, total_supply }; let mut groups = SigningGroups::new(); groups .add_sender(definition_mention, definition_account_id, self.0) .and_then(|()| groups.add_sender(supply_mention, supply_account_id, self.0)) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - let message = nssa::public_transaction::Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx(&Program::token(), account_ids, instruction, groups) + .await } pub async fn send_new_definition_private_owned_supply( @@ -168,38 +143,19 @@ impl Token<'_> { recipient_mention: &CliAccountMention, ) -> Result { let account_ids = vec![sender_account_id, recipient_account_id]; - let program_id = nssa::program::Program::token().id(); - let instruction = Instruction::Transfer { amount_to_transfer: amount }; + let instruction = Instruction::Transfer { + amount_to_transfer: amount, + }; let mut groups = SigningGroups::new(); groups .add_sender(sender_mention, sender_account_id, self.0) .and_then(|()| groups.add_recipient(recipient_mention, recipient_account_id, self.0)) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx(&Program::token(), account_ids, instruction, groups) + .await } pub async fn send_transfer_transaction_private_owned_account( @@ -315,12 +271,14 @@ impl Token<'_> { sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, + sender_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let instruction = Instruction::Transfer { amount_to_transfer: amount, }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); + let key_path = sender_mention.key_path().map(str::to_owned); self.0 .send_privacy_preserving_tx( @@ -332,7 +290,7 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, + &key_path, ) .await .map(|(resp, secrets)| { @@ -351,12 +309,14 @@ impl Token<'_> { recipient_vpk: ViewingPublicKey, recipient_identifier: Identifier, amount: u128, + sender_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let instruction = Instruction::Transfer { amount_to_transfer: amount, }; let instruction_data = Program::serialize_instruction(instruction).expect("Instruction should serialize"); + let key_path = sender_mention.key_path().map(str::to_owned); self.0 .send_privacy_preserving_tx( @@ -370,7 +330,7 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, + &key_path, ) .await .map(|(resp, secrets)| { @@ -397,31 +357,11 @@ impl Token<'_> { let mut groups = SigningGroups::new(); groups .add_sender(holder_mention, holder_account_id, self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - let message = nssa::public_transaction::Message::try_new(Program::token().id(), account_ids, nonces, instruction) - .expect("Instruction should serialize"); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx(&Program::token(), account_ids, instruction, groups) + .await } pub async fn send_burn_transaction_private_owned_account( @@ -544,31 +484,11 @@ impl Token<'_> { groups .add_sender(definition_mention, definition_account_id, self.0) .and_then(|()| groups.add_recipient(holder_mention, holder_account_id, self.0)) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + .map_err(ExecutionFailureKind::from_anyhow)?; - let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await - .map_err(ExecutionFailureKind::SequencerError)?; - - let message = nssa::public_transaction::Message::try_new(Program::token().id(), account_ids, nonces, instruction).unwrap(); - - let pin = if groups.needs_pin() { - read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? - .as_str() - .to_owned() - } else { - String::new() - }; - let sigs = groups.sign_all(&message.hash(), &pin) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - - let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + self.0 + .send_public_tx(&Program::token(), account_ids, instruction, groups) + .await } pub async fn send_mint_transaction_private_owned_account( @@ -750,5 +670,4 @@ impl Token<'_> { (resp, first) }) } - } diff --git a/wallet/src/signing.rs b/wallet/src/signing.rs index d66d07a9..1a710950 100644 --- a/wallet/src/signing.rs +++ b/wallet/src/signing.rs @@ -112,75 +112,38 @@ impl SigningGroups { Ok(sigs) } - - /// Add a recipient. Same as [`add_sender`] but silently skips accounts with no local - /// key and no keycard path — they are foreign and require neither a signature nor a nonce. - pub fn add_recipient( - &mut self, - mention: &CliAccountMention, - account_id: AccountId, - wallet_core: &WalletCore, - ) -> Result<()> { - if let CliAccountMention::KeyPath(path) = mention { - self.keycard.push((account_id, path.clone())); - return Ok(()); - } - if let Some(key) = wallet_core - .storage() - .key_chain() - .pub_account_signing_key(account_id) - { - self.local.push((account_id, key.clone())); - } - Ok(()) - } - - /// Returns `true` when a PIN is required (at least one keycard signer is present). - #[must_use] - pub const fn needs_pin(&self) -> bool { - !self.keycard.is_empty() - } - - /// Account IDs that require a nonce (every non-foreign signer). - #[must_use] - pub fn signing_ids(&self) -> Vec { - self.local - .iter() - .map(|(id, _)| *id) - .chain(self.keycard.iter().map(|(id, _)| *id)) - .collect() - } - - /// Sign `hash` for every account in the group. - /// - /// Local accounts are signed in pure Rust. Keycard accounts share one Python session. - pub fn sign_all(&self, hash: &[u8; 32], pin: &str) -> Result> { - let mut sigs: Vec<(Signature, PublicKey)> = self - .local - .iter() - .map(|(_, key)| { - ( - Signature::new(key, hash), - PublicKey::new_from_private_key(key), - ) - }) - .collect(); - - if !self.keycard.is_empty() { - pyo3::Python::with_gil(|py| -> pyo3::PyResult<()> { - python_path::add_python_path(py)?; - let wallet = KeycardWallet::new(py)?; - wallet.connect(py, pin)?; - for (_, path) in &self.keycard { - sigs.push(wallet.sign_message_for_path(py, path, hash)?); - } - drop(wallet.close_session(py)); - Ok(()) - }) - .map_err(anyhow::Error::from)?; - } - - Ok(sigs) - } } +/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction. +pub struct KeycardSessionContext { + pin: String, + wallet: Option, +} + +impl KeycardSessionContext { + pub fn new(pin: impl Into) -> Self { + Self { + pin: pin.into(), + wallet: None, + } + } + + pub fn get_or_connect<'py>( + &'py mut self, + py: Python<'py>, + ) -> pyo3::PyResult<&'py KeycardWallet> { + if self.wallet.is_none() { + python_path::add_python_path(py)?; + let wallet = KeycardWallet::new(py)?; + wallet.connect(py, &self.pin)?; + self.wallet = Some(wallet); + } + Ok(self.wallet.as_ref().unwrap()) + } + + pub fn close(self, py: Python<'_>) { + if let Some(w) = self.wallet { + drop(w.close_session(py)); + } + } +}