hypercall_sdk_types/
wallet_address.rs1use std::{cmp::Ordering, fmt, str::FromStr};
4
5use alloy::hex::FromHexError;
6use alloy::primitives::Address;
7use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
8
9#[cfg(feature = "database")]
11use diesel::{
12 backend::Backend,
13 deserialize::{self, FromSql as DieselFromSql},
14 pg::Pg,
15 serialize::{self, Output, ToSql as DieselToSql},
16 sql_types::Bytea,
17 AsExpression, FromSqlRow,
18};
19
20#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Default)]
30#[cfg_attr(feature = "database", derive(AsExpression, FromSqlRow))]
31#[cfg_attr(feature = "database", diesel(sql_type = Bytea))]
32#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
33#[cfg_attr(feature = "utoipa", schema(value_type = String, example = "0x1234567890abcdef1234567890abcdef12345678"))]
34pub struct WalletAddress(pub Address);
35
36impl WalletAddress {
37 pub fn inner(&self) -> Address {
39 self.0
40 }
41
42 pub fn as_hex(&self) -> String {
44 format!("{:#x}", self.0)
45 }
46
47 pub fn as_bytes(&self) -> &[u8; 20] {
49 self.0.as_ref()
50 }
51}
52
53impl fmt::Display for WalletAddress {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{:#x}", self.0)
56 }
57}
58
59impl From<Address> for WalletAddress {
60 fn from(addr: Address) -> Self {
61 WalletAddress(addr)
62 }
63}
64
65impl From<[u8; 20]> for WalletAddress {
66 fn from(bytes: [u8; 20]) -> Self {
67 WalletAddress(Address::from(bytes))
68 }
69}
70
71impl FromStr for WalletAddress {
72 type Err = FromHexError;
73
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
75 let addr = Address::from_str(s)?;
76 Ok(WalletAddress(addr))
77 }
78}
79
80impl Serialize for WalletAddress {
81 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
82 serializer.serialize_str(&self.as_hex())
83 }
84}
85
86impl<'de> Deserialize<'de> for WalletAddress {
87 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
88 let s = String::deserialize(deserializer)?;
89 WalletAddress::from_str(&s).map_err(D::Error::custom)
90 }
91}
92
93impl PartialOrd for WalletAddress {
94 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95 Some(self.cmp(other))
96 }
97}
98
99impl Ord for WalletAddress {
100 fn cmp(&self, other: &Self) -> Ordering {
101 self.0.as_slice().cmp(other.0.as_slice())
102 }
103}
104
105#[cfg(feature = "database")]
108impl DieselToSql<Bytea, Pg> for WalletAddress {
109 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
110 use std::io::Write;
111 out.write_all(self.0.as_slice())?;
112 Ok(diesel::serialize::IsNull::No)
113 }
114}
115
116#[cfg(feature = "database")]
117impl DieselFromSql<Bytea, Pg> for WalletAddress {
118 fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> deserialize::Result<Self> {
119 let slice = bytes.as_bytes();
120 if slice.len() != 20 {
121 return Err(format!(
122 "Invalid wallet address length: expected 20 bytes, got {}",
123 slice.len()
124 )
125 .into());
126 }
127 let mut arr = [0u8; 20];
128 arr.copy_from_slice(slice);
129 Ok(WalletAddress::from(arr))
130 }
131}
132
133#[cfg(feature = "database")]
136impl sqlx::Type<sqlx::Postgres> for WalletAddress {
137 fn type_info() -> sqlx::postgres::PgTypeInfo {
138 <Vec<u8> as sqlx::Type<sqlx::Postgres>>::type_info()
139 }
140}
141
142#[cfg(feature = "database")]
143impl<'r> sqlx::Decode<'r, sqlx::Postgres> for WalletAddress {
144 fn decode(
145 value: sqlx::postgres::PgValueRef<'r>,
146 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync + 'static>> {
147 let bytes = <Vec<u8> as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
148 if bytes.len() != 20 {
149 return Err(format!(
150 "Invalid wallet address length: expected 20 bytes, got {}",
151 bytes.len()
152 )
153 .into());
154 }
155 let mut arr = [0u8; 20];
156 arr.copy_from_slice(&bytes);
157 Ok(WalletAddress::from(arr))
158 }
159}
160
161#[cfg(feature = "database")]
162impl sqlx::Encode<'_, sqlx::Postgres> for WalletAddress {
163 fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
164 <&[u8] as sqlx::Encode<sqlx::Postgres>>::encode(self.0.as_slice(), buf)
165 }
166}
167
168#[cfg(any(test, feature = "test-utils"))]
185pub fn test_wallet(id: u8) -> WalletAddress {
186 let mut bytes = [0u8; 20];
187 bytes[19] = id;
188 WalletAddress::from(bytes)
189}
190
191#[cfg(any(test, feature = "test-utils"))]
202#[macro_export]
203macro_rules! test_wallet {
204 ($id:expr) => {{
205 let mut bytes = [0u8; 20];
206 bytes[19] = $id;
207 $crate::wallet_address::WalletAddress::from(alloy::primitives::Address::from(bytes))
208 }};
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_wallet_address_roundtrip() {
217 let addr_str = "0x1234567890abcdef1234567890abcdef12345678";
218 let wallet = WalletAddress::from_str(addr_str).unwrap();
219 assert_eq!(wallet.as_hex().to_lowercase(), addr_str.to_lowercase());
220 }
221
222 #[test]
223 fn test_wallet_address_ordering() {
224 let a = WalletAddress::from_str("0x0000000000000000000000000000000000000001").unwrap();
225 let b = WalletAddress::from_str("0x0000000000000000000000000000000000000002").unwrap();
226 assert!(a < b);
227 }
228
229 #[test]
230 fn test_serde_roundtrip() {
231 let addr_str = "0x1234567890abcdef1234567890abcdef12345678";
232 let wallet = WalletAddress::from_str(addr_str).unwrap();
233 let json = sonic_rs::to_string(&wallet).unwrap();
234 let parsed: WalletAddress = sonic_rs::from_str(&json).unwrap();
235 assert_eq!(wallet, parsed);
236 }
237
238 #[test]
239 fn test_test_wallet_helper() {
240 let w1 = test_wallet(1);
241 let w2 = test_wallet(2);
242 let w1_again = test_wallet(1);
243
244 assert_ne!(w1, w2);
245 assert_eq!(w1, w1_again);
246 assert!(w1 < w2);
247
248 assert_eq!(w1.as_hex(), "0x0000000000000000000000000000000000000001");
250 assert_eq!(w2.as_hex(), "0x0000000000000000000000000000000000000002");
251 }
252}