From bc77c07f53e361c52c4c3497a4f335d0da7305ac Mon Sep 17 00:00:00 2001 From: Felipe Gonzalez Date: Mon, 1 Sep 2025 16:15:26 -0300 Subject: [PATCH 1/2] feat: Support industry standard --- src/reports.rs | 17 ++----- src/tx/common.rs | 2 +- src/tx/invoke.rs | 4 +- src/tx/resolve.rs | 4 +- src/wallet/edit.rs | 1 + src/wallet/import.rs | 3 +- src/wallet/types.rs | 119 +++++++++++++++++++++++++++++++++---------- 7 files changed, 104 insertions(+), 46 deletions(-) diff --git a/src/reports.rs b/src/reports.rs index 9d837d4..b318b2c 100644 --- a/src/reports.rs +++ b/src/reports.rs @@ -54,31 +54,24 @@ impl ErrorReport { if !self.details.is_empty() { let _ = writeln!(stderr, " details:"); for (key, value) in &self.details { - let _ = writeln!(stderr, " ∙ {}: {}", key, value); + let _ = writeln!(stderr, " ∙ {key}: {value}"); } } // Print help message if available if let Some(help) = &self.help { - let _ = writeln!(stderr, "💡 {}", help); + let _ = writeln!(stderr, "💡 {help}"); } if !self.logs.is_empty() { let _ = writeln!(stderr, " logs:"); for log in &self.logs { - let _ = writeln!(stderr, " ‣ {}", log); + let _ = writeln!(stderr, " ‣ {log}"); } } let _ = writeln!(stderr); } - - /// Print the error report to stdout with JSON formatting - pub fn print_json(&self) { - let json = serde_json::to_string_pretty(self) - .unwrap_or_else(|_| format!("{{\"error\": \"Failed to serialize error report\"}}")); - println!("{}", json); - } } impl std::fmt::Display for ErrorReport { @@ -86,7 +79,7 @@ impl std::fmt::Display for ErrorReport { write!(f, "Error: {} (Type: {})", self.message, self.kind)?; if let Some(code) = self.code { - write!(f, " [Code: {}]", code)?; + write!(f, " [Code: {code}]")?; } if !self.details.is_empty() { @@ -94,7 +87,7 @@ impl std::fmt::Display for ErrorReport { } if let Some(help) = &self.help { - write!(f, " - Help: {}", help)?; + write!(f, " - Help: {help}")?; } Ok(()) diff --git a/src/tx/common.rs b/src/tx/common.rs index 379528b..d6f73c4 100644 --- a/src/tx/common.rs +++ b/src/tx/common.rs @@ -191,7 +191,7 @@ pub fn define_args( let mut remaining_params = params.clone(); let mut loaded_args = - super::common::load_args(inline_args, file_args.as_deref(), &remaining_params)?; + super::common::load_args(inline_args, file_args, &remaining_params)?; // remove from the remaining params the args we already managed to load from the // file or json diff --git a/src/tx/invoke.rs b/src/tx/invoke.rs index eeec441..1c0ec3d 100644 --- a/src/tx/invoke.rs +++ b/src/tx/invoke.rs @@ -60,10 +60,10 @@ pub async fn run(args: Args, ctx: &crate::Context) -> Result<()> { args.tx3_args_json.as_deref(), args.tx3_args_file.as_deref(), ctx, - &provider, + provider, )?; - let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, &provider).await?; + let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, provider).await?; let cbor = hex::decode(tx).unwrap(); diff --git a/src/tx/resolve.rs b/src/tx/resolve.rs index 3c26884..1c5242a 100644 --- a/src/tx/resolve.rs +++ b/src/tx/resolve.rs @@ -49,10 +49,10 @@ pub async fn run(args: Args, ctx: &crate::Context) -> Result<()> { args.tx3_args_json.as_deref(), args.tx3_args_file.as_deref(), ctx, - &provider, + provider, )?; - let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, &provider).await?; + let TxEnvelope { tx, hash } = super::common::resolve_tx(&prototx, tx_args, provider).await?; let cbor = hex::decode(tx).unwrap(); diff --git a/src/wallet/edit.rs b/src/wallet/edit.rs index bffc9a5..6b7287b 100644 --- a/src/wallet/edit.rs +++ b/src/wallet/edit.rs @@ -71,6 +71,7 @@ pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> { public_key: wallet.public_key.clone(), is_default: new_is_default, is_unsafe: wallet.is_unsafe, + stake_public_key: wallet.stake_public_key.clone(), }; ctx.store.remove_wallet(wallet.clone())?; diff --git a/src/wallet/import.rs b/src/wallet/import.rs index b2c5c3f..1998728 100644 --- a/src/wallet/import.rs +++ b/src/wallet/import.rs @@ -21,7 +21,7 @@ pub struct Args { is_default: Option, } -#[instrument(skip_all, name = "edit")] +#[instrument(skip_all, name = "import")] pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> { let name = match args.name { Some(name) => Name::try_from(name)?, @@ -67,6 +67,7 @@ pub async fn run(args: Args, ctx: &mut crate::Context) -> Result<()> { name, modified: Local::now(), public_key: public_key.as_ref().to_vec(), + stake_public_key: None, is_default: new_is_default, is_unsafe: false, }; diff --git a/src/wallet/types.rs b/src/wallet/types.rs index fce0545..f00c16b 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -32,9 +32,44 @@ const VERSION_SIZE: usize = 1; const SALT_SIZE: usize = 16; const NONCE_SIZE: usize = 12; const TAG_SIZE: usize = 16; +const HARDENED_KEY_START: u32 = 2147483648; pub type NewWallet = (String, Wallet); +pub struct DerivationIndexes(Vec); +impl Default for DerivationIndexes { + fn default() -> Self { + Self(vec![ + HARDENED_KEY_START + 1852, // purpose + HARDENED_KEY_START + 1815, // coin type + HARDENED_KEY_START, // account + 0, // payment + 0, // key index + ]) + } +} + +impl DerivationIndexes { + fn stake() -> Self { + Self(vec![ + HARDENED_KEY_START + 1852, // purpose + HARDENED_KEY_START + 1815, // coin type + HARDENED_KEY_START, // account + 2, // stake + 0, // key index + ]) + } +} + +impl IntoIterator for DerivationIndexes { + type Item = u32; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct Wallet { pub name: Name, @@ -44,6 +79,9 @@ pub struct Wallet { #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(with = "utils::option_hex_vec_u8")] pub private_key: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(with = "utils::option_hex_vec_u8")] + pub stake_public_key: Option>, pub created: DateTime, pub modified: DateTime, pub is_default: bool, @@ -58,22 +96,36 @@ impl Wallet { is_default: bool, is_unsafe: bool, ) -> Result { - let (private_key, mnemonic) = - Bip32PrivateKey::generate_with_mnemonic(OsRng, password.to_string()); - let public_key = private_key.to_public().as_bytes(); + let (root, mnemonic) = Bip32PrivateKey::generate_with_mnemonic(OsRng); + + let mut payment = root.clone(); + let mut stake = root.clone(); - let private_key = private_key.to_ed25519_private_key(); + for idx in DerivationIndexes::default() { + payment = payment.derive(idx); + } + + for idx in DerivationIndexes::stake() { + stake = stake.derive(idx); + } + + let public_key = payment.to_public().as_bytes(); + + let private_key = payment.to_ed25519_private_key(); let private_key = match is_unsafe { true => private_key.as_bytes(), false => encrypt_private_key(OsRng, private_key, &password.to_string()), }; + let stake_public_key = Some(stake.to_public().as_bytes()); + Ok(( mnemonic.to_string(), Self { name: Name::try_from(name)?, private_key: Some(private_key), public_key, + stake_public_key, created: Local::now(), modified: Local::now(), is_default, @@ -89,11 +141,20 @@ impl Wallet { is_default: bool, is_unsafe: bool, ) -> Result { - let private_key = - Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string(), password.to_string())?; - let public_key = private_key.to_public().as_bytes(); + let mut payment = Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string())?; + let mut stake = Bip32PrivateKey::from_bip39_mnenomic(mnemonic.to_string())?; - let private_key = private_key.to_ed25519_private_key(); + for idx in DerivationIndexes::default() { + payment = payment.derive(idx); + } + + for idx in DerivationIndexes::stake() { + stake = stake.derive(idx); + } + let public_key = payment.to_public().as_bytes(); + let stake_public_key = stake.to_public().as_bytes(); + + let private_key = payment.to_ed25519_private_key(); let private_key = match is_unsafe { true => private_key.as_bytes(), false => encrypt_private_key(OsRng, private_key, &password.to_string()), @@ -103,6 +164,7 @@ impl Wallet { name: Name::try_from(name)?, private_key: Some(private_key), public_key, + stake_public_key: Some(stake_public_key), created: Local::now(), modified: Local::now(), is_default, @@ -116,19 +178,27 @@ impl Wallet { .to_ed25519_pubkey(), None => PublicKey::from_str(&hex::encode(&self.public_key)).unwrap(), }; + let delegation_part = match self.stake_public_key.as_ref() { + Some(pk) => ShelleyDelegationPart::key_hash( + Bip32PublicKey::from_bytes(pk.clone().try_into().unwrap()) + .to_ed25519_pubkey() + .compute_hash(), + ), + None => ShelleyDelegationPart::Null, + }; if is_testnet { ShelleyAddress::new( Network::Testnet, ShelleyPaymentPart::key_hash(pk.compute_hash()), - ShelleyDelegationPart::Null, + delegation_part, ) .into() } else { ShelleyAddress::new( Network::Mainnet, ShelleyPaymentPart::key_hash(pk.compute_hash()), - ShelleyDelegationPart::Null, + delegation_part, ) .into() } @@ -159,11 +229,8 @@ impl Wallet { .map(|x| x.clone().to_vec()) .unwrap_or_default(); - let public_key = Bip32PublicKey::from_bytes(self.public_key.clone().try_into().unwrap()) - .to_ed25519_pubkey(); - vkey_witnesses.push(VKeyWitness { - vkey: public_key.as_ref().to_vec().into(), + vkey: private_key.public_key().as_ref().to_vec().into(), signature: signature.as_ref().to_vec().into(), }); @@ -370,7 +437,7 @@ impl TryFrom<&[u8]> for PrivateKey { } /// Ed25519-BIP32 HD Private Key -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Bip32PrivateKey(ed25519_bip32::XPrv); impl Bip32PrivateKey { const BECH32_HRP: &'static str = "xprv"; @@ -383,10 +450,7 @@ impl Bip32PrivateKey { Self(xprv) } - pub fn generate_with_mnemonic( - mut rng: T, - password: String, - ) -> (Self, Mnemonic) { + pub fn generate_with_mnemonic(mut rng: T) -> (Self, Mnemonic) { let mut buf = [0u8; 64]; rng.fill_bytes(&mut buf); @@ -396,9 +460,9 @@ impl Bip32PrivateKey { let mut pbkdf2_result = [0; XPRV_SIZE]; - const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? + const ITER: u32 = 2048; // TODO: BIP39 says 2048, CML uses 4096? - let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); + let mut mac = Hmac::new(Sha512::new(), &[]); pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); (Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39) @@ -414,15 +478,15 @@ impl Bip32PrivateKey { self.0.as_ref().to_vec() } - pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result { - let bip39 = Mnemonic::parse(mnemonic).context("Error parsing mnemonic")?; + pub fn from_bip39_mnenomic(mnemonic: String) -> Result { + let bip39 = Mnemonic::parse(&mnemonic).context("Error parsing mnemonic")?; let entropy = bip39.to_entropy(); let mut pbkdf2_result = [0; XPRV_SIZE]; - const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096? + const ITER: u32 = 4096; - let mut mac = Hmac::new(Sha512::new(), password.as_bytes()); + let mut mac = Hmac::new(Sha512::new(), &[]); pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result); Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result))) @@ -648,10 +712,9 @@ mod tests { #[test] fn mnemonic_roundtrip() { - let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into()); + let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng); - let xprv_from_mne = - Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap(); + let xprv_from_mne = Bip32PrivateKey::from_bip39_mnenomic(mne.to_string()).unwrap(); assert_eq!(xprv, xprv_from_mne) } From 168b8f795a91c0f361d8a9141286fbd6eb02a559 Mon Sep 17 00:00:00 2001 From: Felipe Gonzalez Date: Mon, 1 Sep 2025 16:17:27 -0300 Subject: [PATCH 2/2] Use same amount of iterations --- src/wallet/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/types.rs b/src/wallet/types.rs index f00c16b..b8bc587 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -460,7 +460,7 @@ impl Bip32PrivateKey { let mut pbkdf2_result = [0; XPRV_SIZE]; - const ITER: u32 = 2048; // TODO: BIP39 says 2048, CML uses 4096? + const ITER: u32 = 4096; let mut mac = Hmac::new(Sha512::new(), &[]); pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);