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
60 changes: 58 additions & 2 deletions evm_rpc_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,14 @@ mod runtime;
use crate::request::{
CallRequest, CallRequestBuilder, FeeHistoryRequest, FeeHistoryRequestBuilder,
GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, GetTransactionCountRequest,
GetTransactionCountRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest,
GetTransactionCountRequestBuilder, GetTransactionReceiptRequest,
GetTransactionReceiptRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest,
SendRawTransactionRequestBuilder,
};
use candid::{CandidType, Principal};
use evm_rpc_types::{
BlockTag, CallArgs, ConsensusStrategy, FeeHistoryArgs, GetLogsArgs, GetTransactionCountArgs,
Hex, RpcConfig, RpcServices,
Hex, Hex32, RpcConfig, RpcServices,
};
use ic_error_types::RejectCode;
use request::{GetLogsRequest, GetLogsRequestBuilder};
Expand Down Expand Up @@ -555,6 +556,61 @@ impl<R> EvmRpcClient<R> {
)
}

/// Call `eth_getTransactionReceipt` on the EVM RPC canister.
///
/// # Examples
///
/// ```rust
/// use alloy_primitives::b256;
/// use evm_rpc_client::EvmRpcClient;
///
/// # use evm_rpc_types::{Hex20, Hex32, Hex256, HexByte, MultiRpcResult, Nat256};
/// # 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(evm_rpc_types::TransactionReceipt {
/// # block_hash: Hex32::from_str("0xf6084155ff2022773b22df3217d16e9df53cbc42689b27ca4789e06b6339beb2").unwrap(),
/// # block_number: Nat256::from(0x52a975_u64),
/// # effective_gas_price: Nat256::from(0x6052340_u64),
/// # gas_used: Nat256::from(0x1308c_u64),
/// # cumulative_gas_used: Nat256::from(0x797db0_u64),
/// # status: Some(Nat256::from(0x1_u8)),
/// # root: None,
/// # transaction_hash: Hex32::from_str("0xa3ece39ae137617669c6933b7578b94e705e765683f260fcfe30eaa41932610f").unwrap(),
/// # contract_address: None,
/// # from: Hex20::from_str("0xd907941c8b3b966546fc408b8c942eb10a4f98df").unwrap(),
/// # // This receipt contains some transactions, but they are left out here since not asserted in the doctest
/// # logs: vec![],
/// # logs_bloom: Hex256::from_str("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000004010000010100000000000000000000000000000000000000000000000000040000080000000000000080000000000000000000000000000000000000000000020000000000000000000000002000000000000000000000000000000000000000000000000000020000000010000000000000000000000000000000000000000000000000000000000").unwrap(),
/// # to: Some(Hex20::from_str("0xd6df5935cd03a768b7b9e92637a01b25e24cb709").unwrap()),
/// # transaction_index: Nat256::from(0x29_u64),
/// # tx_type: HexByte::from(0x0_u8),
/// # })))
/// .build();
///
/// let result = client
/// .get_transaction_receipt(b256!("0xa3ece39ae137617669c6933b7578b94e705e765683f260fcfe30eaa41932610f"))
/// .send()
/// .await
/// .expect_consistent()
/// .unwrap();
///
/// assert!(result.unwrap().status());
/// # Ok(())
/// # }
/// ```
pub fn get_transaction_receipt(
&self,
params: impl Into<Hex32>,
) -> GetTransactionReceiptRequestBuilder<R> {
RequestBuilder::new(
self.clone(),
GetTransactionReceiptRequest::new(params.into()),
10_000_000_000,
)
}

/// Call `eth_sendRawTransaction` on the EVM RPC canister.
///
/// # Examples
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 GetTransactionReceiptRequest(Hex32);

impl GetTransactionReceiptRequest {
pub fn new(params: Hex32) -> Self {
Self(params)
}
}

impl EvmRpcRequest for GetTransactionReceiptRequest {
type Config = RpcConfig;
type Params = Hex32;
type CandidOutput = MultiRpcResult<Option<evm_rpc_types::TransactionReceipt>>;
type Output = MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>;

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

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

pub type GetTransactionReceiptRequestBuilder<R> = RequestBuilder<
R,
RpcConfig,
Hex32,
MultiRpcResult<Option<evm_rpc_types::TransactionReceipt>>,
MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>,
>;

#[derive(Debug, Clone)]
pub struct SendRawTransactionRequest(Hex);

Expand Down Expand Up @@ -305,6 +337,8 @@ pub enum EvmRpcEndpoint {
GetLogs,
/// `eth_getTransactionCount` endpoint.
GetTransactionCount,
/// `eth_getTransactionReceipt` endpoint.
GetTransactionReceipt,
/// `eth_sendRawTransaction` endpoint.
SendRawTransaction,
}
Expand All @@ -318,6 +352,7 @@ impl EvmRpcEndpoint {
Self::GetBlockByNumber => "eth_getBlockByNumber",
Self::GetLogs => "eth_getLogs",
Self::GetTransactionCount => "eth_getTransactionCount",
Self::GetTransactionReceipt => "eth_getTransactionReceipt",
Self::SendRawTransaction => "eth_sendRawTransaction",
}
}
Expand Down
6 changes: 6 additions & 0 deletions evm_rpc_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ impl_hex_string!(Hex32([u8; 32]));
impl_hex_string!(Hex256([u8; 256]));
impl_hex_string!(Hex(Vec<u8>));

impl HexByte {
pub fn into_byte(self) -> u8 {
self.0.into_byte()
}
}

impl Hex20 {
pub fn as_array(&self) -> &[u8; 20] {
&self.0
Expand Down
98 changes: 94 additions & 4 deletions evm_rpc_types/src/response/alloy.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::{Block, FeeHistory, Hex32, LogEntry, Nat256, RpcError, ValidationError};
use alloy_primitives::{B256, U256};
use crate::{
Block, FeeHistory, Hex32, HexByte, LogEntry, Nat256, RpcError, RpcResult, TransactionReceipt,
ValidationError,
};
use alloy_primitives::{Address, B256, U256};
use alloy_rpc_types::BlockTransactions;
use candid::Nat;
use num_bigint::BigUint;
Expand Down Expand Up @@ -127,19 +130,106 @@ impl TryFrom<FeeHistory> for alloy_rpc_types::FeeHistory {
}
}

impl TryFrom<TransactionReceipt> for alloy_rpc_types::TransactionReceipt {
type Error = RpcError;

fn try_from(receipt: TransactionReceipt) -> Result<Self, Self::Error> {
Ok(Self {
inner: alloy_consensus::ReceiptEnvelope::from_typed(
alloy_consensus::TxType::try_from(receipt.tx_type)?,
alloy_consensus::ReceiptWithBloom {
receipt: alloy_consensus::Receipt {
status: validate_receipt_status(
&receipt.block_number,
receipt.root,
receipt.status,
)?,
cumulative_gas_used: try_from_nat256(
receipt.cumulative_gas_used,
"cumulative_gas_used",
)?,
logs: receipt
.logs
.into_iter()
.map(alloy_rpc_types::Log::try_from)
.collect::<RpcResult<Vec<alloy_rpc_types::Log>>>()?,
},
logs_bloom: alloy_primitives::Bloom::from(receipt.logs_bloom),
},
),
transaction_hash: B256::from(receipt.transaction_hash),
transaction_index: Some(try_from_nat256(
receipt.transaction_index,
"transaction_index",
)?),
block_hash: Some(B256::from(receipt.block_hash)),
block_number: Some(try_from_nat256(receipt.block_number, "block_number")?),
gas_used: try_from_nat256(receipt.gas_used, "gas_used")?,
effective_gas_price: try_from_nat256(
receipt.effective_gas_price,
"effective_gas_price",
)?,
blob_gas_used: None,
blob_gas_price: None,
from: Address::from(receipt.from),
to: receipt.to.map(Address::from),
contract_address: receipt.contract_address.map(Address::from),
})
}
}

impl TryFrom<HexByte> for alloy_consensus::TxType {
type Error = RpcError;

fn try_from(value: HexByte) -> Result<Self, Self::Error> {
alloy_consensus::TxType::try_from(value.into_byte()).map_err(|e| {
RpcError::ValidationError(ValidationError::Custom(format!(
"Unable to parse transaction type: {e:?}"
)))
})
}
}

fn validate_difficulty(number: &Nat256, difficulty: Option<Nat256>) -> Result<U256, RpcError> {
const PARIS_BLOCK: u64 = 15_537_394;
if number.as_ref() < &Nat::from(PARIS_BLOCK) {
difficulty
.map(U256::from)
.ok_or(RpcError::ValidationError(ValidationError::Custom(
"Block before Paris upgrade but missing difficulty".into(),
"Missing difficulty field in pre Paris upgrade block".into(),
)))
} else {
match difficulty.map(U256::from) {
None | Some(U256::ZERO) => Ok(U256::ZERO),
_ => Err(RpcError::ValidationError(ValidationError::Custom(
"Block after Paris upgrade with non-zero difficulty".into(),
"Post Paris upgrade block has non-zero difficulty".into(),
))),
}
}
}

fn validate_receipt_status(
number: &Nat256,
root: Option<Hex32>,
status: Option<Nat256>,
) -> Result<alloy_consensus::Eip658Value, RpcError> {
const BYZANTIUM_BLOCK: u64 = 4_370_000;
if number.as_ref() < &Nat::from(BYZANTIUM_BLOCK) {
match root {
None => Err(RpcError::ValidationError(ValidationError::Custom(
"Missing root field in transaction included before the Byzantium upgrade".into(),
))),
Some(root) => Ok(alloy_consensus::Eip658Value::PostState(B256::from(root))),
}
} else {
match status.map(U256::from) {
None => Err(RpcError::ValidationError(ValidationError::Custom(
"Missing status field in transaction included after the Byzantium upgrade".into(),
))),
Some(U256::ZERO) => Ok(alloy_consensus::Eip658Value::Eip658(false)),
Some(U256::ONE) => Ok(alloy_consensus::Eip658Value::Eip658(true)),
Some(_) => Err(RpcError::ValidationError(ValidationError::Custom(
"Post-Byzantium receipt has invalid status (expected 0 or 1)".into(),
))),
}
}
Expand Down
14 changes: 13 additions & 1 deletion evm_rpc_types/src/result/alloy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
Block, FeeHistory, Hex, JsonRpcError, LogEntry, MultiRpcResult, Nat256, RpcError,
SendRawTransactionStatus, ValidationError,
SendRawTransactionStatus, TransactionReceipt, ValidationError,
};

impl From<MultiRpcResult<Vec<LogEntry>>> for MultiRpcResult<Vec<alloy_rpc_types::Log>> {
Expand Down Expand Up @@ -59,3 +59,15 @@ impl From<MultiRpcResult<SendRawTransactionStatus>> for MultiRpcResult<alloy_pri
})
}
}

impl From<MultiRpcResult<Option<TransactionReceipt>>>
for MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>
{
fn from(result: MultiRpcResult<Option<TransactionReceipt>>) -> Self {
result.and_then(|maybe_receipt| {
maybe_receipt
.map(alloy_rpc_types::TransactionReceipt::try_from)
.transpose()
})
}
}
1 change: 1 addition & 0 deletions tests/mock_http_runtime/mock/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl CanisterHttpRequestMatcher for JsonRpcRequestMatcher {
}
}

#[derive(Clone)]
pub struct JsonRpcResponse {
pub status: u16,
pub headers: Vec<CanisterHttpHeader>,
Expand Down
Loading