ethereum_mysql/
sql_fixed_bytes.rs

1pub use alloy::primitives::FixedBytes;
2use std::ops::Deref;
3use std::str::FromStr;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8/// A wrapper around `FixedBytes` that provides a SQL-compatible type for fixed-size byte arrays.
9#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct SqlFixedBytes<const BYTES: usize>(FixedBytes<BYTES>);
12/// A type alias for a 32-byte fixed-size byte array, commonly used for hashes.
13pub type SqlHash = SqlFixedBytes<32>;
14/// A type alias for a 32-byte fixed-size byte array, commonly used for topic hashes.
15pub type SqlTopicHash = SqlFixedBytes<32>;
16
17impl<const BYTES: usize> SqlFixedBytes<BYTES> {
18    /// Creates a new `SqlFixedBytes` from a `[u8; BYTES]`.
19    pub fn new(bytes: [u8; BYTES]) -> Self {
20        SqlFixedBytes(FixedBytes::new(bytes))
21    }
22
23    /// Returns a reference to the inner `FixedBytes<BYTES>`.
24    pub fn inner(&self) -> &FixedBytes<BYTES> {
25        &self.0
26    }
27
28    /// Creates a new `SqlFixedBytes` initialized to zero.
29    pub const ZERO: Self = SqlFixedBytes(FixedBytes::ZERO);
30
31    /// Creates a new `SqlFixedBytes` from a `FixedBytes<BYTES>`.
32    pub const fn from_bytes(bytes: FixedBytes<BYTES>) -> Self {
33        SqlFixedBytes(bytes)
34    }
35
36    /// Attempts to interpret the fixed bytes as an Ethereum address (last 20 bytes).
37    /// Returns None if the length is not 32 or the prefix is not zeroed.
38    pub fn to_address(&self) -> Option<crate::SqlAddress> {
39        if BYTES == 32 {
40            let bytes = self.0.as_slice();
41            // Ethereum address is the last 20 bytes, prefix 12 bytes must be zero
42            if bytes[..12].iter().all(|&b| b == 0) {
43                let mut addr = [0u8; 20];
44                addr.copy_from_slice(&bytes[12..]);
45                return Some(crate::SqlAddress::new(addr));
46            }
47        }
48        None
49    }
50
51    /// Interprets the fixed bytes as a U256 (no check, always possible for 32 bytes).
52    pub fn to_u256(&self) -> crate::SqlU256 {
53        use crate::SqlU256;
54        use alloy::primitives::U256;
55        if BYTES == 32 {
56            let mut arr = [0u8; 32];
57            arr.copy_from_slice(self.0.as_slice());
58            SqlU256::from(U256::from_be_bytes(arr))
59        } else {
60            // For non-32 bytes, fallback to zero
61            SqlU256::ZERO
62        }
63    }
64}
65
66impl<const BYTES: usize> AsRef<FixedBytes<BYTES>> for SqlFixedBytes<BYTES> {
67    fn as_ref(&self) -> &FixedBytes<BYTES> {
68        &self.0
69    }
70}
71
72impl<const BYTES: usize> Deref for SqlFixedBytes<BYTES> {
73    type Target = FixedBytes<BYTES>;
74
75    fn deref(&self) -> &Self::Target {
76        &self.0
77    }
78}
79
80impl<const BYTES: usize> From<FixedBytes<BYTES>> for SqlFixedBytes<BYTES> {
81    fn from(bytes: FixedBytes<BYTES>) -> Self {
82        SqlFixedBytes(bytes)
83    }
84}
85
86impl<const BYTES: usize> From<SqlFixedBytes<BYTES>> for FixedBytes<BYTES> {
87    fn from(sql_bytes: SqlFixedBytes<BYTES>) -> Self {
88        sql_bytes.0
89    }
90}
91
92impl<const BYTES: usize> FromStr for SqlFixedBytes<BYTES> {
93    type Err = <FixedBytes<BYTES> as FromStr>::Err;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        FixedBytes::<BYTES>::from_str(s).map(SqlFixedBytes)
97    }
98}
99
100impl<const BYTES: usize> std::fmt::Display for SqlFixedBytes<BYTES> {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        self.0.fmt(f)
103    }
104}
105
106impl<const BYTES: usize> Default for SqlFixedBytes<BYTES> {
107    fn default() -> Self {
108        Self::ZERO
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use alloy::primitives::FixedBytes;
116    use std::str::FromStr;
117
118    #[test]
119    fn test_from_str_and_display() {
120        let hex = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
121        let val = SqlFixedBytes::<32>::from_str(hex).unwrap();
122        assert_eq!(val.to_string(), hex.to_lowercase());
123    }
124
125    #[test]
126    fn test_zero() {
127        let zero = SqlFixedBytes::<32>::ZERO;
128        assert_eq!(zero.inner().as_slice(), &[0u8; 32]);
129    }
130
131    #[test]
132    fn test_as_ref_and_deref() {
133        let hex = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
134        let val = SqlFixedBytes::<32>::from_str(hex).unwrap();
135        let as_ref: &FixedBytes<32> = val.as_ref();
136        let deref: &FixedBytes<32> = &val;
137        assert_eq!(as_ref, deref);
138    }
139
140    #[cfg(feature = "serde")]
141    #[test]
142    fn test_serde() {
143        let hex = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
144        let val = SqlFixedBytes::<32>::from_str(hex).unwrap();
145        let json = serde_json::to_string(&val).unwrap();
146        let de: SqlFixedBytes<32> = serde_json::from_str(&json).unwrap();
147        assert_eq!(val, de);
148    }
149
150    #[test]
151    fn test_fixed_bytes_5() {
152        let hex = "0x68656c6c6f"; // "hello" in hex
153        let val = SqlFixedBytes::<5>::from_str(hex).unwrap();
154        assert_eq!(val.inner().as_slice(), b"hello");
155        assert_eq!(val.to_string(), hex);
156    }
157
158    #[test]
159    fn test_fixed_bytes_1() {
160        let hex = "0x01";
161        let val = SqlFixedBytes::<1>::from_str(hex).unwrap();
162        assert_eq!(val.inner().as_slice(), &[1u8]);
163        assert_eq!(val.to_string(), hex);
164    }
165
166    #[test]
167    fn test_fixed_bytes_0() {
168        let hex = "0x";
169        let val = SqlFixedBytes::<0>::from_str(hex).unwrap();
170        assert_eq!(val.inner().as_slice(), &[] as &[u8]);
171        assert_eq!(val.to_string(), hex);
172    }
173}