From e39634e6bcebb4027d0260c1c4f033495a030abe Mon Sep 17 00:00:00 2001 From: Alfred Hodler Date: Mon, 2 Sep 2024 05:16:37 +0000 Subject: [PATCH] CLI improvement * CLI version 0.12.3 * CLI can now convert between different xpub versions using the xpub command --- coldcard-cli/Cargo.toml | 3 +- coldcard-cli/src/main.rs | 42 ++++++++++++++++- coldcard-cli/src/xpub_version.rs | 81 ++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 coldcard-cli/src/xpub_version.rs diff --git a/coldcard-cli/Cargo.toml b/coldcard-cli/Cargo.toml index ec59a74..deae820 100644 --- a/coldcard-cli/Cargo.toml +++ b/coldcard-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "coldcard-cli" -version = "0.12.2" +version = "0.12.3" edition = "2021" authors = ["Alfred Hodler "] license = "MIT" @@ -18,6 +18,7 @@ path = "src/main.rs" [dependencies] coldcard = { version = "0.12.2", path = "../coldcard" } +base58 = "0.2.0" base64 = "0.21.7" clap = { version = "3.2.22", features = ["derive"] } hex = "0.4.3" diff --git a/coldcard-cli/src/main.rs b/coldcard-cli/src/main.rs index 9d94667..9d44d16 100644 --- a/coldcard-cli/src/main.rs +++ b/coldcard-cli/src/main.rs @@ -11,6 +11,7 @@ use coldcard::{util, XpubInfo}; use clap::Parser; mod fw_upgrade; +mod xpub_version; #[derive(clap::Parser)] #[clap(author, version, about)] @@ -172,6 +173,10 @@ enum Command { /// The optional derivation path path: Option, + #[clap(arg_enum)] + /// The extended key version to optionally convert to. + version: Option, + /// Include the fingerprint. The output will be two lines. #[clap(long)] xfp: bool, @@ -233,6 +238,29 @@ impl From<&SignMode> for coldcard::SignMode { } } +#[derive(Clone, clap::ArgEnum)] +enum XpubVersion { + Xpub, + Ypub, + Zpub, + Tpub, + Upub, + Vpub, +} + +impl From for xpub_version::Version { + fn from(value: XpubVersion) -> Self { + match value { + XpubVersion::Xpub => Self::Xpub, + XpubVersion::Ypub => Self::Ypub, + XpubVersion::Zpub => Self::Zpub, + XpubVersion::Tpub => Self::Tpub, + XpubVersion::Upub => Self::Upub, + XpubVersion::Vpub => Self::Vpub, + } + } +} + fn main() -> Result<(), Error> { env_logger::init(); @@ -736,11 +764,11 @@ fn handle(cli: Cli) -> Result<(), Error> { } } - Command::Xpub { path, xfp } => { + Command::Xpub { path, version, xfp } => { let path = path .map(|p| protocol::DerivationPath::new(&p)) .transpose()?; - let xpub = cc.xpub(path)?; + let mut xpub = cc.xpub(path)?; if xfp { let pk = util::decode_xpub(&xpub).expect("Unable to decode xpub; Coldcard error"); @@ -749,6 +777,9 @@ fn handle(cli: Cli) -> Result<(), Error> { println!("{}", hex); } + if let Some(version) = version { + xpub = xpub_version::convert_bytes(&xpub, version.into())?; + } println!("{}", xpub); } } @@ -859,6 +890,7 @@ enum Error { NotAuthToken, InvalidPSBT, NoColdcardDetected, + VersionConvert(xpub_version::Error), } impl From for Error { @@ -897,6 +929,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: xpub_version::Error) -> Self { + Self::VersionConvert(error) + } +} + fn prompt<'a>(text: &'static str, choices: &'a [&str]) -> &'a str { let bold = console::Style::new().bold(); diff --git a/coldcard-cli/src/xpub_version.rs b/coldcard-cli/src/xpub_version.rs new file mode 100644 index 0000000..d037cea --- /dev/null +++ b/coldcard-cli/src/xpub_version.rs @@ -0,0 +1,81 @@ +use base58::FromBase58; +use base58::ToBase58; +use coldcard::util::sha256; + +/// Some of the possible extended key version bytes. +#[derive(Debug, Clone, Copy)] +pub enum Version { + Xpub, + Ypub, + Zpub, + Tpub, + Upub, + Vpub, +} + +impl Version { + /// Returns the version bytes for a particular exended key version. + fn bytes(&self) -> [u8; 4] { + match self { + Version::Xpub => XPUB, + Version::Ypub => YPUB, + Version::Zpub => ZPUB, + Version::Tpub => TPUB, + Version::Upub => UPUB, + Version::Vpub => VPUB, + } + } +} + +const XPUB: [u8; 4] = [0x04, 0x88, 0xB2, 0x1E]; +const YPUB: [u8; 4] = [0x04, 0x9D, 0x7C, 0xB2]; +const ZPUB: [u8; 4] = [0x04, 0xB2, 0x47, 0x46]; +const TPUB: [u8; 4] = [0x04, 0x35, 0x87, 0xCF]; +const UPUB: [u8; 4] = [0x04, 0x4A, 0x52, 0x62]; +const VPUB: [u8; 4] = [0x04, 0x5F, 0x1C, 0xF6]; + +/// Converts an extended key to a different version. +pub fn convert_bytes(s: &str, to: Version) -> Result { + let mut decoded = s.from_base58().map_err(|_| Error::InvalidBase58)?; + if decoded.len() != 82 { + return Err(Error::InvalidLength); + } + + decoded[0..4].copy_from_slice(&to.bytes()); + let checksum = sha256(&sha256(&decoded[0..78])); + decoded[78..82].copy_from_slice(&checksum[0..4]); + Ok(decoded.to_base58()) +} + +#[derive(Debug, Clone, Copy)] +pub enum Error { + InvalidBase58, + InvalidLength, +} + +#[cfg(test)] +mod test { + use crate::xpub_version::Version; + + use super::convert_bytes; + + #[test] + fn version_conversion() { + let xpub = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"; + + let zpub = convert_bytes(xpub, Version::Zpub).unwrap(); + assert_eq!(zpub, "zpub6jftahH18ngZxUuv6oSniLNrBCSSE1B4EEU59bwTCEt8x6aS6b2mdfLxbS4QS53g85SWWP6wexqeer516433gYpZQoJie2tcMYdJ1SYYYAL"); + + let ypub = convert_bytes(xpub, Version::Ypub).unwrap(); + assert_eq!(ypub, "ypub6QqdH2c5z7967BioGSfAWFHM1EHzHPBZK7wrND3ZpEWFtzmCqvsD1bgpaE6pSAPkiSKhkuWPCJV6mZTSNMd2tK8xYTcJ48585pZecmSUzWp"); + + let tpub = convert_bytes(xpub, Version::Tpub).unwrap(); + assert_eq!(tpub, "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp"); + + let vpub = convert_bytes(xpub, Version::Vpub).unwrap(); + assert_eq!(vpub, "vpub5SLqN2bLY4WeZJ9SmNJHsyzqVKreTXD4ZnPC22MugDNcjhKX5xNX9QiQWcE4SSRzVWyHWUihpKRT7hckDGNzVc69wSX2JPcfGeNiT5c2XZy"); + + let orig_xpub = convert_bytes(&zpub, Version::Xpub).unwrap(); + assert_eq!(xpub, orig_xpub); + } +}