Skip to main content

kobe_btc/
standard_wallet.rs

1//! Standard (non-HD) Bitcoin wallet implementation.
2//!
3//! A standard wallet uses a single randomly generated private key,
4//! without mnemonic or HD derivation.
5
6#[cfg(feature = "alloc")]
7use alloc::string::{String, ToString};
8
9use bitcoin::{Address, NetworkKind, PrivateKey, key::CompressedPublicKey};
10use zeroize::Zeroizing;
11
12use crate::address::create_address;
13use crate::{AddressType, Error, Network};
14
15/// A standard Bitcoin wallet with a single private key.
16///
17/// This wallet type generates a random private key directly,
18/// without using a mnemonic or HD derivation.
19///
20/// # Example
21///
22/// ```ignore
23/// use kobe_btc::{StandardWallet, Network, AddressType};
24///
25/// let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2wpkh).unwrap();
26/// println!("Address: {}", wallet.address_string());
27/// println!("Private Key (WIF): {}", wallet.private_key_wif().as_str());
28/// ```
29#[derive(Debug)]
30pub struct StandardWallet {
31    /// Bitcoin private key.
32    private_key: PrivateKey,
33    /// Compressed public key derived from private key.
34    public_key: CompressedPublicKey,
35    /// Bitcoin address.
36    address: Address,
37    /// Bitcoin network (mainnet or testnet).
38    network: Network,
39    /// Address type used for this wallet.
40    address_type: AddressType,
41}
42
43impl StandardWallet {
44    /// Generate a new standard wallet with a random private key.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if key generation fails.
49    ///
50    /// # Panics
51    ///
52    /// This function will not panic under normal circumstances.
53    /// The internal `expect` is guaranteed to succeed for valid private keys.
54    ///
55    /// # Note
56    ///
57    /// This function requires the `rand` feature to be enabled.
58    #[cfg(feature = "rand")]
59    pub fn generate(network: Network, address_type: AddressType) -> Result<Self, Error> {
60        let secp = bitcoin::secp256k1::Secp256k1::new();
61        let (secret_key, _) = secp.generate_keypair(&mut bitcoin::secp256k1::rand::thread_rng());
62
63        let private_key = PrivateKey::new(secret_key, network.to_bitcoin_network());
64        let public_key = CompressedPublicKey::from_private_key(&secp, &private_key)
65            .expect("valid private key always produces valid public key");
66
67        let address = create_address(&public_key, network, address_type);
68
69        Ok(Self {
70            private_key,
71            public_key,
72            address,
73            network,
74            address_type,
75        })
76    }
77
78    /// Import a wallet from a WIF (Wallet Import Format) private key.
79    ///
80    /// # Errors
81    ///
82    /// Returns an error if the WIF is invalid.
83    ///
84    /// # Panics
85    ///
86    /// This function will not panic under normal circumstances.
87    /// The internal `expect` is guaranteed to succeed for valid private keys.
88    pub fn from_wif(wif: &str, address_type: AddressType) -> Result<Self, Error> {
89        let private_key: PrivateKey = wif.parse().map_err(|_| Error::InvalidWif)?;
90
91        let network = if private_key.network == NetworkKind::Main {
92            Network::Mainnet
93        } else {
94            Network::Testnet
95        };
96
97        let secp = bitcoin::secp256k1::Secp256k1::new();
98        let public_key = CompressedPublicKey::from_private_key(&secp, &private_key)
99            .expect("valid private key always produces valid public key");
100
101        let address = create_address(&public_key, network, address_type);
102
103        Ok(Self {
104            private_key,
105            public_key,
106            address,
107            network,
108            address_type,
109        })
110    }
111
112    /// Import a wallet from a hex-encoded secret key.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the hex is invalid.
117    ///
118    /// # Panics
119    ///
120    /// This function will not panic under normal circumstances.
121    /// The internal `expect` is guaranteed to succeed for valid private keys.
122    pub fn from_hex(
123        hex_str: &str,
124        network: Network,
125        address_type: AddressType,
126    ) -> Result<Self, Error> {
127        let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
128        let bytes = hex::decode(hex_str).map_err(|_| Error::InvalidHex)?;
129
130        if bytes.len() != 32 {
131            return Err(Error::InvalidPrivateKey);
132        }
133
134        let secret_key = bitcoin::secp256k1::SecretKey::from_slice(&bytes)
135            .map_err(|_| Error::InvalidPrivateKey)?;
136
137        let private_key = PrivateKey::new(secret_key, network.to_bitcoin_network());
138
139        let secp = bitcoin::secp256k1::Secp256k1::new();
140        let public_key = CompressedPublicKey::from_private_key(&secp, &private_key)
141            .expect("valid private key always produces valid public key");
142
143        let address = create_address(&public_key, network, address_type);
144
145        Ok(Self {
146            private_key,
147            public_key,
148            address,
149            network,
150            address_type,
151        })
152    }
153
154    /// Get the secret key as raw bytes (zeroized on drop).
155    #[inline]
156    #[must_use]
157    pub fn secret_bytes(&self) -> Zeroizing<[u8; 32]> {
158        Zeroizing::new(self.private_key.inner.secret_bytes())
159    }
160
161    /// Get the secret key in hex format (zeroized on drop).
162    #[inline]
163    #[must_use]
164    pub fn secret_hex(&self) -> Zeroizing<String> {
165        Zeroizing::new(hex::encode(self.private_key.inner.secret_bytes()))
166    }
167
168    /// Get the secret key in WIF format (zeroized on drop).
169    #[inline]
170    #[must_use]
171    pub fn to_wif(&self) -> Zeroizing<String> {
172        Zeroizing::new(self.private_key.to_wif())
173    }
174
175    /// Get the public key in compressed hex format.
176    #[inline]
177    #[must_use]
178    pub fn pubkey_hex(&self) -> String {
179        self.public_key.to_string()
180    }
181
182    /// Get the Bitcoin address as a string.
183    #[inline]
184    #[must_use]
185    pub fn address(&self) -> String {
186        self.address.to_string()
187    }
188
189    /// Get the network.
190    #[must_use]
191    pub const fn network(&self) -> Network {
192        self.network
193    }
194
195    /// Get the address type.
196    #[must_use]
197    pub const fn address_type(&self) -> AddressType {
198        self.address_type
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[cfg(feature = "rand")]
207    #[test]
208    fn test_generate_mainnet_p2wpkh() {
209        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2wpkh).unwrap();
210        assert!(wallet.address().starts_with("bc1q"));
211        assert_eq!(wallet.network(), Network::Mainnet);
212    }
213
214    #[cfg(feature = "rand")]
215    #[test]
216    fn test_generate_mainnet_p2pkh() {
217        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2pkh).unwrap();
218        assert!(wallet.address().starts_with('1'));
219    }
220
221    #[cfg(feature = "rand")]
222    #[test]
223    fn test_generate_mainnet_p2sh() {
224        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2shP2wpkh).unwrap();
225        assert!(wallet.address().starts_with('3'));
226    }
227
228    #[cfg(feature = "rand")]
229    #[test]
230    fn test_generate_mainnet_p2tr() {
231        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2tr).unwrap();
232        assert!(wallet.address().starts_with("bc1p"));
233    }
234
235    #[cfg(feature = "rand")]
236    #[test]
237    fn test_generate_testnet() {
238        let wallet = StandardWallet::generate(Network::Testnet, AddressType::P2wpkh).unwrap();
239        assert!(wallet.address().starts_with("tb1q"));
240    }
241}