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
5#[cfg(feature = "alloc")]
6use alloc::string::{String, ToString};
7
8use bitcoin::{
9    Address, NetworkKind, PrivateKey, PublicKey, key::CompressedPublicKey, secp256k1::Secp256k1,
10};
11use zeroize::Zeroizing;
12
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    /// Private key.
22    private_key: PrivateKey,
23    /// Compressed public key.
24    public_key: CompressedPublicKey,
25    /// Bitcoin address.
26    address: Address,
27    /// Network.
28    network: Network,
29    /// Address type.
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    /// # Note
41    ///
42    /// This function requires the `rand` feature to be enabled.
43    #[cfg(feature = "rand")]
44    pub fn generate(network: Network, address_type: AddressType) -> Result<Self, Error> {
45        let secp = bitcoin::secp256k1::Secp256k1::new();
46        let (secret_key, _) = secp.generate_keypair(&mut bitcoin::secp256k1::rand::thread_rng());
47
48        let private_key = PrivateKey::new(secret_key, network.to_bitcoin_network());
49        let public_key = CompressedPublicKey::from_private_key(&secp, &private_key)
50            .expect("valid private key always produces valid public key");
51
52        let address = Self::create_address(&public_key, network, address_type);
53
54        Ok(Self {
55            private_key,
56            public_key,
57            address,
58            network,
59            address_type,
60        })
61    }
62
63    /// Import a wallet from a WIF (Wallet Import Format) private key.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the WIF is invalid.
68    pub fn from_wif(wif: &str, address_type: AddressType) -> Result<Self, Error> {
69        let private_key: PrivateKey = wif.parse().map_err(|_| Error::InvalidWif)?;
70
71        let network = if private_key.network == NetworkKind::Main {
72            Network::Mainnet
73        } else {
74            Network::Testnet
75        };
76
77        let secp = bitcoin::secp256k1::Secp256k1::new();
78        let public_key = CompressedPublicKey::from_private_key(&secp, &private_key)
79            .expect("valid private key always produces valid public key");
80
81        let address = Self::create_address(&public_key, network, address_type);
82
83        Ok(Self {
84            private_key,
85            public_key,
86            address,
87            network,
88            address_type,
89        })
90    }
91
92    /// Create an address from a public key.
93    fn create_address(
94        public_key: &CompressedPublicKey,
95        network: Network,
96        address_type: AddressType,
97    ) -> Address {
98        let btc_network = network.to_bitcoin_network();
99
100        match address_type {
101            AddressType::P2pkh => Address::p2pkh(PublicKey::from(*public_key), btc_network),
102            AddressType::P2shP2wpkh => Address::p2shwpkh(public_key, btc_network),
103            AddressType::P2wpkh => Address::p2wpkh(public_key, btc_network),
104            AddressType::P2tr => {
105                let secp = Secp256k1::verification_only();
106                let internal_key = public_key.0.x_only_public_key().0;
107                Address::p2tr(&secp, internal_key, None, btc_network)
108            }
109        }
110    }
111
112    /// Get the private key in WIF format.
113    #[must_use]
114    pub fn private_key_wif(&self) -> Zeroizing<String> {
115        Zeroizing::new(self.private_key.to_wif())
116    }
117
118    /// Get the public key in hex format.
119    #[must_use]
120    pub fn public_key_hex(&self) -> String {
121        self.public_key.to_string()
122    }
123
124    /// Get the Bitcoin address.
125    #[must_use]
126    pub fn address(&self) -> &Address {
127        &self.address
128    }
129
130    /// Get the address as a string.
131    #[must_use]
132    pub fn address_string(&self) -> String {
133        self.address.to_string()
134    }
135
136    /// Get the network.
137    #[must_use]
138    pub const fn network(&self) -> Network {
139        self.network
140    }
141
142    /// Get the address type.
143    #[must_use]
144    pub const fn address_type(&self) -> AddressType {
145        self.address_type
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[cfg(feature = "rand")]
154    #[test]
155    fn test_generate_mainnet_p2wpkh() {
156        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2wpkh).unwrap();
157        assert!(wallet.address_string().starts_with("bc1q"));
158        assert_eq!(wallet.network(), Network::Mainnet);
159    }
160
161    #[cfg(feature = "rand")]
162    #[test]
163    fn test_generate_mainnet_p2pkh() {
164        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2pkh).unwrap();
165        assert!(wallet.address_string().starts_with('1'));
166    }
167
168    #[cfg(feature = "rand")]
169    #[test]
170    fn test_generate_mainnet_p2sh() {
171        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2shP2wpkh).unwrap();
172        assert!(wallet.address_string().starts_with('3'));
173    }
174
175    #[cfg(feature = "rand")]
176    #[test]
177    fn test_generate_mainnet_p2tr() {
178        let wallet = StandardWallet::generate(Network::Mainnet, AddressType::P2tr).unwrap();
179        assert!(wallet.address_string().starts_with("bc1p"));
180    }
181
182    #[cfg(feature = "rand")]
183    #[test]
184    fn test_generate_testnet() {
185        let wallet = StandardWallet::generate(Network::Testnet, AddressType::P2wpkh).unwrap();
186        assert!(wallet.address_string().starts_with("tb1q"));
187    }
188}