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