Skip to content

Commit

Permalink
refactor: move GetLogsArgs and LogEntry to the evm_rpc_types cr…
Browse files Browse the repository at this point in the history
…ate (#261)

Follow-up on #257 to move the types `GetLogsArgs` and `LogEntry` to the `evm_rpc_types` crate, so that the public API of `eth_get_logs` only uses types from that crate.

Additionally, add the types `Hex`, `Hex20`, and `Hex32` as a transparent wrapper around a Candid type `text` to represent Ethereum hex strings (prefixed by `0x`) containing an unbounded, 20 or 32 bytes, respectively.
  • Loading branch information
gregorydemay authored Aug 28, 2024
1 parent c4f1bb3 commit 0a39ab5
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 89 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ assert_matches = "1.5"
[workspace.dependencies]
candid = { version = "0.9" }
getrandom = { version = "0.2", features = ["custom"] }
hex = "0.4.3"
ic-canisters-http-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" }
ic-nervous-system-common = { git = "https://github.com/dfinity/ic", rev = "release-2023-09-27_23-01" }
ic-metrics-encoder = "1.1"
Expand Down
4 changes: 3 additions & 1 deletion evm_rpc_types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- v1.0 `Nat256`: transparent wrapper around a `Nat` to guarantee that it fits in 256 bits.
- v1.0 `Hex`, `Hex20`, and `Hex32`: Candid types wrapping an amount of bytes (`Vec<u8>` for `Hex` and `[u8; N]` for `HexN`) that can be represented as an hexadecimal string (prefixed by `0x`) when serialized.
- v1.0 Move `BlockTag` to this crate.
- v1.0 Move `FeeHistoryArgs` and `FeeHistory` to this crate.
- v1.0 Move `FeeHistoryArgs` and `FeeHistory` to this crate.
- v1.0 Move `GetLogsArgs` and `LogEntry` to this crate.
1 change: 1 addition & 0 deletions evm_rpc_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"

[dependencies]
candid = { workspace = true }
hex = { workspace = true }
num-bigint = { workspace = true }
serde = { workspace = true }

Expand Down
78 changes: 76 additions & 2 deletions evm_rpc_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ use candid::types::{Serializer, Type};
use candid::{CandidType, Nat};
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use std::str::FromStr;

mod request;
mod response;

pub use request::FeeHistoryArgs;
pub use response::FeeHistory;
pub use request::{FeeHistoryArgs, GetLogsArgs};
pub use response::{FeeHistory, LogEntry};

#[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize, Default)]
pub enum BlockTag {
Expand Down Expand Up @@ -94,3 +96,75 @@ macro_rules! impl_from_unchecked {
}
// all the types below are guaranteed to fit in 256 bits
impl_from_unchecked!( Nat256, usize u8 u16 u32 u64 u128 );

macro_rules! impl_hex_string {
($name: ident($data: ty)) => {
#[doc = concat!("Ethereum hex-string (String representation is prefixed by 0x) wrapping a `", stringify!($data), "`. ")]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct $name($data);

impl std::fmt::Display for $name {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str("0x")?;
f.write_str(&hex::encode(&self.0))
}
}

impl From<$data> for $name {
fn from(value: $data) -> Self {
Self(value)
}
}

impl From<$name> for $data {
fn from(value: $name) -> Self {
value.0
}
}

impl CandidType for $name {
fn _ty() -> Type {
String::_ty()
}

fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: Serializer,
{
serializer.serialize_text(&self.to_string())
}
}

impl FromStr for $name {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.starts_with("0x") {
return Err("Ethereum hex string doesn't start with 0x".to_string());
}
hex::FromHex::from_hex(&s[2..])
.map(Self)
.map_err(|e| format!("Invalid Ethereum hex string: {}", e))
}
}

impl TryFrom<String> for $name {
type Error = String;

fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}

impl From<$name> for String {
fn from(value: $name) -> Self {
value.to_string()
}
}
};
}

impl_hex_string!(Hex20([u8; 20]));
impl_hex_string!(Hex32([u8; 32]));
impl_hex_string!(Hex(Vec<u8>));
21 changes: 20 additions & 1 deletion evm_rpc_types/src/request/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{BlockTag, Nat256};
use crate::{BlockTag, Hex20, Hex32, Nat256};
use candid::CandidType;
use serde::Deserialize;

Expand All @@ -21,3 +21,22 @@ pub struct FeeHistoryArgs {
#[serde(rename = "rewardPercentiles")]
pub reward_percentiles: Option<Vec<u8>>,
}

#[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)]
pub struct GetLogsArgs {
/// Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
#[serde(rename = "fromBlock")]
pub from_block: Option<BlockTag>,

/// Integer block number, or "latest" for the last mined block or "pending", "earliest" for not yet mined transactions.
#[serde(rename = "toBlock")]
pub to_block: Option<BlockTag>,

/// Contract address or a list of addresses from which logs should originate.
pub addresses: Vec<Hex20>,

/// Array of 32-byte DATA topics.
/// Topics are order-dependent.
/// Each topic can also be an array of DATA with "or" options.
pub topics: Option<Vec<Vec<Hex32>>>,
}
46 changes: 45 additions & 1 deletion evm_rpc_types/src/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::Nat256;
use crate::{Hex, Hex20, Hex32, Nat256};
use candid::CandidType;
use serde::{Deserialize, Serialize};

Expand All @@ -23,3 +23,47 @@ pub struct FeeHistory {
#[serde(rename = "reward")]
pub reward: Vec<Vec<Nat256>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, CandidType)]
pub struct LogEntry {
/// The address from which this log originated.
pub address: Hex20,

/// Array of 0 to 4 32-byte DATA elements of indexed log arguments.
/// In solidity: The first topic is the event signature hash (e.g. Deposit(address,bytes32,uint256)),
/// unless you declared the event with the anonymous specifier.
pub topics: Vec<Hex32>,

/// Contains one or more 32-byte non-indexed log arguments.
pub data: Hex,

/// The block number in which this log appeared.
/// None if the block is pending.
#[serde(rename = "blockNumber")]
pub block_number: Option<Nat256>,

/// 32-byte hash of the transaction from which this log was created.
/// None if the transaction is still pending.
#[serde(rename = "transactionHash")]
pub transaction_hash: Option<Hex32>,

/// Integer of the transaction's position within the block the log was created from.
/// None if the transaction is still pending.
#[serde(rename = "transactionIndex")]
pub transaction_index: Option<Nat256>,

/// 32-byte hash of the block in which this log appeared.
/// None if the block is pending.
#[serde(rename = "blockHash")]
pub block_hash: Option<Hex32>,

/// Integer of the log index position in the block.
/// None if the log is pending.
#[serde(rename = "logIndex")]
pub log_index: Option<Nat256>,

/// "true" when the log was removed due to a chain reorganization.
/// "false" if it is a valid log.
#[serde(default)]
pub removed: bool,
}
80 changes: 80 additions & 0 deletions evm_rpc_types/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,83 @@ mod nat256 {
uniform32(any::<u8>()).prop_map(|value| BigUint::from_bytes_be(&value))
}
}

mod hex_string {
use crate::{Hex, Hex20, Hex32};
use candid::{CandidType, Decode, Encode};
use proptest::prelude::{Strategy, TestCaseError};
use proptest::{prop_assert, prop_assert_eq, proptest};
use serde::de::DeserializeOwned;
use std::ops::RangeInclusive;
use std::str::FromStr;

proptest! {
#[test]
fn should_encode_decode(
hex20 in arb_var_len_hex_string(20..=20_usize),
hex32 in arb_var_len_hex_string(32..=32_usize),
hex in arb_var_len_hex_string(0..=100_usize)
) {
encode_decode_roundtrip::<Hex20>(&hex20)?;
encode_decode_roundtrip::<Hex32>(&hex32)?;
encode_decode_roundtrip::<Hex>(&hex)?;
}

#[test]
fn should_fail_to_decode_strings_with_wrong_length(
short_hex20 in arb_var_len_hex_string(0..=19_usize),
long_hex20 in arb_var_len_hex_string(21..=100_usize),
short_hex32 in arb_var_len_hex_string(0..=31_usize),
long_hex32 in arb_var_len_hex_string(33..=100_usize),
) {
let decoded_short_hex20 = Decode!(&Encode!(&short_hex20).unwrap(), Hex20);
let decoded_long_hex20 = Decode!(&Encode!(&long_hex20).unwrap(), Hex20);
for result in [decoded_short_hex20, decoded_long_hex20] {
prop_assert!(
result.is_err(),
"Expected error decoding hex20 with wrong length, got: {:?}",
result
);
}

let decoded_short_hex32 = Decode!(&Encode!(&short_hex32).unwrap(), Hex32);
let decoded_long_hex32 = Decode!(&Encode!(&long_hex32).unwrap(), Hex32);
for result in [decoded_short_hex32, decoded_long_hex32] {
prop_assert!(
result.is_err(),
"Expected error decoding hex32 with wrong length, got: {:?}",
result
);
}
}
}

fn encode_decode_roundtrip<T>(value: &str) -> Result<(), TestCaseError>
where
T: FromStr + CandidType + DeserializeOwned + PartialEq + std::fmt::Debug,
<T as FromStr>::Err: std::fmt::Debug,
{
let hex: T = value.parse().unwrap();

let encoded_text_value = Encode!(&value.to_lowercase()).unwrap();
let encoded_hex = Encode!(&hex).unwrap();
prop_assert_eq!(
&encoded_text_value,
&encoded_hex,
"Encode value differ for {}",
value
);

let decoded_hex = Decode!(&encoded_text_value, T).unwrap();
prop_assert_eq!(&decoded_hex, &hex, "Decode value differ for {}", value);
Ok(())
}

fn arb_var_len_hex_string(
num_bytes_range: RangeInclusive<usize>,
) -> impl Strategy<Value = String> {
num_bytes_range.prop_flat_map(|num_bytes| {
proptest::string::string_regex(&format!("0x[0-9a-fA-F]{{{}}}", 2 * num_bytes)).unwrap()
})
}
}
27 changes: 15 additions & 12 deletions src/candid_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ mod cketh_conversion;
use std::str::FromStr;

use async_trait::async_trait;
use candid::Nat;
use cketh_common::{
eth_rpc::{
into_nat, Block, GetLogsParam, Hash, LogEntry, ProviderError, RpcError,
SendRawTransactionResult, ValidationError,
into_nat, Block, Hash, ProviderError, RpcError, SendRawTransactionResult, ValidationError,
},
eth_rpc_client::{
providers::{RpcApi, RpcService},
Expand Down Expand Up @@ -182,14 +182,17 @@ impl CandidRpcClient {

pub async fn eth_get_logs(
&self,
args: candid_types::GetLogsArgs,
) -> MultiRpcResult<Vec<LogEntry>> {
args: evm_rpc_types::GetLogsArgs,
) -> MultiRpcResult<Vec<evm_rpc_types::LogEntry>> {
use crate::candid_rpc::cketh_conversion::{from_log_entries, into_get_logs_param};

if let (
Some(candid_types::BlockTag::Number(from)),
Some(candid_types::BlockTag::Number(to)),
Some(evm_rpc_types::BlockTag::Number(from)),
Some(evm_rpc_types::BlockTag::Number(to)),
) = (&args.from_block, &args.to_block)
{
let (from, to) = (candid::Nat::from(*from), candid::Nat::from(*to));
let from = Nat::from(from.clone());
let to = Nat::from(to.clone());
let block_count = if to > from { to - from } else { from - to };
if block_count > ETH_GET_LOGS_MAX_BLOCKS {
return MultiRpcResult::Consistent(Err(ValidationError::Custom(format!(
Expand All @@ -199,11 +202,11 @@ impl CandidRpcClient {
.into()));
}
}
let args: GetLogsParam = match args.try_into() {
Ok(args) => args,
Err(err) => return MultiRpcResult::Consistent(Err(RpcError::from(err))),
};
process_result(RpcMethod::EthGetLogs, self.client.eth_get_logs(args).await)
process_result(
RpcMethod::EthGetLogs,
self.client.eth_get_logs(into_get_logs_param(args)).await,
)
.map(from_log_entries)
}

pub async fn eth_get_block_by_number(
Expand Down
Loading

0 comments on commit 0a39ab5

Please sign in to comment.