starknet_devnet_server/api/models/
json_rpc_request.rs

1use enum_helper_macros::{AllVariantsSerdeRenames, VariantName};
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5use starknet_types::messaging::{MessageToL1, MessageToL2};
6use starknet_types::rpc::estimate_message_fee::EstimateMessageFeeRequest;
7use starknet_types::rpc::gas_modification::GasModificationRequest;
8use tracing::error;
9
10use crate::api::account_helpers::{BalanceQuery, PredeployedAccountsQuery};
11use crate::api::error::StrictRpcResult;
12use crate::api::models::{
13    AbortingBlocks, AcceptOnL1Request, AccountAddressInput, BlockAndClassHashInput,
14    BlockAndContractAddressInput, BlockAndIndexInput, BlockIdInput,
15    BroadcastedDeclareTransactionInput, BroadcastedDeployAccountTransactionInput,
16    BroadcastedInvokeTransactionInput, CallInput, ClassHashInput, DumpPath, EstimateFeeInput,
17    EventsInput, EventsSubscriptionInput, FlushParameters, GetStorageInput, GetStorageProofInput,
18    IncreaseTime, JsonRpcResponse, L1TransactionHashInput, LoadPath, MintTokensRequest,
19    PostmanLoadL1MessagingContract, RestartParameters, SetTime, SimulateTransactionsInput,
20    SubscriptionBlockIdInput, SubscriptionIdInput, TransactionHashInput,
21    TransactionReceiptSubscriptionInput, TransactionSubscriptionInput,
22};
23use crate::api::serde_helpers::{empty_params, optional_params};
24use crate::rpc_core::error::RpcError;
25use crate::rpc_core::request::RpcMethodCall;
26use crate::rpc_core::response::ResponseResult;
27
28/// Helper trait to easily convert results to rpc results
29pub trait ToRpcResponseResult {
30    fn to_rpc_result(self) -> ResponseResult;
31}
32
33/// Used when there is no defined code to use
34pub const WILDCARD_RPC_ERROR_CODE: i64 = -1;
35
36/// Converts a serializable value into a `ResponseResult`
37pub fn to_rpc_result<T: Serialize>(val: T) -> ResponseResult {
38    match serde_json::to_value(val) {
39        Ok(success) => ResponseResult::Success(success),
40        Err(err) => {
41            error!("Failed serialize rpc response: {:?}", err);
42            ResponseResult::error(RpcError::internal_error())
43        }
44    }
45}
46
47impl ToRpcResponseResult for StrictRpcResult {
48    fn to_rpc_result(self) -> ResponseResult {
49        match self {
50            Ok(JsonRpcResponse::Empty) => to_rpc_result(json!({})),
51            Ok(JsonRpcResponse::Devnet(data)) => to_rpc_result(data),
52            Ok(JsonRpcResponse::Starknet(data)) => to_rpc_result(data),
53            Err(err) => err.api_error_to_rpc_error().into(),
54        }
55    }
56}
57
58#[derive(Deserialize, AllVariantsSerdeRenames, VariantName)]
59#[cfg_attr(test, derive(Debug))]
60#[serde(tag = "method", content = "params")]
61pub enum StarknetSpecRequest {
62    #[serde(rename = "starknet_specVersion", with = "empty_params")]
63    SpecVersion,
64    #[serde(rename = "starknet_getBlockWithTxHashes")]
65    BlockWithTransactionHashes(BlockIdInput),
66    #[serde(rename = "starknet_getBlockWithTxs")]
67    BlockWithFullTransactions(BlockIdInput),
68    #[serde(rename = "starknet_getBlockWithReceipts")]
69    BlockWithReceipts(BlockIdInput),
70    #[serde(rename = "starknet_getStateUpdate")]
71    StateUpdate(BlockIdInput),
72    #[serde(rename = "starknet_getStorageAt")]
73    StorageAt(GetStorageInput),
74    #[serde(rename = "starknet_getStorageProof")]
75    StorageProof(GetStorageProofInput),
76    #[serde(rename = "starknet_getTransactionByHash")]
77    TransactionByHash(TransactionHashInput),
78    #[serde(rename = "starknet_getTransactionByBlockIdAndIndex")]
79    TransactionByBlockAndIndex(BlockAndIndexInput),
80    #[serde(rename = "starknet_getTransactionReceipt")]
81    TransactionReceiptByTransactionHash(TransactionHashInput),
82    #[serde(rename = "starknet_getTransactionStatus")]
83    TransactionStatusByHash(TransactionHashInput),
84    #[serde(rename = "starknet_getMessagesStatus")]
85    MessagesStatusByL1Hash(L1TransactionHashInput),
86    #[serde(rename = "starknet_getClass")]
87    ClassByHash(BlockAndClassHashInput),
88    #[serde(rename = "starknet_getCompiledCasm")]
89    CompiledCasmByClassHash(ClassHashInput),
90    #[serde(rename = "starknet_getClassHashAt")]
91    ClassHashAtContractAddress(BlockAndContractAddressInput),
92    #[serde(rename = "starknet_getClassAt")]
93    ClassAtContractAddress(BlockAndContractAddressInput),
94    #[serde(rename = "starknet_getBlockTransactionCount")]
95    BlockTransactionCount(BlockIdInput),
96    #[serde(rename = "starknet_call")]
97    Call(CallInput),
98    #[serde(rename = "starknet_estimateFee")]
99    EstimateFee(EstimateFeeInput),
100    #[serde(rename = "starknet_blockNumber", with = "empty_params")]
101    BlockNumber,
102    #[serde(rename = "starknet_blockHashAndNumber", with = "empty_params")]
103    BlockHashAndNumber,
104    #[serde(rename = "starknet_chainId", with = "empty_params")]
105    ChainId,
106    #[serde(rename = "starknet_syncing", with = "empty_params")]
107    Syncing,
108    #[serde(rename = "starknet_getEvents")]
109    Events(EventsInput),
110    #[serde(rename = "starknet_getNonce")]
111    ContractNonce(BlockAndContractAddressInput),
112    #[serde(rename = "starknet_addDeclareTransaction")]
113    AddDeclareTransaction(BroadcastedDeclareTransactionInput),
114    #[serde(rename = "starknet_addDeployAccountTransaction")]
115    AddDeployAccountTransaction(BroadcastedDeployAccountTransactionInput),
116    #[serde(rename = "starknet_addInvokeTransaction")]
117    AddInvokeTransaction(BroadcastedInvokeTransactionInput),
118    #[serde(rename = "starknet_estimateMessageFee")]
119    EstimateMessageFee(EstimateMessageFeeRequest),
120    #[serde(rename = "starknet_simulateTransactions")]
121    SimulateTransactions(SimulateTransactionsInput),
122    #[serde(rename = "starknet_traceTransaction")]
123    TraceTransaction(TransactionHashInput),
124    #[serde(rename = "starknet_traceBlockTransactions")]
125    BlockTransactionTraces(BlockIdInput),
126}
127
128#[derive(Deserialize, AllVariantsSerdeRenames, VariantName)]
129#[cfg_attr(test, derive(Debug))]
130#[serde(tag = "method", content = "params")]
131pub enum DevnetSpecRequest {
132    #[serde(rename = "devnet_impersonateAccount")]
133    ImpersonateAccount(AccountAddressInput),
134    #[serde(rename = "devnet_stopImpersonateAccount")]
135    StopImpersonateAccount(AccountAddressInput),
136    #[serde(rename = "devnet_autoImpersonate", with = "empty_params")]
137    AutoImpersonate,
138    #[serde(rename = "devnet_stopAutoImpersonate", with = "empty_params")]
139    StopAutoImpersonate,
140    #[serde(rename = "devnet_dump", with = "optional_params")]
141    Dump(Option<DumpPath>),
142    #[serde(rename = "devnet_load")]
143    Load(LoadPath),
144    #[serde(rename = "devnet_postmanLoad")]
145    PostmanLoadL1MessagingContract(PostmanLoadL1MessagingContract),
146    #[serde(rename = "devnet_postmanFlush", with = "optional_params")]
147    PostmanFlush(Option<FlushParameters>),
148    #[serde(rename = "devnet_postmanSendMessageToL2")]
149    PostmanSendMessageToL2(MessageToL2),
150    #[serde(rename = "devnet_postmanConsumeMessageFromL2")]
151    PostmanConsumeMessageFromL2(MessageToL1),
152    #[serde(rename = "devnet_createBlock", with = "empty_params")]
153    CreateBlock,
154    #[serde(rename = "devnet_abortBlocks")]
155    AbortBlocks(AbortingBlocks),
156    #[serde(rename = "devnet_acceptOnL1")]
157    AcceptOnL1(AcceptOnL1Request),
158    #[serde(rename = "devnet_setGasPrice")]
159    SetGasPrice(GasModificationRequest),
160    #[serde(rename = "devnet_restart", with = "optional_params")]
161    Restart(Option<RestartParameters>),
162    #[serde(rename = "devnet_setTime")]
163    SetTime(SetTime),
164    #[serde(rename = "devnet_increaseTime")]
165    IncreaseTime(IncreaseTime),
166    #[serde(rename = "devnet_getPredeployedAccounts", with = "optional_params")]
167    PredeployedAccounts(Option<PredeployedAccountsQuery>),
168    #[serde(rename = "devnet_getAccountBalance")]
169    AccountBalance(BalanceQuery),
170    #[serde(rename = "devnet_mint")]
171    Mint(MintTokensRequest),
172    #[serde(rename = "devnet_getConfig", with = "empty_params")]
173    DevnetConfig,
174}
175
176#[cfg_attr(test, derive(Debug))]
177pub enum JsonRpcRequest {
178    StarknetSpecRequest(StarknetSpecRequest),
179    DevnetSpecRequest(DevnetSpecRequest),
180    // If adding a new variant, expand `fn deserialize` and `fn all_variants_serde_renames`
181}
182
183impl<'de> Deserialize<'de> for JsonRpcRequest {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        let raw_req = serde_json::Value::deserialize(deserializer)?;
189
190        let method = raw_req.get("method").and_then(|m| m.as_str()).unwrap_or("<missing>");
191
192        match method {
193            method if method.starts_with("starknet_") => Ok(Self::StarknetSpecRequest(
194                serde_json::from_value(raw_req).map_err(serde::de::Error::custom)?,
195            )),
196            method if method.starts_with("devnet_") => Ok(Self::DevnetSpecRequest(
197                serde_json::from_value(raw_req).map_err(serde::de::Error::custom)?,
198            )),
199            invalid => Err(serde::de::Error::custom(format!("Invalid method: {invalid}"))),
200        }
201    }
202}
203
204impl StarknetSpecRequest {
205    pub fn requires_notifying(&self) -> bool {
206        #![warn(clippy::wildcard_enum_match_arm)]
207        match self {
208            Self::AddDeclareTransaction(_)
209            | Self::AddDeployAccountTransaction(_)
210            | Self::AddInvokeTransaction(_) => true,
211            Self::SpecVersion
212            | Self::BlockWithTransactionHashes(_)
213            | Self::BlockWithFullTransactions(_)
214            | Self::BlockWithReceipts(_)
215            | Self::StateUpdate(_)
216            | Self::StorageAt(_)
217            | Self::TransactionByHash(_)
218            | Self::TransactionByBlockAndIndex(_)
219            | Self::TransactionReceiptByTransactionHash(_)
220            | Self::TransactionStatusByHash(_)
221            | Self::MessagesStatusByL1Hash(_)
222            | Self::ClassByHash(_)
223            | Self::CompiledCasmByClassHash(_)
224            | Self::ClassHashAtContractAddress(_)
225            | Self::ClassAtContractAddress(_)
226            | Self::BlockTransactionCount(_)
227            | Self::Call(_)
228            | Self::EstimateFee(_)
229            | Self::BlockNumber
230            | Self::BlockHashAndNumber
231            | Self::ChainId
232            | Self::Syncing
233            | Self::Events(_)
234            | Self::ContractNonce(_)
235            | Self::EstimateMessageFee(_)
236            | Self::SimulateTransactions(_)
237            | Self::TraceTransaction(_)
238            | Self::BlockTransactionTraces(_)
239            | Self::StorageProof(_) => false,
240        }
241    }
242
243    pub fn is_forwardable_to_origin(&self) -> bool {
244        #[warn(clippy::wildcard_enum_match_arm)]
245        match self {
246            Self::BlockWithTransactionHashes(_)
247            | Self::BlockWithFullTransactions(_)
248            | Self::BlockWithReceipts(_)
249            | Self::StateUpdate(_)
250            | Self::StorageAt(_)
251            | Self::TransactionByHash(_)
252            | Self::TransactionByBlockAndIndex(_)
253            | Self::TransactionReceiptByTransactionHash(_)
254            | Self::TransactionStatusByHash(_)
255            | Self::ClassByHash(_)
256            | Self::ClassHashAtContractAddress(_)
257            | Self::ClassAtContractAddress(_)
258            | Self::BlockTransactionCount(_)
259            | Self::Call(_)
260            | Self::EstimateFee(_)
261            | Self::BlockNumber
262            | Self::BlockHashAndNumber
263            | Self::Events(_)
264            | Self::ContractNonce(_)
265            | Self::EstimateMessageFee(_)
266            | Self::SimulateTransactions(_)
267            | Self::TraceTransaction(_)
268            | Self::MessagesStatusByL1Hash(_)
269            | Self::CompiledCasmByClassHash(_)
270            | Self::StorageProof(_)
271            | Self::BlockTransactionTraces(_) => true,
272            Self::SpecVersion
273            | Self::ChainId
274            | Self::Syncing
275            | Self::AddDeclareTransaction(_)
276            | Self::AddDeployAccountTransaction(_)
277            | Self::AddInvokeTransaction(_) => false,
278        }
279    }
280
281    pub fn is_dumpable(&self) -> bool {
282        #[warn(clippy::wildcard_enum_match_arm)]
283        match self {
284            Self::AddDeclareTransaction(_)
285            | Self::AddDeployAccountTransaction(_)
286            | Self::AddInvokeTransaction(_) => true,
287            Self::SpecVersion
288            | Self::BlockWithTransactionHashes(_)
289            | Self::BlockWithFullTransactions(_)
290            | Self::BlockWithReceipts(_)
291            | Self::StateUpdate(_)
292            | Self::StorageAt(_)
293            | Self::TransactionByHash(_)
294            | Self::TransactionByBlockAndIndex(_)
295            | Self::TransactionReceiptByTransactionHash(_)
296            | Self::TransactionStatusByHash(_)
297            | Self::ClassByHash(_)
298            | Self::ClassHashAtContractAddress(_)
299            | Self::ClassAtContractAddress(_)
300            | Self::BlockTransactionCount(_)
301            | Self::Call(_)
302            | Self::EstimateFee(_)
303            | Self::BlockNumber
304            | Self::BlockHashAndNumber
305            | Self::ChainId
306            | Self::Syncing
307            | Self::Events(_)
308            | Self::ContractNonce(_)
309            | Self::EstimateMessageFee(_)
310            | Self::SimulateTransactions(_)
311            | Self::TraceTransaction(_)
312            | Self::BlockTransactionTraces(_)
313            | Self::MessagesStatusByL1Hash(_)
314            | Self::CompiledCasmByClassHash(_)
315            | Self::StorageProof(_) => false,
316        }
317    }
318}
319
320impl DevnetSpecRequest {
321    pub fn requires_notifying(&self) -> bool {
322        #![warn(clippy::wildcard_enum_match_arm)]
323        match self {
324            Self::PostmanFlush(_)
325            | Self::PostmanSendMessageToL2(_)
326            | Self::CreateBlock
327            | Self::AbortBlocks(_)
328            | Self::AcceptOnL1(_)
329            | Self::SetTime(_)
330            | Self::IncreaseTime(_)
331            | Self::Mint(_) => true,
332            Self::ImpersonateAccount(_)
333            | Self::StopImpersonateAccount(_)
334            | Self::AutoImpersonate
335            | Self::StopAutoImpersonate
336            | Self::Dump(_)
337            | Self::Load(_)
338            | Self::PostmanLoadL1MessagingContract(_)
339            | Self::PostmanConsumeMessageFromL2(_)
340            | Self::SetGasPrice(_)
341            | Self::Restart(_)
342            | Self::PredeployedAccounts(_)
343            | Self::AccountBalance(_)
344            | Self::DevnetConfig => false,
345        }
346    }
347
348    /// postmanFlush not dumped because it creates new RPC calls which get dumped
349    pub fn is_dumpable(&self) -> bool {
350        #[warn(clippy::wildcard_enum_match_arm)]
351        match self {
352            Self::ImpersonateAccount(_)
353            | Self::StopImpersonateAccount(_)
354            | Self::AutoImpersonate
355            | Self::StopAutoImpersonate
356            | Self::PostmanLoadL1MessagingContract(_)
357            | Self::PostmanSendMessageToL2(_)
358            | Self::PostmanConsumeMessageFromL2(_)
359            | Self::CreateBlock
360            | Self::AbortBlocks(_)
361            | Self::AcceptOnL1(_)
362            | Self::SetGasPrice(_)
363            | Self::SetTime(_)
364            | Self::IncreaseTime(_)
365            | Self::Mint(_) => true,
366            Self::Dump(_)
367            | Self::Load(_)
368            | Self::PostmanFlush(_)
369            | Self::Restart(_)
370            | Self::PredeployedAccounts(_)
371            | Self::AccountBalance(_)
372            | Self::DevnetConfig => false,
373        }
374    }
375}
376
377impl JsonRpcRequest {
378    pub fn requires_notifying(&self) -> bool {
379        #![warn(clippy::wildcard_enum_match_arm)]
380        match self {
381            Self::StarknetSpecRequest(req) => req.requires_notifying(),
382            Self::DevnetSpecRequest(req) => req.requires_notifying(),
383        }
384    }
385
386    /// Should the request be retried by being forwarded to the forking origin?
387    pub fn is_forwardable_to_origin(&self) -> bool {
388        #[warn(clippy::wildcard_enum_match_arm)]
389        match self {
390            Self::StarknetSpecRequest(req) => req.is_forwardable_to_origin(),
391            Self::DevnetSpecRequest(_) => false,
392        }
393    }
394
395    pub fn is_dumpable(&self) -> bool {
396        #[warn(clippy::wildcard_enum_match_arm)]
397        match self {
398            Self::StarknetSpecRequest(req) => req.is_dumpable(),
399            Self::DevnetSpecRequest(req) => req.is_dumpable(),
400        }
401    }
402
403    pub fn all_variants_serde_renames() -> Vec<String> {
404        let mut all_variants = vec![];
405        for variants in [
406            StarknetSpecRequest::all_variants_serde_renames(),
407            DevnetSpecRequest::all_variants_serde_renames(),
408        ] {
409            all_variants.extend(variants);
410        }
411        all_variants
412    }
413}
414
415pub enum JsonRpcWsRequest {
416    OneTimeRequest(Box<JsonRpcRequest>),
417    SubscriptionRequest(JsonRpcSubscriptionRequest),
418}
419
420impl<'de> Deserialize<'de> for JsonRpcWsRequest {
421    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
422    where
423        D: serde::Deserializer<'de>,
424    {
425        let raw_req = serde_json::Value::deserialize(deserializer)?;
426
427        let method = raw_req.get("method").and_then(|m| m.as_str()).unwrap_or("<missing>");
428
429        if method.starts_with("starknet_subscribe") || method == "starknet_unsubscribe" {
430            Ok(Self::SubscriptionRequest(
431                serde_json::from_value(raw_req).map_err(serde::de::Error::custom)?,
432            ))
433        } else {
434            Ok(Self::OneTimeRequest(
435                serde_json::from_value(raw_req).map_err(serde::de::Error::custom)?,
436            ))
437        }
438    }
439}
440
441#[derive(Deserialize, AllVariantsSerdeRenames, VariantName)]
442#[cfg_attr(test, derive(Debug))]
443#[serde(tag = "method", content = "params")]
444pub enum JsonRpcSubscriptionRequest {
445    #[serde(rename = "starknet_subscribeNewHeads", with = "optional_params")]
446    NewHeads(Option<SubscriptionBlockIdInput>),
447    #[serde(rename = "starknet_subscribeTransactionStatus")]
448    TransactionStatus(TransactionHashInput),
449    #[serde(rename = "starknet_subscribeEvents")]
450    Events(Option<EventsSubscriptionInput>),
451    #[serde(rename = "starknet_subscribeNewTransactions", with = "optional_params")]
452    NewTransactions(Option<TransactionSubscriptionInput>),
453    #[serde(rename = "starknet_subscribeNewTransactionReceipts", with = "optional_params")]
454    NewTransactionReceipts(Option<TransactionReceiptSubscriptionInput>),
455    #[serde(rename = "starknet_unsubscribe")]
456    Unsubscribe(SubscriptionIdInput),
457}
458
459pub fn to_json_rpc_request<D>(call: &RpcMethodCall) -> Result<D, RpcError>
460where
461    D: DeserializeOwned,
462{
463    let params: serde_json::Value = call.params.clone().into();
464    let deserializable_call = json!({
465        "method": call.method,
466        "params": params
467    });
468
469    serde_json::from_value::<D>(deserializable_call).map_err(|err| {
470        let err = err.to_string();
471        // since JSON-RPC specification requires returning a Method Not Found error,
472        // we apply a hacky way to decide - checking the stringified error message
473        if err.contains("Invalid method") || err.contains(&format!("unknown variant `{}`", call.method)) {
474            error!(target: "rpc", method = ?call.method, "failed to deserialize RPC call: unknown method");
475            RpcError::method_not_found()
476        } else {
477            error!(target: "rpc", method = ?call.method, ?err, "failed to deserialize RPC call: invalid params");
478            RpcError::invalid_params(err)
479        }
480    })
481}
482
483impl std::fmt::Display for JsonRpcRequest {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        let variant_name = match self {
486            Self::StarknetSpecRequest(req) => req.variant_name(),
487            Self::DevnetSpecRequest(req) => req.variant_name(),
488        };
489        write!(f, "{}", variant_name)
490    }
491}
492
493#[cfg(test)]
494mod requests_tests {
495
496    use serde_json::json;
497    use starknet_types::felt::felt_from_prefixed_hex;
498
499    use super::{JsonRpcRequest, StarknetSpecRequest};
500    use crate::rpc_core::request::RpcMethodCall;
501    use crate::test_utils::{EXPECTED_INVALID_BLOCK_ID_MSG, assert_contains};
502
503    #[test]
504    fn deserialize_get_block_with_transaction_hashes_request() {
505        let json_str =
506            r#"{"method":"starknet_getBlockWithTxHashes","params":{"block_id":"latest"}}"#;
507        assert_deserialization_succeeds(json_str);
508        assert_deserialization_succeeds(&json_str.replace("latest", "pre_confirmed"));
509
510        assert_deserialization_fails(&json_str.replace("latest", "0x134134"), "Invalid block ID");
511    }
512
513    #[test]
514    fn deserialize_get_block_with_transactions_request() {
515        let json_str = r#"{"method":"starknet_getBlockWithTxs","params":{"block_id":"latest"}}"#;
516        assert_deserialization_succeeds(json_str);
517        assert_deserialization_succeeds(&json_str.replace("latest", "pre_confirmed"));
518
519        assert_deserialization_fails(
520            json_str.replace("latest", "0x134134").as_str(),
521            EXPECTED_INVALID_BLOCK_ID_MSG,
522        );
523    }
524
525    #[test]
526    fn deserialize_get_state_update_request() {
527        let json_str = r#"{"method":"starknet_getStateUpdate","params":{"block_id":"latest"}}"#;
528        assert_deserialization_succeeds(json_str);
529        assert_deserialization_succeeds(&json_str.replace("latest", "pre_confirmed"));
530
531        assert_deserialization_fails(
532            &json_str.replace("latest", "0x134134"),
533            EXPECTED_INVALID_BLOCK_ID_MSG,
534        );
535    }
536
537    #[test]
538    fn deserialize_get_storage_at_request() {
539        let json_str = r#"{"method":"starknet_getStorageAt","params":{"contract_address":"0x134134","key":"0x134134","block_id":"latest"}}"#;
540        assert_deserialization_succeeds(json_str);
541
542        assert_deserialization_fails(
543            &json_str.replace(r#""contract_address":"0x134134""#, r#""contract_address":"123""#),
544            "Missing prefix 0x in 123",
545        );
546
547        assert_deserialization_fails(
548            &json_str.replace(r#""contract_address":"0x134134""#, r#""contract_address": 123"#),
549            "invalid type: number, expected a string",
550        );
551    }
552
553    #[test]
554    fn deserialize_get_transaction_by_hash_request() {
555        let json_str = r#"{"method":"starknet_getTransactionByHash","params":{"transaction_hash":"0x134134"}}"#;
556
557        let request = serde_json::from_str::<JsonRpcRequest>(json_str).unwrap();
558
559        match request {
560            JsonRpcRequest::StarknetSpecRequest(StarknetSpecRequest::TransactionByHash(input)) => {
561                assert!(input.transaction_hash == felt_from_prefixed_hex("0x134134").unwrap());
562            }
563            _ => panic!("Wrong request type"),
564        }
565
566        // Errored json, there is no object just string is passed
567        assert_deserialization_fails(
568            r#"{"method":"starknet_getTransactionByHash","params":"0x134134"}"#,
569            "invalid type: string \"0x134134\", expected struct",
570        );
571        // Errored json, hash is not prefixed with 0x
572        assert_deserialization_fails(
573            r#"{"method":"starknet_getTransactionByHash","params":{"transaction_hash":"134134"}}"#,
574            "expected hex string to be prefixed by '0x'",
575        );
576        // Errored json, hex longer than 64 chars; misleading error message coming from dependency
577        assert_deserialization_fails(
578            r#"{"method":"starknet_getTransactionByHash","params":{"transaction_hash":"0x004134134134134134134134134134134134134134134134134134134134134134"}}"#,
579            "expected hex string to be prefixed by '0x'",
580        );
581    }
582
583    #[test]
584    fn deserialize_get_transaction_by_block_and_index_request() {
585        let json_str = r#"{"method":"starknet_getTransactionByBlockIdAndIndex","params":{"block_id":"latest","index":0}}"#;
586        assert_deserialization_succeeds(json_str);
587
588        assert_deserialization_fails(
589            json_str.replace('0', "\"0x1\"").as_str(),
590            "invalid type: string \"0x1\", expected u64",
591        );
592    }
593
594    #[test]
595    fn deserialize_get_transaction_receipt_request() {
596        let json_str = r#"{"method":"starknet_getTransactionReceipt","params":{"transaction_hash":"0xAAABB"}}"#;
597        assert_deserialization_succeeds(json_str);
598
599        assert_deserialization_fails(
600            json_str.replace("0x", "").as_str(),
601            "expected hex string to be prefixed by '0x'",
602        );
603    }
604
605    #[test]
606    fn deserialize_get_class_request() {
607        let json_str = r#"{"method":"starknet_getClass","params":{"block_id":"latest","class_hash":"0xAAABB"}}"#;
608        assert_deserialization_succeeds(json_str);
609
610        assert_deserialization_fails(
611            json_str.replace("0x", "").as_str(),
612            "expected hex string to be prefixed by '0x'",
613        );
614    }
615
616    #[test]
617    fn deserialize_get_class_hash_at_request() {
618        let json_str = r#"{"method":"starknet_getClassHashAt","params":{"block_id":"latest","contract_address":"0xAAABB"}}"#;
619        assert_deserialization_succeeds(json_str);
620
621        assert_deserialization_fails(
622            json_str.replace("0x", "").as_str(),
623            "Error converting from hex string",
624        );
625    }
626
627    #[test]
628    fn deserialize_get_class_at_request() {
629        let json_str = r#"{"method":"starknet_getClassAt","params":{"block_id":"latest","contract_address":"0xAAABB"}}"#;
630        assert_deserialization_succeeds(json_str);
631
632        assert_deserialization_fails(json_str.replace("0x", "").as_str(), "Missing prefix 0x");
633    }
634
635    #[test]
636    fn deserialize_get_block_transaction_count_request() {
637        let json_str =
638            r#"{"method":"starknet_getBlockTransactionCount","params":{"block_id":"latest"}}"#;
639        assert_deserialization_succeeds(json_str);
640
641        assert_deserialization_fails(
642            json_str.replace("latest", "0x134134").as_str(),
643            EXPECTED_INVALID_BLOCK_ID_MSG,
644        );
645    }
646
647    #[test]
648    fn deserialize_call_request() {
649        let json_str = r#"{
650            "method":"starknet_call",
651            "params":{
652                "block_id":"latest",
653                "request":{
654                    "contract_address":"0xAAABB",
655                    "entry_point_selector":"0x134134",
656                    "calldata":["0x134134"]
657                }
658            }
659        }"#;
660
661        assert_deserialization_succeeds(json_str);
662
663        assert_deserialization_fails(
664            json_str.replace("starknet_call", "starknet_Call").as_str(),
665            "unknown variant `starknet_Call`",
666        );
667
668        assert_deserialization_fails(
669            json_str
670                .replace(r#""contract_address":"0xAAABB""#, r#""contract_address":"123""#)
671                .as_str(),
672            "Error converting from hex string",
673        );
674        assert_deserialization_fails(
675            json_str
676                .replace(
677                    r#""entry_point_selector":"0x134134""#,
678                    r#""entry_point_selector":"134134""#,
679                )
680                .as_str(),
681            "expected hex string to be prefixed by '0x'",
682        );
683        assert_deserialization_fails(
684            json_str.replace(r#""calldata":["0x134134"]"#, r#""calldata":["123"]"#).as_str(),
685            "expected hex string to be prefixed by '0x'",
686        );
687        assert_deserialization_fails(
688            json_str.replace(r#""calldata":["0x134134"]"#, r#""calldata":[123]"#).as_str(),
689            "invalid type: number, expected a 32 byte array ([u8;32]) or a hexadecimal string",
690        );
691    }
692
693    #[test]
694    fn deserialize_deploy_account_fee_estimation_request() {
695        let json_str = r#"{
696            "method":"starknet_estimateFee",
697            "params":{
698                "block_id":"latest",
699                "simulation_flags": [],
700                "request":[
701                    {
702                        "type":"DEPLOY_ACCOUNT",
703                        "resource_bounds": {
704                            "l1_gas": {
705                                "max_amount": "0x1",
706                                "max_price_per_unit": "0x2"
707                            },
708                            "l1_data_gas": {
709                                "max_amount": "0x1",
710                                "max_price_per_unit": "0x2"
711                            },
712                            "l2_gas": {
713                                "max_amount": "0x1",
714                                "max_price_per_unit": "0x2"
715                            }
716                        },
717                        "tip": "0xabc",
718                        "paymaster_data": [],
719                        "version": "0x100000000000000000000000000000003",
720                        "signature": ["0xFF", "0xAA"],
721                        "nonce": "0x0",
722                        "contract_address_salt": "0x01",
723                        "class_hash": "0x01",
724                        "constructor_calldata": ["0x01"],
725                        "nonce_data_availability_mode": "L1",
726                        "fee_data_availability_mode": "L1"
727                    }
728                ]
729            }
730        }"#;
731
732        assert_deserialization_succeeds(json_str);
733
734        assert_deserialization_fails(
735            json_str.replace("estimateFee", "estimate_fee").as_str(),
736            "unknown variant `starknet_estimate_fee`",
737        );
738    }
739
740    fn sample_declare_v3_body() -> serde_json::Value {
741        json!({
742            "type":"DECLARE",
743            "version": "0x3",
744            "signature": [
745                "0x2216f8f4d9abc06e130d2a05b13db61850f0a1d21891c7297b98fd6cc51920d",
746                "0x6aadfb198bbffa8425801a2342f5c6d804745912114d5976f53031cd789bb6d"
747            ],
748            "resource_bounds": {
749                "l1_gas": {
750                    "max_amount": "0x1",
751                    "max_price_per_unit": "0x2"
752                },
753                "l1_data_gas": {
754                    "max_amount": "0x1",
755                    "max_price_per_unit": "0x2"
756                },
757                "l2_gas": {
758                    "max_amount": "0x1",
759                    "max_price_per_unit": "0x2"
760                }
761            },
762            "tip": "0xabc",
763            "paymaster_data": [],
764            "account_deployment_data": [],
765            "nonce": "0x0",
766            "compiled_class_hash":"0x63b33a5f2f46b1445d04c06d7832c48c48ad087ce0803b71f2b8d96353716ca",
767            "sender_address":"0x34ba56f92265f0868c57d3fe72ecab144fc96f97954bbbc4252cef8e8a979ba",
768            "contract_class": {
769                "sierra_program": ["0xAA", "0xBB"],
770                "entry_points_by_type": {
771                    "EXTERNAL": [{"function_idx":0,"selector":"0x362398bec32bc0ebb411203221a35a0301193a96f317ebe5e40be9f60d15320"},{"function_idx":1,"selector":"0x39e11d48192e4333233c7eb19d10ad67c362bb28580c604d67884c85da39695"}],
772                    "L1_HANDLER": [],
773                    "CONSTRUCTOR": [{"function_idx":2,"selector":"0x28ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194"}]
774                },
775                "abi": "[{\"type\": \"function\", \"name\": \"constructor\", \"inputs\": [{\"name\": \"initial_balance\", \"type\": \"core::felt252\"}], \"outputs\": [], \"state_mutability\": \"external\"}, {\"type\": \"function\", \"name\": \"increase_balance\", \"inputs\": [{\"name\": \"amount1\", \"type\": \"core::felt252\"}, {\"name\": \"amount2\", \"type\": \"core::felt252\"}], \"outputs\": [], \"state_mutability\": \"external\"}, {\"type\": \"function\", \"name\": \"get_balance\", \"inputs\": [], \"outputs\": [{\"type\": \"core::felt252\"}], \"state_mutability\": \"view\"}]",
776                "contract_class_version": "0.1.0"
777            },
778            "nonce_data_availability_mode": "L1",
779            "fee_data_availability_mode": "L1"
780        })
781    }
782
783    fn create_declare_request(tx: serde_json::Value) -> serde_json::Value {
784        json!({
785            "method":"starknet_addDeclareTransaction",
786            "params":{
787                "declare_transaction": tx
788            }
789        })
790    }
791
792    fn create_estimate_request(requests: &[serde_json::Value]) -> serde_json::Value {
793        json!({
794            "method": "starknet_estimateFee",
795            "params": {
796                "block_id": "latest",
797                "simulation_flags": [],
798                "request": requests
799            }
800        })
801    }
802
803    #[test]
804    fn deserialize_declare_v3_fee_estimation_request() {
805        assert_deserialization_succeeds(
806            &create_estimate_request(&[sample_declare_v3_body()]).to_string(),
807        );
808        assert_deserialization_succeeds(
809            &create_estimate_request(&[sample_declare_v3_body()]).to_string().replace(
810                r#""version":"0x3""#,
811                r#""version":"0x100000000000000000000000000000003""#,
812            ),
813        );
814    }
815
816    #[test]
817    fn deserialize_get_events_request() {
818        let json_str = r#"{
819            "method":"starknet_getEvents",
820            "params":{
821                "filter":{
822                    "chunk_size": 1,
823                    "address":"0xAAABB",
824                    "keys":[["0xFF"], ["0xAA"]],
825                    "from_block": "latest",
826                    "to_block": "pre_confirmed",
827                    "continuation_token": "0x11"
828                }
829            }
830        }"#;
831
832        assert_deserialization_succeeds(json_str);
833        assert_deserialization_succeeds(
834            json_str.replace(r#""to_block": "pre_confirmed","#, "").as_str(),
835        );
836
837        assert_deserialization_fails(
838            json_str.replace(r#""chunk_size": 1,"#, "").as_str(),
839            "missing field `chunk_size`",
840        );
841    }
842
843    #[test]
844    fn deserialize_get_nonce_request() {
845        let json_str = r#"{
846            "method":"starknet_getNonce",
847            "params":{
848                "block_id":"latest",
849                "contract_address":"0xAAABB"
850            }
851        }"#;
852
853        assert_deserialization_succeeds(json_str);
854        assert_deserialization_fails(
855            json_str.replace(r#""block_id":"latest","#, "").as_str(),
856            "missing field `block_id`",
857        );
858    }
859
860    #[test]
861    fn deserialize_add_deploy_account_transaction_request() {
862        let json_str = r#"{
863            "method":"starknet_addDeployAccountTransaction",
864            "params":{
865                "deploy_account_transaction":{
866                    "type":"DEPLOY_ACCOUNT",
867                    "resource_bounds": {
868                        "l1_gas": {
869                            "max_amount": "0x1",
870                            "max_price_per_unit": "0x2"
871                        },
872                        "l1_data_gas": {
873                            "max_amount": "0x1",
874                            "max_price_per_unit": "0x2"
875                        },
876                        "l2_gas": {
877                            "max_amount": "0x1",
878                            "max_price_per_unit": "0x2"
879                        }
880                    },
881                    "tip": "0xabc",
882                    "paymaster_data": [],
883                    "version": "0x3",
884                    "signature": ["0xFF", "0xAA"],
885                    "nonce": "0x0",
886                    "contract_address_salt": "0x01",
887                    "class_hash": "0x01",
888                    "constructor_calldata": ["0x01"],
889                    "nonce_data_availability_mode": "L1",
890                    "fee_data_availability_mode": "L1"
891                }
892            }
893        }"#;
894
895        assert_deserialization_succeeds(json_str);
896        assert_deserialization_fails(
897            json_str.replace(r#""class_hash": "0x01","#, "").as_str(),
898            "missing field `class_hash`",
899        );
900    }
901
902    #[test]
903    fn deserialize_add_declare_transaction_v3_request() {
904        assert_deserialization_succeeds(
905            &create_declare_request(sample_declare_v3_body()).to_string(),
906        );
907
908        assert_deserialization_fails(
909            &create_declare_request(sample_declare_v3_body())
910                .to_string()
911                .replace(r#""version":"0x3""#, r#""version":"0x123""#),
912            "Invalid version of declare transaction: \"0x123\"",
913        );
914
915        assert_deserialization_fails(
916            &create_estimate_request(&[sample_declare_v3_body()])
917                .to_string()
918                .replace("resource_bounds", "resourceBounds"),
919            "Invalid declare transaction v3: missing field `resource_bounds`",
920        );
921
922        assert_deserialization_fails(
923            &create_declare_request(sample_declare_v3_body())
924                .to_string()
925                .replace(r#""nonce":"0x0""#, r#""nonce":123"#),
926            "Invalid declare transaction v3: invalid type: integer `123`",
927        );
928    }
929
930    #[test]
931    fn deseralize_chain_id_request() {
932        for body in [
933            json!({
934                "method": "starknet_chainId",
935                "params": {}
936            }),
937            json!({
938                "method": "starknet_chainId",
939                "params": []
940            }),
941            json!({
942                "method": "starknet_chainId",
943            }),
944        ] {
945            assert_deserialization_succeeds(body.to_string().as_str())
946        }
947    }
948
949    #[test]
950    fn deserialize_spec_version_request() {
951        for body in [
952            json!({
953                "method": "starknet_specVersion",
954                "params": {}
955            }),
956            json!({
957                "method": "starknet_specVersion",
958                "params": []
959            }),
960            json!({
961                "method": "starknet_specVersion",
962            }),
963        ] {
964            assert_deserialization_succeeds(body.to_string().as_str())
965        }
966    }
967
968    #[test]
969    fn deserialize_devnet_methods_with_optional_body() {
970        for mut body in [
971            json!({
972                "method": "devnet_dump",
973                "params": {}
974            }),
975            json!({
976                "method":"devnet_dump",
977            }),
978            json!({
979                "method":"devnet_dump",
980                "params": {"path": "path"}
981            }),
982            json!({
983                "method":"devnet_getPredeployedAccounts",
984                "params": {"with_balance": true}
985            }),
986            json!({
987                "method":"devnet_getPredeployedAccounts",
988            }),
989            json!({
990                "method":"devnet_getPredeployedAccounts",
991                "params": {}
992            }),
993            json!({
994                "method":"devnet_postmanFlush",
995                "params": {"dry_run": true}
996            }),
997            json!({
998                "method":"devnet_postmanFlush",
999            }),
1000            json!({
1001                "method":"devnet_postmanFlush",
1002                "params": {}
1003            }),
1004        ] {
1005            let mut json_rpc_object = json!({
1006                "jsonrpc": "2.0",
1007                "id": 1,
1008            });
1009
1010            json_rpc_object.as_object_mut().unwrap().append(body.as_object_mut().unwrap());
1011
1012            let RpcMethodCall { method, params, .. } =
1013                serde_json::from_value(json_rpc_object).unwrap();
1014            let params: serde_json::Value = params.into();
1015            let deserializable_call = json!({
1016                "method": &method,
1017                "params": params
1018            });
1019
1020            assert_deserialization_succeeds(deserializable_call.to_string().as_str())
1021        }
1022    }
1023
1024    fn assert_deserialization_succeeds(json_str: &str) {
1025        serde_json::from_str::<JsonRpcRequest>(json_str).unwrap();
1026    }
1027
1028    fn assert_deserialization_fails(json_str: &str, expected_msg: &str) {
1029        match serde_json::from_str::<JsonRpcRequest>(json_str) {
1030            Err(err) => assert_contains(&err.to_string(), expected_msg).unwrap(),
1031            other => panic!("Invalid result: {other:?}"),
1032        }
1033    }
1034}