rustywallet_address/
address.rs

1//! Unified address type for all supported blockchains.
2
3use crate::bitcoin::BitcoinAddress;
4use crate::error::AddressError;
5use crate::ethereum::EthereumAddress;
6use crate::network::Network;
7
8/// Unified address type supporting Bitcoin and Ethereum.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum Address {
11    /// Bitcoin address (P2PKH, P2WPKH, or P2TR)
12    Bitcoin(BitcoinAddress),
13    /// Ethereum address
14    Ethereum(EthereumAddress),
15}
16
17impl Address {
18    /// Parse address from string with auto-detection.
19    ///
20    /// Detects address type based on prefix:
21    /// - `1`, `m`, `n` → Bitcoin P2PKH
22    /// - `bc1q`, `tb1q` → Bitcoin P2WPKH
23    /// - `bc1p`, `tb1p` → Bitcoin P2TR
24    /// - `0x` → Ethereum
25    pub fn parse(s: &str) -> Result<Self, AddressError> {
26        s.parse()
27    }
28
29    /// Validate address string.
30    pub fn validate(s: &str) -> Result<(), AddressError> {
31        s.parse::<Self>().map(|_| ())
32    }
33
34    /// Get the network for this address.
35    pub fn network(&self) -> Network {
36        match self {
37            Address::Bitcoin(addr) => addr.network(),
38            Address::Ethereum(_) => Network::Ethereum,
39        }
40    }
41
42    /// Check if this is a Bitcoin address.
43    #[inline]
44    pub fn is_bitcoin(&self) -> bool {
45        matches!(self, Address::Bitcoin(_))
46    }
47
48    /// Check if this is an Ethereum address.
49    #[inline]
50    pub fn is_ethereum(&self) -> bool {
51        matches!(self, Address::Ethereum(_))
52    }
53}
54
55impl std::fmt::Display for Address {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Address::Bitcoin(addr) => write!(f, "{}", addr),
59            Address::Ethereum(addr) => write!(f, "{}", addr),
60        }
61    }
62}
63
64impl std::str::FromStr for Address {
65    type Err = AddressError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        // Try Ethereum first (0x prefix)
69        if s.starts_with("0x") || s.starts_with("0X") {
70            return s.parse::<EthereumAddress>().map(Address::Ethereum);
71        }
72
73        // Try Bitcoin
74        s.parse::<BitcoinAddress>().map(Address::Bitcoin)
75    }
76}
77
78impl From<BitcoinAddress> for Address {
79    fn from(addr: BitcoinAddress) -> Self {
80        Address::Bitcoin(addr)
81    }
82}
83
84impl From<EthereumAddress> for Address {
85    fn from(addr: EthereumAddress) -> Self {
86        Address::Ethereum(addr)
87    }
88}
89
90/// Common trait for all address types.
91pub trait AddressFormat: Sized {
92    /// Parse from string.
93    fn parse(s: &str) -> Result<Self, AddressError>;
94
95    /// Validate string format.
96    fn validate(s: &str) -> Result<(), AddressError>;
97
98    /// Convert to canonical string representation.
99    fn to_string(&self) -> String;
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::bitcoin::{P2PKHAddress, P2TRAddress, P2WPKHAddress};
106    use rustywallet_keys::private_key::PrivateKey;
107
108    #[test]
109    fn test_address_detection_p2pkh() {
110        let pk = PrivateKey::random();
111        let pubkey = pk.public_key();
112        let addr = P2PKHAddress::from_public_key(&pubkey, Network::BitcoinMainnet).unwrap();
113        let parsed: Address = addr.to_string().parse().unwrap();
114        assert!(parsed.is_bitcoin());
115    }
116
117    #[test]
118    fn test_address_detection_p2wpkh() {
119        let pk = PrivateKey::random();
120        let pubkey = pk.public_key();
121        let addr = P2WPKHAddress::from_public_key(&pubkey, Network::BitcoinMainnet).unwrap();
122        let parsed: Address = addr.to_string().parse().unwrap();
123        assert!(parsed.is_bitcoin());
124    }
125
126    #[test]
127    fn test_address_detection_p2tr() {
128        let pk = PrivateKey::random();
129        let pubkey = pk.public_key();
130        let addr = P2TRAddress::from_public_key(&pubkey, Network::BitcoinMainnet).unwrap();
131        let parsed: Address = addr.to_string().parse().unwrap();
132        assert!(parsed.is_bitcoin());
133    }
134
135    #[test]
136    fn test_address_detection_ethereum() {
137        let pk = PrivateKey::random();
138        let pubkey = pk.public_key();
139        let addr = EthereumAddress::from_public_key(&pubkey).unwrap();
140        let parsed: Address = addr.to_string().parse().unwrap();
141        assert!(parsed.is_ethereum());
142    }
143}