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
61pub 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)), };
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 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 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 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 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" },
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 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", "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 } },
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 });
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}