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)]
30 struct SafeTransaction {
31 address to;
33 uint8 operation;
35 bytes data;
37 uint256 value;
39 }
40
41 #[derive(Debug, PartialEq, Eq)]
42 struct SafeTransactionArgs {
43 address from_address;
44 uint256 nonce;
45 uint256 chain_id;
46 SafeTransaction[] transactions;
47 }
48
49 #[derive(Debug, PartialEq, Eq)]
54 struct SafeTx {
55 address to;
57 uint256 value;
59 bytes data;
61 uint8 operation;
63 uint256 safeTxGas;
65 uint256 baseGas;
67 uint256 gasPrice;
69 address gasToken;
71 address refundReceiver;
73 uint256 nonce;
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TransactionRequest {
89 #[serde(rename = "type")]
90 pub type_: String,
91 pub from: String,
92 pub to: String,
93 #[serde(rename = "proxyWallet")]
94 pub proxy_wallet: String,
95 pub data: String,
96 pub signature: String,
97 }
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct SubmitResponse {
106 #[serde(rename = "transactionID")]
107 pub transaction_id: String,
108 pub state: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct RelayerTransaction {
118 #[serde(rename = "transactionID")]
119 pub transaction_id: String,
120 pub transaction_hash: Option<String>,
121 pub from: Option<String>,
122 pub to: Option<String>,
123 pub proxy_address: Option<String>,
124 pub data: Option<String>,
125 pub nonce: Option<String>,
126 pub value: Option<String>,
127 pub signature: Option<String>,
128 pub state: String,
130 #[serde(rename = "type")]
132 pub kind: Option<String>,
133 pub owner: Option<String>,
134 pub metadata: Option<String>,
135 pub created_at: Option<String>,
136 pub updated_at: Option<String>,
137}
138
139pub fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
141where
142 D: serde::Deserializer<'de>,
143{
144 use serde::de;
145
146 struct NonceVisitor;
147
148 impl<'de> de::Visitor<'de> for NonceVisitor {
149 type Value = u64;
150
151 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
152 formatter.write_str("a u64 or string representing a u64")
153 }
154
155 fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
156 Ok(v)
157 }
158
159 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
160 where
161 E: de::Error,
162 {
163 v.parse().map_err(de::Error::custom)
164 }
165 }
166
167 deserializer.deserialize_any(NonceVisitor)
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct NonceResponse {
173 #[serde(deserialize_with = "deserialize_nonce")]
174 pub nonce: u64,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct RelayerApiKey {
183 pub api_key: String,
185 pub address: String,
187 pub created_at: String,
189 pub updated_at: String,
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
200 fn test_nonce_from_integer() {
201 let json = r#"{"nonce": 42}"#;
202 let resp: NonceResponse = serde_json::from_str(json).unwrap();
203 assert_eq!(resp.nonce, 42);
204 }
205
206 #[test]
207 fn test_nonce_from_string() {
208 let json = r#"{"nonce": "123"}"#;
209 let resp: NonceResponse = serde_json::from_str(json).unwrap();
210 assert_eq!(resp.nonce, 123);
211 }
212
213 #[test]
214 fn test_nonce_from_zero_integer() {
215 let json = r#"{"nonce": 0}"#;
216 let resp: NonceResponse = serde_json::from_str(json).unwrap();
217 assert_eq!(resp.nonce, 0);
218 }
219
220 #[test]
221 fn test_nonce_from_zero_string() {
222 let json = r#"{"nonce": "0"}"#;
223 let resp: NonceResponse = serde_json::from_str(json).unwrap();
224 assert_eq!(resp.nonce, 0);
225 }
226
227 #[test]
228 fn test_nonce_from_large_integer() {
229 let json = r#"{"nonce": 18446744073709551615}"#;
230 let resp: NonceResponse = serde_json::from_str(json).unwrap();
231 assert_eq!(resp.nonce, u64::MAX);
232 }
233
234 #[test]
235 fn test_nonce_from_large_string() {
236 let json = r#"{"nonce": "18446744073709551615"}"#;
237 let resp: NonceResponse = serde_json::from_str(json).unwrap();
238 assert_eq!(resp.nonce, u64::MAX);
239 }
240
241 #[test]
242 fn test_nonce_from_non_numeric_string_fails() {
243 let json = r#"{"nonce": "abc"}"#;
244 let result = serde_json::from_str::<NonceResponse>(json);
245 assert!(result.is_err());
246 }
247
248 #[test]
249 fn test_nonce_from_empty_string_fails() {
250 let json = r#"{"nonce": ""}"#;
251 let result = serde_json::from_str::<NonceResponse>(json);
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn test_nonce_from_null_fails() {
257 let json = r#"{"nonce": null}"#;
258 let result = serde_json::from_str::<NonceResponse>(json);
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_nonce_missing_field_fails() {
264 let json = r#"{}"#;
265 let result = serde_json::from_str::<NonceResponse>(json);
266 assert!(result.is_err());
267 }
268
269 #[test]
272 fn test_wallet_type_as_str() {
273 assert_eq!(WalletType::Safe.as_str(), "SAFE");
274 assert_eq!(WalletType::Proxy.as_str(), "PROXY");
275 }
276
277 #[test]
278 fn test_wallet_type_default_is_safe() {
279 assert_eq!(WalletType::default(), WalletType::Safe);
280 }
281
282 #[test]
285 fn test_transaction_request_serialization() {
286 let tx = TransactionRequest {
287 type_: "SAFE".to_string(),
288 from: "0xabc".to_string(),
289 to: "0xdef".to_string(),
290 proxy_wallet: "0x123".to_string(),
291 data: "0xdeadbeef".to_string(),
292 signature: "0xsig".to_string(),
293 };
294 let json = serde_json::to_value(&tx).unwrap();
295 assert_eq!(json["type"], "SAFE");
296 assert_eq!(json["from"], "0xabc");
297 assert_eq!(json["proxyWallet"], "0x123");
298 }
299
300 #[test]
301 fn test_transaction_request_deserialization() {
302 let json = r#"{
303 "type": "PROXY",
304 "from": "0xabc",
305 "to": "0xdef",
306 "proxyWallet": "0x123",
307 "data": "0xdeadbeef",
308 "signature": "0xsig"
309 }"#;
310 let tx: TransactionRequest = serde_json::from_str(json).unwrap();
311 assert_eq!(tx.type_, "PROXY");
312 assert_eq!(tx.proxy_wallet, "0x123");
313 }
314
315 #[test]
318 fn test_submit_response_new_state() {
319 let json = r#"{
320 "transactionID": "tx-123",
321 "state": "STATE_NEW"
322 }"#;
323 let resp: SubmitResponse = serde_json::from_str(json).unwrap();
324 assert_eq!(resp.transaction_id, "tx-123");
325 assert_eq!(resp.state, "STATE_NEW");
326 }
327
328 #[test]
331 fn test_relayer_transaction_full() {
332 let json = r#"{
333 "transactionID": "0190b317-a1d3-7bec-9b91-eeb6dcd3a620",
334 "transactionHash": "0x38cbfbeae8fffa4e2b187ee5978d3ee9cafc53af0363ed90a35b7ea9016535d8",
335 "from": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
336 "to": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
337 "proxyAddress": "0x6d8c4e9adf5748af82dabe2c6225207770d6b4fa",
338 "data": "0xdeadbeef",
339 "nonce": "60",
340 "value": "",
341 "signature": "0xabc",
342 "state": "STATE_CONFIRMED",
343 "type": "SAFE",
344 "owner": "0x6e0c80c90ea6c15917308f820eac91ce2724b5b5",
345 "metadata": "",
346 "createdAt": "2024-07-14T21:13:08.819782Z",
347 "updatedAt": "2024-07-14T21:13:46.576639Z"
348 }"#;
349 let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
350 assert_eq!(resp.transaction_id, "0190b317-a1d3-7bec-9b91-eeb6dcd3a620");
351 assert_eq!(resp.state, "STATE_CONFIRMED");
352 assert_eq!(resp.kind.as_deref(), Some("SAFE"));
353 assert_eq!(resp.nonce.as_deref(), Some("60"));
354 assert!(resp.transaction_hash.is_some());
355 assert!(resp.created_at.is_some());
356 }
357
358 #[test]
359 fn test_relayer_transaction_pending_minimal() {
360 let json = r#"{
361 "transactionID": "tx-new",
362 "state": "STATE_NEW"
363 }"#;
364 let resp: RelayerTransaction = serde_json::from_str(json).unwrap();
365 assert_eq!(resp.transaction_id, "tx-new");
366 assert_eq!(resp.state, "STATE_NEW");
367 assert!(resp.transaction_hash.is_none());
368 assert!(resp.kind.is_none());
369 }
370
371 #[test]
374 fn test_relayer_api_key_deserializes_openapi_example() {
375 let json = r#"{
378 "apiKey": "01967c03-b8c8-7000-8f68-8b8eaec6fd3d",
379 "address": "0xabc...",
380 "createdAt": "2026-02-24T18:20:11.237485Z",
381 "updatedAt": "2026-02-24T18:20:11.237485Z"
382 }"#;
383 let key: RelayerApiKey = serde_json::from_str(json).unwrap();
384 assert_eq!(key.api_key, "01967c03-b8c8-7000-8f68-8b8eaec6fd3d");
385 assert_eq!(key.address, "0xabc...");
386 assert_eq!(key.created_at, "2026-02-24T18:20:11.237485Z");
387 assert_eq!(key.updated_at, "2026-02-24T18:20:11.237485Z");
388 }
389
390 #[test]
391 fn test_relayer_api_key_roundtrip_preserves_camel_case() {
392 let key = RelayerApiKey {
393 api_key: "abc".to_string(),
394 address: "0xdef".to_string(),
395 created_at: "2026-01-01T00:00:00Z".to_string(),
396 updated_at: "2026-01-02T00:00:00Z".to_string(),
397 };
398 let serialized = serde_json::to_value(&key).unwrap();
399 assert_eq!(serialized["apiKey"], "abc");
400 assert_eq!(serialized["address"], "0xdef");
401 assert_eq!(serialized["createdAt"], "2026-01-01T00:00:00Z");
402 assert_eq!(serialized["updatedAt"], "2026-01-02T00:00:00Z");
403
404 let round: RelayerApiKey = serde_json::from_value(serialized).unwrap();
405 assert_eq!(round.api_key, "abc");
406 assert_eq!(round.updated_at, "2026-01-02T00:00:00Z");
407 }
408
409 #[test]
410 fn test_relayer_api_key_list_deserializes() {
411 let json = r#"[
412 {
413 "apiKey": "key-1",
414 "address": "0xa1",
415 "createdAt": "2026-01-01T00:00:00Z",
416 "updatedAt": "2026-01-01T00:00:00Z"
417 },
418 {
419 "apiKey": "key-2",
420 "address": "0xa2",
421 "createdAt": "2026-01-02T00:00:00Z",
422 "updatedAt": "2026-01-02T00:00:00Z"
423 }
424 ]"#;
425 let keys: Vec<RelayerApiKey> = serde_json::from_str(json).unwrap();
426 assert_eq!(keys.len(), 2);
427 assert_eq!(keys[0].api_key, "key-1");
428 assert_eq!(keys[1].api_key, "key-2");
429 }
430}