diff --git a/integration_tests/tests/amm.rs b/integration_tests/tests/amm.rs index 2d5a7132..475d748a 100644 --- a/integration_tests/tests/amm.rs +++ b/integration_tests/tests/amm.rs @@ -120,6 +120,8 @@ async fn amm_public() -> Result<()> { name: "A NAM1".to_owned(), total_supply: 37, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; info!("Waiting for next block creation"); @@ -152,6 +154,8 @@ async fn amm_public() -> Result<()> { name: "A NAM2".to_owned(), total_supply: 37, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; info!("Waiting for next block creation"); @@ -205,6 +209,8 @@ async fn amm_public() -> Result<()> { user_holding_lp_label: None, balance_a: 3, balance_b: 3, + user_holding_a_key_path: None, + user_holding_b_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -250,6 +256,7 @@ async fn amm_public() -> Result<()> { amount_in: 2, min_amount_out: 1, token_definition: definition_account_id_1.to_string(), + key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -295,6 +302,7 @@ async fn amm_public() -> Result<()> { amount_in: 2, min_amount_out: 1, token_definition: definition_account_id_2.to_string(), + key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -342,6 +350,8 @@ async fn amm_public() -> Result<()> { min_amount_lp: 1, max_amount_a: 2, max_amount_b: 2, + user_holding_a_key_path: None, + user_holding_b_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -389,6 +399,7 @@ async fn amm_public() -> Result<()> { balance_lp: 2, min_amount_a: 1, min_amount_b: 1, + user_holding_lp_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; @@ -545,6 +556,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { supply_account_label: None, name: "TOKEN1".to_owned(), total_supply: 10, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -572,6 +585,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { supply_account_label: None, name: "TOKEN2".to_owned(), total_supply: 10, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; @@ -601,6 +616,8 @@ async fn amm_new_pool_using_labels() -> Result<()> { user_holding_lp_label: Some(holding_lp_label), balance_a: 3, balance_b: 3, + user_holding_a_key_path: None, + user_holding_b_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::AMM(subcommand)).await?; tokio::time::sleep(Duration::from_secs(TIME_TO_WAIT_FOR_BLOCK_SECONDS)).await; diff --git a/integration_tests/tests/ata.rs b/integration_tests/tests/ata.rs index 06b0ac3d..b702acf5 100644 --- a/integration_tests/tests/ata.rs +++ b/integration_tests/tests/ata.rs @@ -74,6 +74,8 @@ async fn create_ata_initializes_holding_account() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -85,8 +87,9 @@ async fn create_ata_initializes_holding_account() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), + owner: Some(format_public_account_id(owner_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -138,6 +141,8 @@ async fn create_ata_is_idempotent() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply: 100, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -149,8 +154,9 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), + owner: Some(format_public_account_id(owner_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -162,8 +168,9 @@ async fn create_ata_is_idempotent() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(owner_account_id), + owner: Some(format_public_account_id(owner_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -218,6 +225,8 @@ async fn transfer_and_burn_via_ata() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -240,16 +249,18 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(sender_account_id), + owner: Some(format_public_account_id(sender_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(recipient_account_id), + owner: Some(format_public_account_id(recipient_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -284,10 +295,11 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Send { - from: format_public_account_id(sender_account_id), + from: Some(format_public_account_id(sender_account_id)), token_definition: definition_account_id.to_string(), to: recipient_ata_id.to_string(), amount: transfer_amount, + from_key_path: None, }), ) .await?; @@ -322,9 +334,10 @@ async fn transfer_and_burn_via_ata() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Burn { - holder: format_public_account_id(sender_account_id), + holder: Some(format_public_account_id(sender_account_id)), token_definition: definition_account_id.to_string(), amount: burn_amount, + key_path: None, }), ) .await?; @@ -379,6 +392,8 @@ async fn create_ata_with_private_owner() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply: 100, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -390,8 +405,9 @@ async fn create_ata_with_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(owner_account_id), + owner: Some(format_private_account_id(owner_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -453,6 +469,8 @@ async fn transfer_via_ata_private_owner() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -475,16 +493,18 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(sender_account_id), + owner: Some(format_private_account_id(sender_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_public_account_id(recipient_account_id), + owner: Some(format_public_account_id(recipient_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -519,10 +539,11 @@ async fn transfer_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Send { - from: format_private_account_id(sender_account_id), + from: Some(format_private_account_id(sender_account_id)), token_definition: definition_account_id.to_string(), to: recipient_ata_id.to_string(), amount: transfer_amount, + from_key_path: None, }), ) .await?; @@ -582,6 +603,8 @@ async fn burn_via_ata_private_owner() -> Result<()> { supply_account_label: None, name: "TEST".to_owned(), total_supply, + definition_key_path: None, + supply_key_path: None, }), ) .await?; @@ -600,8 +623,9 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Create { - owner: format_private_account_id(holder_account_id), + owner: Some(format_private_account_id(holder_account_id)), token_definition: definition_account_id.to_string(), + key_path: None, }), ) .await?; @@ -636,9 +660,10 @@ async fn burn_via_ata_private_owner() -> Result<()> { wallet::cli::execute_subcommand( ctx.wallet_mut(), Command::Ata(AtaSubcommand::Burn { - holder: format_private_account_id(holder_account_id), + holder: Some(format_private_account_id(holder_account_id)), token_definition: definition_account_id.to_string(), amount: burn_amount, + key_path: None, }), ) .await?; diff --git a/integration_tests/tests/token.rs b/integration_tests/tests/token.rs index 8c3023f5..b4c9ae2e 100644 --- a/integration_tests/tests/token.rs +++ b/integration_tests/tests/token.rs @@ -83,6 +83,8 @@ async fn create_and_transfer_public_token() -> Result<()> { definition_account_label: None, supply_account_id: Some(format_public_account_id(supply_account_id)), supply_account_label: None, + definition_key_path: None, + supply_key_path: None, name: name.clone(), total_supply, }; @@ -233,6 +235,8 @@ async fn create_and_transfer_public_token() -> Result<()> { holder_vpk: None, holder_identifier: None, amount: mint_amount, + definition_key_path: None, + holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -336,6 +340,8 @@ async fn create_and_transfer_token_with_private_supply() -> Result<()> { definition_account_label: None, supply_account_id: Some(format_private_account_id(supply_account_id)), supply_account_label: None, + definition_key_path: None, + supply_key_path: None, name: name.clone(), total_supply, }; @@ -502,6 +508,8 @@ async fn create_token_with_private_definition() -> Result<()> { definition_account_label: None, supply_account_id: Some(format_public_account_id(supply_account_id)), supply_account_label: None, + definition_key_path: None, + supply_key_path: None, name: name.clone(), total_supply, }; @@ -577,6 +585,8 @@ async fn create_token_with_private_definition() -> Result<()> { holder_vpk: None, holder_identifier: None, amount: mint_amount_public, + definition_key_path: None, + holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -626,6 +636,8 @@ async fn create_token_with_private_definition() -> Result<()> { holder_vpk: None, holder_identifier: None, amount: mint_amount_private, + definition_key_path: None, + holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -706,6 +718,8 @@ async fn create_token_with_private_definition_and_supply() -> Result<()> { supply_account_label: None, name, total_supply, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -885,6 +899,8 @@ async fn shielded_token_transfer() -> Result<()> { supply_account_label: None, name, total_supply, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1014,6 +1030,8 @@ async fn deshielded_token_transfer() -> Result<()> { supply_account_label: None, name, total_supply, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1127,6 +1145,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { supply_account_label: None, name, total_supply, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1169,6 +1189,8 @@ async fn token_claiming_path_with_private_accounts() -> Result<()> { holder_vpk: Some(hex::encode(holder_keys.viewing_public_key.0)), holder_identifier: Some(holder_identifier), amount: mint_amount, + definition_key_path: None, + holder_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1254,6 +1276,8 @@ async fn create_token_using_labels() -> Result<()> { supply_account_label: Some(supply_label), name: name.clone(), total_supply, + definition_key_path: None, + supply_key_path: None, }; wallet::cli::execute_subcommand(ctx.wallet_mut(), Command::Token(subcommand)).await?; @@ -1354,6 +1378,8 @@ async fn transfer_token_using_from_label() -> Result<()> { definition_account_label: None, supply_account_id: Some(format_public_account_id(supply_account_id)), supply_account_label: None, + definition_key_path: None, + supply_key_path: None, name: "LABEL TEST TOKEN".to_owned(), total_supply, }; diff --git a/keycard_tests.sh b/keycard_tests.sh index 09fb4268..9ec79cf6 100644 --- a/keycard_tests.sh +++ b/keycard_tests.sh @@ -1,63 +1,70 @@ -# Run wallet_with_keycard.sh first +#!/usr/bin/env bash +# keycard_tests.sh — end-to-end keycard + token + AMM tests. +# +# Prerequisites: +# 1. Run wallet_with_keycard.sh once to install dependencies. +# 2. Reset the local chain so all accounts are uninitialized. +# 3. Keycard reader inserted with card loaded (wallet keycard load has been run). +# +# Non-keycard account-creation commands use "|| true" because label conflicts are +# harmless on re-runs against the same wallet storage — the existing labeled account +# (which is uninitialized on a fresh chain) is reused. -source venv/bin/activate # Load the appropriate virtual environment +source venv/bin/activate export KEYCARD_PIN=111111 -# Tests wallet keycard available -# - Checks whether smart reader and keycard are both available. -echo "Test: wallet keycard available" +# ============================================================================= +# Keycard setup +# ============================================================================= +echo "=== Test: wallet keycard available ===" wallet keycard available -echo 'Test: wallet keycard load --mnemonic "final empty hair duty next drastic normal miss wreck wreck strategy omit"' -# Install a new mnemonic phrase to keycard +echo "=== Test: wallet keycard load ===" wallet keycard load --mnemonic "fashion degree mountain wool question damp current pond grow dolphin chronic then" -# Commented out to avoid resetting card constantly -echo "Test: wallet auth-transfer --key-path \"m/44'/60/0\'/0/0\"" +# Register keycard account at path 0. +# auth-transfer init is idempotent: skips gracefully if nonce > 0. +echo "=== Test: auth-transfer init path 0 ===" wallet auth-transfer init --key-path "m/44'/60'/0'/0/0" -echo "Test: wallet account get --key-path \"m/44'/60'/0'/0/0\"" +echo "=== Test: account get path 0 ===" wallet account get --key-path "m/44'/60'/0'/0/0" -echo "Test: wallet pinata claim --key-path \"m/44'/60'/0'/0/0\"" +echo "=== Test: pinata claim path 0 ===" wallet pinata claim --key-path "m/44'/60'/0'/0/0" - -echo "Test: wallet account get --key-path \"m/44'/60'/0'/0/0\"" +echo "=== Test: account get path 0 (after claim) ===" wallet account get --key-path "m/44'/60'/0'/0/0" -echo "Initialize new account (auth-transfer init) and send" +echo "=== Test: auth-transfer init path 1 ===" wallet auth-transfer init --key-path "m/44'/60'/0'/0/1" -wallet auth-transfer send --amount 40 --from-key-path "m/44'/60'/0'/0/0" --to-key-path "m/44'/60'/0'/0/1" -echo "Test: wallet account get --key-path \"m/44'/60'/0'/0/0\"" +echo "=== Test: auth-transfer send path 0 → path 1 ===" +wallet auth-transfer send --amount 40 \ + --from-key-path "m/44'/60'/0'/0/0" \ + --to-key-path "m/44'/60'/0'/0/1" + +echo "=== Test: account get path 0 ===" wallet account get --key-path "m/44'/60'/0'/0/0" - -echo "Test: wallet account get --key-path \"m/44'/60'/0'/0/1\"" +echo "=== Test: account get path 1 ===" wallet account get --key-path "m/44'/60'/0'/0/1" +# ============================================================================= +# (1) Shielded auth-transfer to an owned private account; verify decoded state. +# +# Use --to-label (ShieldedOwned path) so the wallet decodes the received note +# after sync and the balance is visible locally. +# ============================================================================= +echo "" +echo "=== Test (1): Shielded auth-transfer to owned private account ===" +wallet account new private --label priv-target 2>/dev/null || true -# initialize account keys (outside of keycard) -# Eventually use for tokens and shielded -wallet account new private -wallet account new public -wallet account new public -wallet account new public -wallet account new public +wallet auth-transfer send --amount 40 \ + --from-key-path "m/44'/60'/0'/0/0" \ + --to-label priv-target +echo "Shielded auth-transfer sent" -# Initialize Token A -wallet token new --definition-account-id "Public/4rXJzAEVn9Av1bK1RR4orTJP8dJDzRuoTBRsXVn1pwcK" --supply-account-id "Public/3PfkXqePVRnet5H1PbnfgeWykBrqX3KPPeMBESJt4QEd" --total-supply 1000 --name LEZT -echo "Token A" -# Initialize Token B -wallet token new --definition-account-id "Public/DjJx9ccoRyv1xxmHmpFy8mATeKq3Es1DnobjT4EZ4ab2" --supply-account-id "Public/EKgmwG9n7jMYkKaTYdZa7ELyYZq5f43oBKuCiu3t3Tm8" --total-supply 1000 --name LEET -echo "Token B" -# Send Token A to a new wallet account - -# Send from non keycard account to an account owned by keycard. -#wallet token send --from "Public/3PfkXqePVRnet5H1PbnfgeWykBrqX3KPPeMBESJt4QEd" --to "Public/6iYPF671bMDEkADFvHgcJDrYHJMqZv6cYbxVMsUU7LFE" --amount 400 -# This fails due to lack of initialization for Token Account -#echo "Transfer 1" - -wallet auth-transfer send --amount 40 --from-key-path "m/44'/60'/0'/0/0" --to-npk "55204e2934045b044f06d8222b454d46b54788f33c7dec4f6733d441703bb0e6" --to-vpk "02a8626b0c0ad9383c5678dad48c3969b4174fb377cdb03a6259648032c774cec8" -echo "Transfer 2" +wallet account sync-private +echo "Private target account state (should show decoded balance = 40):" +wallet account get --account-label priv-target \ No newline at end of file diff --git a/keycard_tests_2.sh b/keycard_tests_2.sh new file mode 100644 index 00000000..509b6c09 --- /dev/null +++ b/keycard_tests_2.sh @@ -0,0 +1,188 @@ +source venv/bin/activate +export KEYCARD_PIN=111111 + +# ============================================================================= +# (2) Initialize token definitions + initial supply holdings for LEZ and LEE. +# All without keycard. +# ============================================================================= +echo "" +echo "=== Test (2): Create LEZ and LEE token definitions (without keycard) ===" + +wallet account new public --label lez-def 2>/dev/null || true +wallet account new public --label lez-supply 2>/dev/null || true +wallet account new public --label lee-def 2>/dev/null || true +wallet account new public --label lee-supply 2>/dev/null || true + +LEZ_DEF_ID=$(wallet account id --account-label lez-def) +LEE_DEF_ID=$(wallet account id --account-label lee-def) + +wallet token new \ + --definition-account-label lez-def \ + --supply-account-label lez-supply \ + --total-supply 100000 \ + --name LEZ +echo "LEZ token created" + +wallet token new \ + --definition-account-label lee-def \ + --supply-account-label lee-supply \ + --total-supply 100000 \ + --name LEE +echo "LEE token created" + +# ============================================================================= +# (3) Initialize LEE token holding accounts: +# - two public keycard holders (paths 2 and 3) +# - one private holder (without keycard) +# +# token init is idempotent: skips if the holder already has token data. +# ============================================================================= +echo "" +echo "=== Test (3): Initialize LEE token holding accounts ===" + +wallet token init \ + --definition-account-id "Public/$LEE_DEF_ID" \ + --holder-key-path "m/44'/60'/0'/0/2" +echo "LEE holding initialized for keycard m/44'/60'/0'/0/2" + +wallet token init \ + --definition-account-id "Public/$LEE_DEF_ID" \ + --holder-key-path "m/44'/60'/0'/0/3" +echo "LEE holding initialized for keycard m/44'/60'/0'/0/3" + +wallet account new private --label lee-priv-holder 2>/dev/null || true +wallet token init \ + --definition-account-id "Public/$LEE_DEF_ID" \ + --holder-account-label lee-priv-holder +echo "Private LEE holding initialized" + +# Fund the two keycard LEE holdings from the supply. +# Only the sender (lee-supply, stored key) needs to sign for a token Transfer +# to an already-initialized holding. +wallet token send \ + --from-label lee-supply \ + --to-key-path "m/44'/60'/0'/0/2" \ + --amount 5000 +echo "Transferred 5000 LEE → keycard path 2" + +wallet token send \ + --from-label lee-supply \ + --to-key-path "m/44'/60'/0'/0/3" \ + --amount 5000 +echo "Transferred 5000 LEE → keycard path 3" + +echo "Keycard path 2 LEE state (balance should be 5000):" +wallet account get --key-path "m/44'/60'/0'/0/2" +echo "Keycard path 3 LEE state (balance should be 5000):" +wallet account get --key-path "m/44'/60'/0'/0/3" + +# ============================================================================= +# (4) Shielded (public → private) LEE transfer from keycard holding to the +# private LEE holding account. +# ============================================================================= +echo "" +echo "=== Test (4): Shielded transfer keycard LEE holding → private LEE holding ===" + +wallet token send \ + --from-key-path "m/44'/60'/0'/0/2" \ + --to-label lee-priv-holder \ + --amount 500 +echo "Shielded transfer complete (500 LEE: path-2 keycard → private holder)" + +wallet account sync-private +echo "Private LEE holder state (balance should be 500):" +wallet account get --account-label lee-priv-holder + +# ============================================================================= +# (5) Create AMM pool for LEZ/LEE (without keycard) +# ============================================================================= +echo "" +echo "=== Test (5): Create AMM pool for LEZ/LEE (without keycard) ===" + +wallet account new public --label amm-lp-lez-holding 2>/dev/null || true +wallet account new public --label amm-lp-lee-holding 2>/dev/null || true +wallet account new public --label amm-lp-lp-holding 2>/dev/null || true + +wallet token init \ + --definition-account-id "Public/$LEZ_DEF_ID" \ + --holder-account-label amm-lp-lez-holding +wallet token init \ + --definition-account-id "Public/$LEE_DEF_ID" \ + --holder-account-label amm-lp-lee-holding + +wallet token send --from-label lez-supply --to-label amm-lp-lez-holding --amount 40000 +wallet token send --from-label lee-supply --to-label amm-lp-lee-holding --amount 40000 + +wallet amm new \ + --user-holding-a-label amm-lp-lez-holding \ + --user-holding-b-label amm-lp-lee-holding \ + --user-holding-lp-label amm-lp-lp-holding \ + --balance-a 40000 \ + --balance-b 40000 +echo "AMM pool created for LEZ/LEE" + +# ============================================================================= +# (6) Swaps, add liquidity, remove liquidity using keycard holding accounts. +# +# Path layout: +# path 2 → LEE holding (4500 LEE after step 4) +# path 3 → LEE holding (5000 LEE) +# path 4 → fresh; initialized below as LEZ holding (receives swapped LEZ) +# ============================================================================= +echo "" +echo "=== Test (6a): Initialize LEZ holding for keycard path 4 (swap output) ===" +wallet token init \ + --definition-account-id "Public/$LEZ_DEF_ID" \ + --holder-key-path "m/44'/60'/0'/0/4" +echo "LEZ holding initialized for keycard m/44'/60'/0'/0/4" + +# Resolve raw account IDs needed for the swap --user-holding-* args. +PATH2_ID=$(wallet account id --key-path "m/44'/60'/0'/0/2") +PATH3_ID=$(wallet account id --key-path "m/44'/60'/0'/0/3") +PATH4_ID=$(wallet account id --key-path "m/44'/60'/0'/0/4") + +echo "Path 2: $PATH2_ID Path 3: $PATH3_ID Path 4: $PATH4_ID" +echo "LEE def ID: $LEE_DEF_ID" + +echo "" +echo "=== Test (6b): Swap LEE → LEZ (path 2 sells LEE, path 4 receives LEZ) ===" +# user-holding-b (path 2) is the input (LEE); user-holding-a (path 4) receives LEZ. +# --key-path signs for the input account (path 2). +wallet amm swap-exact-input \ + --user-holding-a "Public/$PATH4_ID" \ + --user-holding-b "Public/$PATH2_ID" \ + --amount-in 500 \ + --min-amount-out 1 \ + --token-definition "$LEE_DEF_ID" \ + --key-path "m/44'/60'/0'/0/2" +echo "Swap LEE→LEZ complete via keycard" + +echo "Path 4 (LEZ) state:" +wallet account get --key-path "m/44'/60'/0'/0/4" +echo "Path 2 (LEE) state:" +wallet account get --key-path "m/44'/60'/0'/0/2" + +echo "" +echo "=== Test (6c): Add liquidity (path 4 LEZ + path 3 LEE) ===" +wallet amm add-liquidity \ + --user-holding-a-key-path "m/44'/60'/0'/0/4" \ + --user-holding-b-key-path "m/44'/60'/0'/0/3" \ + --user-holding-lp-label amm-lp-lp-holding \ + --min-amount-lp 1 \ + --max-amount-a 200 \ + --max-amount-b 200 +echo "Add liquidity complete via keycard" + +echo "" +echo "=== Test (6d): Remove liquidity (LP from amm-lp-lp-holding) ===" +wallet amm remove-liquidity \ + --user-holding-a-label amm-lp-lez-holding \ + --user-holding-b-label amm-lp-lee-holding \ + --user-holding-lp-label amm-lp-lp-holding \ + --balance-lp 1000 \ + --min-amount-a 1 \ + --min-amount-b 1 +echo "Remove liquidity complete" + +echo "" +echo "=== All keycard tests finished ===" diff --git a/keycard_wallet/src/lib.rs b/keycard_wallet/src/lib.rs index c27ecd1c..c825de28 100644 --- a/keycard_wallet/src/lib.rs +++ b/keycard_wallet/src/lib.rs @@ -91,6 +91,17 @@ 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 { + let mut padded = [0u8; 64]; + padded[..32].copy_from_slice(&py_signature[..32]); + padded[33..].copy_from_slice(&py_signature[32..]); + padded.to_vec() + } else { + py_signature + }; + let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec| { PyErr::new::(format!( "Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})", diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5ed99d90..0d1e8332 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -65,6 +65,20 @@ pub enum AccountSubcommand { #[arg(short, long)] label: String, }, + /// Print the raw account ID (without privacy prefix) for shell scripting. + /// + /// Example: LEE_DEF=$(wallet account id --account-label lee-def) + Id { + /// Account label. + #[arg(long, conflicts_with = "account_id", required_unless_present_any = ["account_id", "key_path"])] + account_label: Option, + /// Valid 32 byte base58 string with privacy prefix. + #[arg(long, conflicts_with = "account_label")] + account_id: Option, + /// Key path (uses Keycard). + #[arg(long, conflicts_with = "account_id", conflicts_with = "account_label")] + key_path: Option, + }, } /// Represents generic register CLI subcommand. @@ -461,6 +475,22 @@ impl WalletSubcommand for AccountSubcommand { Ok(SubcommandReturnValue::Empty) } + Self::Id { + account_label, + account_id, + key_path, + } => { + let resolved = resolve_id_or_label( + account_id, + account_label, + &wallet_core.storage.labels, + &wallet_core.storage.user_data, + key_path.as_deref(), + )?; + let (raw_id, _) = parse_addr_with_privacy_prefix(&resolved)?; + println!("{raw_id}"); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/wallet/src/cli/programs/ata.rs b/wallet/src/cli/programs/ata.rs index de46a898..f7ec4676 100644 --- a/wallet/src/cli/programs/ata.rs +++ b/wallet/src/cli/programs/ata.rs @@ -146,7 +146,12 @@ impl WalletSubcommand for AtaSubcommand { amount, from_key_path, } => { - let (from_str, from_privacy) = parse_addr_with_privacy_prefix(&from)?; + let from_resolved = match (from, from_key_path.as_deref()) { + (Some(f), _) => f, + (None, Some(kp)) => resolve_keycard_id(kp)?, + (None, None) => anyhow::bail!("Provide --from or --from-key-path"), + }; + let (from_str, from_privacy) = parse_addr_with_privacy_prefix(&from_resolved)?; let from_id: AccountId = from_str.parse()?; let definition_id: AccountId = token_definition.parse()?; let to_id: AccountId = to.parse()?; @@ -182,9 +187,14 @@ impl WalletSubcommand for AtaSubcommand { holder, token_definition, amount, - key_path + key_path, } => { - let (holder_str, holder_privacy) = parse_addr_with_privacy_prefix(&holder); + let holder_resolved = match (holder, key_path.as_deref()) { + (Some(h), _) => h, + (None, Some(kp)) => resolve_keycard_id(kp)?, + (None, None) => anyhow::bail!("Provide --holder or --key-path"), + }; + let (holder_str, holder_privacy) = parse_addr_with_privacy_prefix(&holder_resolved)?; let holder_id: AccountId = holder_str.parse()?; let definition_id: AccountId = token_definition.parse()?; diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index ef5eb731..1bc8508b 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -95,9 +95,25 @@ impl WalletSubcommand for AuthTransferSubcommand { let (account_id, addr_privacy) = parse_addr_with_privacy_prefix(&resolved)?; + // Skip if already registered — prevents a doomed on-chain rejection when the + // account already has nonce > 0 (which also avoids leaving the keycard in a + // mid-operation state that breaks subsequent signing calls). + let account_id_parsed: nssa::AccountId = account_id.parse()?; + let nonces = wallet_core + .get_accounts_nonces(vec![account_id_parsed]) + .await?; + if nonces.first().is_some_and(|n| n.0 > 0) { + println!( + "Account {account_id} is already registered with the auth-transfer \ + program (nonce={}). Skipping.", + nonces[0].0 + ); + return Ok(SubcommandReturnValue::Empty); + } + match addr_privacy { AccountPrivacyKind::Public => { - let account_id = account_id.parse()?; + let account_id = account_id_parsed; let tx_hash = NativeTokenTransfer(wallet_core) .register_account(account_id, key_path.as_deref()) @@ -112,7 +128,7 @@ impl WalletSubcommand for AuthTransferSubcommand { wallet_core.store_persistent_data().await?; } AccountPrivacyKind::Private => { - let account_id = account_id.parse()?; + let account_id = account_id_parsed; let (tx_hash, secret) = NativeTokenTransfer(wallet_core) .register_account_private(account_id, &key_path) diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index f491b765..ab76d6cf 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -94,11 +94,12 @@ pub enum TokenProgramAgnosticSubcommand { #[arg( long, conflicts_with = "from_label", - required_unless_present = "from_label" + conflicts_with = "from_key_path", + required_unless_present_any = ["from_label", "from_key_path"] )] from: Option, /// From account label (alternative to --from). - #[arg(long, conflicts_with = "from")] + #[arg(long, conflicts_with = "from", conflicts_with = "from_key_path")] from_label: Option, /// to - valid 32 byte base58 string with privacy prefix. #[arg(long, conflicts_with = "to_label")] @@ -179,11 +180,14 @@ pub enum TokenProgramAgnosticSubcommand { #[arg(long, conflicts_with = "definition", conflicts_with = "definition_label")] definition_key_path: Option, /// holder - valid 32 byte base58 string with privacy prefix. - #[arg(long, conflicts_with = "holder_label", required_unless_present_any = ["holder_label", "holder_key_path"])] + #[arg(long, conflicts_with = "holder_label", conflicts_with = "holder_key_path", required_unless_present_any = ["holder_label", "holder_key_path"])] holder: Option, /// Holder account label (alternative to --holder). - #[arg(long, conflicts_with = "holder")] + #[arg(long, conflicts_with = "holder", conflicts_with = "holder_key_path")] holder_label: Option, + /// Key path for the holder account (uses Keycard, for account ID resolution only). + #[arg(long, conflicts_with = "holder", conflicts_with = "holder_label")] + holder_key_path: Option, /// `holder_npk` - valid 32 byte hex string. #[arg(long)] holder_npk: Option, @@ -235,6 +239,26 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { let definition_account_id: AccountId = definition_id.parse()?; let holder_account_id: AccountId = holder_id.parse()?; + // Skip if the holder is already initialised — prevents a ZK-prove panic when + // the account already has token data (e.g. on re-runs against the same chain). + let already_initialized = match holder_privacy { + AccountPrivacyKind::Public => { + let account = wallet_core.get_account_public(holder_account_id).await?; + account != nssa::Account::default() + } + AccountPrivacyKind::Private => wallet_core + .storage + .user_data + .get_private_account(holder_account_id) + .is_some_and(|(_, acct, _)| acct != nssa::Account::default()), + }; + if already_initialized { + println!( + "Holder {holder_id} is already initialized as a token holding. Skipping." + ); + return Ok(SubcommandReturnValue::Empty); + } + let definition_account = match definition_privacy { AccountPrivacyKind::Public => { PrivacyPreservingAccount::Public(definition_account_id) @@ -453,6 +477,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { sender_account_id: from, recipient_account_id: to, balance_to_move: amount, + sender_key_path: from_key_path, }, ) } @@ -561,6 +586,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { definition_key_path, holder, holder_label, + holder_key_path, holder_npk, holder_vpk, holder_identifier, @@ -573,15 +599,16 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { &wallet_core.storage.user_data, definition_key_path.as_deref(), )?; - let holder = match (holder, holder_label) { - (v, None) => v, - (None, Some(label)) => Some(resolve_account_label( + let holder = match (holder, holder_label, holder_key_path.as_deref()) { + (v, None, None) => v, + (None, Some(label), None) => Some(resolve_account_label( &label, &wallet_core.storage.labels, &wallet_core.storage.user_data, )?), - (Some(_), Some(_)) => { - anyhow::bail!("Provide only one of --holder or --holder-label") + (None, None, Some(kp)) => Some(resolve_keycard_id(kp)?), + _ => { + anyhow::bail!("Provide only one of --holder, --holder-label, or --holder-key-path") } }; let underlying_subcommand = match (holder, holder_npk, holder_vpk) { @@ -839,6 +866,8 @@ pub enum TokenProgramSubcommandShielded { recipient_account_id: String, #[arg(short, long)] balance_to_move: u128, + #[arg(long)] + sender_key_path: Option, }, // Transfer tokens using the token program TransferTokenShieldedForeign { @@ -1383,6 +1412,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { sender_account_id, recipient_account_id, balance_to_move, + sender_key_path, } => { let sender_account_id: AccountId = sender_account_id.parse().unwrap(); let recipient_account_id: AccountId = recipient_account_id.parse().unwrap(); @@ -1392,6 +1422,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded { sender_account_id, recipient_account_id, balance_to_move, + sender_key_path, ) .await?; diff --git a/wallet/src/program_facades/amm.rs b/wallet/src/program_facades/amm.rs index f563ba46..2d0c178f 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -8,6 +8,7 @@ use crate::{ExecutionFailureKind, WalletCore}; pub struct Amm<'wallet>(pub &'wallet WalletCore); impl Amm<'_> { + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct; grouping into a struct would add unnecessary indirection")] pub async fn send_new_definition( &self, user_holding_a: AccountId, @@ -118,7 +119,7 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_a) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (nssa::Signature::new(&sk, &msg_hash), nssa::PublicKey::new_from_private_key(&sk)) + (nssa::Signature::new(sk, &msg_hash), nssa::PublicKey::new_from_private_key(sk)) }; let (sig_b, pk_b) = if let Some(kp) = key_path_b { @@ -134,15 +135,15 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_b) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (nssa::Signature::new(&sk, &msg_hash), nssa::PublicKey::new_from_private_key(&sk)) + (nssa::Signature::new(sk, &msg_hash), nssa::PublicKey::new_from_private_key(sk)) }; let mut sigs = vec![sig_a, sig_b]; let mut pks = vec![pk_a, pk_b]; if let Some(sk_lp) = lp_sk { - sigs.push(nssa::Signature::new(&sk_lp, &msg_hash)); - pks.push(nssa::PublicKey::new_from_private_key(&sk_lp)); + sigs.push(nssa::Signature::new(sk_lp, &msg_hash)); + pks.push(nssa::PublicKey::new_from_private_key(sk_lp)); } let witness_set = nssa::public_transaction::WitnessSet::from_list(&message, &sigs, &pks) @@ -361,6 +362,7 @@ impl Amm<'_> { .await?) } + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct; grouping into a struct would add unnecessary indirection")] pub async fn send_add_liquidity( &self, user_holding_a: AccountId, @@ -452,7 +454,7 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_a) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (nssa::Signature::new(&sk, &msg_hash), nssa::PublicKey::new_from_private_key(&sk)) + (nssa::Signature::new(sk, &msg_hash), nssa::PublicKey::new_from_private_key(sk)) }; let (sig_b, pk_b) = if let Some(kp) = key_path_b { @@ -468,7 +470,7 @@ impl Amm<'_> { .user_data .get_pub_account_signing_key(user_holding_b) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (nssa::Signature::new(&sk, &msg_hash), nssa::PublicKey::new_from_private_key(&sk)) + (nssa::Signature::new(sk, &msg_hash), nssa::PublicKey::new_from_private_key(sk)) }; let witness_set = @@ -484,6 +486,7 @@ impl Amm<'_> { .await?) } + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct; grouping into a struct would add unnecessary indirection")] pub async fn send_remove_liquidity( &self, user_holding_a: AccountId, diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index 7269cc16..939ba314 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -56,7 +56,7 @@ impl Token<'_> { .user_data .get_pub_account_signing_key(definition_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (Signature::new(&sk, &msg_hash), PublicKey::new_from_private_key(&sk)) + (Signature::new(sk, &msg_hash), PublicKey::new_from_private_key(sk)) }; let (sig_sup, pk_sup) = if let Some(kp) = supply_key_path { @@ -68,7 +68,7 @@ impl Token<'_> { .user_data .get_pub_account_signing_key(supply_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - (Signature::new(&sk, &msg_hash), PublicKey::new_from_private_key(&sk)) + (Signature::new(sk, &msg_hash), PublicKey::new_from_private_key(sk)) }; let witness_set = nssa::public_transaction::WitnessSet::from_list( @@ -191,17 +191,13 @@ impl Token<'_> { let instruction = Instruction::Transfer { amount_to_transfer: amount, }; - let mut nonces = self + // Only the sender authorises a token Transfer — the recipient holding must already be + // initialised (no recipient signature required, matching the burn pattern). + let nonces = self .0 .get_accounts_nonces(vec![sender_account_id]) .await .map_err(ExecutionFailureKind::SequencerError)?; - let recipient_nonces = self - .0 - .get_accounts_nonces(vec![recipient_account_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - nonces.extend(recipient_nonces); let message = nssa::public_transaction::Message::try_new( program_id, @@ -210,6 +206,7 @@ impl Token<'_> { instruction, ) .unwrap(); + let witness_set = if let Some(sender_key_path) = sender_key_path { let pin = crate::helperfunctions::read_pin().map_err(|e| { ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( @@ -220,39 +217,20 @@ impl Token<'_> { &pin, &sender_key_path, &message.hash(), - ) - .expect("Expect a valid signature"); + )?; WitnessSet::from_list(&message, &[signature], &[public_key]) + .map_err(ExecutionFailureKind::TransactionBuildError)? } else { - let mut private_keys = Vec::new(); let sender_sk = self .0 .storage .user_data .get_pub_account_signing_key(sender_account_id) .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - private_keys.push(sender_sk); - - if let Some(recipient_sk) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(recipient_account_id) - { - private_keys.push(recipient_sk); - } else { - println!( - "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." - ); - } - - Ok(nssa::public_transaction::WitnessSet::for_message( - &message, - &private_keys, - )) + nssa::public_transaction::WitnessSet::for_message(&message, &[sender_sk]) }; - let tx = nssa::PublicTransaction::new(message, witness_set.unwrap()); //TODO: Marvin + let tx = nssa::PublicTransaction::new(message, witness_set); Ok(self .0 @@ -366,6 +344,7 @@ impl Token<'_> { sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, + sender_key_path: Option, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let instruction = Instruction::Transfer { amount_to_transfer: amount, @@ -381,7 +360,7 @@ impl Token<'_> { ], instruction_data, &Program::token().into(), - &None, + &sender_key_path, ) .await .map(|(resp, secrets)| { @@ -503,7 +482,6 @@ impl Token<'_> { key_path, ) .await - .map(|(resp, secrets)| (resp, secrets)) } }