From 54f6d4922b30cd0d3ad8acd521a6217d716b7920 Mon Sep 17 00:00:00 2001 From: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> Date: Fri, 15 May 2026 18:15:54 -0400 Subject: [PATCH] updating logic --- Cargo.lock | 1 + integration_tests/src/setup.rs | 1 + keycard_test_3.sh | 4 +- keycard_tests.sh | 10 +- keycard_tests_2.sh | 210 ++++---- keycard_wallet/Cargo.toml | 3 +- keycard_wallet/src/lib.rs | 44 +- wallet/src/cli/account.rs | 14 + wallet/src/cli/keycard.rs | 32 ++ wallet/src/cli/mod.rs | 42 +- wallet/src/cli/programs/amm.rs | 98 ++-- wallet/src/cli/programs/ata.rs | 18 +- .../src/cli/programs/native_token_transfer.rs | 19 +- wallet/src/cli/programs/token.rs | 82 +++- wallet/src/helperfunctions.rs | 64 --- wallet/src/lib.rs | 74 ++- wallet/src/program_facades/amm.rs | 415 +++++----------- wallet/src/program_facades/ata.rs | 233 +++------ .../native_token_transfer/public.rs | 88 ++-- .../native_token_transfer/shielded.rs | 12 +- wallet/src/program_facades/token.rs | 453 ++++++------------ wallet/src/signing.rs | 144 ++++-- 22 files changed, 850 insertions(+), 1211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7c5fc32..8990e7de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4392,6 +4392,7 @@ dependencies = [ "pyo3", "serde", "serde_json", + "zeroize", ] [[package]] diff --git a/integration_tests/src/setup.rs b/integration_tests/src/setup.rs index c43590d0..116f2d96 100644 --- a/integration_tests/src/setup.rs +++ b/integration_tests/src/setup.rs @@ -298,6 +298,7 @@ async fn claim_funds_from_vault_to_private( ], instruction_data, &program_with_dependencies, + &None, ) .await .context("Failed to submit private vault claim transaction")?; diff --git a/keycard_test_3.sh b/keycard_test_3.sh index 5b9f70c7..bf13aa0e 100644 --- a/keycard_test_3.sh +++ b/keycard_test_3.sh @@ -9,10 +9,10 @@ source venv/bin/activate export KEYCARD_PIN=111111 echo "=== Test: wallet keycard get-private-keys path 10 ===" -wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/10" +wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/10" --reveal echo "=== Test: wallet keycard get-private-keys path 11 ===" -wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/11" +wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/11" --reveal echo "" echo "=== All get-private-keys tests finished ===" diff --git a/keycard_tests.sh b/keycard_tests.sh index 75b1738f..56e4fbc1 100644 --- a/keycard_tests.sh +++ b/keycard_tests.sh @@ -83,17 +83,17 @@ echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\"" wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo" echo "=== Test: account get path 0 ===" -wallet account get --key-path "m/44'/60'/0'/0/0" +wallet account get --account-id "m/44'/60'/0'/0/0" echo "=== Test: account get path 1 ===" -wallet account get --key-path "m/44'/60'/0'/0/1" +wallet account get --account-id "m/44'/60'/0'/0/1" echo "" echo "=== Test (1): Shielded auth-transfer to owned private account ===" wallet auth-transfer send --amount 2 \ - --from-key-path "m/44'/60'/0'/0/0" \ + --from "m/44'/60'/0'/0/0" \ --to-npk "55204e2934045b044f06d8222b454d46b54788f33c7dec4f6733d441703bb0e6" \ --to-vpk "02a8626b0c0ad9383c5678dad48c3969b4174fb377cdb03a6259648032c774cec8" echo "Shielded auth-transfer sent" -sleep 5 -wallet account get --key-path "m/44'/60'/0'/0/0" \ No newline at end of file +sleep 15 +wallet account get --account-id "m/44'/60'/0'/0/0" \ No newline at end of file diff --git a/keycard_tests_2.sh b/keycard_tests_2.sh index 9c82efa2..bb15588c 100644 --- a/keycard_tests_2.sh +++ b/keycard_tests_2.sh @@ -32,7 +32,9 @@ export KEYCARD_PIN=111111 echo "" echo "=== Keycard setup ===" wallet keycard available -wallet keycard load --mnemonic "fashion degree mountain wool question damp current pond grow dolphin chronic then" +export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then" +wallet keycard load +unset KEYCARD_MNEMONIC # ============================================================================= # Create non-keycard wallet accounts @@ -49,10 +51,10 @@ wallet account new public --label amm-lp-fund 2>/dev/null || true # (1) Create LEZ token — definition AND supply via keycard paths # ============================================================================= echo "" -echo "=== (1) Create LEZ token (keycard def=path2, supply=path1) ===" +echo "=== (1) Create LEZ token (keycard def=path2, supply=path3) ===" wallet token new \ - --definition-key-path "m/44'/60'/0'/0/2" \ - --supply-key-path "m/44'/60'/0'/0/3" \ + --definition-account-id "m/44'/60'/0'/0/2" \ + --supply-account-id "m/44'/60'/0'/0/3" \ --name LEZ \ --total-supply 100000 echo "LEZ token created" @@ -61,29 +63,29 @@ echo "LEZ token created" # (2) Create LEE token — definition AND supply via keycard paths # ============================================================================= echo "" -echo "=== (2) Create LEE token (keycard def=path4, supply=path3) ===" +echo "=== (2) Create LEE token (keycard def=path4, supply=path5) ===" wallet token new \ - --definition-key-path "m/44'/60'/0'/0/4" \ - --supply-key-path "m/44'/60'/0'/0/5" \ + --definition-account-id "m/44'/60'/0'/0/4" \ + --supply-account-id "m/44'/60'/0'/0/5" \ --name LEE \ --total-supply 100000 echo "LEE token created" sleep 15 -LEZ_DEF_ID=$(wallet account id --key-path "m/44'/60'/0'/0/2") -LEE_DEF_ID=$(wallet account id --key-path "m/44'/60'/0'/0/4") +LEZ_DEF_ID=$(wallet account id --account-id "m/44'/60'/0'/0/2") +LEE_DEF_ID=$(wallet account id --account-id "m/44'/60'/0'/0/4") echo "LEZ definition ID: $LEZ_DEF_ID" echo "LEE definition ID: $LEE_DEF_ID" echo "Keycard path 2 (LEZ definition) state:" -wallet account get --key-path "m/44'/60'/0'/0/2" +wallet account get --account-id "m/44'/60'/0'/0/2" echo "Keycard path 3 (LEZ supply) state:" -wallet account get --key-path "m/44'/60'/0'/0/3" +wallet account get --account-id "m/44'/60'/0'/0/3" echo "Keycard path 4 (LEE definition) state:" -wallet account get --key-path "m/44'/60'/0'/0/4" +wallet account get --account-id "m/44'/60'/0'/0/4" echo "Keycard path 5 (LEE supply) state:" -wallet account get --key-path "m/44'/60'/0'/0/5" +wallet account get --account-id "m/44'/60'/0'/0/5" # ============================================================================= # Initialize token holding accounts @@ -91,31 +93,36 @@ wallet account get --key-path "m/44'/60'/0'/0/5" echo "" echo "=== Initialize token holding accounts ===" -# Keycard path 6: LEZ holding -wallet token init \ - --definition-account-id "Public/$LEZ_DEF_ID" \ - --holder-key-path "m/44'/60'/0'/0/6" -echo "LEZ holding initialized for keycard path 4" +# Keycard path 6: LEZ holding (mint 0 to initialize) +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder "m/44'/60'/0'/0/6" \ + --amount 0 +echo "LEZ holding initialized for keycard path 6" # Keycard path 7: LEE holding -wallet token init \ - --definition-account-id "Public/$LEE_DEF_ID" \ - --holder-key-path "m/44'/60'/0'/0/7" -echo "LEE holding initialized for keycard path 5" +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) -wallet token init \ - --definition-account-id "Public/$LEZ_DEF_ID" \ - --holder-account-label pub-receiver +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 init \ - --definition-account-id "Public/$LEZ_DEF_ID" \ - --holder-account-label amm-lez-fund -wallet token init \ - --definition-account-id "Public/$LEE_DEF_ID" \ - --holder-account-label amm-lee-fund +wallet token mint \ + --definition "m/44'/60'/0'/0/2" \ + --holder amm-lez-fund \ + --amount 0 +wallet token mint \ + --definition "m/44'/60'/0'/0/4" \ + --holder amm-lee-fund \ + --amount 0 echo "AMM seed holdings initialized" # ============================================================================= @@ -125,39 +132,39 @@ echo "" echo "=== Fund keycard holdings and AMM seed accounts ===" wallet token send \ - --from-key-path "m/44'/60'/0'/0/3" \ - --to-key-path "m/44'/60'/0'/0/6" \ + --from "m/44'/60'/0'/0/3" \ + --to "m/44'/60'/0'/0/6" \ --amount 20000 -echo "Transferred 20000 LEZ → keycard path 4" +echo "Transferred 20000 LEZ → keycard path 6" wallet token send \ - --from-key-path "m/44'/60'/0'/0/5" \ - --to-key-path "m/44'/60'/0'/0/7" \ + --from "m/44'/60'/0'/0/5" \ + --to "m/44'/60'/0'/0/7" \ --amount 20000 -echo "Transferred 20000 LEE → keycard path 5" +echo "Transferred 20000 LEE → keycard path 7" wallet token send \ - --from-key-path "m/44'/60'/0'/0/3" \ - --to-label amm-lez-fund \ + --from "m/44'/60'/0'/0/3" \ + --to amm-lez-fund \ --amount 10000 echo "Transferred 10000 LEZ → amm-lez-fund" wallet token send \ - --from-key-path "m/44'/60'/0'/0/5" \ - --to-label amm-lee-fund \ + --from "m/44'/60'/0'/0/5" \ + --to amm-lee-fund \ --amount 10000 echo "Transferred 10000 LEE → amm-lee-fund" sleep 15 echo "Keycard path 6 (LEZ holding) state (balance should be 20000):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "Keycard path 7 (LEE holding) state (balance should be 20000):" -wallet account get --key-path "m/44'/60'/0'/0/7" +wallet account get --account-id "m/44'/60'/0'/0/7" echo "amm-lez-fund state (balance should be 10000):" -wallet account get --account-label amm-lez-fund +wallet account get --account-id amm-lez-fund echo "amm-lee-fund state (balance should be 10000):" -wallet account get --account-label amm-lee-fund +wallet account get --account-id amm-lee-fund # ============================================================================= # (3) Token transfer: keycard path 6 (LEZ) → public account @@ -165,17 +172,17 @@ wallet account get --account-label amm-lee-fund echo "" echo "=== (3) Token transfer: keycard path 6 → pub-receiver (public) ===" wallet token send \ - --from-key-path "m/44'/60'/0'/0/6" \ - --to-label pub-receiver \ + --from "m/44'/60'/0'/0/6" \ + --to pub-receiver \ --amount 1000 echo "Transferred 1000 LEZ: keycard path 6 → pub-receiver" sleep 15 echo "Keycard path 6 (LEZ) state (balance should be 19000):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "pub-receiver state (balance should be 1000):" -wallet account get --account-label pub-receiver +wallet account get --account-id pub-receiver # ============================================================================= # (4) Token transfer: keycard path 6 (LEZ) → private account (shielded) @@ -186,8 +193,8 @@ PRIV_RECEIVER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | h echo "Fresh private receiver account: $PRIV_RECEIVER" wallet token send \ - --from-key-path "m/44'/60'/0'/0/6" \ - --to "$PRIV_RECEIVER" \ + --from "m/44'/60'/0'/0/6" \ + --to "$PRIV_RECEIVER" \ --amount 500 echo "Shielded transfer of 500 LEZ: keycard path 6 → $PRIV_RECEIVER" @@ -196,7 +203,7 @@ wallet account sync-private sleep 15 echo "Keycard path 6 (LEZ) state (balance should be 18500):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "priv-receiver state (balance should be 500):" wallet account get --account-id "$PRIV_RECEIVER" @@ -206,17 +213,17 @@ wallet account get --account-id "$PRIV_RECEIVER" echo "" echo "=== (5) Token mint: keycard def path 2 mints 2000 LEZ to keycard path 6 ===" wallet token mint \ - --definition-key-path "m/44'/60'/0'/0/2" \ - --holder-key-path "m/44'/60'/0'/0/6" \ + --definition "m/44'/60'/0'/0/2" \ + --holder "m/44'/60'/0'/0/6" \ --amount 2000 -echo "Minted 2000 LEZ to keycard path 4" +echo "Minted 2000 LEZ to keycard path 6" sleep 15 echo "Keycard path 2 (LEZ definition) state (total supply should have increased):" -wallet account get --key-path "m/44'/60'/0'/0/2" +wallet account get --account-id "m/44'/60'/0'/0/2" echo "Keycard path 6 (LEZ holding) state (balance should be 20500):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" # ============================================================================= # (6) Token burn with keycard — holder is keycard path 6 @@ -224,17 +231,17 @@ wallet account get --key-path "m/44'/60'/0'/0/6" echo "" echo "=== (6) Token burn: keycard path 6 burns 500 LEZ ===" wallet token burn \ - --definition "Public/$LEZ_DEF_ID" \ - --holder-key-path "m/44'/60'/0'/0/6" \ + --definition "Public/$LEZ_DEF_ID" \ + --holder "m/44'/60'/0'/0/6" \ --amount 500 -echo "Burned 500 LEZ from keycard path 4" +echo "Burned 500 LEZ from keycard path 6" sleep 15 echo "Keycard path 2 (LEZ definition) state (total supply should reflect burn):" -wallet account get --key-path "m/44'/60'/0'/0/2" +wallet account get --account-id "m/44'/60'/0'/0/2" echo "Keycard path 6 (LEZ holding) state (balance should be 20000):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" # ============================================================================= # (7) Create AMM pool for LEZ/LEE — without keycard @@ -243,9 +250,9 @@ echo "" echo "=== (7) Create AMM pool for LEZ/LEE (without keycard) ===" wallet amm new \ - --user-holding-a-label amm-lez-fund \ - --user-holding-b-label amm-lee-fund \ - --user-holding-lp-label amm-lp-fund \ + --user-holding-a amm-lez-fund \ + --user-holding-b amm-lee-fund \ + --user-holding-lp amm-lp-fund \ --balance-a 10000 \ --balance-b 10000 echo "AMM pool created for LEZ/LEE" @@ -253,12 +260,12 @@ echo "AMM pool created for LEZ/LEE" sleep 15 echo "amm-lez-fund state (balance should be 0 — contributed to pool):" -wallet account get --account-label amm-lez-fund +wallet account get --account-id amm-lez-fund echo "amm-lee-fund state (balance should be 0 — contributed to pool):" -wallet account get --account-label amm-lee-fund +wallet account get --account-id amm-lee-fund echo "Initial LP holding state (should hold initial LP tokens):" -wallet account get --account-label amm-lp-fund -LP_DEF_ID=$(wallet account get --account-label amm-lp-fund | grep -o '"definition_id":"[^"]*"' | awk -F'"' '{print $4}') +wallet account get --account-id amm-lp-fund +LP_DEF_ID=$(wallet account get --account-id amm-lp-fund | grep -o '"definition_id":"[^"]*"' | awk -F'"' '{print $4}') echo "LP token definition ID: $LP_DEF_ID" # ============================================================================= @@ -268,8 +275,8 @@ echo "LP token definition ID: $LP_DEF_ID" echo "" echo "=== (8) Swap: keycard path 7 sells 500 LEE, keycard path 6 receives LEZ ===" wallet amm swap-exact-input \ - --user-holding-a-key-path "m/44'/60'/0'/0/6" \ - --user-holding-b-key-path "m/44'/60'/0'/0/7" \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ --amount-in 500 \ --min-amount-out 1 \ --token-definition "$LEE_DEF_ID" @@ -278,31 +285,32 @@ echo "Swap LEE → LEZ complete via keycard" sleep 15 echo "Keycard path 6 (LEZ holding) state (balance should have increased):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "Keycard path 7 (LEE holding) state (balance should have decreased by 500):" -wallet account get --key-path "m/44'/60'/0'/0/7" +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 init \ - --definition-account-id "Public/$LP_DEF_ID" \ - --holder-key-path "m/44'/60'/0'/0/8" +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 --key-path "m/44'/60'/0'/0/8" +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 \ - --user-holding-a-key-path "m/44'/60'/0'/0/6" \ - --user-holding-b-key-path "m/44'/60'/0'/0/7" \ - --user-holding-lp-key-path "m/44'/60'/0'/0/8" \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ --max-amount-a 1000 \ --max-amount-b 1000 \ --min-amount-lp 1 @@ -311,11 +319,11 @@ echo "Add liquidity complete via keycard" sleep 15 echo "Keycard path 6 (LEZ holding) state (balance should have decreased):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "Keycard path 7 (LEE holding) state (balance should have decreased):" -wallet account get --key-path "m/44'/60'/0'/0/7" +wallet account get --account-id "m/44'/60'/0'/0/7" echo "Keycard path 8 (LP holding) state (should have received LP tokens):" -wallet account get --key-path "m/44'/60'/0'/0/8" +wallet account get --account-id "m/44'/60'/0'/0/8" # ============================================================================= # (10) Remove liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8) @@ -323,9 +331,9 @@ wallet account get --key-path "m/44'/60'/0'/0/8" echo "" echo "=== (10) Remove liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ===" wallet amm remove-liquidity \ - --user-holding-a-key-path "m/44'/60'/0'/0/6" \ - --user-holding-b-key-path "m/44'/60'/0'/0/7" \ - --user-holding-lp-key-path "m/44'/60'/0'/0/8" \ + --user-holding-a "m/44'/60'/0'/0/6" \ + --user-holding-b "m/44'/60'/0'/0/7" \ + --user-holding-lp "m/44'/60'/0'/0/8" \ --balance-lp 500 \ --min-amount-a 1 \ --min-amount-b 1 @@ -334,22 +342,22 @@ echo "Remove liquidity complete via keycard" sleep 15 echo "Keycard path 6 (LEZ holding) state (balance should have increased):" -wallet account get --key-path "m/44'/60'/0'/0/6" +wallet account get --account-id "m/44'/60'/0'/0/6" echo "Keycard path 7 (LEE holding) state (balance should have increased):" -wallet account get --key-path "m/44'/60'/0'/0/7" +wallet account get --account-id "m/44'/60'/0'/0/7" echo "Keycard path 8 (LP holding) state (balance should have decreased):" -wallet account get --key-path "m/44'/60'/0'/0/8" +wallet account get --account-id "m/44'/60'/0'/0/8" # ============================================================================= # (11) ATA create — keycard path 9 as owner for LEZ # ============================================================================= echo "" echo "=== (11) ATA create: keycard path 9 as owner, LEZ token ===" -ATA_OWNER_ID=$(wallet account id --key-path "m/44'/60'/0'/0/9") +ATA_OWNER_ID=$(wallet account id --account-id "m/44'/60'/0'/0/9") echo "ATA owner (keycard path 9): $ATA_OWNER_ID" wallet ata create \ - --key-path "m/44'/60'/0'/0/9" \ + --owner "m/44'/60'/0'/0/9" \ --token-definition "$LEZ_DEF_ID" echo "ATA created for keycard path 9 / LEZ" @@ -362,8 +370,8 @@ wallet account get --account-id "Public/$LEZ_ATA_ID" # Fund the ATA from LEZ supply (path 3) — setup for tests 12 and 13 wallet token send \ - --from-key-path "m/44'/60'/0'/0/3" \ - --to "Public/$LEZ_ATA_ID" \ + --from "m/44'/60'/0'/0/3" \ + --to "Public/$LEZ_ATA_ID" \ --amount 3000 echo "Funded keycard path 9 ATA with 3000 LEZ" @@ -373,11 +381,11 @@ echo "ATA state after funding (balance should be 3000):" wallet account get --account-id "Public/$LEZ_ATA_ID" # ============================================================================= -# (12) ATA send — keycard path 7's ATA → pub-receiver's ATA +# (12) ATA send — keycard path 9's ATA → pub-receiver's ATA # ============================================================================= echo "" -echo "=== (12) ATA send: keycard path 7's ATA → pub-receiver's ATA ===" -PUB_RECEIVER_ID=$(wallet account id --account-label pub-receiver) +echo "=== (12) ATA send: keycard path 9's ATA → pub-receiver's ATA ===" +PUB_RECEIVER_ID=$(wallet account id --account-id pub-receiver) wallet ata create \ --owner "Public/$PUB_RECEIVER_ID" \ --token-definition "$LEZ_DEF_ID" @@ -391,7 +399,7 @@ echo "pub-receiver ATA state (should be initialized with zero balance):" wallet account get --account-id "Public/$PUB_RECEIVER_ATA_ID" wallet ata send \ - --from-key-path "m/44'/60'/0'/0/9" \ + --from "m/44'/60'/0'/0/9" \ --token-definition "$LEZ_DEF_ID" \ --to "$PUB_RECEIVER_ATA_ID" \ --amount 500 @@ -405,12 +413,12 @@ echo "pub-receiver ATA state (balance should be 500):" wallet account get --account-id "Public/$PUB_RECEIVER_ATA_ID" # ============================================================================= -# (13) ATA burn — keycard path 7's ATA burns 200 LEZ +# (13) ATA burn — keycard path 9's ATA burns 200 LEZ # ============================================================================= echo "" -echo "=== (13) ATA burn: keycard path 7's ATA burns 200 LEZ ===" +echo "=== (13) ATA burn: keycard path 9's ATA burns 200 LEZ ===" wallet ata burn \ - --key-path "m/44'/60'/0'/0/9" \ + --holder "m/44'/60'/0'/0/9" \ --token-definition "$LEZ_DEF_ID" \ --amount 200 echo "Burned 200 LEZ from keycard path 9 ATA" @@ -420,7 +428,7 @@ sleep 15 echo "Keycard path 9 ATA state (balance should be 2300):" wallet account get --account-id "Public/$LEZ_ATA_ID" echo "LEZ definition state (total supply should reflect burn):" -wallet account get --key-path "m/44'/60'/0'/0/2" +wallet account get --account-id "m/44'/60'/0'/0/2" echo "" echo "=== All keycard token + AMM + ATA tests finished ===" diff --git a/keycard_wallet/Cargo.toml b/keycard_wallet/Cargo.toml index f1c35ee7..b2addc9d 100644 --- a/keycard_wallet/Cargo.toml +++ b/keycard_wallet/Cargo.toml @@ -13,4 +13,5 @@ nssa_core.workspace = true pyo3.workspace = true log.workspace = true serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true \ No newline at end of file +serde_json.workspace = true +zeroize = "1" \ No newline at end of file diff --git a/keycard_wallet/src/lib.rs b/keycard_wallet/src/lib.rs index 2ceb33c6..efa77812 100644 --- a/keycard_wallet/src/lib.rs +++ b/keycard_wallet/src/lib.rs @@ -3,6 +3,7 @@ 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}; pub mod python_path; @@ -215,26 +216,39 @@ impl KeycardWallet { &self, py: Python, path: &str, - ) -> PyResult<([u8; 32], [u8; 32])> { + ) -> PyResult<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>)> { let (raw_nsk, raw_vsk): (Vec, Vec) = self .instance .bind(py) .call_method1("get_private_keys_for_path", (path,))? .extract()?; - let nsk: [u8; 32] = raw_nsk.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "expected 32-byte NSK from keycard, got {} bytes", - vec.len() - )) - })?; + let raw_nsk = Zeroizing::new(raw_nsk); + let raw_vsk = Zeroizing::new(raw_vsk); - let vsk: [u8; 32] = raw_vsk.try_into().map_err(|vec: Vec| { - PyErr::new::(format!( - "expected 32-byte VSK from keycard, got {} bytes", - vec.len() - )) - })?; + let nsk = { + if raw_nsk.len() != 32 { + return Err(PyErr::new::(format!( + "expected 32-byte NSK from keycard, got {} bytes", + raw_nsk.len() + ))); + } + let mut arr = Zeroizing::new([0u8; 32]); + arr.copy_from_slice(&raw_nsk); + arr + }; + + let vsk = { + if raw_vsk.len() != 32 { + return Err(PyErr::new::(format!( + "expected 32-byte VSK from keycard, got {} bytes", + raw_vsk.len() + ))); + } + let mut arr = Zeroizing::new([0u8; 32]); + arr.copy_from_slice(&raw_vsk); + arr + }; Ok((nsk, vsk)) } @@ -242,7 +256,7 @@ impl KeycardWallet { pub fn get_private_keys_for_path_with_connect( pin: &str, path: &str, - ) -> PyResult<([u8; 32], [u8; 32])> { + ) -> PyResult<(Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>)> { Python::with_gil(|py| { python_path::add_python_path(py)?; @@ -268,7 +282,7 @@ impl KeycardWallet { key_path: &str, ) -> PyResult { let (nsk, _vsk) = Self::get_private_keys_for_path_with_connect(pin, key_path)?; - let npk = NullifierPublicKey::from(&nsk); + let npk = NullifierPublicKey::from(&*nsk); Ok(format!("Private/{}", AccountId::from((&npk, 0_u128)))) } diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 1dcea1d5..97bbd095 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -15,6 +15,12 @@ use crate::{ /// Represents generic chain CLI subcommand. #[derive(Subcommand, Debug, Clone)] pub enum AccountSubcommand { + /// Resolve an account mention and print just the account ID (no privacy prefix). + Id { + /// Account id with privacy prefix, label, or BIP-32 key path. + #[arg(long)] + account_id: CliAccountMention, + }, /// Get account data. Get { /// Flag to get raw account data. @@ -261,6 +267,14 @@ impl WalletSubcommand for AccountSubcommand { wallet_core: &mut WalletCore, ) -> Result { match self { + Self::Id { account_id } => { + let resolved = account_id.resolve(wallet_core.storage())?; + let id = match resolved { + AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id, + }; + println!("{id}"); + Ok(SubcommandReturnValue::Empty) + } Self::Get { raw, keys, diff --git a/wallet/src/cli/keycard.rs b/wallet/src/cli/keycard.rs index ead1e84b..0234f6ff 100644 --- a/wallet/src/cli/keycard.rs +++ b/wallet/src/cli/keycard.rs @@ -16,6 +16,18 @@ pub enum KeycardSubcommand { Disconnect, Init, Load, + /// Retrieve the private keys (NSK, VSK) for a given BIP-32 key path. + /// + /// Prints raw key material to stdout — intended for debugging only. + /// Requires --reveal to confirm intent. + GetPrivateKeys { + /// BIP-32 derivation path, e.g. `m/44'/60'/0'/0/0`. + #[arg(long)] + key_path: String, + /// Confirm that raw NSK and VSK should be disclosed on stdout. + #[arg(long)] + reveal: bool, + }, } impl WalletSubcommand for KeycardSubcommand { @@ -131,6 +143,26 @@ impl WalletSubcommand for KeycardSubcommand { Ok(SubcommandReturnValue::Empty) } + Self::GetPrivateKeys { key_path, reveal } => { + if !reveal { + eprintln!( + "WARNING: pass --reveal to print NSK and VSK. \ + Disclosing either key fully compromises the account's privacy." + ); + return Ok(SubcommandReturnValue::Empty); + } + eprintln!( + "WARNING: NSK and VSK are being printed to stdout. \ + Any terminal log, scrollback, or screen recording captures these keys." + ); + let pin = read_pin()?; + 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)); + Ok(SubcommandReturnValue::Empty) + } } } } diff --git a/wallet/src/cli/mod.rs b/wallet/src/cli/mod.rs index f76faf23..4b3fabb8 100644 --- a/wallet/src/cli/mod.rs +++ b/wallet/src/cli/mod.rs @@ -138,7 +138,7 @@ impl CliAccountMention { Self::KeyPath(path) => { let pin = read_pin()?; let id_str = - keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path) + 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}")) @@ -159,46 +159,6 @@ impl CliAccountMention { } } - /// Resolve to an [`AccountSigner`] for a sender — must sign, never `Foreign`. - pub fn to_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(crate::signing::AccountSigner::Local(id)), - AccountIdWithPrivacy::Private(_) => { - anyhow::bail!("Private accounts not supported as senders here") - } - } - } - - /// 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 { - 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) - { - Some(_) => crate::signing::AccountSigner::Local(id), - None => crate::signing::AccountSigner::Foreign, - }, - ), - AccountIdWithPrivacy::Private(_) => { - anyhow::bail!("Private accounts not supported as recipients here") - } - } - } } impl FromStr for CliAccountMention { diff --git a/wallet/src/cli/programs/amm.rs b/wallet/src/cli/programs/amm.rs index 6173eaf3..c1c0de08 100644 --- a/wallet/src/cli/programs/amm.rs +++ b/wallet/src/cli/programs/amm.rs @@ -131,22 +131,25 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { balance_a, balance_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { Amm(wallet_core) .send_new_definition( - user_holding_a, - user_holding_b, - user_holding_lp, + a, + b, + lp, balance_a, balance_b, + &user_holding_a, + &user_holding_b, + &user_holding_lp, ) .await?; @@ -165,20 +168,22 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { min_amount_out, token_definition, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b) { + 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(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), ) => { Amm(wallet_core) .send_swap_exact_input( - user_holding_a, - user_holding_b, + a, + b, amount_in, min_amount_out, token_definition, + &user_holding_a, + &user_holding_b, ) .await?; @@ -197,20 +202,22 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { max_amount_in, token_definition, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b) { + 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(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), ) => { Amm(wallet_core) .send_swap_exact_output( - user_holding_a, - user_holding_b, + a, + b, exact_amount_out, max_amount_in, token_definition, + &user_holding_a, + &user_holding_b, ) .await?; @@ -230,23 +237,25 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { max_amount_a, max_amount_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { Amm(wallet_core) .send_add_liquidity( - user_holding_a, - user_holding_b, - user_holding_lp, + a, + b, + lp, min_amount_lp, max_amount_a, max_amount_b, + &user_holding_a, + &user_holding_b, ) .await?; @@ -266,23 +275,24 @@ impl WalletSubcommand for AmmProgramAgnosticSubcommand { min_amount_a, min_amount_b, } => { - let user_holding_a = user_holding_a.resolve(wallet_core.storage())?; - let user_holding_b = user_holding_b.resolve(wallet_core.storage())?; - let user_holding_lp = user_holding_lp.resolve(wallet_core.storage())?; - match (user_holding_a, user_holding_b, user_holding_lp) { + let a_id = user_holding_a.resolve(wallet_core.storage())?; + let b_id = user_holding_b.resolve(wallet_core.storage())?; + let lp_id = user_holding_lp.resolve(wallet_core.storage())?; + match (a_id, b_id, lp_id) { ( - AccountIdWithPrivacy::Public(user_holding_a), - AccountIdWithPrivacy::Public(user_holding_b), - AccountIdWithPrivacy::Public(user_holding_lp), + AccountIdWithPrivacy::Public(a), + AccountIdWithPrivacy::Public(b), + AccountIdWithPrivacy::Public(lp), ) => { Amm(wallet_core) .send_remove_liquidity( - user_holding_a, - user_holding_b, - user_holding_lp, + a, + b, + lp, balance_lp, min_amount_a, min_amount_b, + &user_holding_lp, ) .await?; diff --git a/wallet/src/cli/programs/ata.rs b/wallet/src/cli/programs/ata.rs index e4c30d9c..d7e3b223 100644 --- a/wallet/src/cli/programs/ata.rs +++ b/wallet/src/cli/programs/ata.rs @@ -91,13 +91,13 @@ impl WalletSubcommand for AtaSubcommand { owner, token_definition, } => { - let owner = owner.resolve(wallet_core.storage())?; + let owner_resolved = owner.resolve(wallet_core.storage())?; let definition_id = token_definition; - match owner { + match owner_resolved { AccountIdWithPrivacy::Public(owner_id) => { Ata(wallet_core) - .send_create(owner_id, definition_id) + .send_create(owner_id, definition_id, &owner) .await?; Ok(SubcommandReturnValue::Empty) } @@ -127,14 +127,14 @@ impl WalletSubcommand for AtaSubcommand { to, amount, } => { - let from = from.resolve(wallet_core.storage())?; + let from_resolved = from.resolve(wallet_core.storage())?; let definition_id = token_definition; let to_id = to; - match from { + match from_resolved { AccountIdWithPrivacy::Public(from_id) => { Ata(wallet_core) - .send_transfer(from_id, definition_id, to_id, amount) + .send_transfer(from_id, definition_id, to_id, amount, &from) .await?; Ok(SubcommandReturnValue::Empty) } @@ -163,13 +163,13 @@ impl WalletSubcommand for AtaSubcommand { token_definition, amount, } => { - let holder = holder.resolve(wallet_core.storage())?; + let holder_resolved = holder.resolve(wallet_core.storage())?; let definition_id = token_definition; - match holder { + match holder_resolved { AccountIdWithPrivacy::Public(holder_id) => { Ata(wallet_core) - .send_burn(holder_id, definition_id, amount) + .send_burn(holder_id, definition_id, amount, &holder) .await?; Ok(SubcommandReturnValue::Empty) } diff --git a/wallet/src/cli/programs/native_token_transfer.rs b/wallet/src/cli/programs/native_token_transfer.rs index 87c38bef..90caaffb 100644 --- a/wallet/src/cli/programs/native_token_transfer.rs +++ b/wallet/src/cli/programs/native_token_transfer.rs @@ -73,7 +73,7 @@ impl WalletSubcommand for AuthTransferSubcommand { } AccountIdWithPrivacy::Private(account_id) => { let (tx_hash, secret) = NativeTokenTransfer(wallet_core) - .register_account_private(account_id) + .register_account_private(account_id, &None) .await?; println!("Transaction hash is {tx_hash}"); @@ -151,6 +151,7 @@ impl WalletSubcommand for AuthTransferSubcommand { from, to, amount, + from_mention: from_account, }, ) } @@ -175,6 +176,7 @@ impl WalletSubcommand for AuthTransferSubcommand { to_vpk, to_identifier, amount, + from_mention: from_account, }, ) } @@ -247,6 +249,8 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// amount - amount of balance to move. #[arg(long)] amount: u128, + #[arg(skip)] + from_mention: CliAccountMention, }, /// Send native token transfer from `from` to `to` for `amount`. /// @@ -267,6 +271,8 @@ pub enum NativeTokenTransferProgramSubcommandShielded { /// amount - amount of balance to move. #[arg(long)] amount: u128, + #[arg(skip)] + from_mention: CliAccountMention, }, } @@ -318,7 +324,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { match self { Self::PrivateOwned { from, to, amount } => { let (tx_hash, [secret_from, secret_to]) = NativeTokenTransfer(wallet_core) - .send_private_transfer_to_owned_account(from, to, amount) + .send_private_transfer_to_owned_account(from, to, amount, &None) .await?; println!("Transaction hash is {tx_hash}"); @@ -363,6 +369,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandPrivate { to_vpk, to_identifier.unwrap_or_else(rand::random), amount, + &None, ) .await?; @@ -393,9 +400,9 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { wallet_core: &mut WalletCore, ) -> Result { match self { - Self::ShieldedOwned { from, to, amount } => { + Self::ShieldedOwned { from, to, amount, from_mention } => { let (tx_hash, secret) = NativeTokenTransfer(wallet_core) - .send_shielded_transfer(from, to, amount) + .send_shielded_transfer(from, to, amount, &from_mention) .await?; println!("Transaction hash is {tx_hash}"); @@ -421,6 +428,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_vpk, to_identifier, amount, + from_mention, } => { let to_npk_res = hex::decode(to_npk)?; let mut to_npk = [0; 32]; @@ -440,6 +448,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded { to_vpk, to_identifier.unwrap_or_else(rand::random), amount, + &from_mention, ) .await?; @@ -467,7 +476,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommand { } Self::Deshielded { from, to, amount } => { let (tx_hash, secret) = NativeTokenTransfer(wallet_core) - .send_deshielded_transfer(from, to, amount) + .send_deshielded_transfer(from, to, amount, &None) .await?; println!("Transaction hash is {tx_hash}"); diff --git a/wallet/src/cli/programs/token.rs b/wallet/src/cli/programs/token.rs index 61e24e68..1844a628 100644 --- a/wallet/src/cli/programs/token.rs +++ b/wallet/src/cli/programs/token.rs @@ -114,16 +114,18 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { name, total_supply, } => { + let def_mention = definition_account_id.clone(); + let sup_mention = supply_account_id.clone(); 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(definition_account_id), - AccountIdWithPrivacy::Public(supply_account_id), + AccountIdWithPrivacy::Public(_), + AccountIdWithPrivacy::Public(_), ) => TokenProgramSubcommand::Create( CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp { - definition_account_id, - supply_account_id, + definition_account_id: def_mention, + supply_account_id: sup_mention, name, total_supply, }, @@ -173,6 +175,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { to_identifier, amount, } => { + let from_mention = from.clone(); + let to_mention = to.clone(); let from = from.resolve(wallet_core.storage())?; let to = to .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -192,11 +196,11 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { anyhow::bail!("List of public keys is uncomplete"); } (Some(to), None, None) => match (from, to) { - (AccountIdWithPrivacy::Public(from), AccountIdWithPrivacy::Public(to)) => { + (AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => { TokenProgramSubcommand::Public( TokenProgramSubcommandPublic::TransferToken { - sender_account_id: from, - recipient_account_id: to, + sender_account_id: from_mention, + recipient_account_id: to_mention.expect("matched Some branch"), balance_to_move: amount, }, ) @@ -259,15 +263,16 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder, amount, } => { + let holder_mention = holder.clone(); 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(holder), + AccountIdWithPrivacy::Public(_), ) => TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken { definition_account_id: definition, - holder_account_id: holder, + holder_account_id: holder_mention, amount, }), ( @@ -312,6 +317,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { holder_identifier, amount, } => { + let def_mention = definition.clone(); + let hol_mention = holder.clone(); let definition = definition.resolve(wallet_core.storage())?; let holder = holder .map(|account_mention| account_mention.resolve(wallet_core.storage())) @@ -332,12 +339,12 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand { } (Some(holder), None, None) => match (definition, holder) { ( - AccountIdWithPrivacy::Public(definition), - AccountIdWithPrivacy::Public(holder), + AccountIdWithPrivacy::Public(_), + AccountIdWithPrivacy::Public(_), ) => TokenProgramSubcommand::Public( TokenProgramSubcommandPublic::MintToken { - definition_account_id: definition, - holder_account_id: holder, + definition_account_id: def_mention, + holder_account_id: hol_mention.expect("matched Some branch"), amount, }, ), @@ -430,9 +437,9 @@ pub enum TokenProgramSubcommandPublic { // Transfer tokens using the token program TransferToken { #[arg(short, long)] - sender_account_id: AccountId, + sender_account_id: CliAccountMention, #[arg(short, long)] - recipient_account_id: AccountId, + recipient_account_id: CliAccountMention, #[arg(short, long)] balance_to_move: u128, }, @@ -441,16 +448,16 @@ pub enum TokenProgramSubcommandPublic { #[arg(short, long)] definition_account_id: AccountId, #[arg(short, long)] - holder_account_id: AccountId, + holder_account_id: CliAccountMention, #[arg(short, long)] amount: u128, }, // Transfer tokens using the token program MintToken { #[arg(short, long)] - definition_account_id: AccountId, + definition_account_id: CliAccountMention, #[arg(short, long)] - holder_account_id: AccountId, + holder_account_id: CliAccountMention, #[arg(short, long)] amount: u128, }, @@ -620,9 +627,9 @@ pub enum CreateNewTokenProgramSubcommand { /// Definition - public, supply - public. NewPublicDefPublicSupp { #[arg(short, long)] - definition_account_id: AccountId, + definition_account_id: CliAccountMention, #[arg(short, long)] - supply_account_id: AccountId, + supply_account_id: CliAccountMention, #[arg(short, long)] name: String, #[arg(short, long)] @@ -680,11 +687,18 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { recipient_account_id, balance_to_move, } => { + 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 { + anyhow::bail!("Only public accounts supported for token transfer"); + }; Token(wallet_core) .send_transfer_transaction( - sender_account_id, - recipient_account_id, + sender_id, + recipient_id, balance_to_move, + &sender_account_id, + &recipient_account_id, ) .await?; Ok(SubcommandReturnValue::Empty) @@ -694,8 +708,12 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { holder_account_id, amount, } => { + let holder = holder_account_id.resolve(wallet_core.storage())?; + let AccountIdWithPrivacy::Public(holder_id) = holder else { + anyhow::bail!("Only public holder account supported for token burn"); + }; Token(wallet_core) - .send_burn_transaction(definition_account_id, holder_account_id, amount) + .send_burn_transaction(definition_account_id, holder_id, amount, &holder_account_id) .await?; Ok(SubcommandReturnValue::Empty) } @@ -704,8 +722,13 @@ impl WalletSubcommand for TokenProgramSubcommandPublic { holder_account_id, amount, } => { + 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 { + anyhow::bail!("Only public accounts supported for token mint"); + }; Token(wallet_core) - .send_mint_transaction(definition_account_id, holder_account_id, amount) + .send_mint_transaction(def_id, holder_id, amount, &definition_account_id, &holder_account_id) .await?; Ok(SubcommandReturnValue::Empty) } @@ -1307,12 +1330,19 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand { name, total_supply, } => { + 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 { + anyhow::bail!("Only public accounts supported for new token definition"); + }; Token(wallet_core) .send_new_definition( - definition_account_id, - supply_account_id, + def_id, + sup_id, name, total_supply, + &definition_account_id, + &supply_account_id, ) .await?; Ok(SubcommandReturnValue::Empty) diff --git a/wallet/src/helperfunctions.rs b/wallet/src/helperfunctions.rs index e9a185ce..39a6b168 100644 --- a/wallet/src/helperfunctions.rs +++ b/wallet/src/helperfunctions.rs @@ -22,70 +22,6 @@ pub fn read_pin() -> anyhow::Result> { /// Read the mnemonic phrase without echoing it. /// -/// Exactly one of `id` or `label` must be `Some`. If `id` is provided it is -/// returned as-is; if `label` is provided it is resolved via -/// [`resolve_account_label`]. Any other combination returns an error. -pub fn resolve_id_or_label( - id: Option, - label: Option, - labels: &HashMap, - user_data: &NSSAUserData, - key_path: Option<&str>, -) -> Result { - match (id, label, key_path) { - (Some(id), None, None) => Ok(id), - (None, Some(label), None) => resolve_account_label(&label, labels, user_data), - (None, None, Some(key_path)) => resolve_keycard_id(key_path), - _ => anyhow::bail!("provide exactly one of account id, account label or keycard path"), - } -} - -pub fn resolve_keycard_id(key_path: &str) -> Result { - let pin = read_pin()?; - KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path) - .map_err(anyhow::Error::from) -} - -/// Resolve an account label to its full `Privacy/id` string representation. -/// -/// Looks up the label in the labels map and determines whether the account is -/// public or private by checking the user data key trees. -pub fn resolve_account_label( - label: &str, - labels: &HashMap, - user_data: &NSSAUserData, -) -> Result { - let account_id_str = labels - .iter() - .find(|(_, l)| l.to_string() == label) - .map(|(k, _)| k.clone()) - .ok_or_else(|| anyhow::anyhow!("No account found with label '{label}'"))?; - - let account_id: nssa::AccountId = account_id_str.parse()?; - - let privacy = if user_data - .public_key_tree - .account_id_map - .contains_key(&account_id) - || user_data - .default_pub_account_signing_keys - .contains_key(&account_id) - { - "Public" - } else if user_data - .private_key_tree - .account_id_map - .contains_key(&account_id) - || user_data - .default_user_private_accounts - .contains_key(&account_id) - { - "Private" - } else { - anyhow::bail!("Account with label '{label}' not found in wallet"); - }; - - Ok(format!("{privacy}/{account_id_str}")) /// Checks `KEYCARD_MNEMONIC` first for non-interactive callers. Falls back to /// a TTY prompt so the phrase never appears in argv, shell history, or `ps`. pub fn read_mnemonic() -> anyhow::Result> { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 00c90c6b..d66bccfa 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -28,7 +28,6 @@ use nssa_core::{ program::InstructionData, }; pub use privacy_preserving_tx::PrivacyPreservingAccount; -use pyo3::exceptions::PyRuntimeError; use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder}; use storage::Storage; use tokio::io::AsyncWriteExt as _; @@ -575,38 +574,41 @@ impl WalletCore { let mut pre_states = acc_manager.pre_states(); - let keycard_account = if let Some(key_path_str) = key_path.as_deref() { - let account_id = { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)? - }; - let (acc_id, _) = - parse_addr_with_privacy_prefix(&account_id).expect("Valid parsing of account id"); - let account_id: AccountId = acc_id.parse().expect("Expect a valid 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"); - Some(AccountWithMetadata { - account, - is_authorized: true, - account_id, - }) + 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, None) }; let mut nonces: Vec = acc_manager.public_account_nonces().into_iter().collect(); let mut account_ids: Vec = acc_manager.public_account_ids(); - let mut visibility_mask = acc_manager.visibility_mask().to_vec(); - if let Some(acc) = keycard_account.as_ref() { if acc_manager.public_account_ids().contains(&acc.account_id) { if let Some(pre) = pre_states @@ -619,7 +621,6 @@ impl WalletCore { } else { nonces.push(acc.account.nonce); account_ids.push(acc.account_id); - visibility_mask.push(0); pre_states.push(acc.clone()); } } @@ -652,12 +653,35 @@ impl WalletCore { ) .unwrap(); - let witness_set = + 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 779d8c9a..10355c33 100644 --- a/wallet/src/program_facades/amm.rs +++ b/wallet/src/program_facades/amm.rs @@ -1,17 +1,20 @@ use amm_core::{compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda}; use common::{HashType, transaction::NSSATransaction}; -use nssa::{AccountId, program::Program}; +use nssa::{AccountId, program::Program, public_transaction::WitnessSet}; +use pyo3::exceptions::PyRuntimeError; use sequencer_service_rpc::RpcClient as _; use token_core::TokenHolding; -use crate::{ExecutionFailureKind, WalletCore}; +use crate::{ + ExecutionFailureKind, WalletCore, + cli::CliAccountMention, + helperfunctions::read_pin, + signing::SigningGroups, +}; 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" - )] + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_new_definition( &self, user_holding_a: AccountId, @@ -19,8 +22,9 @@ impl Amm<'_> { user_holding_lp: AccountId, balance_a: u128, balance_b: u128, - key_path_a: Option<&str>, - key_path_b: Option<&str>, + a_mention: &CliAccountMention, + b_mention: &CliAccountMention, + lp_mention: &CliAccountMention, ) -> Result { let program = Program::amm(); let amm_program_id = Program::amm().id(); @@ -64,106 +68,40 @@ impl Amm<'_> { user_holding_lp, ]; - // Check if LP has a stored key to determine if LP nonce is needed — before message creation - let lp_sk = self - .0 - .storage - .user_data - .get_pub_account_signing_key(user_holding_lp); + let mut groups = SigningGroups::new(); + groups + .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())))?; - 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 lp_sk.is_some() { - let lp_nonces = self - .0 - .get_accounts_nonces(vec![user_holding_lp]) - .await + 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)?; - if lp_nonces.is_empty() { - nonces.push(nssa_core::account::Nonce(0)); - } else { - nonces.extend(lp_nonces); - } + 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 message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let pin = if key_path_a.is_some() || key_path_b.is_some() { - Some(crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?) + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - None + String::new() }; + let sigs = groups.sign_all(&message.hash(), &pin) + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let (sig_a, pk_a) = if let Some(kp) = key_path_a { - keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().unwrap(), - kp, - &msg_hash, - )? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - let (sig_b, pk_b) = if let Some(kp) = key_path_b { - keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().unwrap(), - kp, - &msg_hash, - )? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - 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)); - } - - let witness_set = nssa::public_transaction::WitnessSet::from_list(&message, &sigs, &pks) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let tx = nssa::PublicTransaction::new(message, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -172,7 +110,7 @@ impl Amm<'_> { .await?) } - #[expect(clippy::too_many_arguments, reason = "To fix later")] + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_swap_exact_input( &self, user_holding_a: AccountId, @@ -180,8 +118,8 @@ impl Amm<'_> { swap_amount_in: u128, min_amount_out: u128, token_definition_id_in: AccountId, - user_holding_a_key_path: Option<&str>, - user_holding_b_key_path: Option<&str>, + a_mention: &CliAccountMention, + b_mention: &CliAccountMention, ) -> Result { let instruction = amm_core::Instruction::SwapExactInput { swap_amount_in, @@ -222,59 +160,36 @@ impl Amm<'_> { user_holding_b, ]; - let account_id_auth = if definition_token_a_id == token_definition_id_in { - user_holding_a + let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in { + (user_holding_a, a_mention) } else if definition_token_b_id == token_definition_id_in { - user_holding_b + (user_holding_b, b_mention) } else { - return Err(ExecutionFailureKind::AccountDataError( - token_definition_id_in, - )); + return Err(ExecutionFailureKind::AccountDataError(token_definition_id_in)); }; - let nonces = self - .0 - .get_accounts_nonces(vec![account_id_auth]) - .await + 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 message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let seller_key_path = if definition_token_a_id == token_definition_id_in { - user_holding_a_key_path + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - user_holding_b_key_path - }; - let witness_set = if let Some(kp) = seller_key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig, pk) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .map_err(ExecutionFailureKind::TransactionBuildError)? - } else { - let signing_key = self - .0 - .storage - .user_data - .get_pub_account_signing_key(account_id_auth) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -283,7 +198,7 @@ impl Amm<'_> { .await?) } - #[expect(clippy::too_many_arguments, reason = "To fix later")] + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_swap_exact_output( &self, user_holding_a: AccountId, @@ -291,8 +206,8 @@ impl Amm<'_> { exact_amount_out: u128, max_amount_in: u128, token_definition_id_in: AccountId, - user_holding_a_key_path: Option<&str>, - user_holding_b_key_path: Option<&str>, + a_mention: &CliAccountMention, + b_mention: &CliAccountMention, ) -> Result { let instruction = amm_core::Instruction::SwapExactOutput { exact_amount_out, @@ -333,63 +248,36 @@ impl Amm<'_> { user_holding_b, ]; - let account_id_auth = if definition_token_a_id == token_definition_id_in { - user_holding_a + let (account_id_auth, seller_mention) = if definition_token_a_id == token_definition_id_in { + (user_holding_a, a_mention) } else if definition_token_b_id == token_definition_id_in { - user_holding_b + (user_holding_b, b_mention) } else { - return Err(ExecutionFailureKind::AccountDataError( - token_definition_id_in, - )); + return Err(ExecutionFailureKind::AccountDataError(token_definition_id_in)); }; - let nonces = self - .0 - .get_accounts_nonces(vec![account_id_auth]) - .await + 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 message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let witness_set = if let (Some(kp_a), Some(kp_b)) = - (user_holding_a_key_path, user_holding_b_key_path) - { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig_1, pk_1) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp_a, &msg_hash, - )?; - let (sig_2, pk_2) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp_b, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list( - &message, - &[sig_1, sig_2], - &[pk_1, pk_2], - ) - .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 { - let signing_key = self - .0 - .storage - .user_data - .get_pub_account_signing_key(account_id_auth) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -398,10 +286,7 @@ impl Amm<'_> { .await?) } - #[expect( - clippy::too_many_arguments, - reason = "each parameter is distinct; grouping into a struct would add unnecessary indirection" - )] + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_add_liquidity( &self, user_holding_a: AccountId, @@ -410,8 +295,8 @@ impl Amm<'_> { min_amount_liquidity: u128, max_amount_to_add_token_a: u128, max_amount_to_add_token_b: u128, - key_path_a: Option<&str>, - key_path_b: Option<&str>, + a_mention: &CliAccountMention, + b_mention: &CliAccountMention, ) -> Result { let instruction = amm_core::Instruction::AddLiquidity { min_amount_liquidity, @@ -455,78 +340,29 @@ impl Amm<'_> { user_holding_lp, ]; - let nonces = self - .0 - .get_accounts_nonces(vec![user_holding_a, user_holding_b]) - .await + let mut groups = SigningGroups::new(); + 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())))?; + + 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 message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let pin = if key_path_a.is_some() || key_path_b.is_some() { - Some(crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?) + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - None + String::new() }; + let sigs = groups.sign_all(&message.hash(), &pin) + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let (sig_a, pk_a) = if let Some(kp) = key_path_a { - keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().unwrap(), - kp, - &msg_hash, - )? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - let (sig_b, pk_b) = if let Some(kp) = key_path_b { - keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - pin.as_ref().unwrap(), - kp, - &msg_hash, - )? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - let witness_set = nssa::public_transaction::WitnessSet::from_list( - &message, - &[sig_a, sig_b], - &[pk_a, pk_b], - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let tx = nssa::PublicTransaction::new(message, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -535,10 +371,7 @@ impl Amm<'_> { .await?) } - #[expect( - clippy::too_many_arguments, - reason = "each parameter is distinct; grouping into a struct would add unnecessary indirection" - )] + #[expect(clippy::too_many_arguments, reason = "each parameter is distinct")] pub async fn send_remove_liquidity( &self, user_holding_a: AccountId, @@ -547,7 +380,7 @@ impl Amm<'_> { remove_liquidity_amount: u128, min_amount_to_remove_token_a: u128, min_amount_to_remove_token_b: u128, - key_path_lp: Option<&str>, + lp_mention: &CliAccountMention, ) -> Result { let instruction = amm_core::Instruction::RemoveLiquidity { remove_liquidity_amount, @@ -591,44 +424,28 @@ impl Amm<'_> { user_holding_lp, ]; - let nonces = self - .0 - .get_accounts_nonces(vec![user_holding_lp]) - .await + 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 message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let witness_set = if let Some(kp) = key_path_lp { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig, pk) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .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 { - let signing_key_lp = self - .0 - .storage - .user_data - .get_pub_account_signing_key(user_holding_lp) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key_lp]) + 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, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 diff --git a/wallet/src/program_facades/ata.rs b/wallet/src/program_facades/ata.rs index f27e12d7..13add7c8 100644 --- a/wallet/src/program_facades/ata.rs +++ b/wallet/src/program_facades/ata.rs @@ -4,11 +4,18 @@ use ata_core::{compute_ata_seed, get_associated_token_account_id}; use common::{HashType, transaction::NSSATransaction}; 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}; +use crate::{ + ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, + cli::CliAccountMention, + helperfunctions::read_pin, + signing::SigningGroups, +}; pub struct Ata<'wallet>(pub &'wallet WalletCore); @@ -17,7 +24,7 @@ impl Ata<'_> { &self, owner_id: AccountId, definition_id: AccountId, - key_path: Option<&str>, + owner_mention: &CliAccountMention, ) -> Result { let program = Program::ata(); let ata_program_id = program.id(); @@ -27,54 +34,31 @@ impl Ata<'_> { ); let account_ids = vec![owner_id, definition_id, ata_id]; - - let nonces = self - .0 - .get_accounts_nonces(vec![owner_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - let instruction = ata_core::Instruction::Create { ata_program_id }; - let message = nssa::public_transaction::Message::try_new( - program.id(), - account_ids, - nonces, - instruction, - )?; + 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 msg_hash = message.hash(); - let witness_set = if let Some(kp) = key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig, pk) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .map_err(ExecutionFailureKind::TransactionBuildError)? + 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 { - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(owner_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); + Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) } pub async fn send_transfer( @@ -83,7 +67,7 @@ impl Ata<'_> { definition_id: AccountId, recipient_id: AccountId, amount: u128, - key_path: Option<&str>, + owner_mention: &CliAccountMention, ) -> Result { let program = Program::ata(); let ata_program_id = program.id(); @@ -93,57 +77,31 @@ impl Ata<'_> { ); let account_ids = vec![owner_id, sender_ata_id, recipient_id]; + let instruction = ata_core::Instruction::Transfer { ata_program_id, amount }; - let nonces = self - .0 - .get_accounts_nonces(vec![owner_id]) - .await + 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 instruction = ata_core::Instruction::Transfer { - ata_program_id, - amount, - }; + let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction)?; - let message = nssa::public_transaction::Message::try_new( - program.id(), - account_ids, - nonces, - instruction, - )?; - - let msg_hash = message.hash(); - let witness_set = if let Some(kp) = key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig, pk) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .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 { - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(owner_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); + Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) } pub async fn send_burn( @@ -151,7 +109,7 @@ impl Ata<'_> { owner_id: AccountId, definition_id: AccountId, amount: u128, - key_path: Option<&str>, + owner_mention: &CliAccountMention, ) -> Result { let program = Program::ata(); let ata_program_id = program.id(); @@ -161,57 +119,31 @@ impl Ata<'_> { ); let account_ids = vec![owner_id, holder_ata_id, definition_id]; + let instruction = ata_core::Instruction::Burn { ata_program_id, amount }; - let nonces = self - .0 - .get_accounts_nonces(vec![owner_id]) - .await + 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 instruction = ata_core::Instruction::Burn { - ata_program_id, - amount, - }; + let message = nssa::public_transaction::Message::try_new(program.id(), account_ids, nonces, instruction)?; - let message = nssa::public_transaction::Message::try_new( - program.id(), - account_ids, - nonces, - instruction, - )?; - - let msg_hash = message.hash(); - let witness_set = if let Some(kp) = key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (sig, pk) = keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, kp, &msg_hash, - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .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 { - let Some(signing_key) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(owner_id) - else { - return Err(ExecutionFailureKind::KeyNotFoundError); - }; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); - - Ok(self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?) + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); + Ok(self.0.sequencer_client.send_transaction(NSSATransaction::Public(tx)).await?) } pub async fn send_create_private_owner( @@ -230,18 +162,15 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(definition_id), PrivacyPreservingAccount::Public(ata_id), ]; 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"); @@ -270,18 +199,15 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(sender_ata_id), PrivacyPreservingAccount::Public(recipient_id), ]; 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"); @@ -309,18 +235,15 @@ impl Ata<'_> { Program::serialize_instruction(instruction).expect("Instruction should serialize"); let accounts = vec![ - PrivacyPreservingAccount::PrivateOwned(owner_id), + self.0 + .resolve_private_account(owner_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_ata_id), PrivacyPreservingAccount::Public(definition_id), ]; 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 de064272..60c42189 100644 --- a/wallet/src/program_facades/native_token_transfer/public.rs +++ b/wallet/src/program_facades/native_token_transfer/public.rs @@ -11,7 +11,7 @@ use sequencer_service_rpc::RpcClient as _; use super::NativeTokenTransfer; use crate::{ ExecutionFailureKind, cli::CliAccountMention, helperfunctions::read_pin, - signing::KeycardSessionContext, + signing::SigningGroups, }; impl NativeTokenTransfer<'_> { @@ -23,30 +23,26 @@ 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 account_ids = vec![from, to]; - let signing_ids: Vec = if to_signer.needs_signature() { - vec![from, to] - } else { - vec![from] - }; + let mut groups = SigningGroups::new(); + 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(), + )) + })?; let program_id = Program::authenticated_transfer_program().id(); let nonces = self .0 - .get_accounts_nonces(signing_ids) + .get_accounts_nonces(groups.signing_ids()) .await .map_err(ExecutionFailureKind::SequencerError)?; let message = Message::try_new( program_id, - account_ids, + vec![from, to], nonces, AuthTransferInstruction::Transfer { amount: balance_to_move, @@ -54,7 +50,7 @@ impl NativeTokenTransfer<'_> { ) .map_err(ExecutionFailureKind::TransactionBuildError)?; - let pin = if from_mention.is_keycard() || to_mention.is_keycard() { + let pin = if groups.needs_pin() { read_pin() .map_err(|e| { ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( @@ -67,30 +63,11 @@ impl NativeTokenTransfer<'_> { String::new() }; - let witness_set = pyo3::Python::with_gil(|py| -> pyo3::PyResult { - let mut ctx = KeycardSessionContext::new(&pin); - let hash = message.hash(); + let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) + })?; - let (from_sig, from_pk) = from_signer - .sign(self.0, &mut ctx, py, &hash) - .expect("from signer always produces a signature") - .map_err(|e| pyo3::PyErr::new::(e.to_string()))?; - - let sigs_and_keys = match to_signer - .sign(self.0, &mut ctx, py, &hash) - .transpose() - .map_err(|e| pyo3::PyErr::new::(e.to_string()))? - { - Some((to_sig, to_pk)) => vec![(from_sig, from_pk), (to_sig, to_pk)], - None => vec![(from_sig, from_pk)], - }; - - ctx.close(py); - Ok(WitnessSet::from_raw_parts(sigs_and_keys)) - }) - .map_err(ExecutionFailureKind::KeycardError)?; - - let tx = PublicTransaction::new(message, witness_set); + let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 .sequencer_client @@ -119,11 +96,16 @@ 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 mut groups = SigningGroups::new(); + groups + .add_sender(account_mention, from, self.0) + .map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( + e.to_string(), + )) + })?; - let pin = if account_mention.is_keycard() { + let pin = if groups.needs_pin() { read_pin() .map_err(|e| { ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( @@ -136,21 +118,11 @@ impl NativeTokenTransfer<'_> { String::new() }; - let witness_set = pyo3::Python::with_gil(|py| -> pyo3::PyResult { - let mut ctx = KeycardSessionContext::new(&pin); - let hash = message.hash(); + let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| { + ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())) + })?; - let (sig, pk) = signer - .sign(self.0, &mut ctx, py, &hash) - .expect("account signer always produces a signature") - .map_err(|e| pyo3::PyErr::new::(e.to_string()))?; - - ctx.close(py); - Ok(WitnessSet::from_raw_parts(vec![(sig, pk)])) - }) - .map_err(ExecutionFailureKind::KeycardError)?; - - let tx = PublicTransaction::new(message, witness_set); + let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 .sequencer_client diff --git a/wallet/src/program_facades/native_token_transfer/shielded.rs b/wallet/src/program_facades/native_token_transfer/shielded.rs index 35e84ddb..03015a63 100644 --- a/wallet/src/program_facades/native_token_transfer/shielded.rs +++ b/wallet/src/program_facades/native_token_transfer/shielded.rs @@ -3,7 +3,7 @@ use nssa::AccountId; use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey}; use super::{NativeTokenTransfer, auth_transfer_preparation}; -use crate::{ExecutionFailureKind, PrivacyPreservingAccount}; +use crate::{ExecutionFailureKind, PrivacyPreservingAccount, cli::CliAccountMention}; impl NativeTokenTransfer<'_> { pub async fn send_shielded_transfer( @@ -11,9 +11,10 @@ impl NativeTokenTransfer<'_> { from: AccountId, to: AccountId, balance_to_move: u128, - from_key_path: &Option, + from_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + let key_path = from_mention.key_path().map(str::to_owned); self.0 .send_privacy_preserving_tx_with_pre_check( @@ -26,7 +27,7 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - from_key_path, + &key_path, ) .await .map(|(resp, secrets)| { @@ -45,9 +46,10 @@ impl NativeTokenTransfer<'_> { to_vpk: ViewingPublicKey, to_identifier: Identifier, balance_to_move: u128, - from_key_path: &Option, + from_mention: &CliAccountMention, ) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> { let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move); + let key_path = from_mention.key_path().map(str::to_owned); self.0 .send_privacy_preserving_tx_with_pre_check( @@ -62,7 +64,7 @@ impl NativeTokenTransfer<'_> { instruction_data, &program.into(), tx_pre_check, - from_key_path, + &key_path, ) .await .map(|(resp, secrets)| { diff --git a/wallet/src/program_facades/token.rs b/wallet/src/program_facades/token.rs index f86f4cb9..d1ccf37a 100644 --- a/wallet/src/program_facades/token.rs +++ b/wallet/src/program_facades/token.rs @@ -1,90 +1,56 @@ use common::{HashType, transaction::NSSATransaction}; -use keycard_wallet::KeycardWallet; -use nssa::{AccountId, PublicKey, Signature, program::Program, public_transaction::WitnessSet}; +use nssa::{AccountId, program::Program, public_transaction::WitnessSet}; 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}; +use crate::{ + ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, + cli::CliAccountMention, + helperfunctions::read_pin, + 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, supply_account_id: AccountId, name: String, total_supply: u128, - definition_key_path: Option<&str>, - supply_key_path: Option<&str>, + definition_mention: &CliAccountMention, + 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 nonces = self - .0 - .get_accounts_nonces(account_ids.clone()) - .await + + 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())))?; + + 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 message = nssa::public_transaction::Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - let msg_hash = message.hash(); - let pin = if definition_key_path.is_some() || supply_key_path.is_some() { - Some(crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?) + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - None + String::new() }; + let sigs = groups.sign_all(&message.hash(), &pin) + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let (sig_def, pk_def) = if let Some(kp) = definition_key_path { - KeycardWallet::sign_message_for_path_with_connect(pin.as_ref().unwrap(), kp, &msg_hash)? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - let (sig_sup, pk_sup) = if let Some(kp) = supply_key_path { - KeycardWallet::sign_message_for_path_with_connect(pin.as_ref().unwrap(), kp, &msg_hash)? - } else { - let sk = self - .0 - .storage - .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), - ) - }; - - let witness_set = nssa::public_transaction::WitnessSet::from_list( - &message, - &[sig_def, sig_sup], - &[pk_def, pk_sup], - ) - .map_err(ExecutionFailureKind::TransactionBuildError)?; - - let tx = nssa::PublicTransaction::new(message, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -108,7 +74,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(supply_account_id), + self.0 + .resolve_private_account(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -138,7 +106,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(supply_account_id), ], instruction_data, @@ -169,8 +139,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(supply_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(supply_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -190,79 +164,36 @@ impl Token<'_> { sender_account_id: AccountId, recipient_account_id: AccountId, amount: u128, - sender_key_path: Option, + sender_mention: &CliAccountMention, + 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 nonces = self - .0 - .get_accounts_nonces(vec![sender_account_id]) - .await + 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())))?; + + let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await .map_err(ExecutionFailureKind::SequencerError)?; - let private_keys = if sender_key_path.is_none() { - 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); + let message = nssa::public_transaction::Message::try_new(program_id, account_ids, nonces, instruction).unwrap(); - if let Some(recipient_sk) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(recipient_account_id) - { - private_keys.push(recipient_sk); - let recipient_nonces = self - .0 - .get_accounts_nonces(vec![recipient_account_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - nonces.extend(recipient_nonces); - } else { - println!( - "Receiver's account ({recipient_account_id}) private key not found in wallet. Proceeding with only sender's key." - ); - } - private_keys + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - Vec::new() + String::new() }; + let sigs = groups.sign_all(&message.hash(), &pin) + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let message = nssa::public_transaction::Message::try_new( - program_id, - account_ids, - nonces, - 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::( - e.to_string(), - )) - })?; - let (signature, public_key) = KeycardWallet::sign_message_for_path_with_connect( - &pin, - &sender_key_path, - &message.hash(), - )?; - WitnessSet::from_list(&message, &[signature], &[public_key]) - .map_err(ExecutionFailureKind::TransactionBuildError)? - } else { - nssa::public_transaction::WitnessSet::for_message(&message, &private_keys) - }; - - let tx = nssa::PublicTransaction::new(message, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -286,8 +217,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), - PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(recipient_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -319,7 +254,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::PrivateForeign { npk: recipient_npk, vpk: recipient_vpk, @@ -354,7 +291,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(sender_account_id), + self.0 + .resolve_private_account(sender_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(recipient_account_id), ], instruction_data, @@ -376,7 +315,6 @@ 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, @@ -388,11 +326,13 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(sender_account_id), - PrivacyPreservingAccount::PrivateOwned(recipient_account_id), + self.0 + .resolve_private_account(recipient_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), - &sender_key_path, + &None, ) .await .map(|(resp, secrets)| { @@ -442,129 +382,40 @@ impl Token<'_> { }) } - pub async fn send_initialize_account( - &self, - definition_account: PrivacyPreservingAccount, - holder_account: PrivacyPreservingAccount, - key_path: &Option, - ) -> Result<(HashType, Vec), ExecutionFailureKind> { - let instruction = Instruction::InitializeAccount; - - if definition_account.is_public() && holder_account.is_public() { - let PrivacyPreservingAccount::Public(definition_account_id) = definition_account else { - unreachable!() - }; - let PrivacyPreservingAccount::Public(holder_account_id) = holder_account else { - unreachable!() - }; - - let nonces = self - .0 - .get_accounts_nonces(vec![holder_account_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - let message = nssa::public_transaction::Message::try_new( - Program::token().id(), - vec![definition_account_id, holder_account_id], - nonces, - instruction, - ) - .expect("Instruction should serialize"); - - let witness_set = if let Some(key_path) = key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::< - pyo3::exceptions::PyRuntimeError, - _, - >(e.to_string())) - })?; - let (signature, pub_key) = - keycard_wallet::KeycardWallet::sign_message_for_path_with_connect( - &pin, - key_path, - &message.hash(), - )?; - nssa::public_transaction::WitnessSet::from_list(&message, &[signature], &[pub_key]) - .map_err(ExecutionFailureKind::TransactionBuildError)? - } else { - let signing_key = self - .0 - .storage - .user_data - .get_pub_account_signing_key(holder_account_id) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) - }; - - let tx = nssa::PublicTransaction::new(message, witness_set); - - let hash = self - .0 - .sequencer_client - .send_transaction(NSSATransaction::Public(tx)) - .await?; - Ok((hash, vec![])) - } else { - let instruction_data = - Program::serialize_instruction(instruction).expect("Instruction should serialize"); - - self.0 - .send_privacy_preserving_tx( - vec![definition_account, holder_account], - instruction_data, - &Program::token().into(), - key_path, - ) - .await - } - } - pub async fn send_burn_transaction( &self, definition_account_id: AccountId, holder_account_id: AccountId, amount: u128, - holder_key_path: Option<&str>, + holder_mention: &CliAccountMention, ) -> Result { let account_ids = vec![definition_account_id, holder_account_id]; let instruction = Instruction::Burn { amount_to_burn: amount, }; - let nonces = self - .0 - .get_accounts_nonces(vec![holder_account_id]) - .await + 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())))?; + + 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 message = nssa::public_transaction::Message::try_new(Program::token().id(), account_ids, nonces, instruction) + .expect("Instruction should serialize"); - let msg_hash = message.hash(); - let witness_set = if let Some(kp) = holder_key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; - let (sig, pk) = KeycardWallet::sign_message_for_path_with_connect(&pin, kp, &msg_hash)?; - nssa::public_transaction::WitnessSet::from_list(&message, &[sig], &[pk]) - .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 { - let signing_key = self - .0 - .storage - .user_data - .get_pub_account_signing_key(holder_account_id) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]) + 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, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -588,8 +439,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -619,7 +474,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_account_id), ], instruction_data, @@ -652,7 +509,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -673,78 +532,37 @@ impl Token<'_> { definition_account_id: AccountId, holder_account_id: AccountId, amount: u128, - definition_key_path: Option<&str>, + definition_mention: &CliAccountMention, + holder_mention: &CliAccountMention, ) -> Result { let account_ids = vec![definition_account_id, holder_account_id]; let instruction = Instruction::Mint { amount_to_mint: amount, }; - let mut nonces = self - .0 - .get_accounts_nonces(vec![definition_account_id]) - .await + let mut groups = SigningGroups::new(); + 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())))?; + + let nonces = self.0.get_accounts_nonces(groups.signing_ids()).await .map_err(ExecutionFailureKind::SequencerError)?; - let private_keys = if definition_key_path.is_none() { - let mut private_keys = Vec::new(); - let definition_sk = self - .0 - .storage - .user_data - .get_pub_account_signing_key(definition_account_id) - .ok_or(ExecutionFailureKind::KeyNotFoundError)?; - private_keys.push(definition_sk); + let message = nssa::public_transaction::Message::try_new(Program::token().id(), account_ids, nonces, instruction).unwrap(); - if let Some(holder_sk) = self - .0 - .storage - .user_data - .get_pub_account_signing_key(holder_account_id) - { - private_keys.push(holder_sk); - let holder_nonce: Vec = self - .0 - .get_accounts_nonces(vec![holder_account_id]) - .await - .map_err(ExecutionFailureKind::SequencerError)?; - nonces.extend(holder_nonce); - } else { - println!( - "Holder's account ({holder_account_id}) private key not found in wallet. Proceeding with only definition's key." - ); - } - private_keys + let pin = if groups.needs_pin() { + read_pin() + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))? + .as_str() + .to_owned() } else { - Vec::new() + String::new() }; + let sigs = groups.sign_all(&message.hash(), &pin) + .map_err(|e| ExecutionFailureKind::KeycardError(pyo3::PyErr::new::(e.to_string())))?; - let message = nssa::public_transaction::Message::try_new( - Program::token().id(), - account_ids, - nonces, - instruction, - ) - .unwrap(); - - let witness_set = if let Some(definition_key_path) = definition_key_path { - let pin = crate::helperfunctions::read_pin().map_err(|e| { - ExecutionFailureKind::KeycardError(pyo3::PyErr::new::( - e.to_string(), - )) - })?; - let (signature, public_key) = KeycardWallet::sign_message_for_path_with_connect( - &pin, - definition_key_path, - &message.hash(), - )?; - WitnessSet::from_list(&message, &[signature], &[public_key]) - .map_err(ExecutionFailureKind::TransactionBuildError)? - } else { - nssa::public_transaction::WitnessSet::for_message(&message, &private_keys) - }; - - let tx = nssa::PublicTransaction::new(message, witness_set); + let tx = nssa::PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs)); Ok(self .0 @@ -768,8 +586,12 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -801,7 +623,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::PrivateForeign { npk: holder_npk, vpk: holder_vpk, @@ -836,7 +660,9 @@ impl Token<'_> { self.0 .send_privacy_preserving_tx( vec![ - PrivacyPreservingAccount::PrivateOwned(definition_account_id), + self.0 + .resolve_private_account(definition_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, PrivacyPreservingAccount::Public(holder_account_id), ], instruction_data, @@ -869,7 +695,9 @@ impl Token<'_> { .send_privacy_preserving_tx( vec![ PrivacyPreservingAccount::Public(definition_account_id), - PrivacyPreservingAccount::PrivateOwned(holder_account_id), + self.0 + .resolve_private_account(holder_account_id) + .ok_or(ExecutionFailureKind::KeyNotFoundError)?, ], instruction_data, &Program::token().into(), @@ -922,4 +750,5 @@ impl Token<'_> { (resp, first) }) } + } diff --git a/wallet/src/signing.rs b/wallet/src/signing.rs index 19d45fac..1a710950 100644 --- a/wallet/src/signing.rs +++ b/wallet/src/signing.rs @@ -1,60 +1,116 @@ use anyhow::Result; use keycard_wallet::{KeycardWallet, python_path}; -use nssa::{AccountId, PublicKey, Signature}; +use nssa::{AccountId, PrivateKey, PublicKey, Signature}; use pyo3::Python; -use crate::WalletCore; +use crate::{WalletCore, cli::CliAccountMention}; -/// How a single account participates in signing a transaction. +/// Groups transaction signers by type to minimise Python GIL acquisition. /// -/// Created from [`crate::cli::CliAccountMention`] via `to_signer` / `to_recipient_signer`. -/// Used inside `Python::with_gil` blocks — does not cross async boundaries. -pub enum AccountSigner { - /// Account is in the local wallet; key is looked up from storage at sign time. - Local(AccountId), - /// Account is on a Keycard at the given BIP32 path. - Keycard(String), - /// Foreign account — no signature or nonce required. - Foreign, +/// Local signers are signed in pure Rust; all keycard signers share a single Python session +/// with one `connect` / `close_session` pair. +#[derive(Default)] +pub struct SigningGroups { + local: Vec<(AccountId, PrivateKey)>, + keycard: Vec<(AccountId, String)>, } -impl AccountSigner { +impl SigningGroups { #[must_use] - pub const fn needs_signature(&self) -> bool { - !matches!(self, Self::Foreign) + pub fn new() -> Self { + Self::default() } - /// Sign `hash` and return `(Signature, PublicKey)`, or `None` for `Foreign`. - pub fn sign( - &self, + /// Add a sender. Keycard paths are queued for the hardware session; local accounts + /// have their signing key resolved eagerly. Errors if no key is found. + pub fn add_sender( + &mut self, + mention: &CliAccountMention, + account_id: AccountId, wallet_core: &WalletCore, - ctx: &mut KeycardSessionContext, - py: Python<'_>, - hash: &[u8; 32], - ) -> Option> { - match self { - Self::Local(id) => { - 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) - .and_then(|w| w.sign_message_for_path(py, path, hash)) - .map_err(anyhow::Error::from), - ), - Self::Foreign => None, + ) -> Result<()> { + if let CliAccountMention::KeyPath(path) = mention { + self.keycard.push((account_id, path.clone())); + return Ok(()); } + let key = wallet_core + .storage() + .key_chain() + .pub_account_signing_key(account_id) + .ok_or_else(|| anyhow::anyhow!("signing key not found for account {account_id}"))? + .clone(); + self.local.push((account_id, key)); + Ok(()) + } + + /// 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) } }