use bitcoin::NetworkKind;
use bitcoin::bip32::{ChildNumber, DerivationPath, Xpub};
use clap::Args;
use mk_codec::KeyCard;
use serde_json::json;
use crate::cmd::derive_support::{
AddressType, AddressTypeInference, CliNetwork, infer_address_type, render_address, secp_verify,
};
use crate::cmd::read_mk1_strings;
use crate::error::{CliError, Result};
#[derive(Args, Debug)]
pub struct AddressArgs {
pub mk1_strings: Vec<String>,
#[arg(long, value_enum)]
pub address_type: Option<AddressType>,
#[arg(long, conflicts_with = "range")]
pub count: Option<u32>,
#[arg(long, conflicts_with = "count")]
pub range: Option<String>,
#[arg(long, value_enum, default_value = "receive")]
pub chain: ChainSel,
#[arg(long, value_enum)]
pub network: Option<CliNetwork>,
#[arg(long)]
pub json: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
#[clap(rename_all = "lower")]
pub enum ChainSel {
Receive,
Change,
Both,
}
impl ChainSel {
fn chains(self) -> &'static [u32] {
match self {
ChainSel::Receive => &[0],
ChainSel::Change => &[1],
ChainSel::Both => &[0, 1],
}
}
}
pub fn run(args: AddressArgs) -> Result<u8> {
let strings = read_mk1_strings(&args.mk1_strings)?;
let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect();
let card = mk_codec::decode(&refs)?;
let addr_type = resolve_address_type(&card.origin_path, args.address_type)?;
let network = resolve_network(&card.xpub, args.network)?;
if card.xpub.depth != 3 {
eprintln!(
"warning: card xpub is at depth {} (not the canonical account depth 3); \
addresses are derived relative to this xpub and may not match a standard wallet",
card.xpub.depth
);
}
let indices = resolve_indices(args.count, args.range.as_deref())?;
let secp = secp_verify();
let mut rows: Vec<(u32, u32, String)> = Vec::new();
for &chain in args.chain.chains() {
for &index in &indices {
let dp: DerivationPath = vec![
ChildNumber::from_normal_idx(chain).unwrap(),
ChildNumber::from_normal_idx(index).map_err(|_| {
CliError::UsageError(format!(
"index {index} out of BIP-32 normal range (0..2147483647)"
))
})?,
]
.into();
let child = card.xpub.derive_pub(&secp, &dp).map_err(|e| {
CliError::UsageError(format!("derivation failed at m/{chain}/{index}: {e}"))
})?;
rows.push((
chain,
index,
render_address(&secp, &child, addr_type, network),
));
}
}
if args.json {
emit_json(&card, addr_type, network, &rows)?;
} else {
emit_text(args.chain, &rows);
}
crate::output_advisory::emit_output_class_advisory(
crate::output_advisory::OutputClass::WatchOnly,
&mut std::io::stderr(),
);
Ok(0)
}
fn resolve_address_type(
origin_path: &DerivationPath,
explicit: Option<AddressType>,
) -> Result<AddressType> {
match infer_address_type(origin_path) {
AddressTypeInference::Multisig => Err(CliError::UsageError(format!(
"card origin {origin_path} is a multisig cosigner xpub (BIP-48/BIP-87); single-key \
addresses would not match the wallet. Use descriptor tooling for multisig address \
derivation (e.g. `mnemonic verify-bundle`)"
))),
inference => {
if let Some(t) = explicit {
return Ok(t);
}
match inference {
AddressTypeInference::Inferred(t) => Ok(t),
_ => Err(CliError::UsageError(format!(
"--address-type required: cannot infer an address type from card origin \
{origin_path} (only canonical single-sig account paths \
m/{{44,49,84,86}}'/<coin>'/<account>' auto-infer)"
))),
}
}
}
}
fn resolve_network(xpub: &Xpub, explicit: Option<CliNetwork>) -> Result<CliNetwork> {
let kind = xpub.network;
match explicit {
None => Ok(match kind {
NetworkKind::Main => CliNetwork::Mainnet,
NetworkKind::Test => CliNetwork::Testnet,
}),
Some(net) => {
if net.network_kind() != kind {
return Err(CliError::UsageError(format!(
"--network {} disagrees with the xpub's network ({}); refusing to render \
wrong-network addresses",
net.label(),
if kind == NetworkKind::Main {
"mainnet"
} else {
"testnet"
}
)));
}
Ok(net)
}
}
}
fn resolve_indices(count: Option<u32>, range: Option<&str>) -> Result<Vec<u32>> {
const MAX_NORMAL: u32 = (1u32 << 31) - 1;
match (count, range) {
(Some(_), Some(_)) => unreachable!("clap conflicts_with"),
(Some(c), None) => {
if c > MAX_NORMAL.saturating_add(1) {
return Err(CliError::UsageError(format!(
"--count {c} exceeds the BIP-32 normal-index ceiling (max 2147483648)"
)));
}
Ok((0..c).collect())
}
(None, Some(r)) => {
let (a, b) = r
.split_once(',')
.ok_or_else(|| CliError::UsageError(format!("--range expects `A,B`, got {r:?}")))?;
let a: u32 = a
.trim()
.parse()
.map_err(|e| CliError::UsageError(format!("--range start {a:?}: {e}")))?;
let b: u32 = b
.trim()
.parse()
.map_err(|e| CliError::UsageError(format!("--range end {b:?}: {e}")))?;
if a > b {
return Err(CliError::UsageError(format!(
"--range start {a} must be <= end {b}"
)));
}
if b > MAX_NORMAL {
return Err(CliError::UsageError(format!(
"--range end {b} exceeds the BIP-32 normal-index ceiling (2147483647)"
)));
}
Ok((a..=b).collect())
}
(None, None) => Ok((0..10).collect()),
}
}
fn emit_text(chain: ChainSel, rows: &[(u32, u32, String)]) {
let grouped = matches!(chain, ChainSel::Both);
let mut cur_chain: Option<u32> = None;
for (c, idx, addr) in rows {
if grouped && cur_chain != Some(*c) {
let label = if *c == 0 { "receive" } else { "change" };
println!("{label} (m/{c}/i):");
cur_chain = Some(*c);
}
println!(" {idx} {addr}");
}
}
fn emit_json(
card: &KeyCard,
addr_type: AddressType,
network: CliNetwork,
rows: &[(u32, u32, String)],
) -> Result<()> {
let addresses: Vec<_> = rows
.iter()
.map(|(c, i, a)| json!({ "chain": c, "index": i, "address": a }))
.collect();
let envelope = json!({
"schema_version": 1,
"xpub": card.xpub.to_string(),
"origin_path": card.origin_path.to_string(),
"address_type": addr_type.label(),
"network": network.label(),
"addresses": addresses,
});
let s = serde_json::to_string(&envelope)
.map_err(|e| CliError::UsageError(format!("json serialization: {e}")))?;
println!("{s}");
Ok(())
}