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