Skip to main content

kobe_btc/
types.rs

1//! Common types for Bitcoin wallet operations.
2
3#[cfg(feature = "alloc")]
4use alloc::{format, string::ToString};
5use core::fmt;
6use core::str::FromStr;
7
8#[cfg(feature = "alloc")]
9use crate::{Error, Network};
10
11/// Bitcoin address types.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum AddressType {
14    /// Pay to Public Key Hash (Legacy) - starts with 1 or m/n
15    P2pkh,
16    /// Pay to Script Hash wrapping P2WPKH (`SegWit` compatible) - starts with 3 or 2
17    P2shP2wpkh,
18    /// Pay to Witness Public Key Hash (Native `SegWit`) - starts with bc1q or tb1q
19    #[default]
20    P2wpkh,
21    /// Pay to Taproot (Taproot/SegWit v1) - starts with bc1p or tb1p
22    P2tr,
23}
24
25impl AddressType {
26    /// Get the BIP purpose for this address type.
27    #[inline]
28    #[must_use]
29    pub const fn purpose(self) -> u32 {
30        match self {
31            Self::P2pkh => 44,
32            Self::P2shP2wpkh => 49,
33            Self::P2wpkh => 84,
34            Self::P2tr => 86,
35        }
36    }
37
38    /// Get address type name.
39    #[inline]
40    #[must_use]
41    pub const fn name(self) -> &'static str {
42        match self {
43            Self::P2pkh => "P2PKH (Legacy)",
44            Self::P2shP2wpkh => "P2SH-P2WPKH (SegWit)",
45            Self::P2wpkh => "P2WPKH (Native SegWit)",
46            Self::P2tr => "P2TR (Taproot)",
47        }
48    }
49}
50
51impl fmt::Display for AddressType {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{}", self.name())
54    }
55}
56
57/// Error returned when parsing an invalid address type string.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct ParseAddressTypeError;
60
61impl fmt::Display for ParseAddressTypeError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(
64            f,
65            "invalid address type, expected: p2pkh, p2sh, p2wpkh, or p2tr"
66        )
67    }
68}
69
70#[cfg(feature = "std")]
71impl std::error::Error for ParseAddressTypeError {}
72
73impl FromStr for AddressType {
74    type Err = ParseAddressTypeError;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        match s.to_lowercase().as_str() {
78            "p2pkh" | "legacy" => Ok(Self::P2pkh),
79            "p2sh" | "p2sh-p2wpkh" | "segwit" | "nested-segwit" => Ok(Self::P2shP2wpkh),
80            "p2wpkh" | "native-segwit" | "bech32" => Ok(Self::P2wpkh),
81            "p2tr" | "taproot" | "bech32m" => Ok(Self::P2tr),
82            _ => Err(ParseAddressTypeError),
83        }
84    }
85}
86
87/// BIP32 derivation path.
88#[cfg(feature = "alloc")]
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct DerivationPath {
91    inner: bitcoin::bip32::DerivationPath,
92}
93
94#[cfg(feature = "alloc")]
95impl DerivationPath {
96    /// Create a BIP44/49/84 standard path.
97    ///
98    /// Format: `m/purpose'/coin_type'/account'/change/address_index`
99    ///
100    /// # Panics
101    ///
102    /// This function will not panic as the path format is always valid.
103    #[must_use]
104    pub fn bip_standard(
105        address_type: AddressType,
106        network: Network,
107        account: u32,
108        change: bool,
109        address_index: u32,
110    ) -> Self {
111        let purpose = address_type.purpose();
112        let coin_type = network.coin_type();
113        let change_val = i32::from(change);
114
115        let path_str = format!("m/{purpose}'/{coin_type}'/{account}'/{change_val}/{address_index}");
116
117        Self {
118            inner: path_str.parse().expect("valid BIP standard path"),
119        }
120    }
121
122    /// Create from a custom path string.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if the path string is invalid.
127    pub fn from_path_str(path: &str) -> Result<Self, Error> {
128        let inner = bitcoin::bip32::DerivationPath::from_str(path)
129            .map_err(|e| Error::InvalidDerivationPath(e.to_string()))?;
130        Ok(Self { inner })
131    }
132
133    /// Get the inner bitcoin derivation path.
134    #[inline]
135    #[must_use]
136    pub const fn inner(&self) -> &bitcoin::bip32::DerivationPath {
137        &self.inner
138    }
139}
140
141#[cfg(feature = "alloc")]
142impl fmt::Display for DerivationPath {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "m/{}", self.inner)
145    }
146}
147
148#[cfg(feature = "alloc")]
149impl AsRef<bitcoin::bip32::DerivationPath> for DerivationPath {
150    fn as_ref(&self) -> &bitcoin::bip32::DerivationPath {
151        &self.inner
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_address_type_from_str() {
161        assert_eq!("p2pkh".parse::<AddressType>().unwrap(), AddressType::P2pkh);
162        assert_eq!("legacy".parse::<AddressType>().unwrap(), AddressType::P2pkh);
163        assert_eq!(
164            "p2sh".parse::<AddressType>().unwrap(),
165            AddressType::P2shP2wpkh
166        );
167        assert_eq!(
168            "segwit".parse::<AddressType>().unwrap(),
169            AddressType::P2shP2wpkh
170        );
171        assert_eq!(
172            "p2wpkh".parse::<AddressType>().unwrap(),
173            AddressType::P2wpkh
174        );
175        assert_eq!(
176            "native-segwit".parse::<AddressType>().unwrap(),
177            AddressType::P2wpkh
178        );
179        assert_eq!("p2tr".parse::<AddressType>().unwrap(), AddressType::P2tr);
180        assert_eq!("taproot".parse::<AddressType>().unwrap(), AddressType::P2tr);
181    }
182
183    #[test]
184    fn test_address_type_from_str_case_insensitive() {
185        assert_eq!("P2PKH".parse::<AddressType>().unwrap(), AddressType::P2pkh);
186        assert_eq!("TAPROOT".parse::<AddressType>().unwrap(), AddressType::P2tr);
187    }
188
189    #[test]
190    fn test_address_type_from_str_invalid() {
191        assert!("invalid".parse::<AddressType>().is_err());
192        assert!("".parse::<AddressType>().is_err());
193    }
194
195    #[test]
196    fn test_address_type_purpose() {
197        assert_eq!(AddressType::P2pkh.purpose(), 44);
198        assert_eq!(AddressType::P2shP2wpkh.purpose(), 49);
199        assert_eq!(AddressType::P2wpkh.purpose(), 84);
200        assert_eq!(AddressType::P2tr.purpose(), 86);
201    }
202
203    #[test]
204    fn test_address_type_default() {
205        assert_eq!(AddressType::default(), AddressType::P2wpkh);
206    }
207}