Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions evm_rpc_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -553,6 +554,43 @@ impl<R> EvmRpcClient<R> {
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<dyn std::error::Error>> {
/// 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"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a follow-up PR: here we could build an actual transaction with alloy and sign it with t-sig. The example then becomes rather long though, especially with signing. Some ideas:

  • add a helper function (similar to the one for the SOL RPC client) for signing alloy transactions
  • add support for eth_sendTransaction where we wouldn't need to do the transaction serialization

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea!

/// .send()
/// .await
/// .expect_consistent();
///
/// assert_eq!(result, Ok(b256!("0x33469b22e9f636356c4160a87eb19df52b7412e8eac32a4a55ffe88ea8350788")));
/// # Ok(())
/// # }
/// ```
pub fn send_raw_transaction(
&self,
params: impl Into<Hex>,
) -> SendRawTransactionRequestBuilder<R> {
RequestBuilder::new(
self.clone(),
SendRawTransactionRequest::new(params.into()),
10_000_000_000,
)
}
}

impl<R: Runtime> EvmRpcClient<R> {
Expand Down
35 changes: 35 additions & 0 deletions evm_rpc_client/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,38 @@ impl<R> GetTransactionCountRequestBuilder<R> {
}
}

#[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<evm_rpc_types::SendRawTransactionStatus>;
type Output = MultiRpcResult<alloy_primitives::B256>;

fn endpoint(&self) -> EvmRpcEndpoint {
EvmRpcEndpoint::SendRawTransaction
}

fn params(self) -> Self::Params {
self.0
}
}

pub type SendRawTransactionRequestBuilder<R> = RequestBuilder<
R,
RpcConfig,
Hex,
MultiRpcResult<evm_rpc_types::SendRawTransactionStatus>,
MultiRpcResult<alloy_primitives::B256>,
>;

/// Ethereum RPC endpoint supported by the EVM RPC canister.
pub trait EvmRpcRequest {
/// Type of RPC config for that request.
Expand Down Expand Up @@ -273,6 +305,8 @@ pub enum EvmRpcEndpoint {
GetLogs,
/// `eth_getTransactionCount` endpoint.
GetTransactionCount,
/// `eth_sendRawTransaction` endpoint.
SendRawTransaction,
}

impl EvmRpcEndpoint {
Expand All @@ -284,6 +318,7 @@ impl EvmRpcEndpoint {
Self::GetBlockByNumber => "eth_getBlockByNumber",
Self::GetLogs => "eth_getLogs",
Self::GetTransactionCount => "eth_getTransactionCount",
Self::SendRawTransaction => "eth_sendRawTransaction",
}
}
}
Expand Down
28 changes: 27 additions & 1 deletion evm_rpc_types/src/result/alloy.rs
Original file line number Diff line number Diff line change
@@ -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<MultiRpcResult<Vec<LogEntry>>> for MultiRpcResult<Vec<alloy_rpc_types::Log>> {
fn from(result: MultiRpcResult<Vec<LogEntry>>) -> Self {
Expand Down Expand Up @@ -33,3 +36,26 @@ impl From<MultiRpcResult<Hex>> for MultiRpcResult<alloy_primitives::Bytes> {
result.map(alloy_primitives::Bytes::from)
}
}

impl From<MultiRpcResult<SendRawTransactionStatus>> for MultiRpcResult<alloy_primitives::B256> {
fn from(result: MultiRpcResult<SendRawTransactionStatus>) -> 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregorydemay I noticed that we do some extra parsing for the result of eth_sendRawTransaction and return a non-API result (i.e. some JSON-RPC errors get converted to an Ok(SendRawTransactionStatus)). Obviously alloy doesn't have an equivalent type and normally one would just get the raw JSON RPC error.

IIUC the JSON-RPC processing is done to avoid getting inconsistent results due to differing error codes/messages between providers. However, I think it would be also OK to return the canonicalized errors still as a JSON-RPC error, just with a standardized error message and code.

In terms of error canonicalization, I think the exact message is not very important, but the code is less clear to me. Most providers return something along the lines of -32_000 (see here), and according to the JSON-RPC spec it's the beginning of the "Server Error" range, so IMO that could be a reasonable value.

Another direction, would be to add a new optional flag to the RPC config for eth_sendRawTransaction that disables the error processing for that call. The problem there is that we would then again get inconsistent results due to the different providers' differing error messages/codes...

Finally, we could either create a new SendRawTransactionStatus where the Ok variant contains a B256 or return a Result<Option<B256>,SendRawTransactionError> where SendRawTransactionError is an enum with variants for InsufficientFunds, NonceTooLow and NonceTooHigh. This would be probably the simplest option, and I'm not a fan of returning a JSON error wrapped in an Ok but this is closest to the Candid interface.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think the OK return type should be B256 and not Option<B256>. The reason why the canister return an optional is because the RLP decoding may fail and we want to be lenient and still send the transaction. However, here with the client we will start by a transaction built with alloy so we should always be able to compute that transaction hash, no?
  2. I think the error back in a JSON-RPC error is not a bad idea and matches what you would get when using alloy directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the OK return type should be B256 and not Option. The reason why the canister return an optional is because the RLP decoding may fail and we want to be lenient and still send the transaction. However, here with the client we will start by a transaction built with alloy so we should always be able to compute that transaction hash, no?

Ah I see, that makes sense. I've changed the return type to be B256 and not an Option<B256>. I think I got confused because it seems some providers sometimes return a null transaction hash if there was an error, but it seems that's not really API-compliant and is also not consistent with alloy.

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(),
})),
})
}
}
Loading