Skip to main content

kobe_btc/
deriver.rs

1//! Bitcoin address derivation from a unified wallet.
2
3#[cfg(feature = "alloc")]
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8use core::marker::PhantomData;
9
10use bitcoin::{PrivateKey, bip32::Xpriv, key::CompressedPublicKey, secp256k1::Secp256k1};
11use kobe_core::{Derive, DerivedAccount, Wallet};
12use zeroize::Zeroizing;
13
14use crate::address::create_address;
15use crate::{AddressType, DerivationPath, Error, Network};
16
17/// Bitcoin address deriver from a unified wallet seed.
18///
19/// This deriver takes a seed from [`kobe::Wallet`] and derives
20/// Bitcoin addresses following BIP32/44/49/84 standards.
21#[derive(Debug)]
22pub struct Deriver<'a> {
23    /// Master extended private key.
24    master_key: Xpriv,
25    /// Cached secp256k1 context (~768KB, reused across derivations).
26    secp: Secp256k1<bitcoin::secp256k1::All>,
27    /// Bitcoin network (mainnet or testnet).
28    network: Network,
29    /// Phantom data to track wallet lifetime.
30    _wallet: PhantomData<&'a Wallet>,
31}
32
33/// A derived Bitcoin address with associated keys and metadata.
34#[derive(Debug, Clone)]
35#[non_exhaustive]
36pub struct DerivedAddress {
37    /// Derivation path used (e.g., `m/84'/0'/0'/0/0`).
38    pub path: DerivationPath,
39    /// Private key in hex format (zeroized on drop).
40    pub private_key_hex: Zeroizing<String>,
41    /// Private key in WIF format (zeroized on drop).
42    pub private_key_wif: Zeroizing<String>,
43    /// Public key in compressed hex format.
44    pub public_key_hex: String,
45    /// Bitcoin address string.
46    pub address: String,
47    /// Address type used for derivation.
48    pub address_type: AddressType,
49}
50
51impl<'a> Deriver<'a> {
52    /// Create a new Bitcoin deriver from a wallet.
53    ///
54    /// # Errors
55    ///
56    /// Returns an error if the master key derivation fails.
57    #[inline]
58    pub fn new(wallet: &'a Wallet, network: Network) -> Result<Self, Error> {
59        let master_key = Xpriv::new_master(network.to_bitcoin_network(), wallet.seed())?;
60
61        Ok(Self {
62            master_key,
63            secp: Secp256k1::new(),
64            network,
65            _wallet: PhantomData,
66        })
67    }
68
69    /// Derive a Bitcoin address using P2WPKH (Native SegWit) by default.
70    ///
71    /// Uses path: `m/84'/0'/0'/0/{index}` for mainnet
72    ///
73    /// # Arguments
74    ///
75    /// * `index` - The address index
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if derivation fails.
80    #[inline]
81    pub fn derive(&self, index: u32) -> Result<DerivedAddress, Error> {
82        self.derive_with(AddressType::P2wpkh, index)
83    }
84
85    /// Derive a Bitcoin address with a specific address type.
86    ///
87    /// This method supports different address formats:
88    /// - **P2pkh** (Legacy): `m/44'/coin'/0'/0/{index}`
89    /// - **P2shP2wpkh** (Nested SegWit): `m/49'/coin'/0'/0/{index}`
90    /// - **P2wpkh** (Native SegWit): `m/84'/coin'/0'/0/{index}`
91    /// - **P2tr** (Taproot): `m/86'/coin'/0'/0/{index}`
92    ///
93    /// # Arguments
94    ///
95    /// * `address_type` - Type of address (determines BIP purpose: 44/49/84/86)
96    /// * `index` - The address index
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if derivation fails.
101    #[inline]
102    pub fn derive_with(
103        &self,
104        address_type: AddressType,
105        index: u32,
106    ) -> Result<DerivedAddress, Error> {
107        let path = DerivationPath::bip_standard(address_type, self.network, 0, false, index)?;
108        self.derive_path(&path, address_type)
109    }
110
111    /// Derive multiple addresses using P2WPKH (Native SegWit) by default.
112    ///
113    /// # Arguments
114    ///
115    /// * `start` - Starting address index
116    /// * `count` - Number of addresses to derive
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if any derivation fails.
121    #[inline]
122    pub fn derive_many(&self, start: u32, count: u32) -> Result<Vec<DerivedAddress>, Error> {
123        self.derive_many_with(AddressType::P2wpkh, start, count)
124    }
125
126    /// Derive multiple addresses with a specific address type.
127    ///
128    /// # Arguments
129    ///
130    /// * `address_type` - Type of address to derive
131    /// * `start` - Starting index
132    /// * `count` - Number of addresses to derive
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if any derivation fails.
137    pub fn derive_many_with(
138        &self,
139        address_type: AddressType,
140        start: u32,
141        count: u32,
142    ) -> Result<Vec<DerivedAddress>, Error> {
143        let end = start.checked_add(count).ok_or_else(|| {
144            Error::InvalidDerivationPath("index overflow: start + count exceeds u32::MAX".into())
145        })?;
146        (start..end)
147            .map(|index| self.derive_with(address_type, index))
148            .collect()
149    }
150
151    /// Derive an address at a custom derivation path.
152    ///
153    /// This is the lowest-level derivation method, allowing full control
154    /// over the derivation path.
155    ///
156    /// # Arguments
157    ///
158    /// * `path` - BIP-32 derivation path
159    /// * `address_type` - Type of address to generate
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if derivation fails.
164    ///
165    /// # Panics
166    ///
167    /// This function will not panic under normal circumstances.
168    /// The internal `expect` is guaranteed to succeed for valid private keys.
169    pub fn derive_path(
170        &self,
171        path: &DerivationPath,
172        address_type: AddressType,
173    ) -> Result<DerivedAddress, Error> {
174        let derived = self.master_key.derive_priv(&self.secp, path.inner())?;
175
176        let private_key = PrivateKey::new(derived.private_key, self.network.to_bitcoin_network());
177        let public_key = CompressedPublicKey::from_private_key(&self.secp, &private_key)
178            .map_err(|_| Error::InvalidPrivateKey)?;
179
180        let address = create_address(&public_key, self.network, address_type);
181
182        let private_key_bytes = Zeroizing::new(derived.private_key.secret_bytes());
183
184        Ok(DerivedAddress {
185            path: path.clone(),
186            private_key_hex: Zeroizing::new(hex::encode(private_key_bytes)),
187            private_key_wif: Zeroizing::new(private_key.to_wif()),
188            public_key_hex: public_key.to_string(),
189            address: address.to_string(),
190            address_type,
191        })
192    }
193
194    /// Get the network.
195    #[must_use]
196    pub const fn network(&self) -> Network {
197        self.network
198    }
199
200    /// Internal: derive a [`DerivedAccount`] at a string path with default P2WPKH.
201    fn derive_account_at_path(&self, path_str: &str) -> Result<DerivedAccount, Error> {
202        let path = DerivationPath::from_path_str(path_str)?;
203        let da = self.derive_path(&path, AddressType::P2wpkh)?;
204        Ok(DerivedAccount::new(
205            da.path.to_string(),
206            da.private_key_hex,
207            da.public_key_hex,
208            da.address,
209        ))
210    }
211}
212
213impl Derive for Deriver<'_> {
214    type Error = Error;
215
216    fn derive(&self, index: u32) -> Result<DerivedAccount, Error> {
217        let da = self.derive_with(AddressType::P2wpkh, index)?;
218        Ok(DerivedAccount::new(
219            da.path.to_string(),
220            da.private_key_hex,
221            da.public_key_hex,
222            da.address,
223        ))
224    }
225
226    fn derive_path(&self, path: &str) -> Result<DerivedAccount, Error> {
227        self.derive_account_at_path(path)
228    }
229}
230
231#[cfg(test)]
232#[allow(clippy::unwrap_used)]
233mod tests {
234    use super::*;
235
236    const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
237
238    fn test_wallet() -> Wallet {
239        Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap()
240    }
241
242    #[test]
243    fn kat_p2wpkh() {
244        let wallet = test_wallet();
245        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
246        let addr = deriver.derive_with(AddressType::P2wpkh, 0).unwrap();
247        assert_eq!(addr.address, "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu");
248        assert_eq!(addr.path.to_string(), "m/84'/0'/0'/0/0");
249    }
250
251    #[test]
252    fn kat_p2pkh() {
253        let wallet = test_wallet();
254        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
255        let addr = deriver.derive_with(AddressType::P2pkh, 0).unwrap();
256        assert_eq!(addr.address, "1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA");
257        assert_eq!(addr.path.to_string(), "m/44'/0'/0'/0/0");
258    }
259
260    #[test]
261    fn kat_p2sh_p2wpkh() {
262        let wallet = test_wallet();
263        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
264        let addr = deriver.derive_with(AddressType::P2shP2wpkh, 0).unwrap();
265        assert_eq!(addr.address, "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf");
266        assert_eq!(addr.path.to_string(), "m/49'/0'/0'/0/0");
267    }
268
269    #[test]
270    fn p2tr_prefix() {
271        let wallet = test_wallet();
272        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
273        let addr = deriver.derive_with(AddressType::P2tr, 0).unwrap();
274        assert!(addr.address.starts_with("bc1p"));
275        assert_eq!(addr.path.to_string(), "m/86'/0'/0'/0/0");
276    }
277
278    #[test]
279    fn derive_default_is_p2wpkh() {
280        let wallet = test_wallet();
281        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
282        let def = deriver.derive(0).unwrap();
283        let explicit = deriver.derive_with(AddressType::P2wpkh, 0).unwrap();
284        assert_eq!(def.address, explicit.address);
285    }
286
287    #[test]
288    fn testnet_prefix() {
289        let wallet = test_wallet();
290        let deriver = Deriver::new(&wallet, Network::Testnet).unwrap();
291        let addr = deriver.derive(0).unwrap();
292        assert!(addr.address.starts_with("tb1q"));
293        assert_eq!(addr.path.to_string(), "m/84'/1'/0'/0/0");
294    }
295
296    #[test]
297    fn derive_many_unique() {
298        let wallet = test_wallet();
299        let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
300        let addrs = deriver.derive_many(0, 5).unwrap();
301        assert_eq!(addrs.len(), 5);
302        let set: std::collections::HashSet<_> = addrs.iter().map(|a| &a.address).collect();
303        assert_eq!(set.len(), 5);
304    }
305
306    #[test]
307    fn passphrase_changes_addresses() {
308        let wallet1 = Wallet::from_mnemonic(TEST_MNEMONIC, None).unwrap();
309        let wallet2 = Wallet::from_mnemonic(TEST_MNEMONIC, Some("password")).unwrap();
310        let d1 = Deriver::new(&wallet1, Network::Mainnet).unwrap();
311        let d2 = Deriver::new(&wallet2, Network::Mainnet).unwrap();
312        assert_ne!(d1.derive(0).unwrap().address, d2.derive(0).unwrap().address);
313    }
314}