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