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