addressed deferred comments

This commit is contained in:
Marvin Jones 2026-05-22 14:23:44 -04:00
parent ada4bf3e0a
commit b0593b34fb
19 changed files with 349 additions and 224 deletions

3
.gitignore vendored
View File

@ -12,3 +12,6 @@ result
wallet-ffi/wallet_ffi.h
bedrock_signing_key
integration_tests/configs/debug/
venv/
keycard_wallet/python/__pycache__/
keycard_wallet/python/keycard-py/

View File

@ -60,6 +60,29 @@ Unset it when done:
unset KEYCARD_PIN
```
## Pairing password
The pairing password is used to establish a secure channel between the wallet and the card. It is set permanently on the card during `wallet keycard init` and must match on every subsequent re-pair.
The default password (`KeycardDefaultPairing`) is [recommended](https://docs.keycard.tech/en/developers/core) for most users. Wallet CLI allows advance users the flexibility to set their own pairing password.
To use a custom pairing password, set it before `init`:
```bash
export KEYCARD_PAIRING_PASSWORD=my-custom-password
wallet keycard init
```
After a successful initializaation, subsequent commands (`connect`, transfers) use the cached pairing index and key — the pairing password is not needed again until the pairing is cleared.
**Important:** if you initialized with a custom password, `KEYCARD_PAIRING_PASSWORD` must be set in every session where re-pairing can occur (after `disconnect`, or on a new machine). If the env var is missing then wallet CLI will attempt to use the default password. As a result, pairing will fail.
Unset the pairing password variable when done:
```bash
unset KEYCARD_PAIRING_PASSWORD
```
## Keycard Commands
### Keycard
@ -473,22 +496,34 @@ Transaction data is ...
## Testing
Tests for Keycard commands are in `keycard_wallet/tests/keycard_tests.sh`. Run from the repo root with a Keycard connected:
Tests for Keycard commands are in `keycard_wallet/tests/`.
| Test file | Description |
|---|---|
| `keycard_tests.sh` | Core Keycard wallet commands and `auth-transfer` commands |
| `keycard_tests_2.sh` | Tests Keycard wallet commands for `amma`, `token` and `ata` programs |
| `keycard_test_3.sh` | Demonstrates retrieving private account keys from keycard |
| `keycard_power_recovery_tests.sh` | Modified test file of `keycard_tests.sh` to test power recovery paths |
Run from the repo root with a Keycard connected:
```bash
bash keycard_wallet/tests/keycard_tests.sh
bash keycard_wallet/tests/keycard_tests_2.sh
bash keycard_wallet/tests/keycard_test_3.sh
bash keycard_wallet/tests/keycard_power_recovery_tests.sh
```
## SigningGroups
## SigningGroup
`SigningGroups` (`wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved.
`SigningGroup` (`wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved.
Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing.
Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce.
```
SigningGroups {
SigningGroup {
local: [(AccountId, PrivateKey)], // signed in pure Rust
keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session
}

View File

@ -1,134 +0,0 @@
#!/bin/bash
# Run wallet_with_keycard.sh first
source venv/bin/activate # Load the appropriate virtual environment
source venv/bin/activate
export KEYCARD_PIN=111111
# =============================================================================
# Keycard setup
# =============================================================================
echo "=== Test: wallet keycard available ==="
wallet keycard available
# Install a new mnemonic phrase to keycard
echo "Test: wallet keycard load"
export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then"
wallet keycard load
unset KEYCARD_MNEMONIC
echo "Test: wallet auth-transfer init --account-id \"m/44'/60'/0'/0/0\""
wallet auth-transfer init --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet pinata claim --to \"m/44'/60'/0'/0/0\""
wallet pinata claim --to "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet auth-transfer init and send between two keycard accounts"
wallet auth-transfer init --account-id "m/44'/60'/0'/0/1"
wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\""
wallet account get --account-id "m/44'/60'/0'/0/1"
# Send from keycard account to a local wallet account
echo "Test: create local wallet account"
LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+')
echo "Created local account: Public/${LOCAL_ACCOUNT_ID}"
echo "Test: wallet auth-transfer init local account"
wallet auth-transfer init --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo "Test: wallet auth-transfer send from keycard to local account"
wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/${LOCAL_ACCOUNT_ID}"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\""
wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
# Create a local wallet account, fund it, and send to keycard account (co-signed: local key + keycard)
echo "Test: wallet auth-transfer send from local account to keycard account"
wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1"
echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\""
wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\""
wallet account get --account-id "m/44'/60'/0'/0/1"
# Send from keycard account to a local wallet account (foreign recipient — no signature needed)
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
echo "Test: wallet auth-transfer send from keycard to local account"
wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
echo "=== Test: account get path 0 ==="
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "=== Test: account get path 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 "m/44'/60'/0'/0/0" \
--to-npk "55204e2934045b044f06d8222b454d46b54788f33c7dec4f6733d441703bb0e6" \
--to-vpk "02a8626b0c0ad9383c5678dad48c3969b4174fb377cdb03a6259648032c774cec8"
echo "Shielded auth-transfer sent"
sleep 15
wallet account get --account-id "m/44'/60'/0'/0/0"
# =============================================================================
# Test (2): Deshielded auth-transfer — private account → keycard recipient
# A fresh private account is funded, then sends native tokens to keycard
# path 1. The private sender is handled by the ZK circuit; the keycard
# recipient does not sign — resolve() derives its account ID from the card.
# =============================================================================
echo ""
echo "=== Test (2): Deshielded auth-transfer: private account → keycard path 1 ==="
PRIV_SENDER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1)
echo "Fresh private sender account: $PRIV_SENDER"
wallet auth-transfer init --account-id "$PRIV_SENDER"
echo "Test: wallet pinata claim to private sender"
wallet pinata claim --to "$PRIV_SENDER"
sleep 15
echo "priv-sender state after claim:"
wallet account get --account-id "$PRIV_SENDER"
wallet auth-transfer send \
--from "$PRIV_SENDER" \
--to "m/44'/60'/0'/0/1" \
--amount 5
echo "Deshielded transfer of 5: $PRIV_SENDER → keycard path 1"
sleep 15
echo "priv-sender state (balance should have decreased by 5):"
wallet account get --account-id "$PRIV_SENDER"
echo "Keycard path 1 state (balance should have increased by 5):"
wallet account get --account-id "m/44'/60'/0'/0/1"

View File

@ -4,14 +4,18 @@ from ecdsa import VerifyingKey, SECP256k1
from keycard.keycard import KeyCard
from keycard.commands.export_lee_key import export_lee_key
from mnemonic import Mnemonic
from keycard import constants
from mnemonic import Mnemonic
from keycard import constants
import keycard
import os
import secrets
DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing"
def _pairing_password() -> str:
return os.environ.get("KEYCARD_PAIRING_PASSWORD", DEFAULT_PAIRING_PASSWORD)
class KeycardWallet:
def __init__(self):
self.card = KeyCard()
@ -37,7 +41,7 @@ class KeycardWallet:
return False
return True
def initialize(self, pin: str) -> bool:
def initialize(self, pin: str, pairing_password: str | None = None) -> bool:
try:
self.card.select()
@ -45,14 +49,18 @@ class KeycardWallet:
raise RuntimeError("Card is already initialized")
puk = ''.join(secrets.choice('0123456789') for _ in range(12))
self.card.init(pin, puk, DEFAULT_PAIRING_PASSWORD)
self.card.init(pin, puk, pairing_password or _pairing_password())
print(f"Keycard PUK: {puk}")
print("Record this PUK and store it somewhere safe. It cannot be recovered.")
return True
except Exception as e:
raise RuntimeError(f"Error initializing keycard: {e}") from e
def setup_communication(self, pin: str, password = DEFAULT_PAIRING_PASSWORD) -> bool:
def _reconnect(self) -> None:
self.card = KeyCard()
self.card.select()
def _pair(self, pin: str, password: str) -> tuple[int, bytes]:
self.card.select()
if not self.card.is_initialized:
@ -70,14 +78,28 @@ class KeycardWallet:
self.card.unpair(pairing_index)
except Exception:
pass
raise RuntimeError(f"Error setting up communication: {e}") from e
raise RuntimeError(f"Error opening secure channel after fresh pair: {e}") from e
return True
return pairing_index, pairing_key
def get_pairing_data(self) -> tuple[int, bytes]:
return (self.pairing_index, self.pairing_key)
def pair(self, pin: str, password: str | None = None) -> tuple[int, bytes]:
password = password or _pairing_password()
try:
return self._pair(pin, password)
except TransportError as e:
print(f"Transport error during fresh pair ({e}), attempting card reset and retry...")
try:
self._reconnect()
result = self._pair(pin, password)
print("Retry succeeded after card reset.")
return result
except TransportError as e2:
raise RuntimeError(
"Card lost power and did not recover after reset. "
"Try reseating the card in the reader."
) from e2
def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool:
def _setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool:
self.card.select()
if not self.card.is_initialized:
@ -94,6 +116,22 @@ class KeycardWallet:
return True
def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool:
try:
return self._setup_communication_with_pairing(pin, pairing_index, pairing_key)
except TransportError as e:
print(f"Transport error during stored pairing ({e}), attempting card reset and retry...")
try:
self._reconnect()
result = self._setup_communication_with_pairing(pin, pairing_index, pairing_key)
print("Retry succeeded after card reset.")
return result
except TransportError as e2:
raise RuntimeError(
"Card lost power and did not recover after reset. "
"Try reseating the card in the reader."
) from e2
def close_session(self) -> bool:
return True

View File

@ -56,10 +56,10 @@ impl KeycardWallet {
.extract()
}
pub fn get_pairing_data(&self, py: Python<'_>) -> PyResult<(u8, Vec<u8>)> {
pub fn pair(&self, py: Python<'_>, pin: &str) -> PyResult<(u8, Vec<u8>)> {
self.instance
.bind(py)
.call_method0("get_pairing_data")?
.call_method1("pair", (pin,))?
.extract()
}
@ -96,20 +96,11 @@ impl KeycardWallet {
{
return Ok(());
}
self.setup_communication(py, pin)?;
if let Ok((index, key)) = self.get_pairing_data(py) {
save_pairing(&KeycardPairingData { index, key });
}
let (index, key) = self.pair(py, pin)?;
save_pairing(&KeycardPairingData { index, key });
Ok(())
}
pub fn setup_communication(&self, py: Python<'_>, pin: &str) -> PyResult<bool> {
self.instance
.bind(py)
.call_method1("setup_communication", (pin,))?
.extract()
}
pub fn disconnect(&self, py: Python) -> PyResult<bool> {
self.instance.bind(py).call_method0("disconnect")?.extract()
}
@ -269,19 +260,9 @@ impl KeycardWallet {
) -> PyResult<PrivateKeyPair> {
Python::with_gil(|py| {
python_path::add_python_path(py)?;
let wallet = Self::new(py)?;
let is_connected = wallet.setup_communication(py, pin)?;
if is_connected {
log::info!("\u{2705} Keycard is now connected to wallet.");
} else {
log::info!("\u{274c} Keycard is not connected to wallet.");
}
wallet.connect(py, pin)?;
let result = wallet.get_private_keys_for_path(py, path);
drop(wallet.disconnect(py));
result
})

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
Forces the card in the first available reader into the unpowered state via
PC/SC SCARD_UNPOWER_CARD. Run immediately before a wallet command to simulate
the power-loss condition reported on some USB reader/driver combinations.
Either:
- pcscd re-powers the card on the next SCardConnect, so wallet
commands will succeed without triggering the retry path.
- the card stays unpowered, triggering TransportError
and exercising the retry wrapper in pair() / setup_communication_with_pairing().
"""
import sys
from smartcard.scard import (
SCardEstablishContext, SCardListReaders, SCardConnect, SCardDisconnect,
SCARD_SCOPE_USER, SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1,
SCARD_UNPOWER_CARD,
)
hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
hresult, reader_list = SCardListReaders(hcontext, [])
if not reader_list:
print("force_unpower: no readers found, skipping.")
sys.exit(0)
hresult, hcard, _ = SCardConnect(
hcontext,
reader_list[0],
SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1,
)
if hresult != 0:
print(f"force_unpower: SCardConnect failed (hresult={hresult:#010x}), skipping.")
sys.exit(0)
SCardDisconnect(hcard, SCARD_UNPOWER_CARD)
print("force_unpower: card powered down.")

View File

@ -0,0 +1,129 @@
#!/bin/bash
# Power-recovery variant of keycard_tests.sh.
#
# Forces a card power cycle before each keycard-backed wallet command to verify
# commands survive mid-session power loss.
#
# On Linux: pcscd re-powers the card automatically on the next connection, so
# commands will succeed without triggering the retry path. This tests resilience
# to power cycles, not the retry wrapper itself.
#
# On Mac (affected driver): the card stays unpowered after force_unpower.py,
# triggering TransportError. You should see "Retry succeeded after card reset."
# in the output of each command, confirming the retry wrapper fires and recovers.
#
# Prerequisites:
# - Run wallet_with_keycard.sh first to set up the venv and dependencies.
# - Run `wallet keycard init` and `wallet keycard connect` before this script.
source venv/bin/activate
export KEYCARD_PIN=111111
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
unpower() {
python "$SCRIPT_DIR/force_unpower.py"
}
echo "Test: wallet keycard available"
wallet keycard available
echo ""
echo "Test: wallet keycard load (after power cycle)"
export KEYCARD_MNEMONIC="fashion degree mountain wool question damp current pond grow dolphin chronic then"
unpower
wallet keycard load
unset KEYCARD_MNEMONIC
echo ""
echo "Test: wallet auth-transfer init --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet auth-transfer init --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet pinata claim --to \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet pinata claim --to "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet auth-transfer init and send between two keycard accounts (after power cycle)"
unpower
wallet auth-transfer init --account-id "m/44'/60'/0'/0/1"
unpower
wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/1"
echo ""
echo "Test: create local wallet account"
LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+')
echo "Created local account: Public/${LOCAL_ACCOUNT_ID}"
echo ""
echo "Test: wallet auth-transfer init local account"
wallet auth-transfer init --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo ""
echo "Test: wallet auth-transfer send from keycard to local account (after power cycle)"
unpower
wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/${LOCAL_ACCOUNT_ID}"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\" (after power cycle)"
unpower
wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo ""
echo "Test: wallet auth-transfer send from local account to keycard account (after power cycle)"
unpower
wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1"
echo ""
echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\" (after power cycle)"
unpower
wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/1"
echo ""
echo "Test: wallet auth-transfer send from keycard to foreign account (after power cycle)"
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
unpower
wallet auth-transfer send --amount 10 --from "m/44'/60'/0'/0/0" --to "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
echo ""
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\" (after power cycle)"
unpower
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "Test: wallet account get foreign account (after power cycle)"
unpower
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"

View File

View File

@ -28,7 +28,8 @@ wallet pinata claim --to "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet auth-transfer init and send between two keycard accounts"
echo ""
echo "=== Test: Keycard account to Keycard account ==="
wallet auth-transfer init --account-id "m/44'/60'/0'/0/1"
wallet auth-transfer send --amount 40 --from "m/44'/60'/0'/0/0" --to "m/44'/60'/0'/0/1"
@ -38,7 +39,8 @@ wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\""
wallet account get --account-id "m/44'/60'/0'/0/1"
# Send from keycard account to a local wallet account
echo ""
echo "=== Test: Keycard account to public local account ==="
echo "Test: create local wallet account"
LOCAL_ACCOUNT_ID=$(wallet account new public 2>&1 | grep -oP '(?<=Public/)\S+')
echo "Created local account: Public/${LOCAL_ACCOUNT_ID}"
@ -56,7 +58,8 @@ wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"Public/${LOCAL_ACCOUNT_ID}\""
wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
# Create a local wallet account, fund it, and send to keycard account (co-signed: local key + keycard)
echo ""
echo "=== Test: public local account to Keycard account ==="
echo "Test: wallet auth-transfer send from local account to keycard account"
wallet auth-transfer send --amount 10 --from "Public/${LOCAL_ACCOUNT_ID}" --to "m/44'/60'/0'/0/1"
@ -67,7 +70,8 @@ wallet account get --account-id "Public/${LOCAL_ACCOUNT_ID}"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/1\""
wallet account get --account-id "m/44'/60'/0'/0/1"
# Send from keycard account to a local wallet account (foreign recipient — no signature needed)
echo ""
echo "=== Test: Keycard account to foreign recipient (no signature required) ==="
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
@ -79,3 +83,44 @@ wallet account get --account-id "m/44'/60'/0'/0/0"
echo "Test: wallet account get --account-id \"m/44'/60'/0'/0/0\""
wallet account get --account-id "Public/7wHg9sbJwc6h3NP1S9bekfAzB8CHifEcxKswCKUt3YQo"
echo ""
echo "=== Test: Shielded auth-transfer to owned private account ==="
wallet auth-transfer send --amount 2 \
--from "m/44'/60'/0'/0/0" \
--to-npk "55204e2934045b044f06d8222b454d46b54788f33c7dec4f6733d441703bb0e6" \
--to-vpk "02a8626b0c0ad9383c5678dad48c3969b4174fb377cdb03a6259648032c774cec8"
echo "Shielded auth-transfer sent"
sleep 15
wallet account get --account-id "m/44'/60'/0'/0/0"
echo ""
echo "=== Test: Deshielded auth-transfer: private account → keycard path 1 ==="
PRIV_SENDER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1)
echo "Fresh private sender account: $PRIV_SENDER"
wallet auth-transfer init --account-id "$PRIV_SENDER"
echo "Test: wallet pinata claim to private sender"
wallet pinata claim --to "$PRIV_SENDER"
sleep 15
echo "priv-sender state after claim:"
wallet account get --account-id "$PRIV_SENDER"
wallet auth-transfer send \
--from "$PRIV_SENDER" \
--to "m/44'/60'/0'/0/1" \
--amount 5
echo "Deshielded transfer of 5: $PRIV_SENDER → keycard path 1"
sleep 15
echo "priv-sender state (balance should have decreased by 5):"
wallet account get --account-id "$PRIV_SENDER"
echo "Keycard path 1 state (balance should have increased by 5):"
wallet account get --account-id "m/44'/60'/0'/0/1"

View File

View File

@ -40,7 +40,7 @@ impl WalletSubcommand for KeycardSubcommand {
match self {
Self::Available => {
Python::with_gil(|py| {
python_path::add_python_path(py).expect("keycard_wallet.py not found");
python_path::add_python_path(py).expect("`wallet::keycard::available`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::available`: invalid data received for pin");
@ -61,7 +61,7 @@ impl WalletSubcommand for KeycardSubcommand {
let pin = read_pin()?;
Python::with_gil(|py| {
python_path::add_python_path(py).expect("keycard_wallet.py not found");
python_path::add_python_path(py).expect("`wallet::keycard::connect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::connect`: invalid keycard wallet provided");
@ -80,7 +80,7 @@ impl WalletSubcommand for KeycardSubcommand {
let pin = read_pin()?;
Python::with_gil(|py| {
python_path::add_python_path(py).expect("keycard_wallet.py not found");
python_path::add_python_path(py).expect("`wallet::keycard::disconnect`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::disconnect`: invalid keycard wallet provided");
@ -103,7 +103,7 @@ impl WalletSubcommand for KeycardSubcommand {
let pin = read_pin()?;
Python::with_gil(|py| {
python_path::add_python_path(py).expect("keycard_wallet.py not found");
python_path::add_python_path(py).expect("`wallet::keycard::init`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::init`: invalid keycard wallet provided");
@ -125,7 +125,7 @@ impl WalletSubcommand for KeycardSubcommand {
let mnemonic = read_mnemonic()?;
Python::with_gil(|py| {
python_path::add_python_path(py).expect("keycard_wallet.py not found");
python_path::add_python_path(py).expect("`wallet::keycard::load`: unable to setup python path");
let wallet = KeycardWallet::new(py)
.expect("`wallet::keycard::load`: invalid keycard wallet provided");

View File

@ -199,7 +199,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::TransferToken {
sender_account_id: from_mention,
recipient_account_id: to_mention.expect("matched Some branch"),
recipient_account_id: to_mention.expect("`wallet::cli::programs::token::Send`: Invalid to_mention account provided"),
balance_to_move: amount,
},
)
@ -318,7 +318,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
amount,
} => {
let def_mention = definition.clone();
let hol_mention = holder.clone();
let holder_mention = holder.clone();
let definition = definition.resolve(wallet_core.storage())?;
let holder = holder
.map(|account_mention| account_mention.resolve(wallet_core.storage()))
@ -342,7 +342,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::MintToken {
definition_account_id: def_mention,
holder_account_id: hol_mention.expect("matched Some branch"),
holder_account_id: holder_mention.expect("`wallet::cli::programs::token::Mint`: Invalid holder_mention account provided"),
amount,
},
)

View File

@ -40,7 +40,7 @@ use crate::{
cli::CliAccountMention,
config::WalletConfigOverrides,
poller::TxPoller,
signing::SigningGroups,
signing::SigningGroup,
storage::key_chain::SharedAccountEntry,
};
@ -89,7 +89,7 @@ pub enum ExecutionFailureKind {
}
impl ExecutionFailureKind {
/// Convert an [`anyhow::Error`] (e.g. from [`SigningGroups`]) into a keycard error.
/// Convert an [`anyhow::Error`] (e.g. from [`SigningGroup`]) into a keycard error.
#[must_use]
#[expect(
clippy::needless_pass_by_value,
@ -565,13 +565,13 @@ impl WalletCore {
}
/// Send a public transaction, fetching nonces automatically from
/// [`SigningGroups::signing_ids`].
/// [`SigningGroup::signing_ids`].
pub async fn send_public_tx<T: serde::Serialize>(
&self,
program: &Program,
account_ids: Vec<AccountId>,
instruction: T,
groups: SigningGroups,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
let nonces = self
.get_accounts_nonces(groups.signing_ids())
@ -591,7 +591,7 @@ impl WalletCore {
account_ids: Vec<AccountId>,
nonces: Vec<Nonce>,
instruction: T,
groups: SigningGroups,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
let message = nssa::public_transaction::Message::try_new(
program.id(),
@ -661,14 +661,14 @@ impl WalletCore {
KeycardWallet::get_public_account_id_for_path_with_connect(&pin, key_path_str)?;
let account_id: AccountId = match account_id_str
.parse::<AccountIdWithPrivacy>()
.expect("Valid parsing of account id")
.expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: invalid account id parsed")
{
AccountIdWithPrivacy::Public(id) | AccountIdWithPrivacy::Private(id) => id,
};
let account = self
.get_account_public(account_id)
.await
.expect("Expect valid account");
.expect("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: unable to retrieve public account");
let pin_str = pin.as_str().to_owned();
(
Some(AccountWithMetadata {

View File

@ -3,7 +3,7 @@ use common::HashType;
use nssa::{AccountId, program::Program};
use token_core::TokenHolding;
use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroups};
use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroup};
pub struct Amm<'wallet>(pub &'wallet WalletCore);
impl Amm<'_> {
@ -61,7 +61,7 @@ impl Amm<'_> {
user_holding_lp,
];
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(a_mention, user_holding_a, self.0)
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
@ -133,7 +133,7 @@ impl Amm<'_> {
));
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
@ -202,7 +202,7 @@ impl Amm<'_> {
));
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
@ -266,7 +266,7 @@ impl Amm<'_> {
user_holding_lp,
];
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(a_mention, user_holding_a, self.0)
.and_then(|()| groups.add_required(b_mention, user_holding_b, self.0))
@ -331,7 +331,7 @@ impl Amm<'_> {
user_holding_lp,
];
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(lp_mention, user_holding_lp, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;

View File

@ -9,7 +9,7 @@ use nssa_core::SharedSecretKey;
use crate::{
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroups,
signing::SigningGroup,
};
pub struct Ata<'wallet>(pub &'wallet WalletCore);
@ -31,7 +31,7 @@ impl Ata<'_> {
let account_ids = vec![owner_id, definition_id, ata_id];
let instruction = ata_core::Instruction::Create { ata_program_id };
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
@ -61,7 +61,7 @@ impl Ata<'_> {
amount,
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
@ -90,7 +90,7 @@ impl Ata<'_> {
amount,
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;

View File

@ -3,7 +3,7 @@ use common::HashType;
use nssa::{AccountId, program::Program};
use super::NativeTokenTransfer;
use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroups};
use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroup};
impl NativeTokenTransfer<'_> {
pub async fn send_public_transfer(
@ -14,7 +14,7 @@ impl NativeTokenTransfer<'_> {
from_mention: &CliAccountMention,
to_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(from_mention, from, self.0)
.and_then(|()| groups.add_optional(to_mention, to, self.0))
@ -37,7 +37,7 @@ impl NativeTokenTransfer<'_> {
from: AccountId,
account_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(account_mention, from, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;

View File

@ -5,7 +5,7 @@ use token_core::Instruction;
use crate::{
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroups,
signing::SigningGroup,
};
pub struct Token<'wallet>(pub &'wallet WalletCore);
@ -23,7 +23,7 @@ impl Token<'_> {
let account_ids = vec![definition_account_id, supply_account_id];
let instruction = Instruction::NewFungibleDefinition { name, total_supply };
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(definition_mention, definition_account_id, self.0)
.and_then(|()| groups.add_required(supply_mention, supply_account_id, self.0))
@ -147,7 +147,7 @@ impl Token<'_> {
amount_to_transfer: amount,
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(sender_mention, sender_account_id, self.0)
.and_then(|()| groups.add_optional(recipient_mention, recipient_account_id, self.0))
@ -350,7 +350,7 @@ impl Token<'_> {
amount_to_burn: amount,
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(holder_mention, holder_account_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
@ -476,7 +476,7 @@ impl Token<'_> {
amount_to_mint: amount,
};
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_required(definition_mention, definition_account_id, self.0)
.and_then(|()| groups.add_optional(holder_mention, holder_account_id, self.0))

View File

@ -10,12 +10,12 @@ use crate::{WalletCore, cli::CliAccountMention};
/// 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 {
pub struct SigningGroup {
local: Vec<(AccountId, PrivateKey)>,
keycard: Vec<(AccountId, String)>,
}
impl SigningGroups {
impl SigningGroup {
#[must_use]
pub fn new() -> Self {
Self::default()

View File

@ -1,12 +0,0 @@
#!/bin/bash
cargo install --path wallet --force
# Install appropriate version of `keycard-py`.
git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git keycard_wallet/python/keycard-py
# Set up virtual environment.
python3 -m venv venv
source venv/bin/activate
pip install pyscard mnemonic ecdsa pyaes
pip install -e keycard_wallet/python/keycard-py