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 the relayer after submitting a transaction.
71///
72/// The `transaction_hash` is `None` until the transaction is mined on-chain.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct RelayerTransactionResponse {
75    #[serde(rename = "transactionID")]
76    pub transaction_id: String,
77    #[serde(rename = "transactionHash")]
78    pub transaction_hash: Option<String>,
79}
80
81/// Deserialize a nonce that may be represented as either a JSON number or string.
82pub fn deserialize_nonce<'de, D>(deserializer: D) -> Result<u64, D::Error>
83where
84    D: serde::Deserializer<'de>,
85{
86    use serde::de;
87
88    struct NonceVisitor;
89
90    impl<'de> de::Visitor<'de> for NonceVisitor {
91        type Value = u64;
92
93        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
94            formatter.write_str("a u64 or string representing a u64")
95        }
96
97        fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> {
98            Ok(v)
99        }
100
101        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
102        where
103            E: de::Error,
104        {
105            v.parse().map_err(de::Error::custom)
106        }
107    }
108
109    deserializer.deserialize_any(NonceVisitor)
110}
111
112/// Response from the relayer's nonce endpoint.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct NonceResponse {
115    #[serde(deserialize_with = "deserialize_nonce")]
116    pub nonce: u64,
117}
118
119/// Response from the relayer's transaction status endpoint.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct TransactionStatusResponse {
122    pub state: String,
123    #[serde(rename = "transactionHash")]
124    pub transaction_hash: Option<String>,
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    // ── deserialize_nonce ───────────────────────────────────────
132
133    #[test]
134    fn test_nonce_from_integer() {
135        let json = r#"{"nonce": 42}"#;
136        let resp: NonceResponse = serde_json::from_str(json).unwrap();
137        assert_eq!(resp.nonce, 42);
138    }
139
140    #[test]
141    fn test_nonce_from_string() {
142        let json = r#"{"nonce": "123"}"#;
143        let resp: NonceResponse = serde_json::from_str(json).unwrap();
144        assert_eq!(resp.nonce, 123);
145    }
146
147    #[test]
148    fn test_nonce_from_zero_integer() {
149        let json = r#"{"nonce": 0}"#;
150        let resp: NonceResponse = serde_json::from_str(json).unwrap();
151        assert_eq!(resp.nonce, 0);
152    }
153
154    #[test]
155    fn test_nonce_from_zero_string() {
156        let json = r#"{"nonce": "0"}"#;
157        let resp: NonceResponse = serde_json::from_str(json).unwrap();
158        assert_eq!(resp.nonce, 0);
159    }
160
161    #[test]
162    fn test_nonce_from_large_integer() {
163        let json = r#"{"nonce": 18446744073709551615}"#;
164        let resp: NonceResponse = serde_json::from_str(json).unwrap();
165        assert_eq!(resp.nonce, u64::MAX);
166    }
167
168    #[test]
169    fn test_nonce_from_large_string() {
170        let json = r#"{"nonce": "18446744073709551615"}"#;
171        let resp: NonceResponse = serde_json::from_str(json).unwrap();
172        assert_eq!(resp.nonce, u64::MAX);
173    }
174
175    #[test]
176    fn test_nonce_from_non_numeric_string_fails() {
177        let json = r#"{"nonce": "abc"}"#;
178        let result = serde_json::from_str::<NonceResponse>(json);
179        assert!(result.is_err());
180    }
181
182    #[test]
183    fn test_nonce_from_empty_string_fails() {
184        let json = r#"{"nonce": ""}"#;
185        let result = serde_json::from_str::<NonceResponse>(json);
186        assert!(result.is_err());
187    }
188
189    #[test]
190    fn test_nonce_from_null_fails() {
191        let json = r#"{"nonce": null}"#;
192        let result = serde_json::from_str::<NonceResponse>(json);
193        assert!(result.is_err());
194    }
195
196    #[test]
197    fn test_nonce_missing_field_fails() {
198        let json = r#"{}"#;
199        let result = serde_json::from_str::<NonceResponse>(json);
200        assert!(result.is_err());
201    }
202
203    // ── WalletType ──────────────────────────────────────────────
204
205    #[test]
206    fn test_wallet_type_as_str() {
207        assert_eq!(WalletType::Safe.as_str(), "SAFE");
208        assert_eq!(WalletType::Proxy.as_str(), "PROXY");
209    }
210
211    #[test]
212    fn test_wallet_type_default_is_safe() {
213        assert_eq!(WalletType::default(), WalletType::Safe);
214    }
215
216    // ── TransactionRequest serde ────────────────────────────────
217
218    #[test]
219    fn test_transaction_request_serialization() {
220        let tx = TransactionRequest {
221            type_: "SAFE".to_string(),
222            from: "0xabc".to_string(),
223            to: "0xdef".to_string(),
224            proxy_wallet: "0x123".to_string(),
225            data: "0xdeadbeef".to_string(),
226            signature: "0xsig".to_string(),
227        };
228        let json = serde_json::to_value(&tx).unwrap();
229        assert_eq!(json["type"], "SAFE");
230        assert_eq!(json["from"], "0xabc");
231        assert_eq!(json["proxyWallet"], "0x123");
232    }
233
234    #[test]
235    fn test_transaction_request_deserialization() {
236        let json = r#"{
237            "type": "PROXY",
238            "from": "0xabc",
239            "to": "0xdef",
240            "proxyWallet": "0x123",
241            "data": "0xdeadbeef",
242            "signature": "0xsig"
243        }"#;
244        let tx: TransactionRequest = serde_json::from_str(json).unwrap();
245        assert_eq!(tx.type_, "PROXY");
246        assert_eq!(tx.proxy_wallet, "0x123");
247    }
248
249    // ── RelayerTransactionResponse serde ────────────────────────
250
251    #[test]
252    fn test_relayer_response_with_hash() {
253        let json = r#"{
254            "transactionID": "tx-123",
255            "transactionHash": "0xabcdef"
256        }"#;
257        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
258        assert_eq!(resp.transaction_id, "tx-123");
259        assert_eq!(resp.transaction_hash.as_deref(), Some("0xabcdef"));
260    }
261
262    #[test]
263    fn test_relayer_response_without_hash() {
264        let json = r#"{
265            "transactionID": "tx-456",
266            "transactionHash": null
267        }"#;
268        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
269        assert_eq!(resp.transaction_id, "tx-456");
270        assert!(resp.transaction_hash.is_none());
271    }
272
273    #[test]
274    fn test_relayer_response_missing_hash_field() {
275        let json = r#"{"transactionID": "tx-789"}"#;
276        let resp: RelayerTransactionResponse = serde_json::from_str(json).unwrap();
277        assert_eq!(resp.transaction_id, "tx-789");
278        assert!(resp.transaction_hash.is_none());
279    }
280
281    // ── TransactionStatusResponse serde ─────────────────────────
282
283    #[test]
284    fn test_transaction_status_response() {
285        let json = r#"{
286            "state": "CONFIRMED",
287            "transactionHash": "0xabc123"
288        }"#;
289        let resp: TransactionStatusResponse = serde_json::from_str(json).unwrap();
290        assert_eq!(resp.state, "CONFIRMED");
291        assert_eq!(resp.transaction_hash.as_deref(), Some("0xabc123"));
292    }
293
294    #[test]
295    fn test_transaction_status_pending() {
296        let json = r#"{
297            "state": "PENDING",
298            "transactionHash": null
299        }"#;
300        let resp: TransactionStatusResponse = serde_json::from_str(json).unwrap();
301        assert_eq!(resp.state, "PENDING");
302        assert!(resp.transaction_hash.is_none());
303    }
304}