Skip to content

Commit a45ff3b

Browse files
committed
XC-412: Add client support for eth_getTransactionReceipt
1 parent 9a56210 commit a45ff3b

File tree

7 files changed

+563
-221
lines changed

7 files changed

+563
-221
lines changed

evm_rpc_client/src/lib.rs

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,14 @@ mod runtime;
110110
use crate::request::{
111111
CallRequest, CallRequestBuilder, FeeHistoryRequest, FeeHistoryRequestBuilder,
112112
GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, GetTransactionCountRequest,
113-
GetTransactionCountRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest,
113+
GetTransactionCountRequestBuilder, GetTransactionReceiptRequest,
114+
GetTransactionReceiptRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest,
114115
SendRawTransactionRequestBuilder,
115116
};
116117
use candid::{CandidType, Principal};
117118
use evm_rpc_types::{
118119
BlockTag, CallArgs, ConsensusStrategy, FeeHistoryArgs, GetLogsArgs, GetTransactionCountArgs,
119-
Hex, RpcConfig, RpcServices,
120+
Hex, Hex32, RpcConfig, RpcServices,
120121
};
121122
use ic_error_types::RejectCode;
122123
use request::{GetLogsRequest, GetLogsRequestBuilder};
@@ -555,6 +556,61 @@ impl<R> EvmRpcClient<R> {
555556
)
556557
}
557558

559+
/// Call `eth_getTransactionReceipt` on the EVM RPC canister.
560+
///
561+
/// # Examples
562+
///
563+
/// ```rust
564+
/// use alloy_primitives::b256;
565+
/// use evm_rpc_client::EvmRpcClient;
566+
///
567+
/// # use evm_rpc_types::{Hex20, Hex32, Hex256, HexByte, MultiRpcResult, Nat256};
568+
/// # use std::str::FromStr;
569+
/// # #[tokio::main]
570+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
571+
/// let client = EvmRpcClient::builder_for_ic()
572+
/// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(evm_rpc_types::TransactionReceipt {
573+
/// # block_hash: Hex32::from_str("0xf6084155ff2022773b22df3217d16e9df53cbc42689b27ca4789e06b6339beb2").unwrap(),
574+
/// # block_number: Nat256::from(0x52a975_u64),
575+
/// # effective_gas_price: Nat256::from(0x6052340_u64),
576+
/// # gas_used: Nat256::from(0x1308c_u64),
577+
/// # cumulative_gas_used: Nat256::from(0x797db0_u64),
578+
/// # status: Some(Nat256::from(0x1_u8)),
579+
/// # root: None,
580+
/// # transaction_hash: Hex32::from_str("0xa3ece39ae137617669c6933b7578b94e705e765683f260fcfe30eaa41932610f").unwrap(),
581+
/// # contract_address: None,
582+
/// # from: Hex20::from_str("0xd907941c8b3b966546fc408b8c942eb10a4f98df").unwrap(),
583+
/// # // This receipt contains some transactions, but they are left out here since not asserted in the doctest
584+
/// # logs: vec![],
585+
/// # logs_bloom: Hex256::from_str("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000004010000010100000000000000000000000000000000000000000000000000040000080000000000000080000000000000000000000000000000000000000000020000000000000000000000002000000000000000000000000000000000000000000000000000020000000010000000000000000000000000000000000000000000000000000000000").unwrap(),
586+
/// # to: Some(Hex20::from_str("0xd6df5935cd03a768b7b9e92637a01b25e24cb709").unwrap()),
587+
/// # transaction_index: Nat256::from(0x29_u64),
588+
/// # tx_type: HexByte::from(0x0_u8),
589+
/// # })))
590+
/// .build();
591+
///
592+
/// let result = client
593+
/// .get_transaction_receipt(b256!("0xa3ece39ae137617669c6933b7578b94e705e765683f260fcfe30eaa41932610f"))
594+
/// .send()
595+
/// .await
596+
/// .expect_consistent()
597+
/// .unwrap();
598+
///
599+
/// assert!(result.unwrap().status());
600+
/// # Ok(())
601+
/// # }
602+
/// ```
603+
pub fn get_transaction_receipt(
604+
&self,
605+
params: impl Into<Hex32>,
606+
) -> GetTransactionReceiptRequestBuilder<R> {
607+
RequestBuilder::new(
608+
self.clone(),
609+
GetTransactionReceiptRequest::new(params.into()),
610+
10_000_000_000,
611+
)
612+
}
613+
558614
/// Call `eth_sendRawTransaction` on the EVM RPC canister.
559615
///
560616
/// # Examples

evm_rpc_client/src/request/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,38 @@ impl<R> GetTransactionCountRequestBuilder<R> {
242242
}
243243
}
244244

245+
#[derive(Debug, Clone)]
246+
pub struct GetTransactionReceiptRequest(Hex32);
247+
248+
impl GetTransactionReceiptRequest {
249+
pub fn new(params: Hex32) -> Self {
250+
Self(params)
251+
}
252+
}
253+
254+
impl EvmRpcRequest for GetTransactionReceiptRequest {
255+
type Config = RpcConfig;
256+
type Params = Hex32;
257+
type CandidOutput = MultiRpcResult<Option<evm_rpc_types::TransactionReceipt>>;
258+
type Output = MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>;
259+
260+
fn endpoint(&self) -> EvmRpcEndpoint {
261+
EvmRpcEndpoint::GetTransactionReceipt
262+
}
263+
264+
fn params(self) -> Self::Params {
265+
self.0
266+
}
267+
}
268+
269+
pub type GetTransactionReceiptRequestBuilder<R> = RequestBuilder<
270+
R,
271+
RpcConfig,
272+
Hex32,
273+
MultiRpcResult<Option<evm_rpc_types::TransactionReceipt>>,
274+
MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>,
275+
>;
276+
245277
#[derive(Debug, Clone)]
246278
pub struct SendRawTransactionRequest(Hex);
247279

@@ -305,6 +337,8 @@ pub enum EvmRpcEndpoint {
305337
GetLogs,
306338
/// `eth_getTransactionCount` endpoint.
307339
GetTransactionCount,
340+
/// `eth_getTransactionReceipt` endpoint.
341+
GetTransactionReceipt,
308342
/// `eth_sendRawTransaction` endpoint.
309343
SendRawTransaction,
310344
}
@@ -318,6 +352,7 @@ impl EvmRpcEndpoint {
318352
Self::GetBlockByNumber => "eth_getBlockByNumber",
319353
Self::GetLogs => "eth_getLogs",
320354
Self::GetTransactionCount => "eth_getTransactionCount",
355+
Self::GetTransactionReceipt => "eth_getTransactionReceipt",
321356
Self::SendRawTransaction => "eth_sendRawTransaction",
322357
}
323358
}

evm_rpc_types/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,12 @@ impl_hex_string!(Hex32([u8; 32]));
215215
impl_hex_string!(Hex256([u8; 256]));
216216
impl_hex_string!(Hex(Vec<u8>));
217217

218+
impl HexByte {
219+
pub fn into_byte(self) -> u8 {
220+
self.0.into_byte()
221+
}
222+
}
223+
218224
impl Hex20 {
219225
pub fn as_array(&self) -> &[u8; 20] {
220226
&self.0

evm_rpc_types/src/response/alloy.rs

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use crate::{Block, FeeHistory, Hex32, LogEntry, Nat256, RpcError, ValidationError};
2-
use alloy_primitives::{B256, U256};
1+
use crate::{
2+
Block, FeeHistory, Hex32, HexByte, LogEntry, Nat256, RpcError, RpcResult, TransactionReceipt,
3+
ValidationError,
4+
};
5+
use alloy_primitives::{Address, B256, U256};
36
use alloy_rpc_types::BlockTransactions;
47
use candid::Nat;
58
use num_bigint::BigUint;
@@ -127,19 +130,106 @@ impl TryFrom<FeeHistory> for alloy_rpc_types::FeeHistory {
127130
}
128131
}
129132

133+
impl TryFrom<TransactionReceipt> for alloy_rpc_types::TransactionReceipt {
134+
type Error = RpcError;
135+
136+
fn try_from(receipt: TransactionReceipt) -> Result<Self, Self::Error> {
137+
Ok(Self {
138+
inner: alloy_consensus::ReceiptEnvelope::from_typed(
139+
alloy_consensus::TxType::try_from(receipt.tx_type)?,
140+
alloy_consensus::ReceiptWithBloom {
141+
receipt: alloy_consensus::Receipt {
142+
status: validate_receipt_status(
143+
&receipt.block_number,
144+
receipt.root,
145+
receipt.status,
146+
)?,
147+
cumulative_gas_used: try_from_nat256(
148+
receipt.cumulative_gas_used,
149+
"cumulative_gas_used",
150+
)?,
151+
logs: receipt
152+
.logs
153+
.into_iter()
154+
.map(alloy_rpc_types::Log::try_from)
155+
.collect::<RpcResult<Vec<alloy_rpc_types::Log>>>()?,
156+
},
157+
logs_bloom: alloy_primitives::Bloom::from(receipt.logs_bloom),
158+
},
159+
),
160+
transaction_hash: B256::from(receipt.transaction_hash),
161+
transaction_index: Some(try_from_nat256(
162+
receipt.transaction_index,
163+
"transaction_index",
164+
)?),
165+
block_hash: Some(B256::from(receipt.block_hash)),
166+
block_number: Some(try_from_nat256(receipt.block_number, "block_number")?),
167+
gas_used: try_from_nat256(receipt.gas_used, "gas_used")?,
168+
effective_gas_price: try_from_nat256(
169+
receipt.effective_gas_price,
170+
"effective_gas_price",
171+
)?,
172+
blob_gas_used: None,
173+
blob_gas_price: None,
174+
from: Address::from(receipt.from),
175+
to: receipt.to.map(Address::from),
176+
contract_address: receipt.contract_address.map(Address::from),
177+
})
178+
}
179+
}
180+
181+
impl TryFrom<HexByte> for alloy_consensus::TxType {
182+
type Error = RpcError;
183+
184+
fn try_from(value: HexByte) -> Result<Self, Self::Error> {
185+
alloy_consensus::TxType::try_from(value.into_byte()).map_err(|e| {
186+
RpcError::ValidationError(ValidationError::Custom(format!(
187+
"Unable to parse transaction type: {e:?}"
188+
)))
189+
})
190+
}
191+
}
192+
130193
fn validate_difficulty(number: &Nat256, difficulty: Option<Nat256>) -> Result<U256, RpcError> {
131194
const PARIS_BLOCK: u64 = 15_537_394;
132195
if number.as_ref() < &Nat::from(PARIS_BLOCK) {
133196
difficulty
134197
.map(U256::from)
135198
.ok_or(RpcError::ValidationError(ValidationError::Custom(
136-
"Block before Paris upgrade but missing difficulty".into(),
199+
"Missing difficulty field in pre Paris upgrade block".into(),
137200
)))
138201
} else {
139202
match difficulty.map(U256::from) {
140203
None | Some(U256::ZERO) => Ok(U256::ZERO),
141204
_ => Err(RpcError::ValidationError(ValidationError::Custom(
142-
"Block after Paris upgrade with non-zero difficulty".into(),
205+
"Post Paris upgrade block has non-zero difficulty".into(),
206+
))),
207+
}
208+
}
209+
}
210+
211+
fn validate_receipt_status(
212+
number: &Nat256,
213+
root: Option<Hex32>,
214+
status: Option<Nat256>,
215+
) -> Result<alloy_consensus::Eip658Value, RpcError> {
216+
const BYZANTIUM_BLOCK: u64 = 4_370_000;
217+
if number.as_ref() < &Nat::from(BYZANTIUM_BLOCK) {
218+
match root {
219+
None => Err(RpcError::ValidationError(ValidationError::Custom(
220+
"Missing root field in transaction included before the Byzantium upgrade".into(),
221+
))),
222+
Some(root) => Ok(alloy_consensus::Eip658Value::PostState(B256::from(root))),
223+
}
224+
} else {
225+
match status.map(U256::from) {
226+
None => Err(RpcError::ValidationError(ValidationError::Custom(
227+
"Missing status field in transaction included after the Byzantium upgrade".into(),
228+
))),
229+
Some(U256::ZERO) => Ok(alloy_consensus::Eip658Value::Eip658(false)),
230+
Some(U256::ONE) => Ok(alloy_consensus::Eip658Value::Eip658(true)),
231+
Some(_) => Err(RpcError::ValidationError(ValidationError::Custom(
232+
"Post-Byzantium receipt has invalid status (expected 0 or 1)".into(),
143233
))),
144234
}
145235
}

evm_rpc_types/src/result/alloy.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
Block, FeeHistory, Hex, JsonRpcError, LogEntry, MultiRpcResult, Nat256, RpcError,
3-
SendRawTransactionStatus, ValidationError,
3+
SendRawTransactionStatus, TransactionReceipt, ValidationError,
44
};
55

66
impl From<MultiRpcResult<Vec<LogEntry>>> for MultiRpcResult<Vec<alloy_rpc_types::Log>> {
@@ -59,3 +59,15 @@ impl From<MultiRpcResult<SendRawTransactionStatus>> for MultiRpcResult<alloy_pri
5959
})
6060
}
6161
}
62+
63+
impl From<MultiRpcResult<Option<TransactionReceipt>>>
64+
for MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>
65+
{
66+
fn from(result: MultiRpcResult<Option<TransactionReceipt>>) -> Self {
67+
result.and_then(|maybe_receipt| {
68+
maybe_receipt
69+
.map(alloy_rpc_types::TransactionReceipt::try_from)
70+
.transpose()
71+
})
72+
}
73+
}

tests/mock_http_runtime/mock/json/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ impl CanisterHttpRequestMatcher for JsonRpcRequestMatcher {
142142
}
143143
}
144144

145+
#[derive(Clone)]
145146
pub struct JsonRpcResponse {
146147
pub status: u16,
147148
pub headers: Vec<CanisterHttpHeader>,

0 commit comments

Comments
 (0)