Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: move GetLogsArgs and LogEntry to the evm_rpc_types crate #261

Merged
merged 10 commits into from
Aug 28, 2024
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`: transparent wrapper around a Candid type `text` to represent Ethereum hex strings (prefixed by `0x`) containing an unbounded, 20 or 32 bytes, respectively.
gregorydemay marked this conversation as resolved.
Show resolved Hide resolved
- 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 Bytes DATA topics.
gregorydemay marked this conversation as resolved.
Show resolved Hide resolved
/// 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