Merge a927955e04444ee4c761364296c36aafd7ec97fa into 006647bc8362fd5489d27906652a4a182fe1911d

This commit is contained in:
jonesmarvin8 2026-05-23 00:16:00 +00:00 committed by GitHub
commit 6cb40fa532
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 2539 additions and 1285 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/

1140
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -133,7 +133,6 @@ url = { version = "2.5.4", features = ["serde"] }
tokio-retry = "0.3.0"
schemars = "1.2"
async-stream = "0.3.6"
criterion = { version = "0.8", features = ["html_reports"] }
logos-blockchain-common-http-client = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
logos-blockchain-key-management-system-service = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "ee281a447d95a951752461ee0a6e88eb4a0f17cf" }
@ -161,6 +160,8 @@ actix-web = { version = "4.13.0", default-features = false, features = [
clap = { version = "4.5.42", features = ["derive", "env"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] }
pyo3 = { version = "0.24", features = ["auto-initialize"] }
zeroize = "1"
criterion = { version = "0.8", features = ["html_reports"] }
# Profile for leptos WASM release builds
[profile.wasm-release]

View File

@ -60,17 +60,41 @@ 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
| Command | Description |
|-----------------------------|------------------------------------------------------------|
| `wallet keycard available` | Checks whether a Keycard reader and card are accessible |
| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK |
| `wallet keycard connect` | Establishes and saves a pairing with the Keycard |
| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing |
| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard |
| Command | Description |
|----------------------------------|-----------------------------------------------------------------------|
| `wallet keycard available` | Checks whether a Keycard reader and card are accessible |
| `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK |
| `wallet keycard connect` | Establishes and saves a pairing with the Keycard |
| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing |
| `wallet keycard load` | Loads a mnemonic phrase onto the Keycard |
| `wallet keycard get-private-keys`| Prints NSK and VSK for a BIP-32 path — **debug builds only** (see below) |
1. Check keycard availability
```bash
@ -122,6 +146,31 @@ Keycard PIN:
✅ Keycard unpaired and pairing cleared.
```
6. Get private keys for a BIP-32 path (**debug builds only**)
`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. The command is only available in debug builds and requires `--reveal` to confirm intent.
First install the wallet with the `keycard-debug` feature:
```bash
cargo install --path wallet --force --features keycard-debug
```
Then run the command:
```bash
wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" --reveal
# Output:
WARNING: NSK and VSK are being printed to stdout. Any terminal log, scrollback, or screen recording captures these keys.
Keycard PIN:
NSK: 55e505bf925e536c843a12ebc08c41ca5f4761eeeb7fa33725f0b44e6f1ac2e4
VSK: 30f798893977a7b7263d1f77abf58e11e014428c92030d6a02fe363cceb41ffa
```
To restore the standard build without `keycard-debug` afterwards:
```bash
cargo install --path wallet --force
```
### Pinata (testnet)
| Command | Description |
@ -213,25 +262,270 @@ Keycard PIN:
Transaction hash is 7d4c1b8e2f903a56fd19084b3c8b25d07e8f243829bc50addf6e2c78b4b09e45
```
### Token program
`--definition`, `--holder`, `--from`, and `--to` each accept any of:
- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`)
- An account ID with privacy prefix (e.g. `Public/9bKm...`)
- An account label (e.g. `my-account`)
The token program requires both the definition account and the holder/recipient to sign when both are owned. If only one is a Keycard path, only that account signs via the card; the other signs locally or is treated as foreign.
**Shielded transfers** (public Keycard sender → private recipient) are supported. The Keycard signs the public sender's authorization; the ZK circuit handles the private recipient side.
| Command | Description |
|--------------------|-------------------------------------------------------|
| `wallet token new` | Creates a new token definition with an initial supply |
| `wallet token send`| Transfers tokens between accounts |
| `wallet token mint`| Mints tokens to a holder account |
| `wallet token burn`| Burns tokens from a holder account |
1. Create a new token — definition and supply both on Keycard
```bash
wallet token new \
--definition-account-id "m/44'/60'/0'/0/2" \
--supply-account-id "m/44'/60'/0'/0/3" \
--name LEZ \
--total-supply 100000
# Output:
Keycard PIN:
Transaction hash is a3f1c8e2049b7d56fe19084b3c8b25d07e8f243829bc50addf6e2c78b4b09d11
Transaction data is ...
```
2. Transfer tokens between two Keycard accounts (public → public)
```bash
wallet token send \
--from "m/44'/60'/0'/0/3" \
--to "m/44'/60'/0'/0/6" \
--amount 20000
# Output:
Keycard PIN:
Transaction hash is b2e4d9f1038c6e45ad28175c4d9c36e18bf9354930cd61beef59f3e89c5a0e22
Transaction data is ...
```
3. Transfer tokens from a Keycard account to a private account (shielded)
```bash
wallet token send \
--from "m/44'/60'/0'/0/6" \
--to "Private/CJwKfrb3DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7Rb" \
--amount 500
# Output:
Keycard PIN:
Transaction hash is c5f7e0a2149d8f67be39286d5eaa47f29cg0465041de72cff06a4f9ad6b1f33
```
4. Mint tokens — Keycard definition account mints to a Keycard holder
```bash
wallet token mint \
--definition "m/44'/60'/0'/0/2" \
--holder "m/44'/60'/0'/0/6" \
--amount 2000
# Output:
Keycard PIN:
Transaction hash is d6g8f1b3250e9a78cf4a397e6fbb58g3ah1567152ef83dgg17b5g0be7c2g0g44
Transaction data is ...
```
5. Burn tokens — Keycard holder burns from its own account
```bash
wallet token burn \
--definition "Public/9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \
--holder "m/44'/60'/0'/0/6" \
--amount 500
# Output:
Keycard PIN:
Transaction hash is e7h9g2c4361f0b89dg5b408f7gcc69h4bi2678263fg94ehh28c6h1cf8d3h1h55
Transaction data is ...
```
### AMM program
AMM operations are **public only** — all holdings involved must be public accounts. Keycard accounts can be used for any or all of the holding accounts.
`--user-holding-a`, `--user-holding-b`, and `--user-holding-lp` each accept any of:
- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`)
- An account ID with privacy prefix (e.g. `Public/9bKm...`)
- An account label (e.g. `my-account`)
For swaps, only the seller's holding signs — the wallet identifies which holding corresponds to the input token and signs only that account.
| Command | Description |
|----------------------------|-------------------------------------------------------|
| `wallet amm new` | Creates a new AMM liquidity pool |
| `wallet amm swap-exact-input` | Swaps specifying exact input amount |
| `wallet amm swap-exact-output` | Swaps specifying exact output amount |
| `wallet amm add-liquidity` | Adds liquidity to an existing pool |
| `wallet amm remove-liquidity` | Removes liquidity from a pool |
1. Create a new AMM pool — all holdings on Keycard
```bash
wallet amm new \
--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-a 10000 \
--balance-b 10000
# Output:
Keycard PIN:
Transaction hash is f8i0h3d5472g1c90eh6c519g8hdd70i5cj3789374gh05fii39d7i2dg9e4i2i66
Transaction data is ...
```
2. Swap exact input — Keycard account sells LEE, receives LEZ
```bash
wallet amm swap-exact-input \
--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 "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6"
# Output:
Keycard PIN:
Transaction hash is g9j1i4e6583h2d01fi7d620h9iee81j6dk4890485hi16gjj40e8j3eh0f5j3j77
Transaction data is ...
```
3. Add liquidity — all three holdings on Keycard
```bash
wallet amm add-liquidity \
--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
# Output:
Keycard PIN:
Transaction hash is h0k2j5f7694i3e12gj8e731i0jff92k7el5901596ij27hkk51f9k4fi1g6k4k88
Transaction data is ...
```
4. Remove liquidity — LP holding on Keycard
```bash
wallet amm remove-liquidity \
--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
# Output:
Keycard PIN:
Transaction hash is i1l3k6g8705j4f23hk9f842j1kgg03l8fm6012607jk38ill62g0l5gj2h7l5l99
Transaction data is ...
```
### ATA program
The Associated Token Account program derives a deterministic token holding address from an owner account and a token definition. Keycard accounts can be used as the owner.
`--owner` and `--from`/`--holder` accept any of:
- A BIP-32 key path — uses Keycard (e.g. `m/44'/60'/0'/0/0`)
- An account ID with privacy prefix (e.g. `Public/9bKm...`)
- An account label (e.g. `my-account`)
| Command | Description |
|--------------------|------------------------------------------------------------------|
| `wallet ata address` | Derives and prints the ATA address (local only, no network) |
| `wallet ata create` | Creates the ATA on-chain |
| `wallet ata send` | Sends tokens from the owner's ATA to a recipient |
| `wallet ata burn` | Burns tokens from the owner's ATA |
| `wallet ata list` | Lists ATAs for a given owner across token definitions |
1. Derive an ATA address for a Keycard account
```bash
# First resolve the Keycard account ID
OWNER_ID=$(wallet account id --account-id "m/44'/60'/0'/0/9")
wallet ata address \
--owner "$OWNER_ID" \
--token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6"
# Output:
DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7RbCJwKfrb3
```
2. Create an ATA — Keycard account as owner
```bash
wallet ata create \
--owner "m/44'/60'/0'/0/9" \
--token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6"
# Output:
Keycard PIN:
Transaction hash is j2m4l7h9816k5g34il0g953k2lhh14m9gn7123718kl49jmm73h1m6hk3i8m6m00
Transaction data is ...
```
3. Send tokens from a Keycard ATA to another account
```bash
wallet ata send \
--from "m/44'/60'/0'/0/9" \
--token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \
--to "DFMmFvujQSB5ARcRTAa8EdP6eWm2hmSkF7RbCJwKfrb3" \
--amount 500
# Output:
Keycard PIN:
Transaction hash is k3n5m8i0927l6h45jm1h064l3mii25n0ho8234829lm50knn84i2n7il4j9n7n11
Transaction data is ...
```
4. Burn tokens from a Keycard ATA
```bash
wallet ata burn \
--holder "m/44'/60'/0'/0/9" \
--token-definition "9bKmZ4n7PqVRxEtY3dWsQjA2cHrFT5LpDoGXM8wJuNv6" \
--amount 200
# Output:
Keycard PIN:
Transaction hash is l4o6n9j1038m7i56kn2i175m4njj36o1ip9345930mn61loo95j3o8jm5k0o8o22
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

@ -52,6 +52,7 @@ async fn main() {
accounts,
Program::serialize_instruction(greeting).unwrap(),
&program.into(),
None,
)
.await
.unwrap();

View File

@ -60,6 +60,7 @@ async fn main() {
accounts,
Program::serialize_instruction(instruction).unwrap(),
&program_with_dependencies,
None,
)
.await
.unwrap();

View File

@ -106,6 +106,7 @@ async fn main() {
accounts,
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
None,
)
.await
.unwrap();
@ -147,6 +148,7 @@ async fn main() {
accounts,
Program::serialize_instruction(instruction).unwrap(),
&program.into(),
None,
)
.await
.unwrap();

View File

@ -57,6 +57,7 @@ async fn fund_private_pda(
Program::serialize_instruction((seed, amount, auth_transfer_id, true))
.context("failed to serialize pda_fund_spend_proxy fund instruction")?,
proxy_program,
None,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;
@ -93,6 +94,7 @@ async fn spend_private_pda(
Program::serialize_instruction((seed, amount, auth_transfer_id, false))
.context("failed to serialize pda_fund_spend_proxy instruction")?,
spend_program,
None,
)
.await
.map_err(|e| anyhow::anyhow!("{e}"))?;

View File

@ -137,6 +137,7 @@ unsafe extern "C" {
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
key_path: *const c_char,
out_result: *mut FfiTransferResult,
) -> error::WalletFfiError;
@ -882,6 +883,7 @@ fn test_wallet_ffi_transfer_shielded() -> Result<()> {
&raw const to_keys,
&raw const to_identifier,
&raw const amount,
std::ptr::null(),
&raw mut transfer_result,
)
.unwrap();

View File

@ -13,3 +13,4 @@ pyo3.workspace = true
log.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
zeroize.workspace = true

View File

@ -3,15 +3,18 @@ from keycard.exceptions import APDUError, TransportError
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
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 +40,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 +48,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 +77,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 +115,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
@ -161,4 +198,24 @@ class KeycardWallet:
return signature.signature
except Exception as e:
raise RuntimeError(f"Error signing message: {e}") from e
raise RuntimeError(f"Error signing message: {e}") from e
def get_private_keys_for_path(self, path: str = "m/44'/60'/0'/0/0") -> bytes | None:
try:
if not self.card.is_secure_channel_open or not self.card.is_pin_verified:
return None
private_keys = export_lee_key(
self.card,
constants.DerivationOption.DERIVE,
path
)
nsk = private_keys.lee_nsk
vsk = private_keys.lee_vsk
return (nsk, vsk)
except Exception as e:
raise RuntimeError(f"Error getting private keys: {e}") from e

View File

@ -3,9 +3,13 @@ use std::path::PathBuf;
use nssa::{AccountId, PublicKey, Signature};
use pyo3::{prelude::*, types::PyAny};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
pub mod python_path;
/// NSK and VSK as fixed-length zeroizing byte arrays.
type PrivateKeyPair = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>);
// TODO: encrypt at rest alongside broader wallet storage encryption work.
#[derive(Serialize, Deserialize)]
pub struct KeycardPairingData {
@ -51,10 +55,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()
}
@ -91,20 +95,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()
}
@ -138,6 +133,10 @@ impl KeycardWallet {
})
}
#[expect(
clippy::arithmetic_side_effects,
reason = "64 - s_stripped.len() is safe: s_stripped.len() ≤ 31 because py_signature.len() is in [32, 63]"
)]
pub fn sign_message_for_path(
&self,
py: Python,
@ -150,6 +149,24 @@ impl KeycardWallet {
.call_method1("sign_message_for_path", (message, path))?
.extract()?;
// The keycard Python library strips leading zeros from S when S < 2^(8k) for some k.
// Left-pad S back to 32 bytes so the full signature is always 64 bytes (R || S).
let py_signature = if py_signature.len() < 64 {
if py_signature.len() < 32 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"signature from keycard too short: {} bytes",
py_signature.len()
)));
}
let s_stripped = &py_signature[32..];
let mut padded = [0_u8; 64];
padded[..32].copy_from_slice(&py_signature[..32]);
padded[(64 - s_stripped.len())..].copy_from_slice(s_stripped);
padded.to_vec()
} else {
py_signature
};
let signature: [u8; 64] = py_signature.try_into().map_err(|vec: Vec<u8>| {
PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"Invalid signature length: expected 64 bytes, got {} (bytes: {:02x?})",
@ -190,11 +207,65 @@ impl KeycardWallet {
Ok(())
}
pub fn get_account_id_for_path_with_connect(pin: &str, key_path: &str) -> PyResult<String> {
pub fn get_public_account_id_for_path_with_connect(
pin: &str,
key_path: &str,
) -> PyResult<String> {
let public_key = Self::get_public_key_for_path_with_connect(pin, key_path)?;
Ok(format!("Public/{}", AccountId::from(&public_key)))
}
pub fn get_private_keys_for_path(&self, py: Python, path: &str) -> PyResult<PrivateKeyPair> {
let (raw_nsk, raw_vsk): (Vec<u8>, Vec<u8>) = self
.instance
.bind(py)
.call_method1("get_private_keys_for_path", (path,))?
.extract()?;
let raw_nsk = Zeroizing::new(raw_nsk);
let raw_vsk = Zeroizing::new(raw_vsk);
let nsk = {
if raw_nsk.len() != 32 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"expected 32-byte NSK from keycard, got {} bytes",
raw_nsk.len()
)));
}
let mut arr = Zeroizing::new([0_u8; 32]);
arr.copy_from_slice(&raw_nsk);
arr
};
let vsk = {
if raw_vsk.len() != 32 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"expected 32-byte VSK from keycard, got {} bytes",
raw_vsk.len()
)));
}
let mut arr = Zeroizing::new([0_u8; 32]);
arr.copy_from_slice(&raw_vsk);
arr
};
Ok((nsk, vsk))
}
pub fn get_private_keys_for_path_with_connect(
pin: &str,
path: &str,
) -> PyResult<PrivateKeyPair> {
Python::with_gil(|py| {
python_path::add_python_path(py)?;
let wallet = Self::new(py)?;
wallet.connect(py, pin)?;
let result = wallet.get_private_keys_for_path(py, path);
drop(wallet.disconnect(py));
result
})
}
}
fn pairing_file_path() -> Option<PathBuf> {

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,117 @@
#!/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.
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

@ -0,0 +1,21 @@
#!/usr/bin/env bash
# keycard_test_3.sh — tests for `wallet keycard get-private-keys`.
#
# Prerequisites:
# 1. Run wallet_with_keycard.sh once to install dependencies.
# 2. Keycard reader inserted with card loaded (wallet keycard load has been run).
source venv/bin/activate
cargo install --path wallet --force --features keycard-debug
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" --reveal
echo "=== Test: wallet keycard get-private-keys path 11 ==="
wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/11" --reveal
echo ""
echo "=== All get-private-keys tests finished ==="

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

@ -0,0 +1,457 @@
#!/usr/bin/env bash
# keycard_tests_2.sh — comprehensive token + AMM keycard integration tests.
#
# Prerequisites:
# 1. Run wallet_with_keycard.sh once to install dependencies.
# 2. Reset the local chain so all accounts are uninitialized.
# 3. Keycard reader inserted with card loaded.
#
# Keycard path layout:
# path 2 → LEZ token definition (keycard)
# path 3 → LEZ token supply (keycard)
# path 4 → LEE token definition (keycard)
# path 5 → LEE token supply (keycard)
# path 6 → LEZ holding (keycard — transfers, mint, burn, swap, liquidity)
# path 7 → LEE holding (keycard — swap, add/remove liquidity)
# path 8 → LP holding (keycard — add/remove liquidity)
# path 9 → ATA owner (keycard — ATA create, send, burn)
#
# Non-keycard accounts:
# pub-receiver → public account (target for keycard → public token transfer)
# priv-receiver → private account (target for keycard → private token transfer)
# amm-lez-fund → public LEZ holding used to seed the AMM pool
# amm-lee-fund → public LEE holding used to seed the AMM pool
# (LP holding for amm new is created fresh each run — no persistent label)
source venv/bin/activate
export KEYCARD_PIN=111111
# =============================================================================
# Keycard setup
# =============================================================================
echo ""
echo "=== Keycard setup ==="
wallet keycard available
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
# =============================================================================
echo ""
echo "=== Create non-keycard accounts ==="
wallet account new public --label pub-receiver 2>/dev/null || true
wallet account new public --label amm-lez-fund 2>/dev/null || true
wallet account new public --label amm-lee-fund 2>/dev/null || true
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=path3) ==="
wallet token new \
--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"
# =============================================================================
# (2) Create LEE token — definition AND supply via keycard paths
# =============================================================================
echo ""
echo "=== (2) Create LEE token (keycard def=path4, supply=path5) ==="
wallet token new \
--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 --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 --account-id "m/44'/60'/0'/0/2"
echo "Keycard path 3 (LEZ supply) state:"
wallet account get --account-id "m/44'/60'/0'/0/3"
echo "Keycard path 4 (LEE definition) state:"
wallet account get --account-id "m/44'/60'/0'/0/4"
echo "Keycard path 5 (LEE supply) state:"
wallet account get --account-id "m/44'/60'/0'/0/5"
# =============================================================================
# Initialize token holding accounts
# =============================================================================
echo ""
echo "=== Initialize token holding accounts ==="
# 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 (different definition — safe to submit immediately)
wallet token mint \
--definition "m/44'/60'/0'/0/4" \
--holder "m/44'/60'/0'/0/7" \
--amount 0
echo "LEE holding initialized for keycard path 7"
# Wait for path2 (LEZ def) and path4 (LEE def) nonces to be confirmed before reusing them
sleep 15
# pub-receiver: public LEZ holding
wallet token mint \
--definition "m/44'/60'/0'/0/2" \
--holder pub-receiver \
--amount 0
echo "LEZ holding initialized for pub-receiver"
# amm-lee-fund: LEE holding (different definition — safe to submit with pub-receiver)
wallet token mint \
--definition "m/44'/60'/0'/0/4" \
--holder amm-lee-fund \
--amount 0
echo "LEE holding initialized for amm-lee-fund"
# Wait for path2 nonce to be confirmed before the third LEZ mint
sleep 15
# amm-lez-fund: LEZ holding
wallet token mint \
--definition "m/44'/60'/0'/0/2" \
--holder amm-lez-fund \
--amount 0
echo "AMM seed holdings initialized"
# =============================================================================
# Fund keycard holdings and AMM seed accounts from supply
# =============================================================================
echo ""
echo "=== Fund keycard holdings and AMM seed accounts ==="
wallet token send \
--from "m/44'/60'/0'/0/3" \
--to "m/44'/60'/0'/0/6" \
--amount 20000
echo "Transferred 20000 LEZ → keycard path 6"
wallet token send \
--from "m/44'/60'/0'/0/5" \
--to "m/44'/60'/0'/0/7" \
--amount 20000
echo "Transferred 20000 LEE → keycard path 7"
# Wait for path3 and path5 nonces to be confirmed before reusing them
sleep 15
wallet token send \
--from "m/44'/60'/0'/0/3" \
--to amm-lez-fund \
--amount 10000
echo "Transferred 10000 LEZ → amm-lez-fund"
wallet token send \
--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 --account-id "m/44'/60'/0'/0/6"
echo "Keycard path 7 (LEE holding) state (balance should be 20000):"
wallet account get --account-id "m/44'/60'/0'/0/7"
echo "amm-lez-fund state (balance should be 10000):"
wallet account get --account-id amm-lez-fund
echo "amm-lee-fund state (balance should be 10000):"
wallet account get --account-id amm-lee-fund
# =============================================================================
# (3) Token transfer: keycard path 6 (LEZ) → public account
# =============================================================================
echo ""
echo "=== (3) Token transfer: keycard path 6 → pub-receiver (public) ==="
wallet token send \
--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 --account-id "m/44'/60'/0'/0/6"
echo "pub-receiver state (balance should be 1000):"
wallet account get --account-id pub-receiver
# =============================================================================
# (4) Token transfer: keycard path 6 (LEZ) → private account (shielded)
# =============================================================================
echo ""
echo "=== (4) Token transfer: keycard path 6 → priv-receiver (private, shielded) ==="
PRIV_RECEIVER=$(wallet account new private | grep -o 'Private/[^[:space:]]*' | head -1)
echo "Fresh private receiver account: $PRIV_RECEIVER"
wallet token send \
--from "m/44'/60'/0'/0/6" \
--to "$PRIV_RECEIVER" \
--amount 500
echo "Shielded transfer of 500 LEZ: keycard path 6 → $PRIV_RECEIVER"
wallet account sync-private
sleep 15
echo "Keycard path 6 (LEZ) state (balance should be 18500):"
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"
# =============================================================================
# (5) Token transfer: private account → keycard path 6 (deshielded)
# Uses priv-receiver from test (4) which holds 500 LEZ.
# The private sender is handled by the ZK circuit; the keycard recipient
# does not sign — resolve() derives its account ID from the card only.
# =============================================================================
echo ""
echo "=== (5) Token transfer: priv-receiver (private) → keycard path 6 (deshielded) ==="
wallet token send \
--from "$PRIV_RECEIVER" \
--to "m/44'/60'/0'/0/6" \
--amount 300
echo "Deshielded transfer of 300 LEZ: $PRIV_RECEIVER → keycard path 6"
wallet account sync-private
sleep 15
echo "priv-receiver state (balance should be 200):"
wallet account get --account-id "$PRIV_RECEIVER"
echo "Keycard path 6 (LEZ) state (balance should be 18800):"
wallet account get --account-id "m/44'/60'/0'/0/6"
# =============================================================================
# (6) Token mint with keycard — definition signed by keycard path 2
# =============================================================================
echo ""
echo "=== (6) Token mint: keycard def path 2 mints 2000 LEZ to keycard path 6 ==="
wallet token mint \
--definition "m/44'/60'/0'/0/2" \
--holder "m/44'/60'/0'/0/6" \
--amount 2000
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 --account-id "m/44'/60'/0'/0/2"
echo "Keycard path 6 (LEZ holding) state (balance should be 20500):"
wallet account get --account-id "m/44'/60'/0'/0/6"
# =============================================================================
# (7) Token burn with keycard — holder is keycard path 6
# =============================================================================
echo ""
echo "=== (7) Token burn: keycard path 6 burns 500 LEZ ==="
wallet token burn \
--definition "Public/$LEZ_DEF_ID" \
--holder "m/44'/60'/0'/0/6" \
--amount 500
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 --account-id "m/44'/60'/0'/0/2"
echo "Keycard path 6 (LEZ holding) state (balance should be 20000):"
wallet account get --account-id "m/44'/60'/0'/0/6"
# =============================================================================
# (8) Create AMM pool for LEZ/LEE — without keycard
# =============================================================================
echo ""
echo "=== (8) Create AMM pool for LEZ/LEE (without keycard) ==="
wallet amm new \
--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"
sleep 15
echo "amm-lez-fund state (balance should be 0 — contributed to pool):"
wallet account get --account-id amm-lez-fund
echo "amm-lee-fund state (balance should be 0 — contributed to pool):"
wallet account get --account-id amm-lee-fund
echo "Initial LP holding state (should hold initial LP tokens):"
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"
# =============================================================================
# (9) Swap tokens owned by keycard accounts
# keycard path 7 (LEE) sells 500 LEE; keycard path 6 (LEZ) receives LEZ
# =============================================================================
echo ""
echo "=== (9) Swap: keycard path 7 sells 500 LEE, keycard path 6 receives LEZ ==="
wallet amm swap-exact-input \
--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"
echo "Swap LEE → LEZ complete via keycard"
sleep 15
echo "Keycard path 6 (LEZ holding) state (balance should have increased):"
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 --account-id "m/44'/60'/0'/0/7"
# =============================================================================
# (10) Add liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8)
# =============================================================================
echo ""
echo "=== (10) Add liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ==="
wallet amm add-liquidity \
--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
echo "Add liquidity complete via keycard"
sleep 15
echo "Keycard path 6 (LEZ holding) state (balance should have decreased):"
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 --account-id "m/44'/60'/0'/0/7"
echo "Keycard path 8 (LP holding) state (should have received LP tokens):"
wallet account get --account-id "m/44'/60'/0'/0/8"
# =============================================================================
# (11) Remove liquidity — keycard accounts for holding A (path 6), B (path 7), LP (path 8)
# =============================================================================
echo ""
echo "=== (11) Remove liquidity (keycard path 6=LEZ, path 7=LEE, path 8=LP) ==="
wallet amm remove-liquidity \
--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
echo "Remove liquidity complete via keycard"
sleep 15
echo "Keycard path 6 (LEZ holding) state (balance should have increased):"
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 --account-id "m/44'/60'/0'/0/7"
echo "Keycard path 8 (LP holding) state (balance should have decreased):"
wallet account get --account-id "m/44'/60'/0'/0/8"
# =============================================================================
# (12) ATA create — keycard path 9 as owner for LEZ
# =============================================================================
echo ""
echo "=== (12) ATA create: keycard path 9 as owner, LEZ token ==="
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 \
--owner "m/44'/60'/0'/0/9" \
--token-definition "$LEZ_DEF_ID"
echo "ATA created for keycard path 9 / LEZ"
sleep 15
LEZ_ATA_ID=$(wallet ata address --owner "$ATA_OWNER_ID" --token-definition "$LEZ_DEF_ID")
echo "Keycard path 9 LEZ ATA ID: $LEZ_ATA_ID"
echo "ATA state (should be initialized with zero balance):"
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 "m/44'/60'/0'/0/3" \
--to "Public/$LEZ_ATA_ID" \
--amount 3000
echo "Funded keycard path 9 ATA with 3000 LEZ"
sleep 15
echo "ATA state after funding (balance should be 3000):"
wallet account get --account-id "Public/$LEZ_ATA_ID"
# =============================================================================
# (13) ATA send — keycard path 9's ATA → pub-receiver's ATA
# =============================================================================
echo ""
echo "=== (13) 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"
echo "ATA created for pub-receiver / LEZ"
sleep 15
PUB_RECEIVER_ATA_ID=$(wallet ata address --owner "$PUB_RECEIVER_ID" --token-definition "$LEZ_DEF_ID")
echo "pub-receiver LEZ ATA ID: $PUB_RECEIVER_ATA_ID"
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 "m/44'/60'/0'/0/9" \
--token-definition "$LEZ_DEF_ID" \
--to "$PUB_RECEIVER_ATA_ID" \
--amount 500
echo "Sent 500 LEZ: keycard path 9 ATA → pub-receiver ATA"
sleep 15
echo "Keycard path 9 ATA state (balance should be 2500):"
wallet account get --account-id "Public/$LEZ_ATA_ID"
echo "pub-receiver ATA state (balance should be 500):"
wallet account get --account-id "Public/$PUB_RECEIVER_ATA_ID"
# =============================================================================
# (14) ATA burn — keycard path 9's ATA burns 200 LEZ
# =============================================================================
echo ""
echo "=== (14) ATA burn: keycard path 9's ATA burns 200 LEZ ==="
wallet ata burn \
--holder "m/44'/60'/0'/0/9" \
--token-definition "$LEZ_DEF_ID" \
--amount 200
echo "Burned 200 LEZ from keycard path 9 ATA"
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 --account-id "m/44'/60'/0'/0/2"
echo ""
echo "=== All keycard token + AMM + ATA tests finished ==="

View File

@ -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")?;

View File

@ -1,6 +1,9 @@
//! Token transfer functions.
use std::{ffi::CString, ptr};
use std::{
ffi::{c_char, CStr, CString},
ptr,
};
use nssa::AccountId;
use wallet::{
@ -17,6 +20,14 @@ use crate::{
FfiPrivateAccountKeys,
};
fn optional_c_str(ptr: *const c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
let c_str = unsafe { CStr::from_ptr(ptr) };
c_str.to_str().ok().map(str::to_owned)
}
/// Send a public token transfer.
///
/// Transfers tokens from one public account to another on the network.
@ -140,6 +151,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
to_keys: *const FfiPrivateAccountKeys,
to_identifier: *const FfiU128,
amount: *const [u8; 16],
key_path: *const c_char,
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
@ -176,6 +188,10 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let from_mention = optional_c_str(key_path).map_or_else(
|| CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)),
CliAccountMention::KeyPath,
);
let transfer = NativeTokenTransfer(&wallet);
@ -185,6 +201,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded(
to_vpk,
to_identifier,
amount,
&from_mention,
)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
@ -262,7 +279,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_deshielded(
let from_id = AccountId::new(unsafe { (*from).data });
let to_id = AccountId::new(unsafe { (*to).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_deshielded_transfer(from_id, to_id, amount)) {
@ -357,7 +373,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private(
};
let to_identifier = u128::from_le_bytes(unsafe { (*to_identifier).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_private_transfer_to_outer_account(
@ -423,6 +438,7 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned(
from: *const FfiBytes32,
to: *const FfiBytes32,
amount: *const [u8; 16],
key_path: *const c_char,
out_result: *mut FfiTransferResult,
) -> WalletFfiError {
let wrapper = match get_wallet(handle) {
@ -446,10 +462,14 @@ pub unsafe extern "C" fn wallet_ffi_transfer_shielded_owned(
let from_id = AccountId::new(unsafe { (*from).data });
let to_id = AccountId::new(unsafe { (*to).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let from_mention = optional_c_str(key_path).map_or_else(
|| CliAccountMention::Id(AccountIdWithPrivacy::Public(from_id)),
CliAccountMention::KeyPath,
);
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_shielded_transfer(from_id, to_id, amount)) {
match block_on(transfer.send_shielded_transfer(from_id, to_id, amount, &from_mention)) {
Ok((tx_hash, _shared_key)) => {
let tx_hash = CString::new(tx_hash.to_string())
.map_or(ptr::null_mut(), std::ffi::CString::into_raw);
@ -529,7 +549,6 @@ pub unsafe extern "C" fn wallet_ffi_transfer_private_owned(
let from_id = AccountId::new(unsafe { (*from).data });
let to_id = AccountId::new(unsafe { (*to).data });
let amount = u128::from_le_bytes(unsafe { *amount });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.send_private_transfer_to_owned_account(from_id, to_id, amount)) {
@ -673,7 +692,6 @@ pub unsafe extern "C" fn wallet_ffi_register_private_account(
};
let account_id = AccountId::new(unsafe { (*account_id).data });
let transfer = NativeTokenTransfer(&wallet);
match block_on(transfer.register_account_private(account_id)) {

View File

@ -787,6 +787,7 @@ enum WalletFfiError wallet_ffi_transfer_shielded(struct WalletHandle *handle,
const struct FfiPrivateAccountKeys *to_keys,
const struct FfiU128 *to_identifier,
const uint8_t (*amount)[16],
const char *key_path,
struct FfiTransferResult *out_result);
/**
@ -894,6 +895,7 @@ enum WalletFfiError wallet_ffi_transfer_shielded_owned(struct WalletHandle *hand
const struct FfiBytes32 *from,
const struct FfiBytes32 *to,
const uint8_t (*amount)[16],
const char *key_path,
struct FfiTransferResult *out_result);
/**

View File

@ -21,7 +21,7 @@ ata_core.workspace = true
bip39.workspace = true
pyo3.workspace = true
rpassword = "7"
zeroize = "1"
zeroize.workspace = true
keycard_wallet.workspace = true
anyhow.workspace = true
@ -46,6 +46,9 @@ optfield = "0.4.0"
url.workspace = true
derive_more = { workspace = true, features = ["display"] }
[features]
keycard-debug = []
[dev-dependencies]
tempfile.workspace = true
key_protocol = { workspace = true, features = ["test_utils"] }

View File

@ -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<SubcommandReturnValue> {
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,

View File

@ -16,6 +16,20 @@ 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.
/// Only available when built with the `keycard-debug` feature.
#[cfg(feature = "keycard-debug")]
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 {
@ -26,7 +40,8 @@ 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");
@ -47,7 +62,8 @@ 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");
@ -66,7 +82,8 @@ 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");
@ -89,7 +106,8 @@ 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");
@ -111,7 +129,8 @@ 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");
@ -131,6 +150,27 @@ impl WalletSubcommand for KeycardSubcommand {
Ok(SubcommandReturnValue::Empty)
}
#[cfg(feature = "keycard-debug")]
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)
}
}
}
}

View File

@ -138,13 +138,23 @@ impl CliAccountMention {
Self::KeyPath(path) => {
let pin = read_pin()?;
let id_str =
keycard_wallet::KeycardWallet::get_account_id_for_path_with_connect(&pin, path)
.map_err(anyhow::Error::from)?;
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}"))
}
}
}
#[must_use]
pub fn key_path(&self) -> Option<&str> {
match self {
Self::KeyPath(path) => Some(path),
Self::Id(_) | Self::Label(_) => None,
}
}
}
impl FromStr for CliAccountMention {

View File

@ -131,25 +131,31 @@ 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)
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
@ -165,23 +171,25 @@ 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) {
(
AccountIdWithPrivacy::Public(user_holding_a),
AccountIdWithPrivacy::Public(user_holding_b),
) => {
Amm(wallet_core)
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
@ -197,23 +205,25 @@ 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) {
(
AccountIdWithPrivacy::Public(user_holding_a),
AccountIdWithPrivacy::Public(user_holding_b),
) => {
Amm(wallet_core)
let a_id = user_holding_a.resolve(wallet_core.storage())?;
let b_id = user_holding_b.resolve(wallet_core.storage())?;
match (a_id, b_id) {
(AccountIdWithPrivacy::Public(a), AccountIdWithPrivacy::Public(b)) => {
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
@ -230,26 +240,32 @@ 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)
let tx_hash = 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,
&user_holding_lp,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {
@ -266,26 +282,30 @@ 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)
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
_ => {

View File

@ -91,14 +91,18 @@ 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)
let tx_hash = Ata(wallet_core)
.send_create(owner_id, definition_id, &owner)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(owner_id) => {
@ -127,15 +131,19 @@ 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)
let tx_hash = Ata(wallet_core)
.send_transfer(from_id, definition_id, to_id, amount, &from)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(from_id) => {
@ -163,14 +171,18 @@ 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)
let tx_hash = Ata(wallet_core)
.send_burn(holder_id, definition_id, amount, &holder)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
AccountIdWithPrivacy::Private(holder_id) => {

View File

@ -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,
},
}
@ -393,9 +399,14 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
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 +432,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 +452,7 @@ impl WalletSubcommand for NativeTokenTransferProgramSubcommandShielded {
to_vpk,
to_identifier.unwrap_or_else(rand::random),
amount,
&from_mention,
)
.await?;

View File

@ -114,20 +114,21 @@ 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),
) => TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id,
supply_account_id,
name,
total_supply,
},
),
(AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => {
TokenProgramSubcommand::Create(
CreateNewTokenProgramSubcommand::NewPublicDefPublicSupp {
definition_account_id: def_mention,
supply_account_id: sup_mention,
name,
total_supply,
},
)
}
(
AccountIdWithPrivacy::Public(definition_account_id),
AccountIdWithPrivacy::Private(supply_account_id),
@ -173,6 +174,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 +195,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("`wallet::cli::programs::token::Send`: Invalid to_mention account provided"),
balance_to_move: amount,
},
)
@ -226,6 +229,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
sender_account_id: from,
recipient_account_id: to,
balance_to_move: amount,
sender_mention: from_mention,
},
)
}
@ -247,6 +251,7 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
recipient_vpk: to_vpk,
recipient_identifier: to_identifier,
balance_to_move: amount,
sender_mention: from_mention,
},
),
},
@ -259,17 +264,17 @@ 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),
) => TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken {
definition_account_id: definition,
holder_account_id: holder,
amount,
}),
(AccountIdWithPrivacy::Public(definition), AccountIdWithPrivacy::Public(_)) => {
TokenProgramSubcommand::Public(TokenProgramSubcommandPublic::BurnToken {
definition_account_id: definition,
holder_account_id: holder_mention,
amount,
})
}
(
AccountIdWithPrivacy::Private(definition),
AccountIdWithPrivacy::Private(holder),
@ -312,6 +317,8 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
holder_identifier,
amount,
} => {
let def_mention = definition.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()))
@ -331,16 +338,15 @@ impl WalletSubcommand for TokenProgramAgnosticSubcommand {
anyhow::bail!("List of public keys is uncomplete");
}
(Some(holder), None, None) => match (definition, holder) {
(
AccountIdWithPrivacy::Public(definition),
AccountIdWithPrivacy::Public(holder),
) => TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::MintToken {
definition_account_id: definition,
holder_account_id: holder,
amount,
},
),
(AccountIdWithPrivacy::Public(_), AccountIdWithPrivacy::Public(_)) => {
TokenProgramSubcommand::Public(
TokenProgramSubcommandPublic::MintToken {
definition_account_id: def_mention,
holder_account_id: holder_mention.expect("`wallet::cli::programs::token::Mint`: Invalid holder_mention account provided"),
amount,
},
)
}
(
AccountIdWithPrivacy::Private(definition),
AccountIdWithPrivacy::Private(holder),
@ -430,9 +436,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 +447,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,
},
@ -561,6 +567,8 @@ pub enum TokenProgramSubcommandShielded {
recipient_account_id: AccountId,
#[arg(short, long)]
balance_to_move: u128,
#[arg(skip)]
sender_mention: CliAccountMention,
},
// Transfer tokens using the token program
TransferTokenShieldedForeign {
@ -577,6 +585,8 @@ pub enum TokenProgramSubcommandShielded {
recipient_identifier: Option<u128>,
#[arg(short, long)]
balance_to_move: u128,
#[arg(skip)]
sender_mention: CliAccountMention,
},
// Burn tokens using the token program
BurnTokenShielded {
@ -620,9 +630,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,13 +690,28 @@ impl WalletSubcommand for TokenProgramSubcommandPublic {
recipient_account_id,
balance_to_move,
} => {
Token(wallet_core)
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");
};
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
Self::BurnToken {
@ -694,9 +719,22 @@ impl WalletSubcommand for TokenProgramSubcommandPublic {
holder_account_id,
amount,
} => {
Token(wallet_core)
.send_burn_transaction(definition_account_id, 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");
};
let tx_hash = Token(wallet_core)
.send_burn_transaction(
definition_account_id,
holder_id,
amount,
&holder_account_id,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
Self::MintToken {
@ -704,9 +742,26 @@ impl WalletSubcommand for TokenProgramSubcommandPublic {
holder_account_id,
amount,
} => {
Token(wallet_core)
.send_mint_transaction(definition_account_id, 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");
};
let tx_hash = Token(wallet_core)
.send_mint_transaction(
def_id,
holder_id,
amount,
&definition_account_id,
&holder_account_id,
)
.await?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}
@ -1026,6 +1081,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
recipient_vpk,
recipient_identifier,
balance_to_move,
sender_mention,
} => {
let recipient_npk_res = hex::decode(recipient_npk)?;
let mut recipient_npk = [0; 32];
@ -1046,6 +1102,7 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
recipient_vpk,
recipient_identifier.unwrap_or_else(rand::random),
balance_to_move,
&sender_mention,
)
.await?;
@ -1065,12 +1122,14 @@ impl WalletSubcommand for TokenProgramSubcommandShielded {
sender_account_id,
recipient_account_id,
balance_to_move,
sender_mention,
} => {
let (tx_hash, secret_recipient) = Token(wallet_core)
.send_transfer_transaction_shielded_owned_account(
sender_account_id,
recipient_account_id,
balance_to_move,
&sender_mention,
)
.await?;
@ -1307,14 +1366,27 @@ impl WalletSubcommand for CreateNewTokenProgramSubcommand {
name,
total_supply,
} => {
Token(wallet_core)
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");
};
let tx_hash = 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?;
println!("Transaction hash is {tx_hash}");
let transfer_tx = wallet_core.poll_native_token_transfer(tx_hash).await?;
println!("Transaction data is {transfer_tx:?}");
wallet_core.store_persistent_data()?;
Ok(SubcommandReturnValue::Empty)
}
}

View File

@ -1,5 +1,6 @@
#![expect(
clippy::print_stdout,
clippy::print_stderr,
reason = "This is a CLI application, printing to stdout and stderr is expected and convenient"
)]
#![expect(
@ -14,15 +15,20 @@ use bip39::Mnemonic;
use common::{HashType, transaction::NSSATransaction};
use config::WalletConfig;
use key_protocol::key_management::key_tree::chain_index::ChainIndex;
use keycard_wallet::KeycardWallet;
use log::info;
use nssa::{
Account, AccountId, PrivacyPreservingTransaction,
Account, AccountId, PrivacyPreservingTransaction, PublicKey, PublicTransaction, Signature,
privacy_preserving_transaction::{
circuit::ProgramWithDependencies, message::EncryptedAccountData,
},
program::Program,
public_transaction::WitnessSet as PublicWitnessSet,
};
use nssa_core::{
Commitment, MembershipProof, SharedSecretKey, account::Nonce, program::InstructionData,
Commitment, MembershipProof, SharedSecretKey,
account::{AccountWithMetadata, Nonce},
program::InstructionData,
};
pub use privacy_preserving_tx::PrivacyPreservingAccount;
use sequencer_service_rpc::{RpcClient as _, SequencerClient, SequencerClientBuilder};
@ -31,8 +37,10 @@ use tokio::io::AsyncWriteExt as _;
use crate::{
account::{AccountIdWithPrivacy, Label},
cli::CliAccountMention,
config::WalletConfigOverrides,
poller::TxPoller,
signing::SigningGroup,
storage::key_chain::SharedAccountEntry,
};
@ -80,6 +88,20 @@ pub enum ExecutionFailureKind {
KeycardError(#[from] pyo3::PyErr),
}
impl ExecutionFailureKind {
/// Convert an [`anyhow::Error`] (e.g. from [`SigningGroup`]) into a keycard error.
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "used as a method reference in map_err"
)]
pub fn from_anyhow(e: anyhow::Error) -> Self {
Self::KeycardError(pyo3::PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
e.to_string(),
))
}
}
#[expect(clippy::partial_pub_fields, reason = "TODO: make all fields private")]
pub struct WalletCore {
config_path: PathBuf,
@ -542,15 +564,76 @@ impl WalletCore {
Ok(())
}
/// Send a public transaction, fetching nonces automatically from
/// [`SigningGroup::signing_ids`].
pub async fn send_public_tx<T: serde::Serialize>(
&self,
program: &Program,
account_ids: Vec<AccountId>,
instruction: T,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
let nonces = self
.get_accounts_nonces(groups.signing_ids())
.await
.map_err(ExecutionFailureKind::SequencerError)?;
self.send_public_tx_with_nonces(program, account_ids, nonces, instruction, groups)
.await
}
/// Send a public transaction with caller-supplied nonces.
///
/// Use this when the caller needs to assemble or augment nonces before submission
/// (e.g. injecting a keycard account nonce that was fetched separately).
pub async fn send_public_tx_with_nonces<T: serde::Serialize>(
&self,
program: &Program,
account_ids: Vec<AccountId>,
nonces: Vec<Nonce>,
instruction: T,
groups: SigningGroup,
) -> Result<HashType, ExecutionFailureKind> {
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)?;
let pin = if groups.needs_pin() {
crate::helperfunctions::read_pin()
.map_err(ExecutionFailureKind::from_anyhow)?
.as_str()
.to_owned()
} else {
String::new()
};
let sigs = groups
.sign_all(&message.hash(), &pin)
.map_err(ExecutionFailureKind::from_anyhow)?;
let tx = PublicTransaction::new(message, PublicWitnessSet::from_raw_parts(sigs));
Ok(self
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_privacy_preserving_tx(
&self,
accounts: Vec<PrivacyPreservingAccount>,
instruction_data: InstructionData,
program: &ProgramWithDependencies,
mention: Option<&CliAccountMention>,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
self.send_privacy_preserving_tx_with_pre_check(accounts, instruction_data, program, |_| {
Ok(())
})
self.send_privacy_preserving_tx_with_pre_check(
accounts,
instruction_data,
program,
|_| Ok(()),
mention,
)
.await
}
@ -560,10 +643,67 @@ impl WalletCore {
instruction_data: InstructionData,
program: &ProgramWithDependencies,
tx_pre_check: impl FnOnce(&[&Account]) -> Result<(), ExecutionFailureKind>,
mention: Option<&CliAccountMention>,
) -> Result<(HashType, Vec<SharedSecretKey>), ExecutionFailureKind> {
let acc_manager = privacy_preserving_tx::AccountManager::new(self, accounts).await?;
let pre_states = acc_manager.pre_states();
let mut pre_states = acc_manager.pre_states();
let (keycard_account, keycard_pin, keycard_path) = if let Some(key_path_str) =
mention.and_then(CliAccountMention::key_path)
{
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::<AccountIdWithPrivacy>()
.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("`wallet::lib::send_privacy_preserving_tx_with_pre_check`: unable to retrieve public account");
let pin_str = pin.as_str().to_owned();
(
Some(AccountWithMetadata {
account,
is_authorized: true,
account_id,
}),
Some(pin_str),
Some(key_path_str.to_owned()),
)
} else {
(None, None, None)
};
let mut nonces: Vec<Nonce> = acc_manager.public_account_nonces().into_iter().collect();
let mut account_ids: Vec<AccountId> = acc_manager.public_account_ids();
if let Some(acc) = keycard_account.as_ref() {
if acc_manager.public_account_ids().contains(&acc.account_id) {
if let Some(pre) = pre_states
.iter_mut()
.find(|p| p.account_id == acc.account_id)
{
pre.is_authorized = true;
}
nonces.push(acc.account.nonce);
} else {
nonces.push(acc.account.nonce);
account_ids.push(acc.account_id);
pre_states.push(acc.clone());
}
}
tx_pre_check(
&pre_states
.iter()
@ -582,8 +722,8 @@ impl WalletCore {
let message =
nssa::privacy_preserving_transaction::message::Message::try_from_circuit_output(
acc_manager.public_account_ids(),
Vec::from_iter(acc_manager.public_account_nonces()),
account_ids,
nonces,
private_account_keys
.iter()
.map(|keys| (keys.npk, keys.vpk.clone(), keys.epk.clone()))
@ -593,11 +733,38 @@ impl WalletCore {
.unwrap();
let witness_set =
nssa::privacy_preserving_transaction::witness_set::WitnessSet::for_message(
&message,
proof,
&acc_manager.public_account_auth(),
);
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

View File

@ -1,13 +1,13 @@
use amm_core::{compute_liquidity_token_pda, compute_pool_pda, compute_vault_pda};
use common::{HashType, transaction::NSSATransaction};
use common::HashType;
use nssa::{AccountId, program::Program};
use sequencer_service_rpc::RpcClient as _;
use token_core::TokenHolding;
use crate::{ExecutionFailureKind, WalletCore};
use crate::{ExecutionFailureKind, WalletCore, cli::CliAccountMention, signing::SigningGroup};
pub struct Amm<'wallet>(pub &'wallet WalletCore);
impl Amm<'_> {
#[expect(clippy::too_many_arguments, reason = "each parameter is distinct")]
pub async fn send_new_definition(
&self,
user_holding_a: AccountId,
@ -15,6 +15,9 @@ impl Amm<'_> {
user_holding_lp: AccountId,
balance_a: u128,
balance_b: u128,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let program = Program::amm();
let amm_program_id = Program::amm().id();
@ -58,69 +61,19 @@ impl Amm<'_> {
user_holding_lp,
];
let mut nonces = self
.0
.get_accounts_nonces(vec![user_holding_a, user_holding_b])
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))
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let signing_key_a = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_a)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(signing_key_a);
let signing_key_b = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_b)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(signing_key_b);
if let Some(signing_key_lp) = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_lp)
{
private_keys.push(signing_key_lp);
let lp_nonces = self
.0
.get_accounts_nonces(vec![user_holding_lp])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(lp_nonces);
} 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 witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
#[expect(clippy::too_many_arguments, reason = "each parameter is distinct")]
pub async fn send_swap_exact_input(
&self,
user_holding_a: AccountId,
@ -128,6 +81,8 @@ impl Amm<'_> {
swap_amount_in: u128,
min_amount_out: u128,
token_definition_id_in: AccountId,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
@ -168,49 +123,26 @@ 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,
));
};
let nonces = self
.0
.get_accounts_nonces(vec![account_id_auth])
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let signing_key = self
.0
.storage
.key_chain()
.pub_account_signing_key(account_id_auth)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
#[expect(clippy::too_many_arguments, reason = "each parameter is distinct")]
pub async fn send_swap_exact_output(
&self,
user_holding_a: AccountId,
@ -218,6 +150,8 @@ impl Amm<'_> {
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out,
@ -258,49 +192,26 @@ 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,
));
};
let nonces = self
.0
.get_accounts_nonces(vec![account_id_auth])
let mut groups = SigningGroup::new();
groups
.add_required(seller_mention, account_id_auth, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let signing_key = self
.0
.storage
.key_chain()
.pub_account_signing_key(account_id_auth)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
#[expect(clippy::too_many_arguments, reason = "each parameter is distinct")]
pub async fn send_add_liquidity(
&self,
user_holding_a: AccountId,
@ -309,6 +220,9 @@ impl Amm<'_> {
min_amount_liquidity: u128,
max_amount_to_add_token_a: u128,
max_amount_to_add_token_b: u128,
a_mention: &CliAccountMention,
b_mention: &CliAccountMention,
lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::AddLiquidity {
min_amount_liquidity,
@ -352,48 +266,19 @@ impl Amm<'_> {
user_holding_lp,
];
let nonces = self
.0
.get_accounts_nonces(vec![user_holding_a, user_holding_b])
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))
.and_then(|()| groups.add_optional(lp_mention, user_holding_lp, self.0))
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let signing_key_a = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_a)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let signing_key_b = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_b)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set = nssa::public_transaction::WitnessSet::for_message(
&message,
&[signing_key_a, signing_key_b],
);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
#[expect(clippy::too_many_arguments, reason = "each parameter is distinct")]
pub async fn send_remove_liquidity(
&self,
user_holding_a: AccountId,
@ -402,6 +287,7 @@ impl Amm<'_> {
remove_liquidity_amount: u128,
min_amount_to_remove_token_a: u128,
min_amount_to_remove_token_b: u128,
lp_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let instruction = amm_core::Instruction::RemoveLiquidity {
remove_liquidity_amount,
@ -445,36 +331,12 @@ impl Amm<'_> {
user_holding_lp,
];
let nonces = self
.0
.get_accounts_nonces(vec![user_holding_lp])
let mut groups = SigningGroup::new();
groups
.add_required(lp_mention, user_holding_lp, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let signing_key_lp = self
.0
.storage
.key_chain()
.pub_account_signing_key(user_holding_lp)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key_lp]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
}

View File

@ -1,14 +1,16 @@
use std::collections::HashMap;
use ata_core::{compute_ata_seed, get_associated_token_account_id};
use common::{HashType, transaction::NSSATransaction};
use common::HashType;
use nssa::{
AccountId, privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program,
};
use nssa_core::SharedSecretKey;
use sequencer_service_rpc::RpcClient as _;
use crate::{ExecutionFailureKind, PrivacyPreservingAccount, WalletCore};
use crate::{
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroup,
};
pub struct Ata<'wallet>(pub &'wallet WalletCore);
@ -17,6 +19,7 @@ impl Ata<'_> {
&self,
owner_id: AccountId,
definition_id: AccountId,
owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let program = Program::ata();
let ata_program_id = program.id();
@ -26,36 +29,15 @@ 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 Some(signing_key) = self.0.storage.key_chain().pub_account_signing_key(owner_id) else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
let instruction = ata_core::Instruction::Create { ata_program_id };
let message = nssa::public_transaction::Message::try_new(
program.id(),
account_ids,
nonces,
instruction,
)?;
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
}
pub async fn send_transfer(
@ -64,6 +46,7 @@ impl Ata<'_> {
definition_id: AccountId,
recipient_id: AccountId,
amount: u128,
owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let program = Program::ata();
let ata_program_id = program.id();
@ -73,39 +56,18 @@ impl Ata<'_> {
);
let account_ids = vec![owner_id, sender_ata_id, recipient_id];
let nonces = self
.0
.get_accounts_nonces(vec![owner_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let Some(signing_key) = self.0.storage.key_chain().pub_account_signing_key(owner_id) else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
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 witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
}
pub async fn send_burn(
@ -113,6 +75,7 @@ impl Ata<'_> {
owner_id: AccountId,
definition_id: AccountId,
amount: u128,
owner_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let program = Program::ata();
let ata_program_id = program.id();
@ -122,39 +85,18 @@ impl Ata<'_> {
);
let account_ids = vec![owner_id, holder_ata_id, definition_id];
let nonces = self
.0
.get_accounts_nonces(vec![owner_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let Some(signing_key) = self.0.storage.key_chain().pub_account_signing_key(owner_id) else {
return Err(ExecutionFailureKind::KeyNotFoundError);
};
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 witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
let mut groups = SigningGroup::new();
groups
.add_required(owner_mention, owner_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&program, account_ids, instruction, groups)
.await
}
pub async fn send_create_private_owner(
@ -181,7 +123,12 @@ impl Ata<'_> {
];
self.0
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
.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");
@ -218,7 +165,12 @@ impl Ata<'_> {
];
self.0
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
.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");
@ -254,7 +206,12 @@ impl Ata<'_> {
];
self.0
.send_privacy_preserving_tx(accounts, instruction_data, &ata_with_token_dependency())
.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");

View File

@ -24,6 +24,7 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
None,
)
.await
.map(|(resp, secrets)| {

View File

@ -24,6 +24,7 @@ impl NativeTokenTransfer<'_> {
vec![account],
Program::serialize_instruction(instruction).unwrap(),
&Program::authenticated_transfer_program().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -58,6 +59,7 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
None,
)
.await
.map(|(resp, secrets)| {
@ -91,6 +93,7 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
None,
)
.await
.map(|(resp, secrets)| {

View File

@ -1,17 +1,9 @@
use authenticated_transfer_core::Instruction as AuthTransferInstruction;
use common::{HashType, transaction::NSSATransaction};
use nssa::{
AccountId, PublicTransaction,
program::Program,
public_transaction::{Message, WitnessSet},
};
use pyo3::exceptions::PyRuntimeError;
use sequencer_service_rpc::RpcClient as _;
use common::HashType;
use nssa::{AccountId, program::Program};
use super::NativeTokenTransfer;
use crate::{
ExecutionFailureKind, cli::CliAccountMention, helperfunctions::read_pin, signing::SigningGroups,
};
use crate::{ExecutionFailureKind, cli::CliAccountMention, signing::SigningGroup};
impl NativeTokenTransfer<'_> {
pub async fn send_public_transfer(
@ -22,56 +14,22 @@ impl NativeTokenTransfer<'_> {
from_mention: &CliAccountMention,
to_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::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::<PyRuntimeError, _>(
e.to_string(),
))
})?;
.add_required(from_mention, from, self.0)
.and_then(|()| groups.add_optional(to_mention, to, self.0))
.map_err(ExecutionFailureKind::from_anyhow)?;
let program_id = Program::authenticated_transfer_program().id();
let nonces = self
.0
.get_accounts_nonces(groups.signing_ids())
self.0
.send_public_tx(
&Program::authenticated_transfer_program(),
vec![from, to],
AuthTransferInstruction::Transfer {
amount: balance_to_move,
},
groups,
)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let message = Message::try_new(
program_id,
vec![from, to],
nonces,
AuthTransferInstruction::Transfer {
amount: balance_to_move,
},
)
.map_err(ExecutionFailureKind::TransactionBuildError)?;
let pin = if groups.needs_pin() {
read_pin()
.map_err(|e| {
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<PyRuntimeError, _>(
e.to_string(),
))
})?
.as_str()
.to_owned()
} else {
String::new()
};
let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| {
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<PyRuntimeError, _>(e.to_string()))
})?;
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs));
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn register_account(
@ -79,53 +37,18 @@ impl NativeTokenTransfer<'_> {
from: AccountId,
account_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
let nonces = self
.0
.get_accounts_nonces(vec![from])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let account_ids = vec![from];
let program_id = Program::authenticated_transfer_program().id();
let message = Message::try_new(
program_id,
account_ids,
nonces,
AuthTransferInstruction::Initialize,
)
.map_err(ExecutionFailureKind::TransactionBuildError)?;
let mut groups = SigningGroups::new();
let mut groups = SigningGroup::new();
groups
.add_sender(account_mention, from, self.0)
.map_err(|e| {
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<PyRuntimeError, _>(
e.to_string(),
))
})?;
.add_required(account_mention, from, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
let pin = if groups.needs_pin() {
read_pin()
.map_err(|e| {
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<PyRuntimeError, _>(
e.to_string(),
))
})?
.as_str()
.to_owned()
} else {
String::new()
};
let sigs = groups.sign_all(&message.hash(), &pin).map_err(|e| {
ExecutionFailureKind::KeycardError(pyo3::PyErr::new::<PyRuntimeError, _>(e.to_string()))
})?;
let tx = PublicTransaction::new(message, WitnessSet::from_raw_parts(sigs));
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
self.0
.send_public_tx(
&Program::authenticated_transfer_program(),
vec![from],
AuthTransferInstruction::Initialize,
groups,
)
.await
}
}

View File

@ -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,9 @@ impl NativeTokenTransfer<'_> {
from: AccountId,
to: AccountId,
balance_to_move: u128,
from_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
self.0
.send_privacy_preserving_tx_with_pre_check(
vec![
@ -25,6 +25,7 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
Some(from_mention),
)
.await
.map(|(resp, secrets)| {
@ -43,9 +44,9 @@ impl NativeTokenTransfer<'_> {
to_vpk: ViewingPublicKey,
to_identifier: Identifier,
balance_to_move: u128,
from_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let (instruction_data, program, tx_pre_check) = auth_transfer_preparation(balance_to_move);
self.0
.send_privacy_preserving_tx_with_pre_check(
vec![
@ -59,6 +60,7 @@ impl NativeTokenTransfer<'_> {
instruction_data,
&program.into(),
tx_pre_check,
Some(from_mention),
)
.await
.map(|(resp, secrets)| {

View File

@ -62,6 +62,7 @@ impl Pinata<'_> {
],
nssa::program::Program::serialize_instruction(solution).unwrap(),
&nssa::program::Program::pinata().into(),
None,
)
.await
.map(|(resp, secrets)| {

View File

@ -1,10 +1,12 @@
use common::{HashType, transaction::NSSATransaction};
use common::HashType;
use nssa::{AccountId, program::Program};
use nssa_core::{Identifier, NullifierPublicKey, SharedSecretKey, encryption::ViewingPublicKey};
use sequencer_service_rpc::RpcClient as _;
use token_core::Instruction;
use crate::{ExecutionFailureKind, PrivacyPreservingAccount, WalletCore};
use crate::{
ExecutionFailureKind, PrivacyPreservingAccount, WalletCore, cli::CliAccountMention,
signing::SigningGroup,
};
pub struct Token<'wallet>(pub &'wallet WalletCore);
@ -15,48 +17,21 @@ impl Token<'_> {
supply_account_id: AccountId,
name: String,
total_supply: u128,
definition_mention: &CliAccountMention,
supply_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
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())
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))
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let message = nssa::public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
instruction,
)
.unwrap();
let def_private_key = self
.0
.storage
.key_chain()
.pub_account_signing_key(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let supply_private_key = self
.0
.storage
.key_chain()
.pub_account_signing_key(supply_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let witness_set = nssa::public_transaction::WitnessSet::for_message(
&message,
&[def_private_key, supply_private_key],
);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_new_definition_private_owned_supply(
@ -80,6 +55,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -112,6 +88,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -146,6 +123,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -161,63 +139,23 @@ impl Token<'_> {
sender_account_id: AccountId,
recipient_account_id: AccountId,
amount: u128,
sender_mention: &CliAccountMention,
recipient_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
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 mut nonces = self
.0
.get_accounts_nonces(vec![sender_account_id])
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))
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let sender_sk = self
.0
.storage
.key_chain()
.pub_account_signing_key(sender_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(sender_sk);
if let Some(recipient_sk) = self
.0
.storage
.key_chain()
.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."
);
}
let message = nssa::public_transaction::Message::try_new(
program_id,
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_transfer_transaction_private_owned_account(
@ -244,6 +182,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -282,6 +221,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -314,6 +254,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -330,13 +271,13 @@ impl Token<'_> {
sender_account_id: AccountId,
recipient_account_id: AccountId,
amount: u128,
sender_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let instruction = Instruction::Transfer {
amount_to_transfer: amount,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
self.0
.send_privacy_preserving_tx(
vec![
@ -347,6 +288,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
Some(sender_mention),
)
.await
.map(|(resp, secrets)| {
@ -365,13 +307,13 @@ impl Token<'_> {
recipient_vpk: ViewingPublicKey,
recipient_identifier: Identifier,
amount: u128,
sender_mention: &CliAccountMention,
) -> Result<(HashType, SharedSecretKey), ExecutionFailureKind> {
let instruction = Instruction::Transfer {
amount_to_transfer: amount,
};
let instruction_data =
Program::serialize_instruction(instruction).expect("Instruction should serialize");
self.0
.send_privacy_preserving_tx(
vec![
@ -384,6 +326,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
Some(sender_mention),
)
.await
.map(|(resp, secrets)| {
@ -400,41 +343,21 @@ impl Token<'_> {
definition_account_id: AccountId,
holder_account_id: AccountId,
amount: u128,
holder_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
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])
let mut groups = SigningGroup::new();
groups
.add_required(holder_mention, holder_account_id, self.0)
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups)
.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 signing_key = self
.0
.storage
.key_chain()
.pub_account_signing_key(holder_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &[signing_key]);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_burn_transaction_private_owned_account(
@ -461,6 +384,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -493,6 +417,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -526,6 +451,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -542,63 +468,23 @@ impl Token<'_> {
definition_account_id: AccountId,
holder_account_id: AccountId,
amount: u128,
definition_mention: &CliAccountMention,
holder_mention: &CliAccountMention,
) -> Result<HashType, ExecutionFailureKind> {
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])
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))
.map_err(ExecutionFailureKind::from_anyhow)?;
self.0
.send_public_tx(&Program::token(), account_ids, instruction, groups)
.await
.map_err(ExecutionFailureKind::SequencerError)?;
let mut private_keys = Vec::new();
let definition_sk = self
.0
.storage
.key_chain()
.pub_account_signing_key(definition_account_id)
.ok_or(ExecutionFailureKind::KeyNotFoundError)?;
private_keys.push(definition_sk);
if let Some(holder_sk) = self
.0
.storage
.key_chain()
.pub_account_signing_key(holder_account_id)
{
private_keys.push(holder_sk);
let recipient_nonces = self
.0
.get_accounts_nonces(vec![holder_account_id])
.await
.map_err(ExecutionFailureKind::SequencerError)?;
nonces.extend(recipient_nonces);
} else {
println!(
"Holder's account ({holder_account_id}) private key not found in wallet. Proceeding with only definition's key."
);
}
let message = nssa::public_transaction::Message::try_new(
Program::token().id(),
account_ids,
nonces,
instruction,
)
.unwrap();
let witness_set =
nssa::public_transaction::WitnessSet::for_message(&message, &private_keys);
let tx = nssa::PublicTransaction::new(message, witness_set);
Ok(self
.0
.sequencer_client
.send_transaction(NSSATransaction::Public(tx))
.await?)
}
pub async fn send_mint_transaction_private_owned_account(
@ -625,6 +511,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -663,6 +550,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -695,6 +583,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -728,6 +617,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {
@ -765,6 +655,7 @@ impl Token<'_> {
],
instruction_data,
&Program::token().into(),
None,
)
.await
.map(|(resp, secrets)| {

View File

@ -1,6 +1,7 @@
use anyhow::Result;
use keycard_wallet::{KeycardWallet, python_path};
use nssa::{AccountId, PrivateKey, PublicKey, Signature};
use pyo3::Python;
use crate::{WalletCore, cli::CliAccountMention};
@ -9,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()
@ -22,7 +23,7 @@ impl SigningGroups {
/// 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(
pub fn add_required(
&mut self,
mention: &CliAccountMention,
account_id: AccountId,
@ -42,9 +43,9 @@ impl SigningGroups {
Ok(())
}
/// Add a recipient. Same as [`add_sender`] but silently skips accounts with no local
/// Add a recipient. Same as [`add_required`] 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(
pub fn add_optional(
&mut self,
mention: &CliAccountMention,
account_id: AccountId,
@ -112,3 +113,37 @@ impl SigningGroups {
Ok(sigs)
}
}
/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction.
pub struct KeycardSessionContext {
pin: String,
wallet: Option<KeycardWallet>,
}
impl KeycardSessionContext {
pub fn new(pin: impl Into<String>) -> Self {
Self {
pin: pin.into(),
wallet: None,
}
}
pub fn get_or_connect<'py>(
&'py mut self,
py: Python<'py>,
) -> pyo3::PyResult<&'py KeycardWallet> {
if self.wallet.is_none() {
python_path::add_python_path(py)?;
let wallet = KeycardWallet::new(py)?;
wallet.connect(py, &self.pin)?;
self.wallet = Some(wallet);
}
Ok(self.wallet.as_ref().unwrap())
}
pub fn close(self, py: Python<'_>) {
if let Some(w) = self.wallet {
drop(w.close_session(py));
}
}
}