1#[cfg(feature = "alloc")]
4use alloc::{
5 string::{String, ToString},
6 vec::Vec,
7};
8use core::ops::Deref;
9
10use kobe_primitives::{
11 Derive, DeriveError, DerivedAccount, DerivedPublicKey, Wallet, derive_range,
12};
13use zeroize::Zeroizing;
14
15use crate::address::create_address;
16use crate::wif::encode_wif;
17use crate::{AddressType, DerivationPath, Network};
18
19#[derive(Debug)]
21pub struct Deriver<'a> {
22 wallet: &'a Wallet,
23 network: Network,
24}
25
26#[derive(Clone)]
29pub struct BtcAccount {
30 inner: DerivedAccount,
31 private_key_wif: Zeroizing<String>,
32 address_type: AddressType,
33 bip32_path: DerivationPath,
34}
35
36impl core::fmt::Debug for BtcAccount {
37 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 f.debug_struct("BtcAccount")
39 .field("inner", &self.inner)
40 .field("private_key_wif", &"[REDACTED]")
41 .field("address_type", &self.address_type)
42 .field("bip32_path", &self.bip32_path)
43 .finish()
44 }
45}
46
47impl BtcAccount {
48 #[inline]
50 #[must_use]
51 pub const fn private_key_wif(&self) -> &Zeroizing<String> {
52 &self.private_key_wif
53 }
54
55 #[inline]
57 #[must_use]
58 pub const fn address_type(&self) -> AddressType {
59 self.address_type
60 }
61
62 #[inline]
64 #[must_use]
65 pub const fn bip32_path(&self) -> &DerivationPath {
66 &self.bip32_path
67 }
68
69 #[inline]
71 #[must_use]
72 pub const fn as_derived_account(&self) -> &DerivedAccount {
73 &self.inner
74 }
75
76 #[inline]
78 #[must_use]
79 pub fn into_derived_account(self) -> DerivedAccount {
80 self.inner
81 }
82}
83
84impl Deref for BtcAccount {
85 type Target = DerivedAccount;
86
87 #[inline]
88 fn deref(&self) -> &Self::Target {
89 &self.inner
90 }
91}
92
93impl From<BtcAccount> for DerivedAccount {
94 #[inline]
95 fn from(btc: BtcAccount) -> Self {
96 btc.inner
97 }
98}
99
100impl AsRef<DerivedAccount> for BtcAccount {
101 #[inline]
102 fn as_ref(&self) -> &DerivedAccount {
103 &self.inner
104 }
105}
106
107impl<'a> Deriver<'a> {
108 #[inline]
112 #[must_use]
113 pub const fn new(wallet: &'a Wallet, network: Network) -> Self {
114 Self { wallet, network }
115 }
116
117 #[inline]
123 pub fn derive(&self, index: u32) -> Result<BtcAccount, DeriveError> {
124 self.derive_with(AddressType::P2wpkh, index)
125 }
126
127 #[inline]
133 pub fn derive_with(
134 &self,
135 address_type: AddressType,
136 index: u32,
137 ) -> Result<BtcAccount, DeriveError> {
138 let path = DerivationPath::bip_standard(address_type, self.network, 0, false, index)?;
139 self.derive_structured(&path, address_type)
140 }
141
142 pub fn derive_many_with(
148 &self,
149 address_type: AddressType,
150 start: u32,
151 count: u32,
152 ) -> Result<Vec<BtcAccount>, DeriveError> {
153 derive_range(start, count, |i| self.derive_with(address_type, i))
154 }
155
156 pub fn derive_at(&self, path: &str) -> Result<BtcAccount, DeriveError> {
162 let parsed = DerivationPath::from_path_str(path)?;
163 let address_type = infer_address_type(&parsed).ok_or_else(|| {
164 DeriveError::Path(alloc::format!(
165 "btc: cannot infer address type from path '{path}'; \
166 purpose must be 44'/49'/84'/86'. \
167 Use Deriver::derive_at_with(path, address_type) for custom paths."
168 ))
169 })?;
170 self.derive_structured(&parsed, address_type)
171 }
172
173 pub fn derive_at_with(
179 &self,
180 path: &str,
181 address_type: AddressType,
182 ) -> Result<BtcAccount, DeriveError> {
183 let parsed = DerivationPath::from_path_str(path)?;
184 self.derive_structured(&parsed, address_type)
185 }
186
187 pub fn derive_structured(
193 &self,
194 path: &DerivationPath,
195 address_type: AddressType,
196 ) -> Result<BtcAccount, DeriveError> {
197 let path_string = path.to_string();
198 let derived = self.wallet.derive_secp256k1(&path_string)?;
199
200 let sk = derived.private_key_bytes();
202 let pk = derived.compressed_pubkey();
203
204 let address = create_address(&pk, self.network, address_type)?;
205 let private_key_wif = encode_wif(&sk, self.network)?;
206
207 let inner = DerivedAccount::new(
208 path_string,
209 sk,
210 DerivedPublicKey::Secp256k1Compressed(pk),
211 address,
212 );
213
214 Ok(BtcAccount {
215 inner,
216 private_key_wif,
217 address_type,
218 bip32_path: path.clone(),
219 })
220 }
221
222 #[must_use]
224 pub const fn network(&self) -> Network {
225 self.network
226 }
227}
228
229impl Derive for Deriver<'_> {
230 type Account = BtcAccount;
231 type Error = DeriveError;
232
233 fn derive(&self, index: u32) -> Result<BtcAccount, DeriveError> {
234 self.derive_with(AddressType::P2wpkh, index)
235 }
236
237 fn derive_path(&self, path: &str) -> Result<BtcAccount, DeriveError> {
238 self.derive_at(path)
239 }
240}
241
242fn infer_address_type(path: &DerivationPath) -> Option<AddressType> {
243 let first = path.first_segment()?;
244 if first.is_hardened() {
245 AddressType::from_purpose(first.index())
246 } else {
247 None
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use kobe_primitives::DeriveExt;
254
255 use super::*;
256 use crate::wif::decode_wif;
257
258 const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
259
260 fn test_wallet() -> Wallet {
261 Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap()
262 }
263
264 fn deriver(wallet: &Wallet, network: Network) -> Deriver<'_> {
265 Deriver::new(wallet, network)
266 }
267
268 #[test]
269 fn debug_redacts_wif() {
270 let wallet = test_wallet();
271 let account = deriver(&wallet, Network::Mainnet).derive(0).unwrap();
272 let wif = account.private_key_wif().as_str().to_owned();
273 let dbg = alloc::format!("{account:?}");
274 assert!(dbg.contains("[REDACTED]"));
275 assert!(!dbg.contains(&wif), "Debug must not leak WIF: {dbg}");
276 }
277
278 #[test]
279 fn derived_key_bytes_match_bip84_vector() {
280 let wallet = test_wallet();
281 let d = deriver(&wallet, Network::Mainnet);
282 let account = d.derive_at("m/84'/0'/0'/0/0").unwrap();
283 assert_eq!(
284 account.private_key_hex().as_str(),
285 "4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3"
286 );
287 assert_eq!(
288 account.public_key_hex(),
289 "0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c"
290 );
291 }
292
293 #[test]
294 fn kat_bip84_p2wpkh_abandon_index0() {
295 let wallet = test_wallet();
296 let a = deriver(&wallet, Network::Mainnet)
297 .derive_with(AddressType::P2wpkh, 0)
298 .unwrap();
299 assert_eq!(a.path(), "m/84'/0'/0'/0/0");
300 assert_eq!(a.address(), "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
301 assert_eq!(a.address_type(), AddressType::P2wpkh);
302 assert_eq!(
303 a.private_key_hex().as_str(),
304 "4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3"
305 );
306 }
307
308 #[test]
309 fn kat_bip84_p2wpkh_abandon_index1() {
310 let wallet = test_wallet();
311 let a = deriver(&wallet, Network::Mainnet)
312 .derive_with(AddressType::P2wpkh, 1)
313 .unwrap();
314 assert_eq!(a.path(), "m/84'/0'/0'/0/1");
315 assert_eq!(a.address(), "bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g");
316 }
317
318 #[test]
319 fn kat_bip44_p2pkh_abandon_index0() {
320 let wallet = test_wallet();
321 let a = deriver(&wallet, Network::Mainnet)
322 .derive_with(AddressType::P2pkh, 0)
323 .unwrap();
324 assert_eq!(a.path(), "m/44'/0'/0'/0/0");
325 assert_eq!(a.address(), "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA");
326 }
327
328 #[test]
329 fn kat_bip49_p2sh_p2wpkh_abandon_index0() {
330 let wallet = test_wallet();
331 let a = deriver(&wallet, Network::Mainnet)
332 .derive_with(AddressType::P2shP2wpkh, 0)
333 .unwrap();
334 assert_eq!(a.path(), "m/49'/0'/0'/0/0");
335 assert_eq!(a.address(), "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf");
336 }
337
338 #[test]
339 fn kat_bip86_p2tr_abandon_index0() {
340 let wallet = test_wallet();
341 let a = deriver(&wallet, Network::Mainnet)
342 .derive_with(AddressType::P2tr, 0)
343 .unwrap();
344 assert_eq!(a.path(), "m/86'/0'/0'/0/0");
345 assert_eq!(
346 a.address(),
347 "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
348 );
349 }
350
351 #[test]
354 fn kat_bip86_p2tr_abandon_index1() {
355 let wallet = test_wallet();
356 let a = deriver(&wallet, Network::Mainnet)
357 .derive_with(AddressType::P2tr, 1)
358 .unwrap();
359 assert_eq!(a.path(), "m/86'/0'/0'/0/1");
360 assert_eq!(
361 a.address(),
362 "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"
363 );
364 }
365
366 #[test]
367 fn kat_testnet_p2pkh_abandon_index0() {
368 let wallet = test_wallet();
369 let a = deriver(&wallet, Network::Testnet)
370 .derive_with(AddressType::P2pkh, 0)
371 .unwrap();
372 assert_eq!(a.path(), "m/44'/1'/0'/0/0");
373 assert_eq!(a.address(), "mkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV");
374 }
375
376 #[test]
379 fn kat_testnet_p2sh_p2wpkh_abandon_index0() {
380 let wallet = test_wallet();
381 let a = deriver(&wallet, Network::Testnet)
382 .derive_with(AddressType::P2shP2wpkh, 0)
383 .unwrap();
384 assert_eq!(a.path(), "m/49'/1'/0'/0/0");
385 assert_eq!(a.address(), "2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2");
386 }
387
388 #[test]
389 fn kat_testnet_p2wpkh_abandon_index0() {
390 let wallet = test_wallet();
391 let a = deriver(&wallet, Network::Testnet)
392 .derive_with(AddressType::P2wpkh, 0)
393 .unwrap();
394 assert_eq!(a.path(), "m/84'/1'/0'/0/0");
395 assert_eq!(a.address(), "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl");
396 }
397
398 #[test]
399 fn kat_testnet_p2tr_abandon_index0() {
400 let wallet = test_wallet();
401 let a = deriver(&wallet, Network::Testnet)
402 .derive_with(AddressType::P2tr, 0)
403 .unwrap();
404 assert_eq!(a.path(), "m/86'/1'/0'/0/0");
405 assert_eq!(
406 a.address(),
407 "tb1p8wpt9v4frpf3tkn0srd97pksgsxc5hs52lafxwru9kgeephvs7rqlqt9zj"
408 );
409 }
410
411 #[test]
412 fn default_derive_uses_bip84_p2wpkh() {
413 let wallet = test_wallet();
414 let d = deriver(&wallet, Network::Mainnet);
415 let def = d.derive(0).unwrap();
416 let explicit = d.derive_with(AddressType::P2wpkh, 0).unwrap();
417 assert_eq!(def.address(), explicit.address());
418 assert_eq!(def.path(), explicit.path());
419 }
420
421 #[test]
422 fn derive_many_matches_individual() {
423 let wallet = test_wallet();
424 let d = deriver(&wallet, Network::Mainnet);
425 let batch = d.derive_many(0, 5).unwrap();
426 let single: Vec<_> = (0..5)
427 .map(|i| d.derive_with(AddressType::P2wpkh, i).unwrap())
428 .collect();
429 for (b, s) in batch.iter().zip(single.iter()) {
430 assert_eq!(b.address(), s.address());
431 assert_eq!(b.path(), s.path());
432 }
433 }
434
435 #[test]
436 fn wif_roundtrips_to_private_key_bytes() {
437 let wallet = test_wallet();
438 let a = deriver(&wallet, Network::Mainnet).derive(0).unwrap();
439 let (key, network) = decode_wif(a.private_key_wif().as_str()).unwrap();
440 assert_eq!(key.as_ref(), a.private_key_bytes().as_ref());
441 assert_eq!(network, Network::Mainnet);
442 }
443
444 #[test]
445 fn wif_roundtrips_testnet_private_key_bytes() {
446 let wallet = test_wallet();
447 let a = deriver(&wallet, Network::Testnet).derive(0).unwrap();
448 let (key, network) = decode_wif(a.private_key_wif().as_str()).unwrap();
449 assert_eq!(key.as_ref(), a.private_key_bytes().as_ref());
450 assert_eq!(network, Network::Testnet);
451 }
452
453 #[test]
454 fn passphrase_changes_derivation() {
455 let wallet = test_wallet();
456 let with_pass = Wallet::from_mnemonic(TEST_MNEMONIC, Some("TREZOR")).unwrap();
457 assert_ne!(
458 deriver(&wallet, Network::Mainnet)
459 .derive(0)
460 .unwrap()
461 .address(),
462 deriver(&with_pass, Network::Mainnet)
463 .derive(0)
464 .unwrap()
465 .address(),
466 );
467 }
468
469 #[test]
470 fn derive_path_infers_address_type_from_purpose() {
471 let wallet = test_wallet();
472 let d = deriver(&wallet, Network::Mainnet);
473
474 let legacy = d.derive_path("m/44'/0'/0'/0/0").unwrap();
475 assert_eq!(legacy.address_type(), AddressType::P2pkh);
476 assert!(legacy.address().starts_with('1'));
477
478 let nested = d.derive_path("m/49'/0'/0'/0/0").unwrap();
479 assert_eq!(nested.address_type(), AddressType::P2shP2wpkh);
480 assert!(nested.address().starts_with('3'));
481
482 let native = d.derive_path("m/84'/0'/0'/0/0").unwrap();
483 assert_eq!(native.address_type(), AddressType::P2wpkh);
484 assert!(native.address().starts_with("bc1q"));
485
486 let taproot = d.derive_path("m/86'/0'/0'/0/0").unwrap();
487 assert_eq!(taproot.address_type(), AddressType::P2tr);
488 assert!(taproot.address().starts_with("bc1p"));
489 }
490
491 #[test]
492 fn derive_path_rejects_non_standard_purpose() {
493 let wallet = test_wallet();
494 let d = deriver(&wallet, Network::Mainnet);
495
496 let err = d.derive_path("m/1'/2'/3'").unwrap_err();
497 assert!(matches!(err, DeriveError::Path(_)));
498 if let DeriveError::Path(msg) = &err {
499 assert!(msg.contains("cannot infer address type"));
500 assert!(msg.contains("derive_at_with"));
501 }
502
503 let non_hardened_err = d.derive_path("m/44/0'/0'/0/0").unwrap_err();
504 assert!(matches!(non_hardened_err, DeriveError::Path(_)));
505 }
506
507 #[test]
508 fn derive_at_matches_trait_derive_path() {
509 let wallet = test_wallet();
510 let d = deriver(&wallet, Network::Mainnet);
511 for path in [
512 "m/44'/0'/0'/0/0",
513 "m/49'/0'/0'/0/0",
514 "m/84'/0'/0'/0/0",
515 "m/86'/0'/0'/0/0",
516 ] {
517 let a = d.derive_path(path).unwrap();
518 let b = d.derive_at(path).unwrap();
519 assert_eq!(a.address(), b.address());
520 assert_eq!(a.address_type(), b.address_type());
521 }
522 }
523
524 #[test]
525 fn derive_at_with_accepts_non_standard_purpose() {
526 let wallet = test_wallet();
527 let d = deriver(&wallet, Network::Mainnet);
528
529 let acct = d
530 .derive_at_with("m/7'/0'/0'/0/0", AddressType::P2wpkh)
531 .unwrap();
532 assert_eq!(acct.path(), "m/7'/0'/0'/0/0");
533 assert_eq!(acct.address_type(), AddressType::P2wpkh);
534 assert!(acct.address().starts_with("bc1q"));
535
536 let override_acct = d
537 .derive_at_with("m/84'/0'/0'/0/0", AddressType::P2tr)
538 .unwrap();
539 assert_eq!(override_acct.address_type(), AddressType::P2tr);
540 assert!(override_acct.address().starts_with("bc1p"));
541 }
542
543 #[test]
544 fn derive_structured_matches_derive_at_with() {
545 let wallet = test_wallet();
546 let d = deriver(&wallet, Network::Mainnet);
547 let path_str = "m/84'/0'/0'/0/0";
548 let parsed = DerivationPath::from_path_str(path_str).unwrap();
549 let a = d.derive_structured(&parsed, AddressType::P2wpkh).unwrap();
550 let b = d.derive_at_with(path_str, AddressType::P2wpkh).unwrap();
551 assert_eq!(a.address(), b.address());
552 assert_eq!(a.path(), b.path());
553 }
554}