ethereum_mysql/
sql_address.rs

1pub use alloy::primitives::Address;
2#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4use std::ops::Deref;
5use std::str::FromStr;
6
7/// SQL-compatible wrapper for Ethereum Address.
8///
9/// This type wraps `alloy::primitives::Address` and provides seamless integration
10/// with SQL databases through SQLx. It supports MySQL, PostgreSQL, and SQLite,
11/// storing addresses as VARCHAR/TEXT in the database using the standard hex format (0x-prefixed).
12///
13/// # Database Support
14///
15/// - **MySQL**: Enable with `mysql` feature
16/// - **PostgreSQL**: Enable with `postgres` feature  
17/// - **SQLite**: Enable with `sqlite` feature
18///
19/// # Examples
20///
21/// ```no_run
22/// use ethereum_mysql::SqlAddress;
23/// use alloy::primitives::Address;
24/// use std::str::FromStr;
25///
26/// // Create from raw Address
27/// let addr = Address::ZERO;
28/// let sql_addr = SqlAddress::from(addr);
29///
30/// // Create from string
31/// let sql_addr = SqlAddress::from_str("0x0000000000000000000000000000000000000000").unwrap();
32/// ```
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub struct SqlAddress(Address);
36
37impl SqlAddress {
38    /// The zero address (0x0000000000000000000000000000000000000000)
39    ///
40    /// This constant represents the Ethereum zero address, commonly used as a null value
41    /// or burn address in smart contracts. It's available at compile time.
42    pub const ZERO: Self = SqlAddress(Address::ZERO);
43
44    /// Creates a new SqlAddress from an u8 array.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use ethereum_mysql::SqlAddress;
50    /// use alloy::primitives::Address;
51    ///
52    /// let my_address: SqlAddress = SqlAddress::new([0u8; 20]);
53    /// ```
54    pub fn new(bytes: [u8; 20]) -> Self {
55        SqlAddress(Address::new(bytes))
56    }
57
58    /// Creates a new SqlAddress from an alloy Address (const fn).
59    ///
60    /// This is a `const fn` and can be used in constant contexts, such as static/const variables or macros。
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use ethereum_mysql::SqlAddress;
66    /// use alloy::primitives::Address;
67    ///
68    /// const MY_ADDRESS: SqlAddress = SqlAddress::new_from_address(Address::ZERO);
69    /// ```
70    pub const fn new_from_address(addr: Address) -> Self {
71        SqlAddress(addr)
72    }
73
74    /// Returns a reference to the inner alloy Address.
75    ///
76    /// This method provides access to the underlying `alloy::primitives::Address`
77    /// for use with other Ethereum libraries or blockchain RPC calls.
78    pub fn inner(&self) -> &Address {
79        &self.0
80    }
81
82    /// Consumes self and returns the inner Address.
83    pub fn into_inner(self) -> Address {
84        self.0
85    }
86
87    /// Creates a SqlAddress from a byte slice (must be 20 bytes).
88    ///
89    /// # Panics
90    ///
91    /// Panics if the slice is not exactly 20 bytes.
92    pub fn from_slice(bytes: &[u8]) -> Self {
93        SqlAddress(Address::from_slice(bytes))
94    }
95}
96
97impl AsRef<Address> for SqlAddress {
98    /// Returns a reference to the inner Address.
99    fn as_ref(&self) -> &Address {
100        &self.0
101    }
102}
103
104impl Deref for SqlAddress {
105    type Target = Address;
106
107    /// Dereferences to the inner Address, allowing direct access to Address methods.
108    ///
109    /// This enables calling any method available on `alloy::primitives::Address`
110    /// directly on a `SqlAddress` instance.
111    fn deref(&self) -> &Self::Target {
112        &self.0
113    }
114}
115
116impl From<Address> for SqlAddress {
117    /// Creates a SqlAddress from an alloy Address.
118    fn from(address: Address) -> Self {
119        SqlAddress(address)
120    }
121}
122
123impl From<SqlAddress> for Address {
124    /// Extracts the inner Address from a SqlAddress.
125    fn from(sql_address: SqlAddress) -> Self {
126        sql_address.0
127    }
128}
129
130impl FromStr for SqlAddress {
131    type Err = <Address as FromStr>::Err;
132
133    /// Parses a string into a SqlAddress.
134    ///
135    /// Supports various formats:
136    /// - With 0x prefix: "0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d"
137    /// - Without prefix: "742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d"
138    /// - Mixed case (checksummed) and lowercase formats
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        Ok(SqlAddress(s.parse()?))
141    }
142}
143
144impl std::fmt::Display for SqlAddress {
145    /// Formats the address for display using EIP-55 checksum format.
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        self.0.fmt(f)
148    }
149}
150
151impl Default for SqlAddress {
152    fn default() -> Self {
153        SqlAddress::ZERO
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::sqladdress;
161    use alloy::primitives::Address;
162    use std::str::FromStr;
163
164    const TEST_ADDRESS_STR: &str = "0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d";
165    const ZERO_ADDRESS_STR: &str = "0x0000000000000000000000000000000000000000";
166
167    #[test]
168    fn test_sql_address_creation() {
169        // Create from Address
170        let addr = Address::ZERO;
171        let sql_addr = SqlAddress::from(addr);
172        assert_eq!(sql_addr.into_inner(), addr);
173
174        // Create from string
175        let sql_addr = SqlAddress::from_str(ZERO_ADDRESS_STR).unwrap();
176        assert_eq!(sql_addr.into_inner(), Address::ZERO);
177    }
178
179    #[test]
180    fn test_sql_address_conversions() {
181        let original_addr = TEST_ADDRESS_STR.parse::<Address>().unwrap();
182
183        // Address -> SqlAddress -> Address
184        let sql_addr = SqlAddress::from(original_addr);
185        let converted_back: Address = sql_addr.into();
186        assert_eq!(original_addr, converted_back);
187
188        // String -> SqlAddress -> String (Note: alloy Address uses checksum format)
189        let sql_addr = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
190        let result_str = sql_addr.to_string();
191        assert_eq!(result_str.to_lowercase(), TEST_ADDRESS_STR.to_lowercase());
192    }
193
194    #[test]
195    fn test_sql_address_display() {
196        let sql_addr = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
197        let displayed = format!("{}", sql_addr);
198        // alloy Address uses checksum format, so we compare lowercase versions
199        assert_eq!(displayed.to_lowercase(), TEST_ADDRESS_STR.to_lowercase());
200    }
201
202    #[test]
203    fn test_sql_address_deref() {
204        let sql_addr = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
205
206        // Test Deref trait
207        let _checksum = sql_addr.to_checksum(None);
208
209        // Test AsRef trait
210        let addr_ref: &Address = sql_addr.as_ref();
211        assert_eq!(addr_ref, sql_addr.inner());
212    }
213
214    #[test]
215    fn test_sql_address_equality() {
216        let addr1 = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
217        let addr2 = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
218        let addr3 = SqlAddress::from_str(ZERO_ADDRESS_STR).unwrap();
219
220        assert_eq!(addr1, addr2);
221        assert_ne!(addr1, addr3);
222    }
223
224    #[test]
225    fn test_sql_address_debug() {
226        let sql_addr = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
227        let debug_str = format!("{:?}", sql_addr);
228        assert!(debug_str.contains("SqlAddress"));
229    }
230
231    #[test]
232    fn test_invalid_address() {
233        let invalid_addresses = vec![
234            "invalid",
235            "0x123",                                       // Too short
236            "0xgg42d35Cc6635C0532925a3b8D42cC72b5c2A9A1d", // Contains invalid characters
237            "",                                            // Empty string
238        ];
239
240        for invalid_addr in invalid_addresses {
241            assert!(SqlAddress::from_str(invalid_addr).is_err());
242        }
243    }
244
245    #[cfg(feature = "serde")]
246    #[test]
247    fn test_sql_address_serde() {
248        let sql_addr = SqlAddress::from_str(TEST_ADDRESS_STR).unwrap();
249
250        // Serialize
251        let serialized = serde_json::to_string(&sql_addr).unwrap();
252        assert!(serialized.contains(TEST_ADDRESS_STR.to_lowercase().trim_start_matches("0x")));
253
254        // Deserialize
255        let deserialized: SqlAddress = serde_json::from_str(&serialized).unwrap();
256        assert_eq!(sql_addr, deserialized);
257    }
258
259    #[cfg(feature = "serde")]
260    #[test]
261    fn test_sql_address_serde_with_various_formats() {
262        // Test different input formats
263        let test_cases = vec![
264            // Standard format
265            (TEST_ADDRESS_STR, true),
266            // Without 0x prefix
267            ("742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d", true),
268            // All lowercase
269            ("0x742d35cc6635c0532925a3b8d42cc72b5c2a9a1d", true),
270            // All uppercase
271            ("0x742D35CC6635C0532925A3B8D42CC72B5C2A9A1D", true),
272            // Invalid format
273            ("invalid", false),
274        ];
275
276        for (addr_str, should_succeed) in test_cases {
277            let result = SqlAddress::from_str(addr_str);
278            assert_eq!(
279                result.is_ok(),
280                should_succeed,
281                "Failed for address: {}",
282                addr_str
283            );
284
285            if should_succeed {
286                let sql_addr = result.unwrap();
287                let serialized = serde_json::to_string(&sql_addr).unwrap();
288                let deserialized: SqlAddress = serde_json::from_str(&serialized).unwrap();
289                assert_eq!(sql_addr, deserialized);
290            }
291        }
292    }
293
294    #[test]
295    fn test_sqladdress_macro() {
296        // Test address with 0x prefix
297        let addr1 = sqladdress!("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d");
298        let addr_from_str =
299            SqlAddress::from_str("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d").unwrap();
300        assert_eq!(addr1, addr_from_str);
301
302        // Test address without 0x prefix
303        let addr2 = sqladdress!("742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d");
304        assert_eq!(addr2, addr_from_str);
305
306        // Test zero address
307        let zero_addr = sqladdress!("0x0000000000000000000000000000000000000000");
308        let zero_from_str = SqlAddress::from_str(ZERO_ADDRESS_STR).unwrap();
309        assert_eq!(zero_addr, zero_from_str);
310
311        // Test that macro-created addresses work with all methods
312        let addr = sqladdress!("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d");
313        let _inner = addr.inner();
314        let _string = addr.to_string();
315        let _display = format!("{}", addr);
316        let _debug = format!("{:?}", addr);
317    }
318
319    #[test]
320    fn test_sqladdress_macro_compile_time_validation() {
321        // These are validated at compile time
322        let _valid_addresses = [
323            sqladdress!("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d"),
324            sqladdress!("742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d"),
325            sqladdress!("0x0000000000000000000000000000000000000000"),
326            sqladdress!("0xffffffffffffffffffffffffffffffffffffffff"),
327        ];
328
329        // Verify they are all valid
330        for addr in _valid_addresses.iter() {
331            assert_ne!(
332                *addr,
333                SqlAddress::from_str("0x1111111111111111111111111111111111111111").unwrap()
334            );
335        }
336    }
337
338    #[test]
339    fn test_sql_address_zero_constant() {
340        // Test ZERO constant
341        assert_eq!(
342            SqlAddress::ZERO.to_string(),
343            "0x0000000000000000000000000000000000000000"
344        );
345
346        // Verify ZERO constant equals other creation methods
347        let zero_from_str =
348            SqlAddress::from_str("0x0000000000000000000000000000000000000000").unwrap();
349        let zero_from_macro = sqladdress!("0x0000000000000000000000000000000000000000");
350        let zero_from_alloy = SqlAddress::ZERO;
351
352        assert_eq!(SqlAddress::ZERO, zero_from_str);
353        assert_eq!(SqlAddress::ZERO, zero_from_macro);
354        assert_eq!(SqlAddress::ZERO, zero_from_alloy);
355
356        // Verify other properties of ZERO constant
357        assert_eq!(SqlAddress::ZERO.into_inner(), Address::ZERO);
358        assert_eq!(*SqlAddress::ZERO, Address::ZERO);
359
360        // Verify it works in different contexts
361        const ZERO_CONST: SqlAddress = SqlAddress::ZERO;
362        assert_eq!(ZERO_CONST, SqlAddress::ZERO);
363    }
364
365    #[test]
366    fn test_sql_address_hash() {
367        use std::collections::{HashMap, HashSet};
368
369        let addr1 = sqladdress!("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d");
370        let addr2 = sqladdress!("0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d");
371        let addr3 = sqladdress!("0x1234567890123456789012345678901234567890");
372
373        // Test Hash trait - equal addresses should have equal hashes
374        use std::hash::{DefaultHasher, Hash, Hasher};
375
376        let mut hasher1 = DefaultHasher::new();
377        let mut hasher2 = DefaultHasher::new();
378        let mut hasher3 = DefaultHasher::new();
379
380        addr1.hash(&mut hasher1);
381        addr2.hash(&mut hasher2);
382        addr3.hash(&mut hasher3);
383
384        assert_eq!(hasher1.finish(), hasher2.finish());
385        assert_ne!(hasher1.finish(), hasher3.finish());
386
387        // Test usage in HashSet
388        let mut address_set = HashSet::new();
389        address_set.insert(addr1);
390        address_set.insert(addr2); // Should not increase size since addr1 == addr2
391        address_set.insert(addr3);
392
393        assert_eq!(address_set.len(), 2);
394        assert!(address_set.contains(&addr1));
395        assert!(address_set.contains(&addr2));
396        assert!(address_set.contains(&addr3));
397
398        // Test usage in HashMap
399        let mut address_map = HashMap::new();
400        address_map.insert(addr1, "First address");
401        address_map.insert(addr2, "Same address"); // Should overwrite
402        address_map.insert(addr3, "Different address");
403
404        assert_eq!(address_map.len(), 2);
405        assert_eq!(address_map.get(&addr1), Some(&"Same address"));
406        assert_eq!(address_map.get(&addr2), Some(&"Same address"));
407        assert_eq!(address_map.get(&addr3), Some(&"Different address"));
408    }
409
410    #[test]
411    fn test_sql_address_hash_consistency_with_alloy_address() {
412        use std::hash::{DefaultHasher, Hash, Hasher};
413
414        fn calculate_hash<T: Hash>(t: &T) -> u64 {
415            let mut hasher = DefaultHasher::new();
416            t.hash(&mut hasher);
417            hasher.finish()
418        }
419
420        let test_addresses = [
421            "0x742d35Cc6635C0532925a3b8D42cC72b5c2A9A1d",
422            "0x0000000000000000000000000000000000000000",
423            "0xffffffffffffffffffffffffffffffffffffffff",
424            "0x1234567890123456789012345678901234567890",
425        ];
426
427        for addr_str in &test_addresses {
428            let alloy_addr = Address::from_str(addr_str).unwrap();
429            let sql_addr = SqlAddress::from_str(addr_str).unwrap();
430
431            let alloy_hash = calculate_hash(&alloy_addr);
432            let sql_hash = calculate_hash(&sql_addr);
433
434            // Critical: SqlAddress must produce the same hash as the underlying Address
435            assert_eq!(
436                alloy_hash, sql_hash,
437                "Hash mismatch for address {}: alloy={}, sql={}",
438                addr_str, alloy_hash, sql_hash
439            );
440        }
441
442        // Test conversion consistency
443        let original = Address::from_str(TEST_ADDRESS_STR).unwrap();
444        let sql_wrapped = SqlAddress::from(original);
445        let converted_back: Address = sql_wrapped.into();
446
447        assert_eq!(calculate_hash(&original), calculate_hash(&sql_wrapped));
448        assert_eq!(calculate_hash(&original), calculate_hash(&converted_back));
449        assert_eq!(
450            calculate_hash(&sql_wrapped),
451            calculate_hash(&converted_back)
452        );
453
454        // Test zero address consistency
455        assert_eq!(
456            calculate_hash(&Address::ZERO),
457            calculate_hash(&SqlAddress::ZERO)
458        );
459    }
460}