diff --git a/docs/LEZ testnet v0.1 tutorials/keycard.md b/docs/LEZ testnet v0.1 tutorials/keycard.md index 72f168d4..92224292 100644 --- a/docs/LEZ testnet v0.1 tutorials/keycard.md +++ b/docs/LEZ testnet v0.1 tutorials/keycard.md @@ -64,14 +64,13 @@ unset KEYCARD_PIN ### Keycard -| Command | Description | -|-----------------------------------|--------------------------------------------------------------------------| -| `wallet keycard available` | Checks whether a Keycard reader and card are accessible | -| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK | -| `wallet keycard connect` | Establishes and saves a pairing with the Keycard | -| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing | -| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard | -| `wallet keycard get-private-keys` | Retrieves private account keys (nsk, vsk) for a given BIP32 path | +| Command | Description | +|-----------------------------|------------------------------------------------------------| +| `wallet keycard available` | Checks whether a Keycard reader and card are accessible | +| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK | +| `wallet keycard connect` | Establishes and saves a pairing with the Keycard | +| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing | +| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard | 1. Check keycard availability ```bash @@ -114,17 +113,7 @@ Keycard PIN: ✅ Mnemonic phrase loaded successfully. ``` -5. Get private keys for a path -```bash -wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" - -# Output: -Keycard PIN: -nsk: 55e505bf925e536c843a12ebc08c41ca5f4761eeeb7fa33725f0b44e6f1ac2e4 -vsk: 30f798893977a7b7263d1f77abf58e11e014428c92030d6a02fe363cceb41ffa -``` - -6. Disconnect (unpair and clear saved pairing) +5. Disconnect (unpair and clear saved pairing) ```bash wallet keycard disconnect @@ -172,7 +161,7 @@ Transaction hash is 2c8a4f1e903d5b76e80214c5b82e1d46a105e28930ad71bcce48f2d07b49 | `wallet auth-transfer init` | Registers an account with the auth-transfer program | | `wallet auth-transfer send` | Sends native tokens between accounts | -`--account` (for `init`) and `--from`/`--to` (for `send`) each accept any of: +`--account-id` (for `init`) and `--from`/`--to` (for `send`) each accept any of: - A BIP32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`) - An account ID with privacy prefix (e.g. `Public/9bKm...`) - An account label (e.g. `my-account`) @@ -181,7 +170,7 @@ For `send`, foreign recipient accounts (not in the local wallet and not a Keycar 1. Initialize a Keycard public account ```bash -wallet auth-transfer init --account "m/44'/60'/0'/0/0" +wallet auth-transfer init --account-id "m/44'/60'/0'/0/0" # Output: Keycard PIN: diff --git a/keycard_tests.sh b/keycard_tests.sh index 808791bc..e5ac2f2c 100644 --- a/keycard_tests.sh +++ b/keycard_tests.sh @@ -16,8 +16,8 @@ export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond wallet keycard load unset KEYCARD_MNEMONIC -echo "Test: wallet auth-transfer init --account \"m/44'/60'/0'/0/0\"" -wallet auth-transfer init --account "m/44'/60'/0'/0/0" +echo "Test: wallet auth-transfer init --account-id \"m/44'/60'/0'/0/0\"" +wallet auth-transfer init --account-id "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "m/44'/60'/0'/0/0" @@ -29,7 +29,7 @@ echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "m/44'/60'/0'/0/0" echo "Test: wallet auth-transfer init and send between two keycard accounts" -wallet auth-transfer init --account "m/44'/60'/0'/0/1" +wallet auth-transfer init --account-id "m/44'/60'/0'/0/1" wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" @@ -38,9 +38,44 @@ wallet account get --account-id "m/44'/60'/0'/0/0" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" wallet account get --account-id "m/44'/60'/0'/0/1" +# Send from keycard account to a local wallet account +echo "Test: create local wallet account" +LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+') +echo "Created local account: Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet auth-transfer init local account" +wallet auth-transfer init --account-id "Public/${LOCAL_ACCOUNT_ID}" + + +echo "Test: wallet auth-transfer send from keycard to local account" +wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\"" +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +# Create a local wallet account, fund it, and send to keycard account (co-signed: local key + keycard) + +echo "Test: wallet auth-transfer send from local account to keycard account" +wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1" + +echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\"" +wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\"" +wallet account get --account-id "m/44'/60'/0'/0/1" + # Send from keycard account to a local wallet account (foreign recipient — no signature needed) +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" + echo "Test: wallet auth-transfer send from keycard to local account" wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "m/44'/60'/0'/0/0" + +echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" +wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" diff --git a/keycard_wallet/src/lib.rs b/keycard_wallet/src/lib.rs index 17c62efd..134b6538 100644 --- a/keycard_wallet/src/lib.rs +++ b/keycard_wallet/src/lib.rs @@ -67,7 +67,10 @@ impl KeycardWallet { ) -> PyResult { self.instance .bind(py) - .call_method1("setup_communication_with_pairing", (pin, index, key.to_vec()))? + .call_method1( + "setup_communication_with_pairing", + (pin, index, key.to_vec()), + )? .extract() } @@ -197,7 +200,11 @@ impl KeycardWallet { fn pairing_file_path() -> Option { let home = std::env::var("NSSA_WALLET_HOME_DIR") .map(PathBuf::from) - .or_else(|_| std::env::home_dir().map(|h| h.join(".nssa").join("wallet")).ok_or(())) + .or_else(|_| { + std::env::home_dir() + .map(|h| h.join(".nssa").join("wallet")) + .ok_or(()) + }) .ok()?; Some(home.join("keycard_pairing.json")) } diff --git a/wallet-ffi/src/transfer.rs b/wallet-ffi/src/transfer.rs index 39e9cf71..8f3c47d7 100644 --- a/wallet-ffi/src/transfer.rs +++ b/wallet-ffi/src/transfer.rs @@ -4,8 +4,7 @@ use std::{ffi::CString, ptr}; use nssa::AccountId; use wallet::{ - account::AccountIdWithPrivacy, - cli::CliAccountMention, + account::AccountIdWithPrivacy, cli::CliAccountMention, program_facades::native_token_transfer::NativeTokenTransfer, }; @@ -79,7 +78,13 @@ pub unsafe extern "C" fn wallet_ffi_transfer_public( let from_mention = CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)); let to_mention = CliAccountMention::Id(AccountIdWithPrivacy::Public(to_id)); - match block_on(transfer.send_public_transfer(from_id, to_id, amount, &from_mention, &to_mention)) { + match block_on(transfer.send_public_transfer( + from_id, + to_id, + amount, + &from_mention, + &to_mention, + )) { Ok(tx_hash) => { 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 0c8456d4..ead1e84b 100644 --- a/wallet/src/cli/keycard.rs +++ b/wallet/src/cli/keycard.rs @@ -52,7 +52,8 @@ impl WalletSubcommand for KeycardSubcommand { let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::connect`: invalid keycard wallet provided"); - wallet.connect(py, &pin) + wallet + .connect(py, &pin) .expect("`wallet::keycard::connect`: failed to connect to keycard"); println!("\u{2705} Keycard paired and ready."); @@ -70,10 +71,12 @@ impl WalletSubcommand for KeycardSubcommand { let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::disconnect`: invalid keycard wallet provided"); - wallet.connect(py, &pin) + wallet + .connect(py, &pin) .expect("`wallet::keycard::disconnect`: failed to open session"); - wallet.disconnect(py) + wallet + .disconnect(py) .expect("`wallet::keycard::disconnect`: failed to unpair keycard"); clear_pairing(); @@ -91,7 +94,8 @@ impl WalletSubcommand for KeycardSubcommand { let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::init`: invalid keycard wallet provided"); - let initialized = wallet.initialize(py, &pin) + let initialized = wallet + .initialize(py, &pin) .expect("`wallet::keycard::init`: failed to initialize keycard"); if initialized { @@ -112,7 +116,8 @@ impl WalletSubcommand for KeycardSubcommand { let wallet = KeycardWallet::new(py) .expect("`wallet::keycard::load`: invalid keycard wallet provided"); - wallet.connect(py, &pin) + wallet + .connect(py, &pin) .expect("`wallet::keycard::load`: failed to connect to keycard"); println!("\u{2705} Keycard is now connected to wallet."); diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index 852d9037..f76faf23 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -9,6 +9,7 @@ use futures::TryFutureExt as _; use nssa::{ProgramDeploymentTransaction, program::Program}; use sequencer_service_rpc::RpcClient as _; +pub use crate::helperfunctions::{read_mnemonic, read_pin}; use crate::{ WalletCore, account::{AccountIdWithPrivacy, Label}, @@ -27,8 +28,6 @@ use crate::{ storage::Storage, }; -pub use crate::helperfunctions::{read_mnemonic, read_pin}; - pub mod account; pub mod chain; pub mod config; @@ -138,8 +137,9 @@ impl CliAccountMention { .ok_or_else(|| anyhow::anyhow!("No account found for label `{label}`")), Self::KeyPath(path) => { let pin = read_pin()?; - let id_str = keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path) - .map_err(anyhow::Error::from)?; + let id_str = + keycard_wallet::KeycardWallet::get_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}")) } @@ -175,18 +175,25 @@ impl CliAccountMention { /// Resolve to an [`AccountSigner`] for a recipient — returns `Foreign` when the account /// has no local key and no keycard path, meaning no signature or nonce is required. - pub fn to_recipient_signer(&self, wallet_core: &WalletCore) -> Result { + pub fn to_recipient_signer( + &self, + wallet_core: &WalletCore, + ) -> Result { if let Self::KeyPath(path) = self { return Ok(crate::signing::AccountSigner::Keycard(path.clone())); } let account = self.resolve(wallet_core.storage())?; match account { - AccountIdWithPrivacy::Public(id) => { - Ok(match wallet_core.storage().key_chain().pub_account_signing_key(id) { + AccountIdWithPrivacy::Public(id) => Ok( + match wallet_core + .storage() + .key_chain() + .pub_account_signing_key(id) + { Some(_) => crate::signing::AccountSigner::Local(id), None => crate::signing::AccountSigner::Foreign, - }) - } + }, + ), AccountIdWithPrivacy::Private(_) => { anyhow::bail!("Private accounts not supported as recipients here") } diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index ac60534d..87c38bef 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -58,9 +58,9 @@ impl WalletSubcommand for AuthTransferSubcommand { Self::Init { account_id } => { let resolved = account_id.resolve(wallet_core.storage())?; match resolved { - AccountIdWithPrivacy::Public(account_id) => { + AccountIdWithPrivacy::Public(pub_account_id) => { let tx_hash = NativeTokenTransfer(wallet_core) - .register_account(account_id, &account) + .register_account(pub_account_id, &account_id) .await?; println!("Transaction hash is {tx_hash}"); @@ -124,7 +124,13 @@ impl WalletSubcommand for AuthTransferSubcommand { } (Some(to), None, None) => match (from, to) { (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => { - NativeTokenTransferProgramSubcommand::Public { from, to, amount } + NativeTokenTransferProgramSubcommand::Public { + from, + to, + amount, + from_mention: from_account, + to_mention: to_account.expect("matched Some branch"), + } } ( AccountIdWithPrivacy::Private(from), @@ -481,7 +487,13 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { Ok(SubcommandReturnValue::PrivacyPreservingTransfer { tx_hash }) } - Self::Public { from, to, amount, from_mention, to_mention } => { + Self::Public { + from, + to, + amount, + from_mention, + to_mention, + } => { let tx_hash = NativeTokenTransfer(wallet_core) .send_public_transfer(from, to, amount, &from_mention, &to_mention) .await?; diff --git a/wallet/src/program_facades/native_token_transfer/public.rs b/wallet/src/program_facades/native_token_transfer/public.rs index e819d376..de064272 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -10,9 +10,7 @@ use sequencer_service_rpc::RpcClient as _; use super::NativeTokenTransfer; use crate::{ - ExecutionFailureKind, - cli::CliAccountMention, - helperfunctions::read_pin, + ExecutionFailureKind, cli::CliAccountMention, helperfunctions::read_pin, signing::KeycardSessionContext, }; @@ -25,13 +23,12 @@ impl NativeTokenTransfer<'_> { from_mention: &CliAccountMention, to_mention: &CliAccountMention, ) -> Result { - - let from_signer = from_mention - .to_signer(self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let to_signer = to_mention - .to_recipient_signer(self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + let from_signer = from_mention.to_signer(self.0).map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) + })?; + let to_signer = to_mention.to_recipient_signer(self.0).map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) + })?; let account_ids = vec![from, to]; let signing_ids: Vec = if to_signer.needs_signature() { @@ -51,13 +48,19 @@ impl NativeTokenTransfer<'_> { program_id, account_ids, nonces, - AuthTransferInstruction::Transfer { amount: balance_to_move }, + AuthTransferInstruction::Transfer { + amount: balance_to_move, + }, ) .map_err(ExecutionFailureKind::TransactionBuildError)?; let pin = if from_mention.is_keycard() || to_mention.is_keycard() { read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + })? .as_str() .to_owned() } else { @@ -93,34 +96,6 @@ impl NativeTokenTransfer<'_> { .sequencer_client .send_transaction(NSSATransaction::Public(tx)) .await?) - } else { - println!( - "Receiver's account ({to}) private key not found in wallet. Proceeding with only sender's key." - ); - } - - let message = Message::try_new( - program_id, - account_ids, - nonces, - AuthTransferInstruction::Transfer { - amount: balance_to_move, - }, - ) - .unwrap(); - let witness_set = WitnessSet::for_message(&message, &private_keys); - - let tx = PublicTransaction::new(message, witness_set); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) - } else { - Err(ExecutionFailureKind::InsufficientFundsError) - } - } pub async fn register_account( @@ -144,13 +119,17 @@ impl NativeTokenTransfer<'_> { ) .map_err(ExecutionFailureKind::TransactionBuildError)?; - let signer = account_mention - .to_signer(self.0) - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; + let signer = account_mention.to_signer(self.0).map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) + })?; let pin = if account_mention.is_keycard() { read_pin() - .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + })? .as_str() .to_owned() } else { diff --git a/wallet/src/signing.rs b/wallet/src/signing.rs index 911aadff..19d45fac 100644 --- a/wallet/src/signing.rs +++ b/wallet/src/signing.rs @@ -34,8 +34,19 @@ impl AccountSigner { ) -> Option> { match self { Self::Local(id) => { - let key = wallet_core.storage().key_chain().pub_account_signing_key(*id)?; - Some(Ok((Signature::new(key, hash), PublicKey::new_from_private_key(key)))) + let key = wallet_core + .storage() + .key_chain() + .pub_account_signing_key(*id); + Some(key.map_or_else( + || Err(anyhow::anyhow!("signing key not found for account {id}")), + |key| { + Ok(( + Signature::new(key, hash), + PublicKey::new_from_private_key(key), + )) + }, + )) } Self::Keycard(path) => Some( ctx.get_or_connect(py) @@ -55,10 +66,16 @@ pub struct KeycardSessionContext { impl KeycardSessionContext { pub fn new(pin: impl Into) -> Self { - Self { pin: pin.into(), wallet: None } + Self { + pin: pin.into(), + wallet: None, + } } - pub fn get_or_connect<'py>(&'py mut self, py: Python<'py>) -> pyo3::PyResult<&'py KeycardWallet> { + 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)?;