#[cfg(feature = "alloc")]
use alloc::{format, string::String, vec::Vec};
use bech32::{Bech32, Hrp};
pub use kobe_primitives::DerivedAccount;
use kobe_primitives::bip32::DerivedSecp256k1Key;
use kobe_primitives::{Derive, DeriveExt, Wallet};
use zeroize::Zeroizing;
use crate::DeriveError;
#[derive(Debug)]
pub struct Deriver<'a> {
wallet: &'a Wallet,
}
impl<'a> Deriver<'a> {
#[inline]
#[must_use]
pub const fn new(wallet: &'a Wallet) -> Self {
Self { wallet }
}
pub fn derive_many(&self, start: u32, count: u32) -> Result<Vec<DerivedAccount>, DeriveError> {
<Self as DeriveExt>::derive_many(self, start, count)
}
fn derive_at_path(&self, path: &str) -> Result<DerivedAccount, DeriveError> {
let key = DerivedSecp256k1Key::derive(self.wallet.seed(), path)?;
let compressed = key.compressed_pubkey();
let mut xonly = [0u8; 32];
xonly.copy_from_slice(
compressed
.get(1..)
.ok_or_else(|| DeriveError::Bech32(String::from("unreachable: short pubkey")))?,
);
let nsec_hrp = Hrp::parse("nsec")
.map_err(|e| DeriveError::Bech32(format!("invalid nsec HRP: {e}")))?;
let npub_hrp = Hrp::parse("npub")
.map_err(|e| DeriveError::Bech32(format!("invalid npub HRP: {e}")))?;
let priv_bytes = key.private_key_bytes();
let nsec = bech32::encode::<Bech32>(nsec_hrp, &*priv_bytes)
.map_err(|e| DeriveError::Bech32(format!("nsec encoding failed: {e}")))?;
let npub = bech32::encode::<Bech32>(npub_hrp, &xonly)
.map_err(|e| DeriveError::Bech32(format!("npub encoding failed: {e}")))?;
Ok(DerivedAccount::new(
String::from(path),
Zeroizing::new(nsec),
hex::encode(xonly),
npub,
))
}
}
impl Derive for Deriver<'_> {
type Error = DeriveError;
fn derive(&self, index: u32) -> Result<DerivedAccount, DeriveError> {
self.derive_at_path(&format!("m/44'/1237'/{index}'/0/0"))
}
fn derive_path(&self, path: &str) -> Result<DerivedAccount, DeriveError> {
self.derive_at_path(path)
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing, reason = "test assertions")]
mod tests {
use super::*;
const TV1_MNEMONIC: &str =
"leader monkey parrot ring guide accident before fence cannon height naive bean";
const TV1_PRIV_HEX: &str = "7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a";
const TV1_NSEC: &str = "nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp";
const TV1_PUB_HEX: &str = "17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917";
const TV1_NPUB: &str = "npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu";
const TV2_MNEMONIC: &str = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade";
const TV2_PRIV_HEX: &str = "c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add";
const TV2_NSEC: &str = "nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel";
const TV2_PUB_HEX: &str = "d41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573";
const TV2_NPUB: &str = "npub16sdj9zv4f8sl85e45vgq9n7nsgt5qphpvmf7vk8r5hhvmdjxx4es8rq74h";
fn wallet(mnemonic: &str) -> Wallet {
Wallet::from_mnemonic(mnemonic, None).unwrap()
}
#[test]
fn derive_correct_path() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
assert_eq!(a.path, "m/44'/1237'/0'/0/0");
}
#[test]
fn derive_returns_npub_prefix() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
assert!(a.address.starts_with("npub1"));
assert!(a.private_key.starts_with("nsec1"));
}
#[test]
fn derive_pubkey_is_xonly_32_bytes() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
let bytes = hex::decode(&a.public_key).unwrap();
assert_eq!(bytes.len(), 32);
}
#[test]
fn kat_nip06_vector1() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
assert_eq!(a.public_key, TV1_PUB_HEX);
assert_eq!(a.address, TV1_NPUB);
assert_eq!(a.private_key.as_str(), TV1_NSEC);
}
#[test]
fn kat_nip06_vector1_private_key_hex_roundtrip() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
let (hrp, data) = bech32::decode(a.private_key.as_str()).unwrap();
assert_eq!(hrp.as_str(), "nsec");
assert_eq!(hex::encode(data), TV1_PRIV_HEX);
}
#[test]
fn kat_nip06_vector2() {
let w = wallet(TV2_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
assert_eq!(a.public_key, TV2_PUB_HEX);
assert_eq!(a.address, TV2_NPUB);
assert_eq!(a.private_key.as_str(), TV2_NSEC);
}
#[test]
fn kat_nip06_vector2_private_key_hex_roundtrip() {
let w = wallet(TV2_MNEMONIC);
let a = Deriver::new(&w).derive(0).unwrap();
let (hrp, data) = bech32::decode(a.private_key.as_str()).unwrap();
assert_eq!(hrp.as_str(), "nsec");
assert_eq!(hex::encode(data), TV2_PRIV_HEX);
}
#[test]
fn derive_many_unique_across_accounts() {
let w = wallet(TV1_MNEMONIC);
let d = Deriver::new(&w);
let accounts = d.derive_many(0, 3).unwrap();
assert_eq!(accounts.len(), 3);
assert_eq!(accounts[0].path, "m/44'/1237'/0'/0/0");
assert_eq!(accounts[1].path, "m/44'/1237'/1'/0/0");
assert_eq!(accounts[2].path, "m/44'/1237'/2'/0/0");
assert_ne!(accounts[0].address, accounts[1].address);
assert_ne!(accounts[1].address, accounts[2].address);
}
#[test]
fn deterministic() {
let w1 = wallet(TV1_MNEMONIC);
let w2 = wallet(TV1_MNEMONIC);
let a1 = Deriver::new(&w1).derive(0).unwrap();
let a2 = Deriver::new(&w2).derive(0).unwrap();
assert_eq!(a1.address, a2.address);
assert_eq!(a1.private_key.as_str(), a2.private_key.as_str());
}
#[test]
fn passphrase_changes_address() {
let w1 = Wallet::from_mnemonic(TV1_MNEMONIC, None).unwrap();
let w2 = Wallet::from_mnemonic(TV1_MNEMONIC, Some("pass")).unwrap();
assert_ne!(
Deriver::new(&w1).derive(0).unwrap().address,
Deriver::new(&w2).derive(0).unwrap().address,
);
}
#[test]
fn derive_path_custom() {
let w = wallet(TV1_MNEMONIC);
let a = Deriver::new(&w).derive_path("m/44'/1237'/5'/0/0").unwrap();
assert_eq!(a.path, "m/44'/1237'/5'/0/0");
assert!(a.address.starts_with("npub1"));
}
}