1use alloy::sol;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
6pub enum WalletType {
7 #[default]
9 Safe,
10 Proxy,
12}
13
14impl WalletType {
15 pub fn as_str(&self) -> &'static str {
17 match self {
18 WalletType::Safe => "SAFE",
19 WalletType::Proxy => "PROXY",
20 }
21 }
22}
23
24sol! {
25 #[derive(Debug, PartialEq, Eq)]
26 struct SafeTransaction {
27 address to;
28 uint8 operation;
29 bytes data;
30 uint256 value;
31 }
32
33 #[derive(Debug, PartialEq, Eq)]
34 struct SafeTransactionArgs {
35 address from_address;
36 uint256 nonce;
37 uint256 chain_id;
38 SafeTransaction[] transactions;
39 }
40
41 #[derive(Debug, PartialEq, Eq)]
42 struct SafeTx {
43 address to;
44 uint256 value;
45 bytes data;
46 uint8 operation;
47 uint256 safeTxGas;
48 uint256 baseGas;
49 uint256 gasPrice;
50 address gasToken;
51 address refundReceiver;
52 uint256 nonce;
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TransactionRequest {
59 #[serde(rename = "type")]
60 pub type_: String,
61 pub from: String,
62 pub to: String,
63 #[serde(rename = "proxyWallet")]
64 pub proxy_wallet: String,
65 pub data: String,
66 pub signature: String,
67 }
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SubmitResponse {
76 #[serde(rename = "transactionID")]
77 pub transaction_id: String,
78 pub state: String,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct RelayerTransaction {
88 #[serde(rename = "transactionID")]
89 pub transaction_id: String,
90 pub transaction_hash: Option<String>,
91 pub from: Option<String>,
92 pub to: Option<String>,
93 pub proxy_address: Option<String>,
94 pub data: Option<String>,
95 pub nonce: Option<String>,
96 pub value: Option<String>,
97 pub signature: Option<String>,
98 pub state: String,
100 #[serde(rename = "type")]
102 pub kind: Option<String>,
103 pub owner: Option<String>,
104 pub metadata: Option<String>,
105 pub created_at: Option<String>,
106 pub updated_at: Option<String>,
107}
108
109pub fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
111where
112 D: serde::Deserializer<'de>,
113{
114 use serde::de;
115
116 struct NonceVisitor;
117
118 impl<'de> de::Visitor<'de> for NonceVisitor {
119 type Value = u64;
120
121 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
122 formatter.write_str("a u64 or string representing a u64")
123 }
124
125 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
126 Ok(v)
127 }
128
129 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
130 where
131 E: de::Error,
132 {
133 v.parse().map_err(de::Error::custom)
134 }
135 }
136
137 deserializer.deserialize_any(NonceVisitor)
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct NonceResponse {
143 #[serde(deserialize_with = "deserialize_nonce")]
144 pub nonce: u64,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct RelayerApiKey {
153 pub api_key: String,
155 pub address: String,
157 pub created_at: String,
159 pub updated_at: String,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
170 fn test_nonce_from_integer() {
171 let json = r#"{"nonce": 42}"#;
172 let resp: NonceResponse = serde_json::from_str(json).unwrap();
173 assert_eq!(resp.nonce, 42);
174 }
175
176 #[test]
177 fn test_nonce_from_string() {
178 let json = r#"{"nonce": "123"}"#;
179 let resp: NonceResponse = serde_json::from_str(json).unwrap();
180 assert_eq!(resp.nonce, 123);
181 }
182
183 #[test]
184 fn test_nonce_from_zero_integer() {
185 let json = r#"{"nonce": 0}"#;
186 let resp: NonceResponse = serde_json::from_str(json).unwrap();
187 assert_eq!(resp.nonce, 0);
188 }
189
190 #[test]
191 fn test_nonce_from_zero_string() {
192 let json = r#"{"nonce": "0"}"#;
193 let resp: NonceResponse = serde_json::from_str(json).unwrap();
194 assert_eq!(resp.nonce, 0);
195 }
196
197 #[test]
198 fn test_nonce_from_large_integer() {
199 let json = r#"{"nonce": 18446744073709551615}"#;
200 let resp: NonceResponse = serde_json::from_str(json).unwrap();
201 assert_eq!(resp.nonce, u64::MAX);
202 }
203
204 #[test]
205 fn test_nonce_from_large_string() {
206 let json = r#"{"nonce": "18446744073709551615"}"#;
207 let resp: NonceResponse = serde_json::from_str(json).unwrap();
208 assert_eq!(resp.nonce, u64::MAX);
209 }
210
211 #[test]
212 fn test_nonce_from_non_numeric_string_fails() {
213 let json = r#"{"nonce": "abc"}"#;
214 let result = serde_json::from_str::<NonceResponse>(json);
215 assert!(result.is_err());
216 }
217
218 #[test]
219 fn test_nonce_from_empty_string_fails() {
220 let json = r#"{"nonce": ""}"#;
221 let result = serde_json::from_str::<NonceResponse>(json);
222 assert!(result.is_err());
223 }
224
225 #[test]
226 fn test_nonce_from_null_fails() {
227 let json = r#"{"nonce": null}"#;
228 let result = serde_json::from_str::<NonceResponse>(json);
229 assert!(result.is_err());
230 }
231
232 #[test]
233 fn test_nonce_missing_field_fails() {
234 let json = r#"{}"#;
235 let result = serde_json::from_str::<NonceResponse>(json);
236 assert!(result.is_err());
237 }
238
239 #[test]
242 fn test_wallet_type_as_str() {
243 assert_eq!(WalletType::Safe.as_str(), "SAFE");
244 assert_eq!(WalletType::Proxy.as_str(), "PROXY");
245 }
246
247 #[test]
248 fn test_wallet_type_default_is_safe() {
249 assert_eq!(WalletType::default(), WalletType::Safe);
250 }
251
252 #[test]
255 fn test_transaction_request_serialization() {
256 let tx = TransactionRequest {
257 type_: "SAFE".to_string(),
258 from: "0xabc".to_string(),
259 to: "0xdef".to_string(),
260 proxy_wallet: "0x123".to_string(),
261 data: "0xdeadbeef".to_string(),
262 signature: "0xsig".to_string(),
263 };
264 let json = serde_json::to_value(&tx).unwrap();
265 assert_eq!(json["type"], "SAFE");
266 assert_eq!(json["from"], "0xabc");
267 assert_eq!(json["proxyWallet"], "0x123");
268 }
269
270 #[test]
271 fn test_transaction_request_deserialization() {
272 let json = r#"{
273 "type": "PROXY",
274 "from": "0xabc",
275 "to": "0xdef",
276 "proxyWallet": "0x123",
277 "data": "0xdeadbeef",
278 "signature": "0xsig"
279 }"#;
280 let tx: TransactionRequest = serde_json::from_str(json).unwrap();
281 assert_eq!(tx.type_, "PROXY");
282 assert_eq!(tx.proxy_wallet, "0x123");
283 }
284
285 #[test]
288 fn test_submit_response_new_state() {
289 let json = r#"{
290 "transactionID": "tx-123",
291 "state": "STATE_NEW"
292 }"#;
293 let resp: SubmitResponse = serde_json::from_str(json).unwrap();
294 assert_eq!(resp.transaction_id, "tx-123");
295 assert_eq!(resp.state, "STATE_NEW");
296 }
297
298 #[test]
301 fn test_relayer_transaction_full() {
302 let json = r#"{
303 "transactionID": "0190b317-a1d3-7bec-9b91-eeb6dcd3a620",
304 "transactionHash": "0x38cbfbeae8fffa4e2b187ee5978d3ee9cafc53af0363ed90a35b7ea9016535d8",
305 "from": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
306 "to": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
307 "proxyAddress": "0x6d8c4e9adf5748af82dabe2c6225207770d6b4fa",
308 "data": "0xdeadbeef",
309 "nonce": "60",
310 "value": "",
311 "signature": "0xabc",
312 "state": "STATE_CONFIRMED",
313 "type": "SAFE",
314 "owner": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
315 "metadata": "",
316 "createdAt": "2024-07-14T21:13:08.819782Z",
317 "updatedAt": "2024-07-14T21:13:46.576639Z"
318 }"#;
319 let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
320 assert_eq!(resp.transaction_id, "0190b317-a1d3-7bec-9b91-eeb6dcd3a620");
321 assert_eq!(resp.state, "STATE_CONFIRMED");
322 assert_eq!(resp.kind.as_deref(), Some("SAFE"));
323 assert_eq!(resp.nonce.as_deref(), Some("60"));
324 assert!(resp.transaction_hash.is_some());
325 assert!(resp.created_at.is_some());
326 }
327
328 #[test]
329 fn test_relayer_transaction_pending_minimal() {
330 let json = r#"{
331 "transactionID": "tx-new",
332 "state": "STATE_NEW"
333 }"#;
334 let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
335 assert_eq!(resp.transaction_id, "tx-new");
336 assert_eq!(resp.state, "STATE_NEW");
337 assert!(resp.transaction_hash.is_none());
338 assert!(resp.kind.is_none());
339 }
340
341 #[test]
344 fn test_relayer_api_key_deserializes_openapi_example() {
345 let json = r#"{
348 "apiKey": "01967c03-b8c8-7000-8f68-8b8eaec6fd3d",
349 "address": "0xabc...",
350 "createdAt": "2026-02-24T18:20:11.237485Z",
351 "updatedAt": "2026-02-24T18:20:11.237485Z"
352 }"#;
353 let key: RelayerApiKey = serde_json::from_str(json).unwrap();
354 assert_eq!(key.api_key, "01967c03-b8c8-7000-8f68-8b8eaec6fd3d");
355 assert_eq!(key.address, "0xabc...");
356 assert_eq!(key.created_at, "2026-02-24T18:20:11.237485Z");
357 assert_eq!(key.updated_at, "2026-02-24T18:20:11.237485Z");
358 }
359
360 #[test]
361 fn test_relayer_api_key_roundtrip_preserves_camel_case() {
362 let key = RelayerApiKey {
363 api_key: "abc".to_string(),
364 address: "0xdef".to_string(),
365 created_at: "2026-01-01T00:00:00Z".to_string(),
366 updated_at: "2026-01-02T00:00:00Z".to_string(),
367 };
368 let serialized = serde_json::to_value(&key).unwrap();
369 assert_eq!(serialized["apiKey"], "abc");
370 assert_eq!(serialized["address"], "0xdef");
371 assert_eq!(serialized["createdAt"], "2026-01-01T00:00:00Z");
372 assert_eq!(serialized["updatedAt"], "2026-01-02T00:00:00Z");
373
374 let round: RelayerApiKey = serde_json::from_value(serialized).unwrap();
375 assert_eq!(round.api_key, "abc");
376 assert_eq!(round.updated_at, "2026-01-02T00:00:00Z");
377 }
378
379 #[test]
380 fn test_relayer_api_key_list_deserializes() {
381 let json = r#"[
382 {
383 "apiKey": "key-1",
384 "address": "0xa1",
385 "createdAt": "2026-01-01T00:00:00Z",
386 "updatedAt": "2026-01-01T00:00:00Z"
387 },
388 {
389 "apiKey": "key-2",
390 "address": "0xa2",
391 "createdAt": "2026-01-02T00:00:00Z",
392 "updatedAt": "2026-01-02T00:00:00Z"
393 }
394 ]"#;
395 let keys: Vec<RelayerApiKey> = serde_json::from_str(json).unwrap();
396 assert_eq!(keys.len(), 2);
397 assert_eq!(keys[0].api_key, "key-1");
398 assert_eq!(keys[1].api_key, "key-2");
399 }
400}