Skip to main content

polyoxide_relay/
types.rs

1use alloy::sol;
2use serde::{Deserialize, Serialize};
3
4/// Wallet type for the relayer API
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
6pub enum WalletType {
7    /// Safe wallet - requires explicit deployment before first transaction
8    #[default]
9    Safe,
10    /// Proxy wallet - auto-deploys on first transaction (Magic Link users)
11    Proxy,
12}
13
14impl WalletType {
15    /// Returns the API string representation ("SAFE" or "PROXY").
16    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/// Serializable transaction submission payload sent to the relayer.
57#[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    // Add signature params if needed
68}
69
70/// Response from `POST /submit` after a transaction submission.
71///
72/// The OpenAPI spec guarantees only `transactionID` and `state`; the onchain
73/// transaction hash must be fetched later via `GET /transaction`.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SubmitResponse {
76    #[serde(rename = "transactionID")]
77    pub transaction_id: String,
78    /// Current state of the transaction (e.g. `STATE_NEW`).
79    pub state: String,
80}
81
82/// Full relayer transaction record returned by `GET /transaction`.
83///
84/// Mirrors the OpenAPI `RelayerTransaction` schema.
85#[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    /// Current state (e.g. `STATE_NEW`, `STATE_MINED`, `STATE_CONFIRMED`, ...).
99    pub state: String,
100    /// Transaction type (`SAFE` or `PROXY`).
101    #[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
109/// Deserialize a nonce that may be represented as either a JSON number or string.
110pub 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/// Response from the relayer's nonce endpoint.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct NonceResponse {
143    #[serde(deserialize_with = "deserialize_nonce")]
144    pub nonce: u64,
145}
146
147/// A relayer API key record returned by `GET /relayer/api/keys`.
148///
149/// Mirrors the OpenAPI `RelayerApiKey` schema.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct RelayerApiKey {
153    /// The relayer API key identifier (UUID).
154    pub api_key: String,
155    /// The on-chain address that owns this key.
156    pub address: String,
157    /// RFC3339 timestamp when the key was created.
158    pub created_at: String,
159    /// RFC3339 timestamp when the key was last updated.
160    pub updated_at: String,
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    // ── deserialize_nonce ───────────────────────────────────────
168
169    #[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    // ── WalletType ──────────────────────────────────────────────
240
241    #[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    // ── TransactionRequest serde ────────────────────────────────
253
254    #[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    // ── SubmitResponse serde ────────────────────────────────────
286
287    #[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    // ── RelayerTransaction serde ────────────────────────────────
299
300    #[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    // ── RelayerApiKey serde ─────────────────────────────────────
342
343    #[test]
344    fn test_relayer_api_key_deserializes_openapi_example() {
345        // Example lifted from docs/specs/relay/openapi.yaml for
346        // `/relayer/api/keys` response schema.
347        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}