2026-04-17 19:08:45 -04:00
|
|
|
use anyhow::Result;
|
|
|
|
|
use clap::Subcommand;
|
2026-05-14 21:19:25 -04:00
|
|
|
use keycard_wallet::{KeycardWallet, clear_pairing, python_path};
|
2026-04-17 19:08:45 -04:00
|
|
|
use pyo3::prelude::*;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
WalletCore,
|
2026-05-12 18:07:44 -04:00
|
|
|
cli::{SubcommandReturnValue, WalletSubcommand, read_mnemonic, read_pin},
|
2026-04-17 19:08:45 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// Represents generic chain CLI subcommand.
|
|
|
|
|
#[derive(Subcommand, Debug, Clone)]
|
|
|
|
|
pub enum KeycardSubcommand {
|
|
|
|
|
Available,
|
2026-05-14 21:19:25 -04:00
|
|
|
Connect,
|
|
|
|
|
Disconnect,
|
|
|
|
|
Init,
|
2026-05-12 18:07:44 -04:00
|
|
|
Load,
|
2026-05-15 18:15:54 -04:00
|
|
|
/// 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.
|
2026-05-18 15:33:26 -04:00
|
|
|
/// Only available when built with the `keycard-debug` feature.
|
|
|
|
|
#[cfg(feature = "keycard-debug")]
|
2026-05-15 18:15:54 -04:00
|
|
|
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,
|
|
|
|
|
},
|
2026-04-17 19:08:45 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WalletSubcommand for KeycardSubcommand {
|
|
|
|
|
async fn handle_subcommand(
|
|
|
|
|
self,
|
|
|
|
|
_wallet_core: &mut WalletCore,
|
|
|
|
|
) -> Result<SubcommandReturnValue> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Available => {
|
|
|
|
|
Python::with_gil(|py| {
|
2026-05-22 20:15:36 -04:00
|
|
|
python_path::add_python_path(py)
|
|
|
|
|
.expect("`wallet::keycard::available`: unable to setup python path");
|
2026-04-17 19:08:45 -04:00
|
|
|
|
2026-04-28 21:33:12 -04:00
|
|
|
let wallet = KeycardWallet::new(py)
|
|
|
|
|
.expect("`wallet::keycard::available`: invalid data received for pin");
|
|
|
|
|
let available = wallet.is_unpaired_keycard_available(py).expect(
|
|
|
|
|
"`wallet::keycard::available`: received invalid data from Keycard wrapper",
|
|
|
|
|
);
|
2026-04-17 19:08:45 -04:00
|
|
|
|
2026-04-21 18:27:14 -04:00
|
|
|
if available {
|
|
|
|
|
println!("\u{2705} Keycard is available.");
|
|
|
|
|
} else {
|
|
|
|
|
println!("\u{274c} Keycard is not available.");
|
|
|
|
|
}
|
2026-04-17 19:08:45 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
2026-04-23 17:45:43 -04:00
|
|
|
}
|
2026-05-14 21:19:25 -04:00
|
|
|
Self::Connect => {
|
|
|
|
|
let pin = read_pin()?;
|
|
|
|
|
|
|
|
|
|
Python::with_gil(|py| {
|
2026-05-22 20:15:36 -04:00
|
|
|
python_path::add_python_path(py)
|
|
|
|
|
.expect("`wallet::keycard::connect`: unable to setup python path");
|
2026-05-14 21:19:25 -04:00
|
|
|
|
|
|
|
|
let wallet = KeycardWallet::new(py)
|
|
|
|
|
.expect("`wallet::keycard::connect`: invalid keycard wallet provided");
|
|
|
|
|
|
2026-05-15 09:07:35 -04:00
|
|
|
wallet
|
|
|
|
|
.connect(py, &pin)
|
2026-05-14 21:19:25 -04:00
|
|
|
.expect("`wallet::keycard::connect`: failed to connect to keycard");
|
|
|
|
|
|
|
|
|
|
println!("\u{2705} Keycard paired and ready.");
|
|
|
|
|
drop(wallet.close_session(py));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
|
|
|
|
Self::Disconnect => {
|
|
|
|
|
let pin = read_pin()?;
|
|
|
|
|
|
|
|
|
|
Python::with_gil(|py| {
|
2026-05-22 20:15:36 -04:00
|
|
|
python_path::add_python_path(py)
|
|
|
|
|
.expect("`wallet::keycard::disconnect`: unable to setup python path");
|
2026-05-14 21:19:25 -04:00
|
|
|
|
|
|
|
|
let wallet = KeycardWallet::new(py)
|
|
|
|
|
.expect("`wallet::keycard::disconnect`: invalid keycard wallet provided");
|
|
|
|
|
|
2026-05-15 09:07:35 -04:00
|
|
|
wallet
|
|
|
|
|
.connect(py, &pin)
|
2026-05-14 21:19:25 -04:00
|
|
|
.expect("`wallet::keycard::disconnect`: failed to open session");
|
|
|
|
|
|
2026-05-15 09:07:35 -04:00
|
|
|
wallet
|
|
|
|
|
.disconnect(py)
|
2026-05-14 21:19:25 -04:00
|
|
|
.expect("`wallet::keycard::disconnect`: failed to unpair keycard");
|
|
|
|
|
|
|
|
|
|
clear_pairing();
|
|
|
|
|
println!("\u{2705} Keycard unpaired and pairing cleared.");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
|
|
|
|
Self::Init => {
|
|
|
|
|
let pin = read_pin()?;
|
|
|
|
|
|
|
|
|
|
Python::with_gil(|py| {
|
2026-05-22 20:15:36 -04:00
|
|
|
python_path::add_python_path(py)
|
|
|
|
|
.expect("`wallet::keycard::init`: unable to setup python path");
|
2026-05-14 21:19:25 -04:00
|
|
|
|
|
|
|
|
let wallet = KeycardWallet::new(py)
|
|
|
|
|
.expect("`wallet::keycard::init`: invalid keycard wallet provided");
|
|
|
|
|
|
2026-05-15 09:07:35 -04:00
|
|
|
let initialized = wallet
|
|
|
|
|
.initialize(py, &pin)
|
2026-05-14 21:19:25 -04:00
|
|
|
.expect("`wallet::keycard::init`: failed to initialize keycard");
|
|
|
|
|
|
|
|
|
|
if initialized {
|
|
|
|
|
clear_pairing();
|
|
|
|
|
println!("\u{2705} Keycard initialized successfully.");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
2026-05-12 18:07:44 -04:00
|
|
|
Self::Load => {
|
2026-04-30 19:02:33 -04:00
|
|
|
let pin = read_pin()?;
|
2026-05-12 18:07:44 -04:00
|
|
|
let mnemonic = read_mnemonic()?;
|
2026-04-30 19:02:33 -04:00
|
|
|
|
2026-04-17 19:08:45 -04:00
|
|
|
Python::with_gil(|py| {
|
2026-05-22 20:15:36 -04:00
|
|
|
python_path::add_python_path(py)
|
|
|
|
|
.expect("`wallet::keycard::load`: unable to setup python path");
|
2026-04-17 19:08:45 -04:00
|
|
|
|
2026-04-28 21:33:12 -04:00
|
|
|
let wallet = KeycardWallet::new(py)
|
|
|
|
|
.expect("`wallet::keycard::load`: invalid keycard wallet provided");
|
2026-04-17 19:08:45 -04:00
|
|
|
|
2026-05-15 09:07:35 -04:00
|
|
|
wallet
|
|
|
|
|
.connect(py, &pin)
|
2026-05-14 21:19:25 -04:00
|
|
|
.expect("`wallet::keycard::load`: failed to connect to keycard");
|
2026-04-21 18:27:14 -04:00
|
|
|
|
2026-05-14 21:19:25 -04:00
|
|
|
println!("\u{2705} Keycard is now connected to wallet.");
|
|
|
|
|
if wallet.load_mnemonic(py, &mnemonic).is_ok() {
|
|
|
|
|
println!("\u{2705} Mnemonic phrase loaded successfully.");
|
2026-04-21 18:27:14 -04:00
|
|
|
} else {
|
2026-05-14 21:19:25 -04:00
|
|
|
println!("\u{274c} Failed to load mnemonic phrase.");
|
2026-04-21 18:27:14 -04:00
|
|
|
}
|
2026-05-14 21:19:25 -04:00
|
|
|
drop(wallet.close_session(py));
|
2026-04-23 17:45:43 -04:00
|
|
|
});
|
2026-04-17 19:08:45 -04:00
|
|
|
|
2026-04-23 17:45:43 -04:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
2026-05-18 15:33:26 -04:00
|
|
|
#[cfg(feature = "keycard-debug")]
|
2026-05-15 18:15:54 -04:00
|
|
|
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)?;
|
2026-05-17 12:32:43 -04:00
|
|
|
println!("NSK: {}", hex::encode(*nsk));
|
|
|
|
|
println!("VSK: {}", hex::encode(*vsk));
|
2026-05-15 18:15:54 -04:00
|
|
|
Ok(SubcommandReturnValue::Empty)
|
|
|
|
|
}
|
2026-04-17 19:08:45 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|