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    pub fn as_str(&self) -> &'static str {
16        match self {
17            WalletType::Safe => "SAFE",
18            WalletType::Proxy => "PROXY",
19        }
20    }
21}
22
23sol! {
24    #[derive(Debug, PartialEq, Eq)]
25    struct SafeTransaction {
26        address to;
27        uint8 operation;
28        bytes data;
29        uint256 value;
30    }
31
32    #[derive(Debug, PartialEq, Eq)]
33    struct SafeTransactionArgs {
34        address from_address;
35        uint256 nonce;
36        uint256 chain_id;
37        SafeTransaction[] transactions;
38    }
39
40    #[derive(Debug, PartialEq, Eq)]
41    struct SafeTx {
42        address to;
43        uint256 value;
44        bytes data;
45        uint8 operation;
46        uint256 safeTxGas;
47        uint256 baseGas;
48        uint256 gasPrice;
49        address gasToken;
50        address refundReceiver;
51        uint256 nonce;
52    }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TransactionRequest {
57    #[serde(rename = "type")]
58    pub type_: String,
59    pub from: String,
60    pub to: String,
61    #[serde(rename = "proxyWallet")]
62    pub proxy_wallet: String,
63    pub data: String,
64    pub signature: String,
65    // Add signature params if needed
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RelayerTransactionResponse {
70    #[serde(rename = "transactionID")]
71    pub transaction_id: String,
72    #[serde(rename = "transactionHash")]
73    pub transaction_hash: Option<String>,
74}
75
76pub fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
77where
78    D: serde::Deserializer<'de>,
79{
80    use serde::de;
81
82    struct NonceVisitor;
83
84    impl<'de> de::Visitor<'de> for NonceVisitor {
85        type Value = u64;
86
87        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
88            formatter.write_str("a u64 or string representing a u64")
89        }
90
91        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
92            Ok(v)
93        }
94
95        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
96        where
97            E: de::Error,
98        {
99            v.parse().map_err(de::Error::custom)
100        }
101    }
102
103    deserializer.deserialize_any(NonceVisitor)
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct NonceResponse {
108    #[serde(deserialize_with = "deserialize_nonce")]
109    pub nonce: u64,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct TransactionStatusResponse {
114    pub state: String,
115    #[serde(rename = "transactionHash")]
116    pub transaction_hash: Option<String>,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    // ── deserialize_nonce ───────────────────────────────────────
124
125    #[test]
126    fn test_nonce_from_integer() {
127        let json = r#"{"nonce": 42}"#;
128        let resp: NonceResponse = serde_json::from_str(json).unwrap();
129        assert_eq!(resp.nonce, 42);
130    }
131
132    #[test]
133    fn test_nonce_from_string() {
134        let json = r#"{"nonce": "123"}"#;
135        let resp: NonceResponse = serde_json::from_str(json).unwrap();
136        assert_eq!(resp.nonce, 123);
137    }
138
139    #[test]
140    fn test_nonce_from_zero_integer() {
141        let json = r#"{"nonce": 0}"#;
142        let resp: NonceResponse = serde_json::from_str(json).unwrap();
143        assert_eq!(resp.nonce, 0);
144    }
145
146    #[test]
147    fn test_nonce_from_zero_string() {
148        let json = r#"{"nonce": "0"}"#;
149        let resp: NonceResponse = serde_json::from_str(json).unwrap();
150        assert_eq!(resp.nonce, 0);
151    }
152
153    #[test]
154    fn test_nonce_from_large_integer() {
155        let json = r#"{"nonce": 18446744073709551615}"#;
156        let resp: NonceResponse = serde_json::from_str(json).unwrap();
157        assert_eq!(resp.nonce, u64::MAX);
158    }
159
160    #[test]
161    fn test_nonce_from_large_string() {
162        let json = r#"{"nonce": "18446744073709551615"}"#;
163        let resp: NonceResponse = serde_json::from_str(json).unwrap();
164        assert_eq!(resp.nonce, u64::MAX);
165    }
166
167    #[test]
168    fn test_nonce_from_non_numeric_string_fails() {
169        let json = r#"{"nonce": "abc"}"#;
170        let result = serde_json::from_str::<NonceResponse>(json);
171        assert!(result.is_err());
172    }
173
174    #[test]
175    fn test_nonce_from_empty_string_fails() {
176        let json = r#"{"nonce": ""}"#;
177        let result = serde_json::from_str::<NonceResponse>(json);
178        assert!(result.is_err());
179    }
180
181    #[test]
182    fn test_nonce_from_null_fails() {
183        let json = r#"{"nonce": null}"#;
184        let result = serde_json::from_str::<NonceResponse>(json);
185        assert!(result.is_err());
186    }
187
188    #[test]
189    fn test_nonce_missing_field_fails() {
190        let json = r#"{}"#;
191        let result = serde_json::from_str::<NonceResponse>(json);
192        assert!(result.is_err());
193    }
194
195    // ── WalletType ──────────────────────────────────────────────
196
197    #[test]
198    fn test_wallet_type_as_str() {
199        assert_eq!(WalletType::Safe.as_str(), "SAFE");
200        assert_eq!(WalletType::Proxy.as_str(), "PROXY");
201    }
202
203    #[test]
204    fn test_wallet_type_default_is_safe() {
205        assert_eq!(WalletType::default(), WalletType::Safe);
206    }
207
208    // ── TransactionRequest serde ────────────────────────────────
209
210    #[test]
211    fn test_transaction_request_serialization() {
212        let tx = TransactionRequest {
213            type_: "SAFE".to_string(),
214            from: "0xabc".to_string(),
215            to: "0xdef".to_string(),
216            proxy_wallet: "0x123".to_string(),
217            data: "0xdeadbeef".to_string(),
218            signature: "0xsig".to_string(),
219        };
220        let json = serde_json::to_value(&tx).unwrap();
221        assert_eq!(json["type"], "SAFE");
222        assert_eq!(json["from"], "0xabc");
223        assert_eq!(json["proxyWallet"], "0x123");
224    }
225
226    #[test]
227    fn test_transaction_request_deserialization() {
228        let json = r#"{
229            "type": "PROXY",
230            "from": "0xabc",
231            "to": "0xdef",
232            "proxyWallet": "0x123",
233            "data": "0xdeadbeef",
234            "signature": "0xsig"
235        }"#;
236        let tx: TransactionRequest = serde_json::from_str(json).unwrap();
237        assert_eq!(tx.type_, "PROXY");
238        assert_eq!(tx.proxy_wallet, "0x123");
239    }
240
241    // ── RelayerTransactionResponse serde ────────────────────────
242
243    #[test]
244    fn test_relayer_response_with_hash() {
245        let json = r#"{
246            "transactionID": "tx-123",
247            "transactionHash": "0xabcdef"
248        }"#;
249        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
250        assert_eq!(resp.transaction_id, "tx-123");
251        assert_eq!(resp.transaction_hash.as_deref(), Some("0xabcdef"));
252    }
253
254    #[test]
255    fn test_relayer_response_without_hash() {
256        let json = r#"{
257            "transactionID": "tx-456",
258            "transactionHash": null
259        }"#;
260        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
261        assert_eq!(resp.transaction_id, "tx-456");
262        assert!(resp.transaction_hash.is_none());
263    }
264
265    #[test]
266    fn test_relayer_response_missing_hash_field() {
267        let json = r#"{"transactionID": "tx-789"}"#;
268        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
269        assert_eq!(resp.transaction_id, "tx-789");
270        assert!(resp.transaction_hash.is_none());
271    }
272
273    // ── TransactionStatusResponse serde ─────────────────────────
274
275    #[test]
276    fn test_transaction_status_response() {
277        let json = r#"{
278            "state": "CONFIRMED",
279            "transactionHash": "0xabc123"
280        }"#;
281        let resp: TransactionStatusResponse = serde_json::from_str(json).unwrap();
282        assert_eq!(resp.state, "CONFIRMED");
283        assert_eq!(resp.transaction_hash.as_deref(), Some("0xabc123"));
284    }
285
286    #[test]
287    fn test_transaction_status_pending() {
288        let json = r#"{
289            "state": "PENDING",
290            "transactionHash": null
291        }"#;
292        let resp: TransactionStatusResponse = serde_json::from_str(json).unwrap();
293        assert_eq!(resp.state, "PENDING");
294        assert!(resp.transaction_hash.is_none());
295    }
296}