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