openzeppelin_relayer/models/rpc/
mod.rs

1use serde::{Deserialize, Serialize};
2use utoipa::ToSchema;
3
4mod json_rpc;
5pub use json_rpc::*;
6
7mod solana;
8pub use solana::*;
9
10mod stellar;
11pub use stellar::*;
12
13mod evm;
14pub use evm::*;
15
16mod error;
17pub use error::*;
18
19use crate::models::ApiError;
20
21#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
22#[serde(untagged)]
23pub enum NetworkRpcResult {
24    Solana(SolanaRpcResult),
25    Stellar(StellarRpcResult),
26    Evm(EvmRpcResult),
27}
28
29#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq)]
30#[serde(untagged)]
31#[serde(deny_unknown_fields)]
32pub enum NetworkRpcRequest {
33    Solana(SolanaRpcRequest),
34    Stellar(StellarRpcRequest),
35    Evm(EvmRpcRequest),
36}
37
38/// Converts a raw JSON-RPC request to the internal NetworkRpcRequest format.
39///
40/// This function parses a raw JSON-RPC request and converts it into the appropriate
41/// internal request type based on the specified network type. It handles the
42/// JSON-RPC 2.0 specification including proper ID handling (String, Number, or Null).
43///
44/// # Arguments
45///
46/// * `request` - A raw JSON value containing the JSON-RPC request
47/// * `network_type` - The type of network (EVM, Solana, or Stellar) to parse the request for
48///
49/// # Returns
50///
51/// Returns a `Result` containing the parsed `JsonRpcRequest` on success, or an `ApiError` on failure.
52///
53/// # Examples
54///
55/// ```rust,ignore
56/// use serde_json::json;
57/// use crate::models::{convert_to_internal_rpc_request, NetworkType};
58///
59/// let request = json!({
60///     "jsonrpc": "2.0",
61///     "method": "eth_getBalance",
62///     "params": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "latest"],
63///     "id": 1
64/// });
65///
66/// let result = convert_to_internal_rpc_request(request, &NetworkType::Evm)?;
67/// ```
68pub fn convert_to_internal_rpc_request(
69    request: serde_json::Value,
70    network_type: &crate::models::NetworkType,
71) -> Result<JsonRpcRequest<NetworkRpcRequest>, ApiError> {
72    let jsonrpc = request
73        .get("jsonrpc")
74        .and_then(|v| v.as_str())
75        .unwrap_or("2.0")
76        .to_string();
77
78    let id = match request.get("id") {
79        Some(id_value) => match id_value {
80            serde_json::Value::String(s) => Some(JsonRpcId::String(s.clone())),
81            serde_json::Value::Number(n) => {
82                n.as_i64().map(JsonRpcId::Number).map(Some).ok_or_else(|| {
83                    ApiError::BadRequest(
84                        "Invalid 'id' field: must be a string, integer, or null".to_string(),
85                    )
86                })?
87            }
88            serde_json::Value::Null => None,
89            _ => {
90                return Err(ApiError::BadRequest(
91                    "Invalid 'id' field: must be a string, integer, or null".to_string(),
92                ))
93            }
94        },
95        None => Some(JsonRpcId::Number(1)), // Default ID when none provided
96    };
97
98    let method = request
99        .get("method")
100        .and_then(|v| v.as_str())
101        .ok_or_else(|| ApiError::BadRequest("Missing 'method' field".to_string()))?;
102
103    if method.is_empty() {
104        return Err(ApiError::BadRequest("Missing 'method' field".to_string()));
105    }
106
107    match network_type {
108        crate::models::NetworkType::Evm => {
109            let params = request
110                .get("params")
111                .cloned()
112                .unwrap_or(serde_json::Value::Null);
113
114            Ok(JsonRpcRequest {
115                jsonrpc,
116                params: NetworkRpcRequest::Evm(crate::models::EvmRpcRequest::RawRpcRequest {
117                    method: method.to_string(),
118                    params,
119                }),
120                id,
121            })
122        }
123        crate::models::NetworkType::Solana => {
124            let params = request
125                .get("params")
126                .cloned()
127                .unwrap_or(serde_json::Value::Null);
128
129            // Decide whether to parse into a typed SolanaRpcRequest or treat as raw.
130            // If the method is unknown (Generic), map to RawRpcRequest preserving the real method name.
131            match crate::models::SolanaRpcMethod::from_string(method) {
132                Some(crate::models::SolanaRpcMethod::Generic(_)) | None => Ok(JsonRpcRequest {
133                    jsonrpc,
134                    params: NetworkRpcRequest::Solana(
135                        crate::models::SolanaRpcRequest::RawRpcRequest {
136                            method: method.to_string(),
137                            params,
138                        },
139                    ),
140                    id,
141                }),
142                // Known methods: construct a minimal JSON with method+params and deserialize into the typed enum
143                Some(_) => {
144                    let json = serde_json::json!({"method": method, "params": params});
145                    let solana_request: crate::models::SolanaRpcRequest =
146                        serde_json::from_value(json).map_err(|e| {
147                            ApiError::BadRequest(format!("Invalid Solana RPC request: {e}"))
148                        })?;
149
150                    Ok(JsonRpcRequest {
151                        jsonrpc,
152                        params: NetworkRpcRequest::Solana(solana_request),
153                        id,
154                    })
155                }
156            }
157        }
158        crate::models::NetworkType::Stellar => {
159            let params = request
160                .get("params")
161                .cloned()
162                .unwrap_or(serde_json::Value::Null);
163
164            Ok(JsonRpcRequest {
165                jsonrpc,
166                params: NetworkRpcRequest::Stellar(
167                    crate::models::StellarRpcRequest::RawRpcRequest {
168                        method: method.to_string(),
169                        params,
170                    },
171                ),
172                id,
173            })
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::models::{EvmRpcRequest, NetworkType, SolanaRpcRequest, StellarRpcRequest};
182    use serde_json::json;
183
184    #[test]
185    fn test_convert_evm_standard_request() {
186        let request = json!({
187            "jsonrpc": "2.0",
188            "method": "eth_getBalance",
189            "params": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "latest"],
190            "id": 1
191        });
192
193        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
194
195        assert_eq!(result.jsonrpc, "2.0");
196        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
197
198        match result.params {
199            NetworkRpcRequest::Evm(EvmRpcRequest::RawRpcRequest { method, params }) => {
200                assert_eq!(method, "eth_getBalance");
201                assert_eq!(params[0], "0x742d35Cc6634C0532925a3b844Bc454e4438f44e");
202                assert_eq!(params[1], "latest");
203            }
204            _ => unreachable!("Expected EVM RawRpcRequest"),
205        }
206    }
207
208    #[test]
209    fn test_convert_evm_missing_method_field() {
210        let request = json!({
211            "jsonrpc": "2.0",
212            "params": ["0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "latest"],
213            "id": 1
214        });
215
216        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
217        assert!(result.is_err());
218    }
219
220    #[test]
221    fn test_convert_evm_with_defaults() {
222        let request = json!({
223            "method": "eth_blockNumber"
224        });
225
226        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
227
228        assert_eq!(result.jsonrpc, "2.0");
229        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
230
231        match result.params {
232            NetworkRpcRequest::Evm(EvmRpcRequest::RawRpcRequest { method, params }) => {
233                assert_eq!(method, "eth_blockNumber");
234                assert_eq!(params, serde_json::Value::Null);
235            }
236            _ => unreachable!("Expected EVM RawRpcRequest"),
237        }
238    }
239
240    #[test]
241    fn test_convert_evm_with_custom_jsonrpc_and_id() {
242        let request = json!({
243            "jsonrpc": "1.0",
244            "method": "eth_chainId",
245            "params": [],
246            "id": 42
247        });
248
249        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
250
251        assert_eq!(result.jsonrpc, "1.0");
252        assert_eq!(result.id, Some(JsonRpcId::Number(42)));
253    }
254
255    #[test]
256    fn test_convert_evm_with_string_id() {
257        let request = json!({
258            "jsonrpc": "2.0",
259            "method": "eth_chainId",
260            "params": [],
261            "id": "test-id"
262        });
263
264        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
265
266        // String ID should be preserved
267        assert_eq!(result.id, Some(JsonRpcId::String("test-id".to_string())));
268    }
269
270    #[test]
271    fn test_convert_evm_with_null_id() {
272        let request = json!({
273            "jsonrpc": "2.0",
274            "method": "eth_chainId",
275            "params": [],
276            "id": null
277        });
278
279        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
280
281        // Null ID should be preserved
282        assert_eq!(result.id, None);
283    }
284
285    #[test]
286    fn test_convert_evm_with_object_params() {
287        let request = json!({
288            "jsonrpc": "2.0",
289            "method": "eth_getTransactionByHash",
290            "params": {
291                "hash": "0x123",
292                "full": true
293            },
294            "id": 1
295        });
296
297        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
298
299        match result.params {
300            NetworkRpcRequest::Evm(EvmRpcRequest::RawRpcRequest { method, params }) => {
301                assert_eq!(method, "eth_getTransactionByHash");
302                assert_eq!(params["hash"], "0x123");
303                assert_eq!(params["full"], true);
304            }
305            _ => unreachable!("Expected EVM RawRpcRequest"),
306        }
307    }
308
309    #[test]
310    fn test_convert_solana_fee_estimate_request() {
311        let request = json!({
312            "jsonrpc": "2.0",
313            "method": "feeEstimate",
314            "params": {
315                "transaction": "base64encodedtransaction",
316                "fee_token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // noboost
317            },
318            "id": 1
319        });
320
321        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana).unwrap();
322
323        assert_eq!(result.jsonrpc, "2.0");
324        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
325
326        match result.params {
327            NetworkRpcRequest::Solana(solana_request) => {
328                // Just verify we got a valid Solana request variant
329                match solana_request {
330                    SolanaRpcRequest::FeeEstimate(_) => {}
331                    _ => unreachable!("Expected FeeEstimate variant"),
332                }
333            }
334            _ => unreachable!("Expected Solana request"),
335        }
336    }
337
338    #[test]
339    fn test_convert_solana_get_supported_tokens_request() {
340        let request = json!({
341            "jsonrpc": "2.0",
342            "method": "getSupportedTokens",
343            "params": {},
344            "id": 2
345        });
346
347        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana).unwrap();
348
349        assert_eq!(result.jsonrpc, "2.0");
350        assert_eq!(result.id, Some(JsonRpcId::Number(2)));
351
352        match result.params {
353            NetworkRpcRequest::Solana(solana_request) => match solana_request {
354                SolanaRpcRequest::GetSupportedTokens(_) => {}
355                _ => unreachable!("Expected GetSupportedTokens variant"),
356            },
357            _ => unreachable!("Expected Solana request"),
358        }
359    }
360
361    #[test]
362    fn test_convert_solana_transfer_transaction_request() {
363        let request = json!({
364            "jsonrpc": "2.0",
365            "method": "transferTransaction",
366            "params": {
367                "amount": 1000000,
368                "token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // noboost
369                "source": "source_address",
370                "destination": "destination_address"
371            },
372            "id": 3
373        });
374
375        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana).unwrap();
376
377        match result.params {
378            NetworkRpcRequest::Solana(solana_request) => match solana_request {
379                SolanaRpcRequest::TransferTransaction(_) => {}
380                _ => unreachable!("Expected TransferTransaction variant"),
381            },
382            _ => unreachable!("Expected Solana request"),
383        }
384    }
385
386    #[test]
387    fn test_convert_solana_invalid_request() {
388        let request = json!({
389            "jsonrpc": "2.0",
390            "method": "",
391            "params": {},
392            "id": 1
393        });
394
395        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana);
396        assert!(result.is_err());
397    }
398
399    #[test]
400    fn test_convert_solana_malformed_request() {
401        let request = json!({
402            "jsonrpc": "2.0",
403            "method": "feeEstimate",
404            "params": {
405                "invalid_field": "value"
406            },
407            "id": 1
408        });
409
410        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana);
411        assert!(result.is_err());
412    }
413
414    #[test]
415    fn test_convert_solana_with_defaults() {
416        let request = json!({
417            "method": "getSupportedTokens",
418            "params": {}
419        });
420
421        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana).unwrap();
422
423        assert_eq!(result.jsonrpc, "2.0");
424        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
425    }
426
427    #[test]
428    fn test_convert_stellar_request() {
429        let request = json!({
430            "jsonrpc": "2.0",
431            "method": "test",
432            "params": "test_params",
433            "id": 1
434        });
435
436        let result = convert_to_internal_rpc_request(request, &NetworkType::Stellar).unwrap();
437
438        assert_eq!(result.jsonrpc, "2.0");
439        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
440
441        match result.params {
442            NetworkRpcRequest::Stellar(stellar_request) => match stellar_request {
443                StellarRpcRequest::RawRpcRequest { method: _, params } => {
444                    assert_eq!(params, serde_json::Value::String("test_params".to_string()));
445                }
446            },
447            _ => unreachable!("Expected Stellar request"),
448        }
449    }
450
451    #[test]
452    fn test_convert_stellar_invalid_request() {
453        let request = json!({
454            "jsonrpc": "2.0",
455            "method": "",
456            "params": {},
457            "id": 1
458        });
459
460        let result = convert_to_internal_rpc_request(request, &NetworkType::Stellar);
461        assert!(result.is_err());
462    }
463
464    #[test]
465    fn test_convert_stellar_with_defaults() {
466        let request = json!({
467            "method": "test",
468            "params": "default_test"
469        });
470
471        let result = convert_to_internal_rpc_request(request, &NetworkType::Stellar).unwrap();
472
473        assert_eq!(result.jsonrpc, "2.0");
474        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
475    }
476
477    #[test]
478    fn test_convert_empty_request() {
479        let request = json!({});
480
481        let result_evm = convert_to_internal_rpc_request(request.clone(), &NetworkType::Evm);
482        assert!(result_evm.is_err());
483
484        let result_solana = convert_to_internal_rpc_request(request.clone(), &NetworkType::Solana);
485        assert!(result_solana.is_err());
486
487        let result_stellar = convert_to_internal_rpc_request(request, &NetworkType::Stellar);
488        assert!(result_stellar.is_err());
489    }
490
491    #[test]
492    fn test_convert_null_request() {
493        let request = serde_json::Value::Null;
494
495        let result_evm = convert_to_internal_rpc_request(request.clone(), &NetworkType::Evm);
496        assert!(result_evm.is_err());
497
498        let result_solana = convert_to_internal_rpc_request(request.clone(), &NetworkType::Solana);
499        assert!(result_solana.is_err());
500
501        let result_stellar = convert_to_internal_rpc_request(request, &NetworkType::Stellar);
502        assert!(result_stellar.is_err());
503    }
504
505    #[test]
506    fn test_convert_array_request() {
507        let request = json!([1, 2, 3]);
508
509        let result_evm = convert_to_internal_rpc_request(request.clone(), &NetworkType::Evm);
510        assert!(result_evm.is_err());
511
512        let result_solana = convert_to_internal_rpc_request(request.clone(), &NetworkType::Solana);
513        assert!(result_solana.is_err());
514
515        let result_stellar = convert_to_internal_rpc_request(request, &NetworkType::Stellar);
516        assert!(result_stellar.is_err());
517    }
518
519    #[test]
520    fn test_convert_solana_unknown_method_maps_to_raw_request() {
521        let request = serde_json::json!({
522            "jsonrpc": "2.0",
523            "id": 1,
524            "method": "getLatestBlockhash",
525            "params": [ { "commitment": "finalized" } ]
526        });
527
528        let result = convert_to_internal_rpc_request(request, &NetworkType::Solana).unwrap();
529
530        assert_eq!(result.jsonrpc, "2.0");
531        assert_eq!(result.id, Some(JsonRpcId::Number(1)));
532
533        match result.params {
534            NetworkRpcRequest::Solana(solana_request) => match solana_request {
535                crate::models::SolanaRpcRequest::RawRpcRequest { method, params } => {
536                    assert_eq!(method, "getLatestBlockhash");
537                    assert_eq!(params[0]["commitment"], "finalized");
538                }
539                _ => unreachable!("Expected RawRpcRequest variant for unknown method"),
540            },
541            _ => unreachable!("Expected Solana request"),
542        }
543    }
544
545    #[test]
546    fn test_convert_evm_non_string_method() {
547        let request = json!({
548            "jsonrpc": "2.0",
549            "method": 123,
550            "params": [],
551            "id": 1
552        });
553
554        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
555        assert!(result.is_err());
556    }
557
558    #[test]
559    fn test_convert_with_large_id() {
560        let request = json!({
561            "jsonrpc": "2.0",
562            "method": "eth_chainId",
563            "params": [],
564            "id": 18446744073709551615u64  // u64::MAX
565        });
566
567        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
568        assert!(result.is_err());
569
570        if let Err(crate::models::ApiError::BadRequest(msg)) = result {
571            assert!(msg.contains("Invalid 'id' field: must be a string, integer, or null"));
572        } else {
573            panic!("Expected BadRequest error");
574        }
575    }
576
577    #[test]
578    fn test_convert_with_zero_id() {
579        let request = json!({
580            "jsonrpc": "2.0",
581            "method": "eth_chainId",
582            "params": [],
583            "id": 0
584        });
585
586        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
587        assert_eq!(result.id, Some(JsonRpcId::Number(0)));
588    }
589
590    #[test]
591    fn test_convert_evm_empty_method() {
592        let request = json!({
593            "jsonrpc": "2.0",
594            "method": "",
595            "params": [],
596            "id": 1
597        });
598
599        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
600
601        assert!(result.is_err());
602    }
603
604    #[test]
605    fn test_convert_evm_very_long_method() {
606        let long_method = "a".repeat(1000);
607        let request = json!({
608            "jsonrpc": "2.0",
609            "method": long_method,
610            "params": [],
611            "id": 1
612        });
613
614        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm).unwrap();
615
616        match result.params {
617            NetworkRpcRequest::Evm(EvmRpcRequest::RawRpcRequest { method, params: _ }) => {
618                assert_eq!(method, long_method);
619            }
620            _ => unreachable!("Expected EVM RawRpcRequest"),
621        }
622    }
623
624    #[test]
625    fn test_convert_with_invalid_id_type_boolean() {
626        let request = json!({
627            "jsonrpc": "2.0",
628            "method": "eth_chainId",
629            "params": [],
630            "id": true
631        });
632
633        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
634        assert!(result.is_err());
635
636        if let Err(crate::models::ApiError::BadRequest(msg)) = result {
637            assert!(msg.contains("Invalid 'id' field: must be a string, integer, or null"));
638        } else {
639            panic!("Expected BadRequest error");
640        }
641    }
642
643    #[test]
644    fn test_convert_with_invalid_id_type_array() {
645        let request = json!({
646            "jsonrpc": "2.0",
647            "method": "eth_chainId",
648            "params": [],
649            "id": [1, 2, 3]
650        });
651
652        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
653        assert!(result.is_err());
654
655        if let Err(crate::models::ApiError::BadRequest(msg)) = result {
656            assert!(msg.contains("Invalid 'id' field: must be a string, integer, or null"));
657        } else {
658            panic!("Expected BadRequest error");
659        }
660    }
661
662    #[test]
663    fn test_convert_with_invalid_id_type_object() {
664        let request = json!({
665            "jsonrpc": "2.0",
666            "method": "eth_chainId",
667            "params": [],
668            "id": {"nested": "object"}
669        });
670
671        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
672        assert!(result.is_err());
673
674        if let Err(crate::models::ApiError::BadRequest(msg)) = result {
675            assert!(msg.contains("Invalid 'id' field: must be a string, integer, or null"));
676        } else {
677            panic!("Expected BadRequest error");
678        }
679    }
680
681    #[test]
682    fn test_convert_with_fractional_id() {
683        let request = json!({
684            "jsonrpc": "2.0",
685            "method": "eth_chainId",
686            "params": [],
687            "id": 42.5
688        });
689
690        let result = convert_to_internal_rpc_request(request, &NetworkType::Evm);
691        assert!(result.is_err());
692
693        if let Err(crate::models::ApiError::BadRequest(msg)) = result {
694            assert!(msg.contains("Invalid 'id' field: must be a string, integer, or null"));
695        } else {
696            panic!("Expected BadRequest error");
697        }
698    }
699}