diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index ec823340..cfe19ac0 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -110,12 +110,13 @@ mod runtime; use crate::request::{ CallRequest, CallRequestBuilder, FeeHistoryRequest, FeeHistoryRequestBuilder, GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, GetTransactionCountRequest, - GetTransactionCountRequestBuilder, Request, RequestBuilder, + GetTransactionCountRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest, + SendRawTransactionRequestBuilder, }; use candid::{CandidType, Principal}; use evm_rpc_types::{ BlockTag, CallArgs, ConsensusStrategy, FeeHistoryArgs, GetLogsArgs, GetTransactionCountArgs, - RpcConfig, RpcServices, + Hex, RpcConfig, RpcServices, }; use ic_error_types::RejectCode; use request::{GetLogsRequest, GetLogsRequestBuilder}; @@ -553,6 +554,43 @@ impl EvmRpcClient { 10_000_000_000, ) } + + /// Call `eth_sendRawTransaction` on the EVM RPC canister. + /// + /// # Examples + /// + /// ```rust + /// use alloy_primitives::{b256, bytes}; + /// use evm_rpc_client::EvmRpcClient; + /// + /// # use evm_rpc_types::{MultiRpcResult, Hex32, SendRawTransactionStatus}; + /// # use std::str::FromStr; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let client = EvmRpcClient::builder_for_ic() + /// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(SendRawTransactionStatus::Ok(Some(Hex32::from_str("0x33469b22e9f636356c4160a87eb19df52b7412e8eac32a4a55ffe88ea8350788").unwrap()))))) + /// .build(); + /// + /// let result = client + /// .send_raw_transaction(bytes!("0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83")) + /// .send() + /// .await + /// .expect_consistent(); + /// + /// assert_eq!(result, Ok(b256!("0x33469b22e9f636356c4160a87eb19df52b7412e8eac32a4a55ffe88ea8350788"))); + /// # Ok(()) + /// # } + /// ``` + pub fn send_raw_transaction( + &self, + params: impl Into, + ) -> SendRawTransactionRequestBuilder { + RequestBuilder::new( + self.clone(), + SendRawTransactionRequest::new(params.into()), + 10_000_000_000, + ) + } } impl EvmRpcClient { diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index 59a7ba9f..2ed9df57 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -242,6 +242,38 @@ impl GetTransactionCountRequestBuilder { } } +#[derive(Debug, Clone)] +pub struct SendRawTransactionRequest(Hex); + +impl SendRawTransactionRequest { + pub fn new(params: Hex) -> Self { + Self(params) + } +} + +impl EvmRpcRequest for SendRawTransactionRequest { + type Config = RpcConfig; + type Params = Hex; + type CandidOutput = MultiRpcResult; + type Output = MultiRpcResult; + + fn endpoint(&self) -> EvmRpcEndpoint { + EvmRpcEndpoint::SendRawTransaction + } + + fn params(self) -> Self::Params { + self.0 + } +} + +pub type SendRawTransactionRequestBuilder = RequestBuilder< + R, + RpcConfig, + Hex, + MultiRpcResult, + MultiRpcResult, +>; + /// Ethereum RPC endpoint supported by the EVM RPC canister. pub trait EvmRpcRequest { /// Type of RPC config for that request. @@ -273,6 +305,8 @@ pub enum EvmRpcEndpoint { GetLogs, /// `eth_getTransactionCount` endpoint. GetTransactionCount, + /// `eth_sendRawTransaction` endpoint. + SendRawTransaction, } impl EvmRpcEndpoint { @@ -284,6 +318,7 @@ impl EvmRpcEndpoint { Self::GetBlockByNumber => "eth_getBlockByNumber", Self::GetLogs => "eth_getLogs", Self::GetTransactionCount => "eth_getTransactionCount", + Self::SendRawTransaction => "eth_sendRawTransaction", } } } diff --git a/evm_rpc_types/src/result/alloy.rs b/evm_rpc_types/src/result/alloy.rs index 01e78c84..f8581c51 100644 --- a/evm_rpc_types/src/result/alloy.rs +++ b/evm_rpc_types/src/result/alloy.rs @@ -1,4 +1,7 @@ -use crate::{Block, FeeHistory, Hex, LogEntry, MultiRpcResult, Nat256}; +use crate::{ + Block, FeeHistory, Hex, JsonRpcError, LogEntry, MultiRpcResult, Nat256, RpcError, + SendRawTransactionStatus, ValidationError, +}; impl From>> for MultiRpcResult> { fn from(result: MultiRpcResult>) -> Self { @@ -33,3 +36,26 @@ impl From> for MultiRpcResult { result.map(alloy_primitives::Bytes::from) } } + +impl From> for MultiRpcResult { + fn from(result: MultiRpcResult) -> Self { + result.and_then(|status| match status { + SendRawTransactionStatus::Ok(maybe_hash) => match maybe_hash { + Some(hash) => Ok(alloy_primitives::B256::from(hash)), + None => Err(RpcError::ValidationError(ValidationError::Custom( + "Unable to compute transaction hash".to_string(), + ))), + }, + error => Err(RpcError::JsonRpcError(JsonRpcError { + code: -32_000, + message: match error { + SendRawTransactionStatus::Ok(_) => unreachable!(), + SendRawTransactionStatus::InsufficientFunds => "Insufficient funds", + SendRawTransactionStatus::NonceTooLow => "Nonce too low", + SendRawTransactionStatus::NonceTooHigh => "Nonce too high", + } + .to_string(), + })), + }) + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 3f057ea6..8f4b7992 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -10,7 +10,7 @@ use crate::{ }, setup::EvmRpcNonblockingSetup, }; -use alloy_primitives::{address, b256, bloom, bytes, U256}; +use alloy_primitives::{address, b256, bloom, bytes, Bytes, B256, U256}; use alloy_rpc_types::{BlockNumberOrTag, BlockTransactions}; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; @@ -23,10 +23,9 @@ use evm_rpc::{ types::{Metrics, ProviderId, RpcAccess, RpcMethod}, }; use evm_rpc_types::{ - BlockTag, ConsensusStrategy, EthMainnetService, EthSepoliaService, GetLogsRpcConfig, Hex, - Hex20, Hex32, HttpOutcallError, InstallArgs, JsonRpcError, LegacyRejectionCode, MultiRpcResult, - Nat256, Provider, ProviderError, RpcApi, RpcError, RpcResult, RpcService, RpcServices, - ValidationError, + BlockTag, ConsensusStrategy, EthMainnetService, EthSepoliaService, GetLogsRpcConfig, Hex20, + HttpOutcallError, InstallArgs, JsonRpcError, LegacyRejectionCode, MultiRpcResult, Nat256, + Provider, ProviderError, RpcApi, RpcError, RpcResult, RpcService, RpcServices, ValidationError, }; use ic_cdk::api::management_canister::main::CanisterId; use ic_error_types::RejectCode; @@ -57,9 +56,9 @@ const MOCK_REQUEST_RESPONSE: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x00112 const MOCK_REQUEST_RESPONSE_BYTES: u64 = 1000; const MOCK_API_KEY: &str = "mock-api-key"; -const MOCK_TRANSACTION: &str = "0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"; -const MOCK_TRANSACTION_HASH: &str = - "0x33469b22e9f636356c4160a87eb19df52b7412e8eac32a4a55ffe88ea8350788"; +const MOCK_TRANSACTION: Bytes = bytes!("0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"); +const MOCK_TRANSACTION_HASH: B256 = + b256!("0x33469b22e9f636356c4160a87eb19df52b7412e8eac32a4a55ffe88ea8350788"); const RPC_SERVICES: &[RpcServices] = &[ RpcServices::EthMainnet(None), @@ -254,19 +253,6 @@ impl EvmRpcSetup { ) } - pub fn eth_send_raw_transaction( - &self, - source: RpcServices, - config: Option, - signed_raw_transaction_hex: &str, - ) -> CallFlow> { - let signed_raw_transaction_hex: Hex = signed_raw_transaction_hex.parse().unwrap(); - self.call_update( - "eth_sendRawTransaction", - Encode!(&source, &config, &signed_raw_transaction_hex).unwrap(), - ) - } - pub fn update_api_keys(&self, api_keys: &[(ProviderId, Option)]) { self.call_update("updateApiKeys", Encode!(&api_keys).unwrap()) .wait() @@ -784,11 +770,6 @@ async fn eth_get_logs_should_fail_when_block_range_too_large() { #[tokio::test] async fn eth_get_block_by_number_should_succeed() { - fn mock_request() -> JsonRpcRequestMatcher { - JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") - .with_params(json!(["latest", false])) - } - fn mock_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "jsonrpc": "2.0", @@ -821,11 +802,11 @@ async fn eth_get_block_by_number_should_succeed() { for (source, offset) in iter::zip(RPC_SERVICES, (0_u64..).step_by(3)) { let mocks = MockHttpOutcallsBuilder::new() - .given(mock_request().with_id(offset)) + .given(get_block_by_number_request().with_id(offset)) .respond_with(mock_response().with_id(offset)) - .given(mock_request().with_id(1 + offset)) + .given(get_block_by_number_request().with_id(1 + offset)) .respond_with(mock_response().with_id(1 + offset)) - .given(mock_request().with_id(2 + offset)) + .given(get_block_by_number_request().with_id(2 + offset)) .respond_with(mock_response().with_id(2 + offset)); let response = setup @@ -876,11 +857,6 @@ async fn eth_get_block_by_number_should_succeed() { #[tokio::test] async fn eth_get_block_by_number_pre_london_fork_should_succeed() { - fn mock_request() -> JsonRpcRequestMatcher { - JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") - .with_params(json!(["latest", false])) - } - fn mock_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "jsonrpc":"2.0", @@ -914,11 +890,11 @@ async fn eth_get_block_by_number_pre_london_fork_should_succeed() { for (source, offset) in iter::zip(RPC_SERVICES, (0_u64..).step_by(3)) { let mocks = MockHttpOutcallsBuilder::new() - .given(mock_request().with_id(offset)) + .given(get_block_by_number_request().with_id(offset)) .respond_with(mock_response().with_id(offset)) - .given(mock_request().with_id(1 + offset)) + .given(get_block_by_number_request().with_id(1 + offset)) .respond_with(mock_response().with_id(1 + offset)) - .given(mock_request().with_id(2 + offset)) + .given(get_block_by_number_request().with_id(2 + offset)) .respond_with(mock_response().with_id(2 + offset)); let response = setup @@ -969,11 +945,6 @@ async fn eth_get_block_by_number_pre_london_fork_should_succeed() { #[tokio::test] async fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsistent() { - fn mock_request() -> JsonRpcRequestMatcher { - JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") - .with_params(json!(["latest", false])) - } - fn mock_response(total_difficulty: Option<&str>) -> JsonRpcResponse { let mut body = json!({ "jsonrpc":"2.0", @@ -1010,9 +981,9 @@ async fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inco let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let mocks = MockHttpOutcallsBuilder::new() - .given(mock_request().with_id(0_u64)) + .given(get_block_by_number_request().with_id(0_u64)) .respond_with(mock_response(Some("0xc70d815d562d3cfa955")).with_id(0_u64)) - .given(mock_request().with_id(1_u64)) + .given(get_block_by_number_request().with_id(1_u64)) .respond_with(mock_response(None).with_id(1_u64)); let response = setup @@ -1153,14 +1124,6 @@ async fn eth_get_transaction_count_should_succeed() { #[tokio::test] async fn eth_fee_history_should_succeed() { - fn mock_request() -> JsonRpcRequestMatcher { - JsonRpcRequestMatcher::with_method("eth_feeHistory").with_params(json!([ - "0x3", - "latest", - [] - ])) - } - fn mock_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "id" : 0, @@ -1177,11 +1140,11 @@ async fn eth_fee_history_should_succeed() { for (source, offset) in iter::zip(RPC_SERVICES, (0_u64..).step_by(3)) { let mocks = MockHttpOutcallsBuilder::new() - .given(mock_request().with_id(offset)) + .given(fee_history_request().with_id(offset)) .respond_with(mock_response().with_id(offset)) - .given(mock_request().with_id(1 + offset)) + .given(fee_history_request().with_id(1 + offset)) .respond_with(mock_response().with_id(1 + offset)) - .given(mock_request().with_id(2 + offset)) + .given(fee_history_request().with_id(2 + offset)) .respond_with(mock_response().with_id(2 + offset)); let response = setup @@ -1208,26 +1171,33 @@ async fn eth_fee_history_should_succeed() { } } -#[test] -fn eth_send_raw_transaction_should_succeed() { - let [response_0, response_1, response_2] = - json_rpc_sequential_id(json!({"id":0,"jsonrpc":"2.0","result":"Ok"})); - for source in RPC_SERVICES { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn eth_send_raw_transaction_should_succeed() { + fn mock_response() -> JsonRpcResponse { + JsonRpcResponse::from(json!({ "id": 0, "jsonrpc": "2.0", "result": "Ok" })) + } + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + for (source, offset) in iter::zip(RPC_SERVICES, (0_u64..).step_by(3)) { + let mocks = MockHttpOutcallsBuilder::new() + .given(send_raw_transaction_request().with_id(offset)) + .respond_with(mock_response().with_id(offset)) + .given(send_raw_transaction_request().with_id(1 + offset)) + .respond_with(mock_response().with_id(1 + offset)) + .given(send_raw_transaction_request().with_id(2 + offset)) + .respond_with(mock_response().with_id(2 + offset)); + let response = setup - .eth_send_raw_transaction(source.clone(), None, MOCK_TRANSACTION) - .mock_http_once(MockOutcallBuilder::new(200, response_0.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_1.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_2.clone())) - .wait() + .client(mocks) + .with_rpc_sources(source.clone()) + .build() + .send_raw_transaction(MOCK_TRANSACTION) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!( - response, - evm_rpc_types::SendRawTransactionStatus::Ok(Some( - Hex32::from_str(MOCK_TRANSACTION_HASH).unwrap() - )) - ); + + assert_eq!(response, MOCK_TRANSACTION_HASH); } } @@ -1506,46 +1476,49 @@ fn candid_rpc_should_reject_empty_service_list() { ); } -#[test] -fn candid_rpc_should_return_inconsistent_results() { - let setup = EvmRpcSetup::new().mock_api_keys(); - let results = setup - .eth_send_raw_transaction( - RpcServices::EthMainnet(Some(vec![ - EthMainnetService::Ankr, - EthMainnetService::Cloudflare, - ])), - None, - MOCK_TRANSACTION, - ) - .mock_http_once(MockOutcallBuilder::new( - 200, - r#"{"id":0,"jsonrpc":"2.0","result":"Ok"}"#, - )) - .mock_http_once(MockOutcallBuilder::new( - 200, - r#"{"id":1,"jsonrpc":"2.0","result":"NonceTooLow"}"#, +#[tokio::test] +async fn candid_rpc_should_return_inconsistent_results() { + let mocks = MockHttpOutcallsBuilder::new() + .given(send_raw_transaction_request().with_id(0_u64)) + .respond_with(JsonRpcResponse::from( + json!({ "id": 0, "jsonrpc": "2.0", "result": "Ok" }), )) - .wait() + .given(send_raw_transaction_request().with_id(1_u64)) + .respond_with(JsonRpcResponse::from( + json!({ "id": 1, "jsonrpc": "2.0", "result": "NonceTooLow" }), + )); + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let results = setup + .client(mocks) + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Ankr, + EthMainnetService::Cloudflare, + ]))) + .build() + .send_raw_transaction(MOCK_TRANSACTION) + .send() + .await .expect_inconsistent(); assert_eq!( results, vec![ ( RpcService::EthMainnet(EthMainnetService::Ankr), - Ok(evm_rpc_types::SendRawTransactionStatus::Ok(Some( - Hex32::from_str(MOCK_TRANSACTION_HASH).unwrap() - ))) + Ok(MOCK_TRANSACTION_HASH) ), ( RpcService::EthMainnet(EthMainnetService::Cloudflare), - Ok(evm_rpc_types::SendRawTransactionStatus::NonceTooLow) + Err(RpcError::JsonRpcError(JsonRpcError { + code: -32_000, + message: "Nonce too low".to_string() + })) ) ] ); let rpc_method = || RpcMethod::EthSendRawTransaction.into(); assert_eq!( - setup.get_metrics(), + setup.get_metrics().await, Metrics { requests: hashmap! { (rpc_method(), ANKR_HOSTNAME.into()) => 1, @@ -1908,37 +1881,34 @@ async fn candid_rpc_should_return_inconsistent_results_with_unexpected_http_stat ); } -#[test] -fn candid_rpc_should_handle_already_known() { - let setup = EvmRpcSetup::new().mock_api_keys(); - let result = setup - .eth_send_raw_transaction( - RpcServices::EthMainnet(Some(vec![ - EthMainnetService::Ankr, - EthMainnetService::Cloudflare, - ])), - None, - MOCK_TRANSACTION, - ) - .mock_http_once(MockOutcallBuilder::new( - 200, - r#"{"id":0,"jsonrpc":"2.0","result":"Ok"}"#, - )) - .mock_http_once(MockOutcallBuilder::new( - 200, - r#"{"id":1,"jsonrpc":"2.0","error":{"code":-32000,"message":"already known"}}"#, +#[tokio::test] +async fn candid_rpc_should_handle_already_known() { + let mocks = MockHttpOutcallsBuilder::new() + .given(send_raw_transaction_request().with_id(0_u64)) + .respond_with(JsonRpcResponse::from( + json!({ "id": 0, "jsonrpc": "2.0", "result": "Ok" }), )) - .wait() + .given(send_raw_transaction_request().with_id(1_u64)) + .respond_with(JsonRpcResponse::from( + json!({ "id": 1, "jsonrpc": "2.0", "error": {"code": -32000, "message": "already known"} }), + )); + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let result = setup + .client(mocks) + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Ankr, + EthMainnetService::Cloudflare, + ]))) + .build() + .send_raw_transaction(MOCK_TRANSACTION) + .send() + .await .expect_consistent(); - assert_eq!( - result, - Ok(evm_rpc_types::SendRawTransactionStatus::Ok(Some( - Hex32::from_str(MOCK_TRANSACTION_HASH).unwrap() - ))) - ); + assert_eq!(result, Ok(MOCK_TRANSACTION_HASH)); let rpc_method = || RpcMethod::EthSendRawTransaction.into(); assert_eq!( - setup.get_metrics(), + setup.get_metrics().await, Metrics { requests: hashmap! { (rpc_method(), ANKR_HOSTNAME.into()) => 1, @@ -1953,34 +1923,40 @@ fn candid_rpc_should_handle_already_known() { ); } -#[test] -fn candid_rpc_should_recognize_rate_limit() { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn candid_rpc_should_recognize_rate_limit() { + let mocks = MockHttpOutcallsBuilder::new() + .given(send_raw_transaction_request().with_id(0_u64)) + .respond_with(CanisterHttpReply::with_status(429).with_body("(Rate limit error message)")) + .given(send_raw_transaction_request().with_id(1_u64)) + .respond_with(CanisterHttpReply::with_status(429).with_body("(Rate limit error message)")); + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let result = setup - .eth_send_raw_transaction( - RpcServices::EthMainnet(Some(vec![ - EthMainnetService::Ankr, - EthMainnetService::Cloudflare, - ])), - None, - MOCK_TRANSACTION, - ) - .mock_http(MockOutcallBuilder::new(429, "(Rate limit error message)")) - .wait() + .client(mocks) + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Ankr, + EthMainnetService::Cloudflare, + ]))) + .build() + .send_raw_transaction(MOCK_TRANSACTION) + .send() + .await .expect_consistent(); + assert_eq!( result, Err(RpcError::HttpOutcallError( HttpOutcallError::InvalidHttpJsonRpcResponse { status: 429, - body: "(Rate limit error message)".to_string(), + body: "\"(Rate limit error message)\"".to_string(), parsing_error: None } )) ); let rpc_method = || RpcMethod::EthSendRawTransaction.into(); assert_eq!( - setup.get_metrics(), + setup.get_metrics().await, Metrics { requests: hashmap! { (rpc_method(), ANKR_HOSTNAME.into()) => 1, @@ -2604,12 +2580,6 @@ async fn should_fail_when_response_id_inconsistent_with_request_id() { #[tokio::test] async fn should_log_request() { - fn mock_request() -> JsonRpcRequestMatcher { - JsonRpcRequestMatcher::with_method("eth_feeHistory") - .with_params(json!(["0x3", "latest", []])) - .with_id(0_u64) - } - fn mock_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "id" : 0, @@ -2625,7 +2595,7 @@ async fn should_log_request() { let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let mocks = MockHttpOutcallsBuilder::new() - .given(mock_request()) + .given(fee_history_request()) .respond_with(mock_response()); let response = setup @@ -2771,6 +2741,24 @@ async fn should_change_default_provider_when_one_keeps_failing() { assert_eq!(response, U256::ONE); } +fn get_block_by_number_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_getBlockByNumber") + .with_params(json!(["latest", false])) + .with_id(0_u64) +} + +fn fee_history_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_feeHistory") + .with_params(json!(["0x3", "latest", []])) + .with_id(0_u64) +} + +fn send_raw_transaction_request() -> JsonRpcRequestMatcher { + JsonRpcRequestMatcher::with_method("eth_sendRawTransaction") + .with_params(json!([MOCK_TRANSACTION.to_string()])) + .with_id(0_u64) +} + fn get_transaction_count_request() -> JsonRpcRequestMatcher { JsonRpcRequestMatcher::with_method("eth_getTransactionCount") .with_params(json!([