From 1a95727b5855ca5afa9359c25e03a7ccd266e579 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 20:39:39 +0100 Subject: [PATCH 1/6] feat: fork from ethers --- crates/test-utils/Cargo.toml | 12 + crates/test-utils/src/anvil.rs | 331 ++++++++ crates/test-utils/src/ganache.rs | 235 ++++++ crates/test-utils/src/genesis.rs | 998 +++++++++++++++++++++++ crates/test-utils/src/geth.rs | 681 ++++++++++++++++ crates/test-utils/src/hash.rs | 101 +++ crates/test-utils/src/lib.rs | 1260 +++++++++++++++++++++++++++++ crates/test-utils/src/moonbeam.rs | 115 +++ crates/test-utils/src/units.rs | 173 ++++ 9 files changed, 3906 insertions(+) create mode 100644 crates/test-utils/Cargo.toml create mode 100644 crates/test-utils/src/anvil.rs create mode 100644 crates/test-utils/src/ganache.rs create mode 100644 crates/test-utils/src/genesis.rs create mode 100644 crates/test-utils/src/geth.rs create mode 100644 crates/test-utils/src/hash.rs create mode 100644 crates/test-utils/src/lib.rs create mode 100644 crates/test-utils/src/moonbeam.rs create mode 100644 crates/test-utils/src/units.rs diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml new file mode 100644 index 00000000000..245c273e527 --- /dev/null +++ b/crates/test-utils/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "alloy-test-utils" +description = "Common Ethereum testing utilities" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true diff --git a/crates/test-utils/src/anvil.rs b/crates/test-utils/src/anvil.rs new file mode 100644 index 00000000000..666c939cb06 --- /dev/null +++ b/crates/test-utils/src/anvil.rs @@ -0,0 +1,331 @@ +use crate::{ + types::{Address, Chain}, + utils::{secret_key_to_address, unused_port}, +}; +use generic_array::GenericArray; +use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; +use std::{ + io::{BufRead, BufReader}, + path::PathBuf, + process::{Child, Command}, + time::{Duration, Instant}, +}; + +/// How long we will wait for anvil to indicate that it is ready. +const ANVIL_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; + +/// An anvil CLI instance. Will close the instance when dropped. +/// +/// Construct this using [`Anvil`]. +pub struct AnvilInstance { + pid: Child, + private_keys: Vec, + addresses: Vec
, + port: u16, + chain_id: Option, +} + +impl AnvilInstance { + /// Returns the private keys used to instantiate this instance + pub fn keys(&self) -> &[K256SecretKey] { + &self.private_keys + } + + /// Returns the addresses used to instantiate this instance + pub fn addresses(&self) -> &[Address] { + &self.addresses + } + + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the chain of the anvil instance + pub fn chain_id(&self) -> u64 { + self.chain_id.unwrap_or_else(|| Chain::AnvilHardhat.into()) + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } +} + +impl Drop for AnvilInstance { + fn drop(&mut self) { + self.pid.kill().expect("could not kill anvil"); + } +} + +/// Builder for launching `anvil`. +/// +/// # Panics +/// +/// If `spawn` is called without `anvil` being available in the user's $PATH +/// +/// # Example +/// +/// ```no_run +/// use ethers_core::utils::Anvil; +/// +/// let port = 8545u16; +/// let url = format!("http://localhost:{}", port).to_string(); +/// +/// let anvil = Anvil::new() +/// .port(port) +/// .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") +/// .spawn(); +/// +/// drop(anvil); // this will kill the instance +/// ``` +#[derive(Debug, Clone, Default)] +#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] +pub struct Anvil { + program: Option, + port: Option, + block_time: Option, + chain_id: Option, + mnemonic: Option, + fork: Option, + fork_block_number: Option, + args: Vec, + timeout: Option, +} + +impl Anvil { + /// Creates an empty Anvil builder. + /// The default port is 8545. The mnemonic is chosen randomly. + /// + /// # Example + /// + /// ``` + /// # use ethers_core::utils::Anvil; + /// fn a() { + /// let anvil = Anvil::default().spawn(); + /// + /// println!("Anvil running at `{}`", anvil.endpoint()); + /// # } + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Creates an Anvil builder which will execute `anvil` at the given path. + /// + /// # Example + /// + /// ``` + /// # use ethers_core::utils::Anvil; + /// fn a() { + /// let anvil = Anvil::at("~/.foundry/bin/anvil").spawn(); + /// + /// println!("Anvil running at `{}`", anvil.endpoint()); + /// # } + /// ``` + pub fn at(path: impl Into) -> Self { + Self::new().path(path) + } + + /// Sets the `path` to the `anvil` cli + /// + /// By default, it's expected that `anvil` is in `$PATH`, see also + /// [`std::process::Command::new()`] + pub fn path>(mut self, path: T) -> Self { + self.program = Some(path.into()); + self + } + + /// Sets the port which will be used when the `anvil` instance is launched. + pub fn port>(mut self, port: T) -> Self { + self.port = Some(port.into()); + self + } + + /// Sets the chain_id the `anvil` instance will use. + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + + /// Sets the mnemonic which will be used when the `anvil` instance is launched. + pub fn mnemonic>(mut self, mnemonic: T) -> Self { + self.mnemonic = Some(mnemonic.into()); + self + } + + /// Sets the block-time in seconds which will be used when the `anvil` instance is launched. + pub fn block_time>(mut self, block_time: T) -> Self { + self.block_time = Some(block_time.into()); + self + } + + /// Sets the `fork-block-number` which will be used in addition to [`Self::fork`]. + /// + /// **Note:** if set, then this requires `fork` to be set as well + pub fn fork_block_number>(mut self, fork_block_number: T) -> Self { + self.fork_block_number = Some(fork_block_number.into()); + self + } + + /// Sets the `fork` argument to fork from another currently running Ethereum client + /// at a given block. Input should be the HTTP location and port of the other client, + /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from + /// using an @ sign: `http://localhost:8545@1599200` + pub fn fork>(mut self, fork: T) -> Self { + self.fork = Some(fork.into()); + self + } + + /// Adds an argument to pass to the `anvil`. + pub fn arg>(mut self, arg: T) -> Self { + self.args.push(arg.into()); + self + } + + /// Adds multiple arguments to pass to the `anvil`. + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.arg(arg); + } + self + } + + /// Sets the timeout which will be used when the `anvil` instance is launched. + pub fn timeout>(mut self, timeout: T) -> Self { + self.timeout = Some(timeout.into()); + self + } + + /// Consumes the builder and spawns `anvil`. + /// + /// # Panics + /// + /// If spawning the instance fails at any point. + #[track_caller] + pub fn spawn(self) -> AnvilInstance { + let mut cmd = if let Some(ref prg) = self.program { + Command::new(prg) + } else { + Command::new("anvil") + }; + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit()); + let port = if let Some(port) = self.port { port } else { unused_port() }; + cmd.arg("-p").arg(port.to_string()); + + if let Some(mnemonic) = self.mnemonic { + cmd.arg("-m").arg(mnemonic); + } + + if let Some(chain_id) = self.chain_id { + cmd.arg("--chain-id").arg(chain_id.to_string()); + } + + if let Some(block_time) = self.block_time { + cmd.arg("-b").arg(block_time.to_string()); + } + + if let Some(fork) = self.fork { + cmd.arg("-f").arg(fork); + } + + if let Some(fork_block_number) = self.fork_block_number { + cmd.arg("--fork-block-number").arg(fork_block_number.to_string()); + } + + cmd.args(self.args); + + let mut child = cmd.spawn().expect("couldnt start anvil"); + + let stdout = child.stdout.take().expect("Unable to get stdout for anvil child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stdout); + + let mut private_keys = Vec::new(); + let mut addresses = Vec::new(); + let mut is_private_key = false; + let mut chain_id = None; + loop { + if start + Duration::from_millis(self.timeout.unwrap_or(ANVIL_STARTUP_TIMEOUT_MILLIS)) <= + Instant::now() + { + panic!("Timed out waiting for anvil to start. Is anvil installed?") + } + + let mut line = String::new(); + reader.read_line(&mut line).expect("Failed to read line from anvil process"); + if line.contains("Listening on") { + break + } + + if line.starts_with("Private Keys") { + is_private_key = true; + } + + if is_private_key && line.starts_with('(') { + let key_str = line + .split("0x") + .last() + .unwrap_or_else(|| panic!("could not parse private key: {}", line)) + .trim(); + let key_hex = hex::decode(key_str).expect("could not parse as hex"); + let key = K256SecretKey::from_bytes(&GenericArray::clone_from_slice(&key_hex)) + .expect("did not get private key"); + addresses.push(secret_key_to_address(&SigningKey::from(&key))); + private_keys.push(key); + } + + if let Some(start_chain_id) = line.find("Chain ID:") { + let rest = &line[start_chain_id + "Chain ID:".len()..]; + if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::() { + chain_id = Some(chain); + }; + } + } + + AnvilInstance { + pid: child, + private_keys, + addresses, + port, + chain_id: self.chain_id.or(chain_id), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_launch_anvil() { + let _ = Anvil::new().spawn(); + } + + #[test] + fn can_launch_anvil_with_more_accounts() { + let _ = Anvil::new().arg("--accounts").arg("20").spawn(); + } + + #[test] + fn assert_chain_id() { + let anvil = Anvil::new().fork("https://rpc.ankr.com/eth").spawn(); + assert_eq!(anvil.chain_id(), 1); + } + + #[test] + fn assert_chain_id_without_rpc() { + let anvil = Anvil::new().spawn(); + assert_eq!(anvil.chain_id(), 31337); + } +} diff --git a/crates/test-utils/src/ganache.rs b/crates/test-utils/src/ganache.rs new file mode 100644 index 00000000000..42141225c49 --- /dev/null +++ b/crates/test-utils/src/ganache.rs @@ -0,0 +1,235 @@ +use crate::{ + types::Address, + utils::{secret_key_to_address, unused_port}, +}; +use generic_array::GenericArray; +use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; +use std::{ + io::{BufRead, BufReader}, + process::{Child, Command}, + time::{Duration, Instant}, +}; + +/// Default amount of time we will wait for ganache to indicate that it is ready. +const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; + +/// A ganache CLI instance. Will close the instance when dropped. +/// +/// Construct this using [`Ganache`]. +pub struct GanacheInstance { + pid: Child, + private_keys: Vec, + addresses: Vec
, + port: u16, +} + +impl GanacheInstance { + /// Returns the private keys used to instantiate this instance + pub fn keys(&self) -> &[K256SecretKey] { + &self.private_keys + } + + /// Returns the addresses used to instantiate this instance + pub fn addresses(&self) -> &[Address] { + &self.addresses + } + + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } +} + +impl Drop for GanacheInstance { + fn drop(&mut self) { + self.pid.kill().expect("could not kill ganache"); + } +} + +/// Builder for launching `ganache-cli`. +/// +/// # Panics +/// +/// If `spawn` is called without `ganache-cli` being available in the user's $PATH +/// +/// # Example +/// +/// ```no_run +/// use ethers_core::utils::Ganache; +/// +/// let port = 8545u16; +/// let url = format!("http://localhost:{}", port).to_string(); +/// +/// let ganache = Ganache::new() +/// .port(port) +/// .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") +/// .spawn(); +/// +/// drop(ganache); // this will kill the instance +/// ``` +#[derive(Clone, Default)] +#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] +pub struct Ganache { + port: Option, + block_time: Option, + mnemonic: Option, + fork: Option, + args: Vec, + startup_timeout: Option, +} + +impl Ganache { + /// Creates an empty Ganache builder. + /// The default port is 8545. The mnemonic is chosen randomly. + pub fn new() -> Self { + Self::default() + } + + /// Sets the startup timeout which will be used when the `ganache-cli` instance is launched in + /// miliseconds. 10_000 miliseconds by default). + pub fn startup_timeout_millis>(mut self, timeout: T) -> Self { + self.startup_timeout = Some(timeout.into()); + self + } + + /// Sets the port which will be used when the `ganache-cli` instance is launched. + pub fn port>(mut self, port: T) -> Self { + self.port = Some(port.into()); + self + } + + /// Sets the mnemonic which will be used when the `ganache-cli` instance is launched. + pub fn mnemonic>(mut self, mnemonic: T) -> Self { + self.mnemonic = Some(mnemonic.into()); + self + } + + /// Sets the block-time which will be used when the `ganache-cli` instance is launched. + pub fn block_time>(mut self, block_time: T) -> Self { + self.block_time = Some(block_time.into()); + self + } + + /// Sets the `fork` argument to fork from another currently running Ethereum client + /// at a given block. Input should be the HTTP location and port of the other client, + /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from + /// using an @ sign: `http://localhost:8545@1599200` + pub fn fork>(mut self, fork: T) -> Self { + self.fork = Some(fork.into()); + self + } + + /// Adds an argument to pass to the `ganache-cli`. + pub fn arg>(mut self, arg: T) -> Self { + self.args.push(arg.into()); + self + } + + /// Adds multiple arguments to pass to the `ganache-cli`. + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.arg(arg); + } + self + } + + /// Consumes the builder and spawns `ganache-cli`. + /// + /// # Panics + /// + /// If spawning the instance fails at any point. + #[track_caller] + pub fn spawn(self) -> GanacheInstance { + let mut cmd = Command::new("ganache-cli"); + cmd.stdout(std::process::Stdio::piped()); + let port = if let Some(port) = self.port { port } else { unused_port() }; + cmd.arg("-p").arg(port.to_string()); + + if let Some(mnemonic) = self.mnemonic { + cmd.arg("-m").arg(mnemonic); + } + + if let Some(block_time) = self.block_time { + cmd.arg("-b").arg(block_time.to_string()); + } + + if let Some(fork) = self.fork { + cmd.arg("-f").arg(fork); + } + + cmd.args(self.args); + + let mut child = cmd.spawn().expect("couldnt start ganache-cli"); + + let stdout = child.stdout.expect("Unable to get stdout for ganache child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stdout); + + let mut private_keys = Vec::new(); + let mut addresses = Vec::new(); + let mut is_private_key = false; + + let startup_timeout = + Duration::from_millis(self.startup_timeout.unwrap_or(GANACHE_STARTUP_TIMEOUT_MILLIS)); + loop { + if start + startup_timeout <= Instant::now() { + panic!("Timed out waiting for ganache to start. Is ganache-cli installed?") + } + + let mut line = String::new(); + reader.read_line(&mut line).expect("Failed to read line from ganache process"); + if line.contains("Listening on") { + break + } + + if line.starts_with("Private Keys") { + is_private_key = true; + } + + if is_private_key && line.starts_with('(') { + let key_str = &line[6..line.len() - 1]; + let key_hex = hex::decode(key_str).expect("could not parse as hex"); + let key = K256SecretKey::from_bytes(&GenericArray::clone_from_slice(&key_hex)) + .expect("did not get private key"); + addresses.push(secret_key_to_address(&SigningKey::from(&key))); + private_keys.push(key); + } + } + + child.stdout = Some(reader.into_inner()); + + GanacheInstance { pid: child, private_keys, addresses, port } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn configurable_startup_timeout() { + Ganache::new().startup_timeout_millis(100000_u64).spawn(); + } + + #[test] + #[ignore] + fn default_startup_works() { + Ganache::new().spawn(); + } +} diff --git a/crates/test-utils/src/genesis.rs b/crates/test-utils/src/genesis.rs new file mode 100644 index 00000000000..72ae2bb0bd1 --- /dev/null +++ b/crates/test-utils/src/genesis.rs @@ -0,0 +1,998 @@ +use std::{collections::HashMap, str::FromStr}; + +use crate::{ + types::{ + serde_helpers::{ + deserialize_stringified_eth_u64, deserialize_stringified_eth_u64_opt, + deserialize_stringified_numeric, deserialize_stringified_numeric_opt, + deserialize_stringified_u64_opt, Numeric, + }, + Address, Bytes, H256, U256, U64, + }, + utils::from_unformatted_hex_map, +}; +use serde::{Deserialize, Serialize}; + +/// This represents the chain configuration, specifying the genesis block, header fields, and hard +/// fork switch blocks. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Genesis { + /// The fork configuration for this network. + #[serde(default)] + pub config: ChainConfig, + + /// The genesis header nonce. + #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + pub nonce: U64, + + /// The genesis header timestamp. + #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + pub timestamp: U64, + + /// The genesis header extra data. + #[serde(default)] + pub extra_data: Bytes, + + /// The genesis header gas limit. + #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + pub gas_limit: U64, + + /// The genesis header difficulty. + #[serde(deserialize_with = "deserialize_stringified_numeric")] + pub difficulty: U256, + + /// The genesis header mix hash. + #[serde(default)] + pub mix_hash: H256, + + /// The genesis header coinbase address. + #[serde(default)] + pub coinbase: Address, + + /// The initial state of the genesis block. + pub alloc: HashMap, + + // The following fields are only included for tests, and should not be used in real genesis + // blocks. + /// The block number + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_eth_u64_opt", + default + )] + pub number: Option, + + /// The block gas gasUsed + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_eth_u64_opt", + default + )] + pub gas_used: Option, + + /// The block parent hash + #[serde(skip_serializing_if = "Option::is_none", default)] + pub parent_hash: Option, + + /// The base fee + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_numeric_opt", + default + )] + pub base_fee_per_gas: Option, +} + +impl Genesis { + /// Creates a chain config using the given chain id. + /// and funds the given address with max coins. + /// + /// Enables all hard forks up to London at genesis. + pub fn new(chain_id: u64, signer_addr: Address) -> Genesis { + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; + + let config = ChainConfig { + chain_id, + eip155_block: Some(0), + eip150_block: Some(0), + eip158_block: Some(0), + + homestead_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + muir_glacier_block: Some(0), + berlin_block: Some(0), + london_block: Some(0), + clique: Some(clique_config), + ..Default::default() + }; + + // fund account + let mut alloc = HashMap::new(); + alloc.insert( + signer_addr, + GenesisAccount { balance: U256::MAX, nonce: None, code: None, storage: None }, + ); + + // put signer address in the extra data, padded by the required amount of zeros + // Clique issue: https://github.com/ethereum/EIPs/issues/225 + // Clique EIP: https://eips.ethereum.org/EIPS/eip-225 + // + // The first 32 bytes are vanity data, so we will populate it with zeros + // This is followed by the signer address, which is 20 bytes + // There are 65 bytes of zeros after the signer address, which is usually populated with the + // proposer signature. Because the genesis does not have a proposer signature, it will be + // populated with zeros. + let extra_data_bytes = [&[0u8; 32][..], signer_addr.as_bytes(), &[0u8; 65][..]].concat(); + let extra_data = Bytes::from(extra_data_bytes); + + Genesis { + config, + alloc, + difficulty: U256::one(), + gas_limit: U64::from(5000000), + extra_data, + ..Default::default() + } + } +} + +/// An account in the state of the genesis block. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct GenesisAccount { + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt", + default + )] + pub nonce: Option, + #[serde(deserialize_with = "deserialize_stringified_numeric")] + pub balance: U256, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub code: Option, + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "from_unformatted_hex_map", + default + )] + pub storage: Option>, +} + +/// Represents a node's chain configuration. +/// +/// See [geth's `ChainConfig` +/// struct](https://github.com/ethereum/go-ethereum/blob/64dccf7aa411c5c7cd36090c3d9b9892945ae813/params/config.go#L349) +/// for the source of each field. +#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Eq)] +#[serde(default, rename_all = "camelCase")] +pub struct ChainConfig { + /// The network's chain ID. + #[serde(default = "one")] + pub chain_id: u64, + + /// The homestead switch block (None = no fork, 0 = already homestead). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub homestead_block: Option, + + /// The DAO fork switch block (None = no fork). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub dao_fork_block: Option, + + /// Whether or not the node supports the DAO hard-fork. + pub dao_fork_support: bool, + + /// The EIP-150 hard fork block (None = no fork). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip150_block: Option, + + /// The EIP-150 hard fork hash. + #[serde(skip_serializing_if = "Option::is_none")] + pub eip150_hash: Option, + + /// The EIP-155 hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip155_block: Option, + + /// The EIP-158 hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip158_block: Option, + + /// The Byzantium hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub byzantium_block: Option, + + /// The Constantinople hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub constantinople_block: Option, + + /// The Petersburg hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub petersburg_block: Option, + + /// The Istanbul hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub istanbul_block: Option, + + /// The Muir Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub muir_glacier_block: Option, + + /// The Berlin hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub berlin_block: Option, + + /// The London hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub london_block: Option, + + /// The Arrow Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub arrow_glacier_block: Option, + + /// The Gray Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub gray_glacier_block: Option, + + /// Virtual fork after the merge to use as a network splitter. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub merge_netsplit_block: Option, + + /// Shanghai switch time. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub shanghai_time: Option, + + /// Cancun switch time. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub cancun_time: Option, + + /// Total difficulty reached that triggers the merge consensus upgrade. + #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_ttd")] + pub terminal_total_difficulty: Option, + + /// A flag specifying that the network already passed the terminal total difficulty. Its + /// purpose is to disable legacy sync without having seen the TTD locally. + pub terminal_total_difficulty_passed: bool, + + /// Ethash parameters. + #[serde(skip_serializing_if = "Option::is_none")] + pub ethash: Option, + + /// Clique parameters. + #[serde(skip_serializing_if = "Option::is_none")] + pub clique: Option, +} + +// used only for serde +#[inline] +const fn one() -> u64 { + 1 +} + +/// Empty consensus configuration for proof-of-work networks. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct EthashConfig {} + +/// Consensus configuration for Clique. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct CliqueConfig { + /// Number of seconds between blocks to enforce. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub period: Option, + + /// Epoch length to reset votes and checkpoints. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub epoch: Option, +} + +fn deserialize_ttd<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(val) => { + if let Some(num) = val.as_str() { + return Numeric::from_str(num) + .map(U256::from) + .map(Some) + .map_err(serde::de::Error::custom) + } + + if let serde_json::Value::Number(num) = val { + // geth serializes ttd as number, for mainnet this exceeds u64 which serde is unable + // to deserialize as integer + if num.as_f64() == Some(5.875e22) { + Ok(Some(U256::from(58750000000000000000000u128))) + } else { + num.as_u64() + .map(U256::from) + .ok_or_else(|| { + serde::de::Error::custom(format!("expected a number got {num}")) + }) + .map(Some) + } + } else { + Err(serde::de::Error::custom(format!("expected a number, got {:?}", val))) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ChainConfig, Genesis, GenesisAccount, H256}; + use crate::{ + types::{Address, Bytes, H160, U256}, + utils::EthashConfig, + }; + use std::{collections::HashMap, str::FromStr}; + + #[test] + fn parse_hive_genesis() { + let geth_genesis = r#" + { + "difficulty": "0x20000", + "gasLimit": "0x1", + "alloc": {}, + "config": { + "ethash": {}, + "chainId": 1 + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_clique_smoke_genesis() { + let geth_genesis = r#" + { + "difficulty": "0x1", + "gasLimit": "0x400000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "timestamp": "0x5c51a607", + "alloc": {} + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_non_hex_prefixed_balance() { + // tests that we can parse balance / difficulty fields that are either hex or decimal + let example_balance_json = r#" + { + "nonce": "0x0000000000000042", + "difficulty": "34747478", + "mixHash": "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234", + "coinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "0x123456", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0xfafbfcfd", + "gasLimit": "0x2fefd8", + "alloc": { + "0x3E951C9f69a06Bc3AD71fF7358DbC56bEd94b9F2": { + "balance": "1000000000000000000000000000" + }, + "0xe228C30d4e5245f967ac21726d5412dA27aD071C": { + "balance": "1000000000000000000000000000" + }, + "0xD59Ce7Ccc6454a2D2C2e06bbcf71D0Beb33480eD": { + "balance": "1000000000000000000000000000" + }, + "0x1CF4D54414eF51b41f9B2238c57102ab2e61D1F2": { + "balance": "1000000000000000000000000000" + }, + "0x249bE3fDEd872338C733cF3975af9736bdCb9D4D": { + "balance": "1000000000000000000000000000" + }, + "0x3fCd1bff94513712f8cD63d1eD66776A67D5F78e": { + "balance": "1000000000000000000000000000" + } + }, + "config": { + "ethash": {}, + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0 + } + } + "#; + + let genesis: Genesis = serde_json::from_str(example_balance_json).unwrap(); + + // check difficulty against hex ground truth + let expected_difficulty = U256::from_str("0x2123456").unwrap(); + assert_eq!(expected_difficulty, genesis.difficulty); + + // check all alloc balances + let dec_balance = U256::from_dec_str("1000000000000000000000000000").unwrap(); + for alloc in &genesis.alloc { + assert_eq!(alloc.1.balance, dec_balance); + } + } + + #[test] + fn parse_hive_rpc_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689", + "eip155Block": 0, + "eip158Block": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x20000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_graphql_genesis() { + let geth_genesis = r#" + { + "config" : {}, + "coinbase" : "0x8888f1f195afa192cfee860698584c030f4c9db1", + "difficulty" : "0x020000", + "extraData" : "0x42", + "gasLimit" : "0x2fefd8", + "mixHash" : "0x2c85bcbce56429100b2108254bb56906257582aeafcbd682bc9af67a9f5aee46", + "nonce" : "0x78cc16f7b4f65485", + "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp" : "0x54c98c81", + "alloc" : { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance" : "0x09184e72a000" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_engine_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "yolov2Block": 0, + "yolov3Block": 0, + "londonBlock": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x30000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + }, + "0000000000000000000000000000000000000316": { + "balance": "0x0", + "code": "0x444355" + }, + "0000000000000000000000000000000000000317": { + "balance": "0x0", + "code": "0x600160003555" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_devp2p_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 19763, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "ethash": {} + }, + "nonce": "0xdeadbeefdeadbeef", + "timestamp": "0x0", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x80000000", + "difficulty": "0x20000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "71562b71999873db5b286df957af199ec94617f7": { + "balance": "0xffffffffffffffffffffffffff" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_execution_apis_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 1337, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "ethash": {} + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x", + "gasLimit": "0x4c4b40", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "658bdf435d810c91414ec09147daa6db62406379": { + "balance": "0x487a9a304539440000" + }, + "aa00000000000000000000000000000000000000": { + "code": "0x6042", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000", + "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303" + }, + "balance": "0x1", + "nonce": "0x1" + }, + "bb00000000000000000000000000000000000000": { + "code": "0x600154600354", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000", + "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303" + }, + "balance": "0x2", + "nonce": "0x1" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x3b9aca00" + } + "#; + + let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + + // ensure the test fields are parsed correctly + assert_eq!(genesis.base_fee_per_gas, Some(1000000000.into())); + assert_eq!(genesis.number, Some(0.into())); + assert_eq!(genesis.gas_used, Some(0.into())); + assert_eq!(genesis.parent_hash, Some(H256::zero())); + } + + #[test] + fn parse_hive_rpc_genesis_full() { + let geth_genesis = r#" + { + "config": { + "clique": { + "period": 1 + }, + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x020000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + } + }, + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + "#; + + let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + let alloc_entry = genesis + .alloc + .get(&H160::from_str("0000000000000000000000000000000000000314").unwrap()) + .expect("missing account for parsed genesis"); + let storage = alloc_entry.storage.as_ref().expect("missing storage for parsed genesis"); + let expected_storage = HashMap::from_iter(vec![ + ( + H256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + H256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000001234", + ) + .unwrap(), + ), + ( + H256::from_str( + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9", + ) + .unwrap(), + H256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(), + ), + ]); + assert_eq!(storage, &expected_storage); + + let expected_code = Bytes::from_str("0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029").unwrap(); + let code = alloc_entry.code.as_ref().expect("missing code for parsed genesis"); + assert_eq!(code, &expected_code); + } + + #[test] + fn test_hive_smoke_alloc_deserialize() { + let hive_genesis = r#" + { + "nonce": "0x0000000000000042", + "difficulty": "0x2123456", + "mixHash": "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234", + "coinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "0x123456", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0xfafbfcfd", + "gasLimit": "0x2fefd8", + "alloc": { + "dbdbdb2cbd23b783741e8d7fcf51e459b497e4a6": { + "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "e6716f9544a56c530d868e4bfbacb172315bdead": { + "balance": "0x11", + "code": "0x12" + }, + "b9c015918bdaba24b4ff057a92a3873d6eb201be": { + "balance": "0x21", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x22" + } + }, + "1a26338f0d905e295fccb71fa9ea849ffa12aaf4": { + "balance": "0x31", + "nonce": "0x32" + }, + "0000000000000000000000000000000000000001": { + "balance": "0x41" + }, + "0000000000000000000000000000000000000002": { + "balance": "0x51" + }, + "0000000000000000000000000000000000000003": { + "balance": "0x61" + }, + "0000000000000000000000000000000000000004": { + "balance": "0x71" + } + }, + "config": { + "ethash": {}, + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0 + } + } + "#; + + let expected_genesis = Genesis { + nonce: 0x0000000000000042.into(), + difficulty: 0x2123456.into(), + mix_hash: H256::from_str("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234").unwrap(), + coinbase: Address::from_str("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), + timestamp: 0x123456.into(), + parent_hash: Some(H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap()), + extra_data: Bytes::from_str("0xfafbfcfd").unwrap(), + gas_limit: 0x2fefd8.into(), + alloc: HashMap::from_iter(vec![ + ( + Address::from_str("0xdbdbdb2cbd23b783741e8d7fcf51e459b497e4a6").unwrap(), + GenesisAccount { + balance: U256::from_str("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0xe6716f9544a56c530d868e4bfbacb172315bdead").unwrap(), + GenesisAccount { + balance: U256::from_str("0x11").unwrap(), + nonce: None, + code: Some(Bytes::from_str("0x12").unwrap()), + storage: None, + }, + ), + ( + Address::from_str("0xb9c015918bdaba24b4ff057a92a3873d6eb201be").unwrap(), + GenesisAccount { + balance: U256::from_str("0x21").unwrap(), + nonce: None, + code: None, + storage: Some(HashMap::from_iter(vec![ + ( + H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001").unwrap(), + H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000022").unwrap(), + ), + ])), + }, + ), + ( + Address::from_str("0x1a26338f0d905e295fccb71fa9ea849ffa12aaf4").unwrap(), + GenesisAccount { + balance: U256::from_str("0x31").unwrap(), + nonce: Some(0x32u64), + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000001").unwrap(), + GenesisAccount { + balance: U256::from_str("0x41").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000002").unwrap(), + GenesisAccount { + balance: U256::from_str("0x51").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000003").unwrap(), + GenesisAccount { + balance: U256::from_str("0x61").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000004").unwrap(), + GenesisAccount { + balance: U256::from_str("0x71").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ]), + config: ChainConfig { + ethash: Some(EthashConfig{}), + chain_id: 10, + homestead_block: Some(0), + eip150_block: Some(0), + eip155_block: Some(0), + eip158_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + ..Default::default() + }, + ..Default::default() + }; + + let deserialized_genesis: Genesis = serde_json::from_str(hive_genesis).unwrap(); + assert_eq!(deserialized_genesis, expected_genesis, "deserialized genesis {deserialized_genesis:#?} does not match expected {expected_genesis:#?}"); + } +} diff --git a/crates/test-utils/src/geth.rs b/crates/test-utils/src/geth.rs new file mode 100644 index 00000000000..6e4742c6264 --- /dev/null +++ b/crates/test-utils/src/geth.rs @@ -0,0 +1,681 @@ +use super::{CliqueConfig, Genesis}; +use crate::{ + types::{Bytes, H256}, + utils::{secret_key_to_address, unused_port}, +}; +use k256::ecdsa::SigningKey; +use std::{ + borrow::Cow, + fs::{create_dir, File}, + io::{BufRead, BufReader}, + net::SocketAddr, + path::PathBuf, + process::{Child, ChildStderr, Command, Stdio}, + time::{Duration, Instant}, +}; +use tempfile::tempdir; + +/// How long we will wait for geth to indicate that it is ready. +const GETH_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); + +/// Timeout for waiting for geth to add a peer. +const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::from_secs(20); + +/// The exposed APIs +const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug"; + +/// The geth command +const GETH: &str = "geth"; + +/// Errors that can occur when working with the [`GethInstance`]. +#[derive(Debug)] +pub enum GethInstanceError { + /// Timed out waiting for a message from geth's stderr. + Timeout(String), + + /// A line could not be read from the geth stderr. + ReadLineError(std::io::Error), + + /// The child geth process's stderr was not captured. + NoStderr, +} + +/// A geth instance. Will close the instance when dropped. +/// +/// Construct this using [`Geth`]. +#[derive(Debug)] +pub struct GethInstance { + pid: Child, + port: u16, + ipc: Option, + data_dir: Option, + p2p_port: Option, + genesis: Option, + clique_private_key: Option, +} + +impl GethInstance { + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the p2p port of this instance + pub fn p2p_port(&self) -> Option { + self.p2p_port + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } + + /// Returns the path to this instances' IPC socket + pub fn ipc_path(&self) -> &Option { + &self.ipc + } + + /// Returns the path to this instances' data directory + pub fn data_dir(&self) -> &Option { + &self.data_dir + } + + /// Returns the genesis configuration used to configure this instance + pub fn genesis(&self) -> &Option { + &self.genesis + } + + /// Returns the private key used to configure clique on this instance + pub fn clique_private_key(&self) -> &Option { + &self.clique_private_key + } + + /// Takes the stderr contained in the child process. + /// + /// This leaves a `None` in its place, so calling methods that require a stderr to be present + /// will fail if called after this. + pub fn stderr(&mut self) -> Result { + self.pid.stderr.take().ok_or(GethInstanceError::NoStderr) + } + + /// Blocks until geth adds the specified peer, using 20s as the timeout. + /// + /// Requires the stderr to be present in the `GethInstance`. + pub fn wait_to_add_peer(&mut self, id: H256) -> Result<(), GethInstanceError> { + let mut stderr = self.pid.stderr.as_mut().ok_or(GethInstanceError::NoStderr)?; + let mut err_reader = BufReader::new(&mut stderr); + let mut line = String::new(); + let start = Instant::now(); + + while start.elapsed() < GETH_DIAL_LOOP_TIMEOUT { + line.clear(); + err_reader.read_line(&mut line).map_err(GethInstanceError::ReadLineError)?; + + // geth ids are trunated + let truncated_id = hex::encode(&id.0[..8]); + if line.contains("Adding p2p peer") && line.contains(&truncated_id) { + return Ok(()) + } + } + Err(GethInstanceError::Timeout("Timed out waiting for geth to add a peer".into())) + } +} + +impl Drop for GethInstance { + fn drop(&mut self) { + self.pid.kill().expect("could not kill geth"); + } +} + +/// Whether or not geth is in `dev` mode and configuration options that depend on the mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GethMode { + /// Options that can be set in dev mode + Dev(DevOptions), + /// Options that cannot be set in dev mode + NonDev(PrivateNetOptions), +} + +impl Default for GethMode { + fn default() -> Self { + Self::Dev(Default::default()) + } +} + +/// Configuration options that can be set in dev mode. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DevOptions { + /// The interval at which the dev chain will mine new blocks. + pub block_time: Option, +} + +/// Configuration options that cannot be set in dev mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PrivateNetOptions { + /// The p2p port to use. + pub p2p_port: Option, + + /// Whether or not peer discovery is enabled. + pub discovery: bool, +} + +impl Default for PrivateNetOptions { + fn default() -> Self { + Self { p2p_port: None, discovery: true } + } +} + +/// Builder for launching `geth`. +/// +/// # Panics +/// +/// If `spawn` is called without `geth` being available in the user's $PATH +/// +/// # Example +/// +/// ```no_run +/// use ethers_core::utils::Geth; +/// +/// let port = 8545u16; +/// let url = format!("http://localhost:{}", port).to_string(); +/// +/// let geth = Geth::new() +/// .port(port) +/// .block_time(5000u64) +/// .spawn(); +/// +/// drop(geth); // this will kill the instance +/// ``` +#[derive(Clone, Debug, Default)] +#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] +pub struct Geth { + program: Option, + port: Option, + authrpc_port: Option, + ipc_path: Option, + data_dir: Option, + chain_id: Option, + insecure_unlock: bool, + genesis: Option, + mode: GethMode, + clique_private_key: Option, +} + +impl Geth { + /// Creates an empty Geth builder. + /// + /// The mnemonic is chosen randomly. + pub fn new() -> Self { + Self::default() + } + + /// Creates a Geth builder which will execute `geth` at the given path. + /// + /// # Example + /// + /// ``` + /// use ethers_core::utils::Geth; + /// # fn a() { + /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn(); + /// + /// println!("Geth running at `{}`", geth.endpoint()); + /// # } + /// ``` + pub fn at(path: impl Into) -> Self { + Self::new().path(path) + } + + /// Returns whether the node is launched in Clique consensus mode + pub fn is_clique(&self) -> bool { + self.clique_private_key.is_some() + } + + /// Sets the `path` to the `geth` executable + /// + /// By default, it's expected that `geth` is in `$PATH`, see also + /// [`std::process::Command::new()`] + pub fn path>(mut self, path: T) -> Self { + self.program = Some(path.into()); + self + } + + /// Sets the Clique Private Key to the `geth` executable, which will be later + /// loaded on the node. + /// + /// The address derived from this private key will be used to set the `miner.etherbase` field + /// on the node. + pub fn set_clique_private_key>(mut self, private_key: T) -> Self { + self.clique_private_key = Some(private_key.into()); + self + } + + /// Sets the port which will be used when the `geth-cli` instance is launched. + /// + /// If port is 0 then the OS will choose a random port. + /// [GethInstance::port] will return the port that was chosen. + pub fn port>(mut self, port: T) -> Self { + self.port = Some(port.into()); + self + } + + /// Sets the port which will be used for incoming p2p connections. + /// + /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode + /// options. + pub fn p2p_port(mut self, port: u16) -> Self { + match self.mode { + GethMode::Dev(_) => { + self.mode = GethMode::NonDev(PrivateNetOptions { + p2p_port: Some(port), + ..Default::default() + }) + } + GethMode::NonDev(ref mut opts) => opts.p2p_port = Some(port), + } + self + } + + /// Sets the block-time which will be used when the `geth-cli` instance is launched. + /// + /// This will put the geth instance in `dev` mode, discarding any previously set options that + /// cannot be used in dev mode. + pub fn block_time>(mut self, block_time: T) -> Self { + self.mode = GethMode::Dev(DevOptions { block_time: Some(block_time.into()) }); + self + } + + /// Sets the chain id for the geth instance. + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + + /// Allow geth to unlock accounts when rpc apis are open. + pub fn insecure_unlock(mut self) -> Self { + self.insecure_unlock = true; + self + } + + /// Disable discovery for the geth instance. + /// + /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode + /// options. + pub fn disable_discovery(mut self) -> Self { + self.inner_disable_discovery(); + self + } + + fn inner_disable_discovery(&mut self) { + match self.mode { + GethMode::Dev(_) => { + self.mode = + GethMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() }) + } + GethMode::NonDev(ref mut opts) => opts.discovery = false, + } + } + + /// Manually sets the IPC path for the socket manually. + pub fn ipc_path>(mut self, path: T) -> Self { + self.ipc_path = Some(path.into()); + self + } + + /// Sets the data directory for geth. + pub fn data_dir>(mut self, path: T) -> Self { + self.data_dir = Some(path.into()); + self + } + + /// Sets the `genesis.json` for the geth instance. + /// + /// If this is set, geth will be initialized with `geth init` and the `--datadir` option will be + /// set to the same value as `data_dir`. + /// + /// This is destructive and will overwrite any existing data in the data directory. + pub fn genesis(mut self, genesis: Genesis) -> Self { + self.genesis = Some(genesis); + self + } + + /// Sets the port for authenticated RPC connections. + pub fn authrpc_port(mut self, port: u16) -> Self { + self.authrpc_port = Some(port); + self + } + + /// Consumes the builder and spawns `geth`. + /// + /// # Panics + /// + /// If spawning the instance fails at any point. + #[track_caller] + pub fn spawn(mut self) -> GethInstance { + let bin_path = match self.program.as_ref() { + Some(bin) => bin.as_os_str(), + None => GETH.as_ref(), + } + .to_os_string(); + let mut cmd = Command::new(&bin_path); + // geth uses stderr for its logs + cmd.stderr(Stdio::piped()); + + // If no port provided, let the os chose it for us + let mut port = self.port.unwrap_or(0); + let port_s = port.to_string(); + + // Open the HTTP API + cmd.arg("--http"); + cmd.arg("--http.port").arg(&port_s); + cmd.arg("--http.api").arg(API); + + // Open the WS API + cmd.arg("--ws"); + cmd.arg("--ws.port").arg(port_s); + cmd.arg("--ws.api").arg(API); + + // pass insecure unlock flag if set + let is_clique = self.is_clique(); + if self.insecure_unlock || is_clique { + cmd.arg("--allow-insecure-unlock"); + } + + if is_clique { + self.inner_disable_discovery(); + } + + // Set the port for authenticated APIs + let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port); + cmd.arg("--authrpc.port").arg(authrpc_port.to_string()); + + // use geth init to initialize the datadir if the genesis exists + if is_clique { + if let Some(genesis) = &mut self.genesis { + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; + genesis.config.clique = Some(clique_config); + + let clique_addr = secret_key_to_address( + self.clique_private_key.as_ref().expect("is_clique == true"), + ); + + // set the extraData field + let extra_data_bytes = + [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat(); + let extra_data = Bytes::from(extra_data_bytes); + genesis.extra_data = extra_data; + + // we must set the etherbase if using clique + // need to use format! / Debug here because the Address Display impl doesn't show + // the entire address + cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}")); + } + + let clique_addr = + secret_key_to_address(self.clique_private_key.as_ref().expect("is_clique == true")); + + self.genesis = Some(Genesis::new( + self.chain_id.expect("chain id must be set in clique mode"), + clique_addr, + )); + + // we must set the etherbase if using clique + // need to use format! / Debug here because the Address Display impl doesn't show the + // entire address + cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}")); + } + + if let Some(ref genesis) = self.genesis { + // create a temp dir to store the genesis file + let temp_genesis_dir_path = + tempdir().expect("should be able to create temp dir for genesis init").into_path(); + + // create a temp dir to store the genesis file + let temp_genesis_path = temp_genesis_dir_path.join("genesis.json"); + + // create the genesis file + let mut file = File::create(&temp_genesis_path).expect("could not create genesis file"); + + // serialize genesis and write to file + serde_json::to_writer_pretty(&mut file, &genesis) + .expect("could not write genesis to file"); + + let mut init_cmd = Command::new(bin_path); + if let Some(ref data_dir) = self.data_dir { + init_cmd.arg("--datadir").arg(data_dir); + } + + // set the stderr to null so we don't pollute the test output + init_cmd.stderr(Stdio::null()); + + init_cmd.arg("init").arg(temp_genesis_path); + let res = init_cmd + .spawn() + .expect("failed to spawn geth init") + .wait() + .expect("failed to wait for geth init to exit"); + if !res.success() { + panic!("geth init failed"); + } + + // clean up the temp dir which is now persisted + std::fs::remove_dir_all(temp_genesis_dir_path) + .expect("could not remove genesis temp dir"); + } + + if let Some(ref data_dir) = self.data_dir { + cmd.arg("--datadir").arg(data_dir); + + // create the directory if it doesn't exist + if !data_dir.exists() { + create_dir(data_dir).expect("could not create data dir"); + } + } + + // Dev mode with custom block time + let mut p2p_port = match self.mode { + GethMode::Dev(DevOptions { block_time }) => { + cmd.arg("--dev"); + if let Some(block_time) = block_time { + cmd.arg("--dev.period").arg(block_time.to_string()); + } + None + } + GethMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => { + // if no port provided, let the os chose it for us + let port = p2p_port.unwrap_or(0); + cmd.arg("--port").arg(port.to_string()); + + // disable discovery if the flag is set + if !discovery { + cmd.arg("--nodiscover"); + } + Some(port) + } + }; + + if let Some(chain_id) = self.chain_id { + cmd.arg("--networkid").arg(chain_id.to_string()); + } + + // debug verbosity is needed to check when peers are added + cmd.arg("--verbosity").arg("4"); + + if let Some(ref ipc) = self.ipc_path { + cmd.arg("--ipcpath").arg(ipc); + } + + let mut child = cmd.spawn().expect("couldnt start geth"); + + let stderr = child.stderr.expect("Unable to get stderr for geth child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stderr); + + // we shouldn't need to wait for p2p to start if geth is in dev mode - p2p is disabled in + // dev mode + let mut p2p_started = matches!(self.mode, GethMode::Dev(_)); + let mut http_started = false; + + loop { + if start + GETH_STARTUP_TIMEOUT <= Instant::now() { + panic!("Timed out waiting for geth to start. Is geth installed?") + } + + let mut line = String::with_capacity(120); + reader.read_line(&mut line).expect("Failed to read line from geth process"); + + if matches!(self.mode, GethMode::NonDev(_)) && line.contains("Started P2P networking") { + p2p_started = true; + } + + if !matches!(self.mode, GethMode::Dev(_)) { + // try to find the p2p port, if not in dev mode + if line.contains("New local node record") { + if let Some(port) = extract_value("tcp=", &line) { + p2p_port = port.parse::().ok(); + } + } + } + + // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened" + // the unauthenticated api is used for regular non-engine API requests + if line.contains("HTTP endpoint opened") || + (line.contains("HTTP server started") && !line.contains("auth=true")) + { + // Extracts the address from the output + if let Some(addr) = extract_endpoint(&line) { + // use the actual http port + port = addr.port(); + } + + http_started = true; + } + + // Encountered an error such as Fatal: Error starting protocol stack: listen tcp + // 127.0.0.1:8545: bind: address already in use + if line.contains("Fatal:") { + panic!("{line}"); + } + + if p2p_started && http_started { + break + } + } + + child.stderr = Some(reader.into_inner()); + + GethInstance { + pid: child, + port, + ipc: self.ipc_path, + data_dir: self.data_dir, + p2p_port, + genesis: self.genesis, + clique_private_key: self.clique_private_key, + } + } +} + +// extracts the value for the given key and line +fn extract_value<'a>(key: &str, line: &'a str) -> Option<&'a str> { + let mut key = Cow::from(key); + if !key.ends_with('=') { + key = Cow::from(format!("{}=", key)); + } + line.find(key.as_ref()).map(|pos| { + let start = pos + key.len(); + let end = line[start..].find(' ').map(|i| start + i).unwrap_or(line.len()); + line[start..end].trim() + }) +} + +// extracts the value for the given key and line +fn extract_endpoint(line: &str) -> Option { + let val = extract_value("endpoint=", line)?; + val.parse::().ok() +} + +// These tests should use a different datadir for each `Geth` spawned +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_extract_address() { + let line = "INFO [07-01|13:20:42.774] HTTP server started endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost"; + assert_eq!(extract_endpoint(line), Some(SocketAddr::from(([127, 0, 0, 1], 8545)))); + } + + #[test] + fn port_0() { + run_with_tempdir(|_| { + let _geth = Geth::new().disable_discovery().port(0u16).spawn(); + }); + } + + /// Allows running tests with a temporary directory, which is cleaned up after the function is + /// called. + /// + /// Helps with tests that spawn a helper instance, which has to be dropped before the temporary + /// directory is cleaned up. + #[track_caller] + fn run_with_tempdir(f: impl Fn(&Path)) { + let temp_dir = tempfile::tempdir().unwrap(); + let temp_dir_path = temp_dir.path(); + f(temp_dir_path); + #[cfg(not(windows))] + temp_dir.close().unwrap(); + } + + #[test] + fn p2p_port() { + run_with_tempdir(|temp_dir_path| { + let geth = Geth::new().disable_discovery().data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert!(p2p_port.is_some()); + }); + } + + #[test] + fn explicit_p2p_port() { + run_with_tempdir(|temp_dir_path| { + // if a p2p port is explicitly set, it should be used + let geth = Geth::new().p2p_port(1234).data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert_eq!(p2p_port, Some(1234)); + }); + } + + #[test] + fn dev_mode() { + run_with_tempdir(|temp_dir_path| { + // dev mode should not have a p2p port, and dev should be the default + let geth = Geth::new().data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert!(p2p_port.is_none(), "{p2p_port:?}"); + }) + } + + #[test] + fn clique_correctly_configured() { + run_with_tempdir(|temp_dir_path| { + let private_key = SigningKey::random(&mut rand::thread_rng()); + let geth = Geth::new() + .set_clique_private_key(private_key) + .chain_id(1337u64) + .data_dir(temp_dir_path) + .spawn(); + + assert!(geth.p2p_port.is_some()); + assert!(geth.clique_private_key().is_some()); + assert!(geth.genesis().is_some()); + }) + } +} diff --git a/crates/test-utils/src/hash.rs b/crates/test-utils/src/hash.rs new file mode 100644 index 00000000000..1855259049d --- /dev/null +++ b/crates/test-utils/src/hash.rs @@ -0,0 +1,101 @@ +//! Various utilities for manipulating Ethereum related data. + +use ethabi::ethereum_types::H256; +use tiny_keccak::{Hasher, Keccak}; + +/// Hash a message according to [EIP-191] (version `0x01`). +/// +/// The final message is a UTF-8 string, encoded as follows: +/// `"\x19Ethereum Signed Message:\n" + message.length + message` +/// +/// This message is then hashed using [Keccak-256](keccak256). +/// +/// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 +pub fn hash_message>(message: T) -> H256 { + const PREFIX: &str = "\x19Ethereum Signed Message:\n"; + + let message = message.as_ref(); + let len = message.len(); + let len_string = len.to_string(); + + let mut eth_message = Vec::with_capacity(PREFIX.len() + len_string.len() + len); + eth_message.extend_from_slice(PREFIX.as_bytes()); + eth_message.extend_from_slice(len_string.as_bytes()); + eth_message.extend_from_slice(message); + + H256(keccak256(ð_message)) +} + +/// Compute the Keccak-256 hash of input bytes. +/// +/// Note that strings are interpreted as UTF-8 bytes, +// TODO: Add Solidity Keccak256 packing support +pub fn keccak256>(bytes: T) -> [u8; 32] { + let mut output = [0u8; 32]; + + let mut hasher = Keccak::v256(); + hasher.update(bytes.as_ref()); + hasher.finalize(&mut output); + + output +} + +/// Calculate the function selector as per the contract ABI specification. This +/// is defined as the first 4 bytes of the Keccak256 hash of the function +/// signature. +pub fn id>(signature: S) -> [u8; 4] { + let mut output = [0u8; 4]; + + let mut hasher = Keccak::v256(); + hasher.update(signature.as_ref().as_bytes()); + hasher.finalize(&mut output); + + output +} + +/// Serialize a type. +/// +/// # Panics +/// +/// If the type returns an error during serialization. +pub fn serialize(t: &T) -> serde_json::Value { + serde_json::to_value(t).expect("Failed to serialize value") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + // from https://emn178.github.io/online-tools/keccak_256.html + fn test_keccak256() { + assert_eq!( + hex::encode(keccak256(b"hello")), + "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + ); + } + + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage + #[test] + fn test_hash_message() { + let hash = hash_message("Hello World"); + + assert_eq!( + hash, + "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2".parse().unwrap() + ); + } + + #[test] + fn simple_function_signature() { + // test vector retrieved from + // https://web3js.readthedocs.io/en/v1.2.4/web3-eth-abi.html#encodefunctionsignature + assert_eq!(id("myMethod(uint256,string)"), [0x24, 0xee, 0x00, 0x97],); + } + + #[test] + fn revert_function_signature() { + assert_eq!(id("Error(string)"), [0x08, 0xc3, 0x79, 0xa0]); + } +} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs new file mode 100644 index 00000000000..b99467d73ad --- /dev/null +++ b/crates/test-utils/src/lib.rs @@ -0,0 +1,1260 @@ +/// Utilities for launching a ganache-cli testnet instance +#[cfg(not(target_arch = "wasm32"))] +mod ganache; +#[cfg(not(target_arch = "wasm32"))] +pub use ganache::{Ganache, GanacheInstance}; + +/// Utilities for launching a go-ethereum dev-mode instance +#[cfg(not(target_arch = "wasm32"))] +mod geth; +#[cfg(not(target_arch = "wasm32"))] +pub use geth::{Geth, GethInstance}; + +/// Utilities for working with a `genesis.json` and other chain config structs. +mod genesis; +pub use genesis::{ChainConfig, CliqueConfig, EthashConfig, Genesis, GenesisAccount}; + +/// Utilities for launching an anvil instance +#[cfg(not(target_arch = "wasm32"))] +mod anvil; +#[cfg(not(target_arch = "wasm32"))] +pub use anvil::{Anvil, AnvilInstance}; + +/// Moonbeam utils +pub mod moonbeam; + +mod hash; +pub use hash::{hash_message, id, keccak256, serialize}; + +mod units; +use serde::{Deserialize, Deserializer}; +pub use units::Units; + +/// Re-export RLP +pub use rlp; + +/// Re-export hex +pub use hex; + +use crate::types::{Address, Bytes, ParseI256Error, H256, I256, U256}; +use elliptic_curve::sec1::ToEncodedPoint; +use ethabi::ethereum_types::FromDecStrErr; +use k256::{ + ecdsa::{SigningKey, VerifyingKey}, + AffinePoint, +}; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + fmt, +}; +use thiserror::Error; + +/// I256 overflows for numbers wider than 77 units. +const OVERFLOW_I256_UNITS: usize = 77; +/// U256 overflows for numbers wider than 78 units. +const OVERFLOW_U256_UNITS: usize = 78; + +// Re-export serde-json for macro usage +#[doc(hidden)] +pub use serde_json as __serde_json; + +#[derive(Error, Debug)] +pub enum ConversionError { + #[error("Unknown units: {0}")] + UnrecognizedUnits(String), + #[error("bytes32 strings must not exceed 32 bytes in length")] + TextTooLong, + #[error(transparent)] + Utf8Error(#[from] std::str::Utf8Error), + #[error(transparent)] + InvalidFloat(#[from] std::num::ParseFloatError), + #[error(transparent)] + FromDecStrError(#[from] FromDecStrErr), + #[error("Overflow parsing string")] + ParseOverflow, + #[error(transparent)] + ParseI256Error(#[from] ParseI256Error), + #[error("Invalid address checksum")] + InvalidAddressChecksum, + #[error(transparent)] + FromHexError(
::Err), +} + +/// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei +pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); + +/// The number of blocks from the past for which the fee rewards are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; +/// The default percentile of gas premiums that are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0; +/// The default max priority fee per gas, used in case the base fee is within a threshold. +pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000; +/// The threshold for base fee below which we use the default priority fee, and beyond which we +/// estimate an appropriate value for priority fee. +pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; +/// The threshold max change/difference (in %) at which we will ignore the fee history values +/// under it. +pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; + +/// This enum holds the numeric types that a possible to be returned by `parse_units` and +/// that are taken by `format_units`. +#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum ParseUnits { + U256(U256), + I256(I256), +} + +impl From for U256 { + fn from(n: ParseUnits) -> Self { + match n { + ParseUnits::U256(n) => n, + ParseUnits::I256(n) => n.into_raw(), + } + } +} + +impl fmt::Display for ParseUnits { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseUnits::U256(val) => val.fmt(f), + ParseUnits::I256(val) => val.fmt(f), + } + } +} + +macro_rules! construct_format_units_from { + ($( $t:ty[$convert:ident] ),*) => { + $( + impl From<$t> for ParseUnits { + fn from(num: $t) -> Self { + Self::$convert(num.into()) + } + } + )* + } +} + +// Generate the From code for the given numeric types below. +construct_format_units_from! { + u8[U256], u16[U256], u32[U256], u64[U256], u128[U256], U256[U256], usize[U256], + i8[I256], i16[I256], i32[I256], i64[I256], i128[I256], I256[I256], isize[I256] +} + +/// Format the output for the user which prefer to see values +/// in ether (instead of wei) +/// +/// Divides the input by 1e18 +/// ``` +/// use ethers_core::{types::U256, utils::format_ether}; +/// +/// let eth = format_ether(1395633240123456000_u128); +/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); +/// ``` +pub fn format_ether>(amount: T) -> String { + // format_units returns Err only if units >= 77. Hense, we can safely unwrap here + format_units(amount, "ether").unwrap() +} + +/// Divides the provided amount with 10^{units} provided. +/// +/// ``` +/// use ethers_core::{types::U256, utils::format_units}; +/// +/// let eth = format_units(1395633240123456000_u128, "ether").unwrap(); +/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); +/// +/// let eth = format_units(U256::from_dec_str("1395633240123456000").unwrap(), "ether").unwrap(); +/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); +/// +/// let eth = format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether").unwrap(); +/// assert_eq!(eth, "1.395633240123456789"); +/// +/// let eth = format_units(i64::MIN, "gwei").unwrap(); +/// assert_eq!(eth, "-9223372036.854775808"); +/// +/// let eth = format_units(i128::MIN, 36).unwrap(); +/// assert_eq!(eth, "-170.141183460469231731687303715884105728"); +/// ``` +pub fn format_units(amount: T, units: K) -> Result +where + T: Into, + K: TryInto, +{ + let units: usize = units.try_into()?.into(); + let amount = amount.into(); + + match amount { + // 2**256 ~= 1.16e77 + ParseUnits::U256(_) if units >= OVERFLOW_U256_UNITS => { + return Err(ConversionError::ParseOverflow) + } + // 2**255 ~= 5.79e76 + ParseUnits::I256(_) if units >= OVERFLOW_I256_UNITS => { + return Err(ConversionError::ParseOverflow) + } + _ => {} + }; + let exp10 = U256::exp10(units); + + // `decimals` are formatted twice because U256 does not support alignment (`:0>width`). + match amount { + ParseUnits::U256(amount) => { + let integer = amount / exp10; + let decimals = (amount % exp10).to_string(); + Ok(format!("{integer}.{decimals:0>units$}")) + } + ParseUnits::I256(amount) => { + let exp10 = I256::from_raw(exp10); + let sign = if amount.is_negative() { "-" } else { "" }; + let integer = (amount / exp10).twos_complement(); + let decimals = ((amount % exp10).twos_complement()).to_string(); + Ok(format!("{sign}{integer}.{decimals:0>units$}")) + } + } +} + +/// Converts the input to a U256 and converts from Ether to Wei. +/// +/// ``` +/// use ethers_core::{ +/// types::U256, +/// utils::{parse_ether, WEI_IN_ETHER}, +/// }; +/// +/// let eth = U256::from(WEI_IN_ETHER); +/// assert_eq!(eth, parse_ether(1u8).unwrap()); +/// assert_eq!(eth, parse_ether(1usize).unwrap()); +/// assert_eq!(eth, parse_ether("1").unwrap()); +/// ``` +pub fn parse_ether(eth: S) -> Result { + Ok(parse_units(eth, "ether")?.into()) +} + +/// Multiplies the provided amount with 10^{units} provided. +/// +/// ``` +/// use ethers_core::{types::U256, utils::parse_units}; +/// let amount_in_eth = U256::from_dec_str("15230001000000000000").unwrap(); +/// let amount_in_gwei = U256::from_dec_str("15230001000").unwrap(); +/// let amount_in_wei = U256::from_dec_str("15230001000").unwrap(); +/// assert_eq!(amount_in_eth, parse_units("15.230001000000000000", "ether").unwrap().into()); +/// assert_eq!(amount_in_gwei, parse_units("15.230001000000000000", "gwei").unwrap().into()); +/// assert_eq!(amount_in_wei, parse_units("15230001000", "wei").unwrap().into()); +/// ``` +/// Example of trying to parse decimal WEI, which should fail, as WEI is the smallest +/// ETH denominator. 1 ETH = 10^18 WEI. +/// ```should_panic +/// use ethers_core::{types::U256, utils::parse_units}; +/// let amount_in_wei = U256::from_dec_str("15230001000").unwrap(); +/// assert_eq!(amount_in_wei, parse_units("15.230001000000000000", "wei").unwrap().into()); +/// ``` +pub fn parse_units(amount: S, units: K) -> Result +where + S: ToString, + K: TryInto + Copy, +{ + let exponent: u32 = units.try_into()?.as_num(); + let mut amount_str = amount.to_string().replace('_', ""); + let negative = amount_str.chars().next().unwrap_or_default() == '-'; + let dec_len = if let Some(di) = amount_str.find('.') { + amount_str.remove(di); + amount_str[di..].len() as u32 + } else { + 0 + }; + + if dec_len > exponent { + // Truncate the decimal part if it is longer than the exponent + let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)]; + if negative { + // Edge case: We have removed the entire number and only the negative sign is left. + // Return 0 as a I256 given the input was signed. + if amount_str == "-" { + Ok(ParseUnits::I256(I256::zero())) + } else { + Ok(ParseUnits::I256(I256::from_dec_str(amount_str)?)) + } + } else { + Ok(ParseUnits::U256(U256::from_dec_str(amount_str)?)) + } + } else if negative { + // Edge case: Only a negative sign was given, return 0 as a I256 given the input was signed. + if amount_str == "-" { + Ok(ParseUnits::I256(I256::zero())) + } else { + let mut n = I256::from_dec_str(&amount_str)?; + n *= I256::from(10) + .checked_pow(exponent - dec_len) + .ok_or(ConversionError::ParseOverflow)?; + Ok(ParseUnits::I256(n)) + } + } else { + let mut a_uint = U256::from_dec_str(&amount_str)?; + a_uint *= U256::from(10) + .checked_pow(U256::from(exponent - dec_len)) + .ok_or(ConversionError::ParseOverflow)?; + Ok(ParseUnits::U256(a_uint)) + } +} + +/// The address for an Ethereum contract is deterministically computed from the +/// address of its creator (sender) and how many transactions the creator has +/// sent (nonce). The sender and nonce are RLP encoded and then hashed with Keccak-256. +pub fn get_contract_address(sender: impl Into
, nonce: impl Into) -> Address { + let mut stream = rlp::RlpStream::new(); + stream.begin_list(2); + stream.append(&sender.into()); + stream.append(&nonce.into()); + + let hash = keccak256(&stream.out()); + + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&hash[12..]); + Address::from(bytes) +} + +/// Returns the CREATE2 address of a smart contract as specified in +/// [EIP1014](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md) +/// +/// keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12..] +pub fn get_create2_address( + from: impl Into
, + salt: impl AsRef<[u8]>, + init_code: impl AsRef<[u8]>, +) -> Address { + let init_code_hash = keccak256(init_code.as_ref()); + get_create2_address_from_hash(from, salt, init_code_hash) +} + +/// Returns the CREATE2 address of a smart contract as specified in +/// [EIP1014](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md), +/// taking the pre-computed hash of the init code as input. +/// +/// keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12..] +/// +/// # Example +/// +/// Calculate the address of a UniswapV3 pool. +/// +/// ``` +/// use ethers_core::{ +/// abi, +/// abi::Token, +/// types::{Address, Bytes, U256}, +/// utils::{get_create2_address_from_hash, keccak256}, +/// }; +/// +/// let init_code_hash = +/// hex::decode("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54").unwrap(); +/// let factory: Address = "0x1F98431c8aD98523631AE4a59f267346ea31F984".parse().unwrap(); +/// let token0: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap(); +/// let token1: Address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".parse().unwrap(); +/// let fee = U256::from(500_u64); +/// +/// // abi.encode(token0 as address, token1 as address, fee as uint256) +/// let input = abi::encode(&[Token::Address(token0), Token::Address(token1), Token::Uint(fee)]); +/// +/// // keccak256(abi.encode(token0, token1, fee)) +/// let salt = keccak256(&input); +/// let pool_address = get_create2_address_from_hash(factory, salt, init_code_hash); +/// +/// assert_eq!( +/// pool_address, +/// "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" // USDC/ETH pool address +/// .parse() +/// .unwrap() +/// ); +/// ``` +pub fn get_create2_address_from_hash( + from: impl Into
, + salt: impl AsRef<[u8]>, + init_code_hash: impl AsRef<[u8]>, +) -> Address { + let from = from.into(); + let salt = salt.as_ref(); + let init_code_hash = init_code_hash.as_ref(); + + let mut bytes = Vec::with_capacity(1 + 20 + salt.len() + init_code_hash.len()); + bytes.push(0xff); + bytes.extend_from_slice(from.as_bytes()); + bytes.extend_from_slice(salt); + bytes.extend_from_slice(init_code_hash); + + let hash = keccak256(bytes); + + let mut bytes = [0u8; 20]; + bytes.copy_from_slice(&hash[12..]); + Address::from(bytes) +} + +/// Convert a raw, uncompressed public key to an address. +/// +/// ### Warning +/// +/// This method **does not** verify that the public key is valid. It is the +/// caller's responsibility to pass a valid public key. Passing an invalid +/// public key will produce an unspendable output. +/// +/// ### Panics +/// +/// When the input is not EXACTLY 64 bytes. +pub fn raw_public_key_to_address>(pubkey: T) -> Address { + let pubkey = pubkey.as_ref(); + assert_eq!(pubkey.len(), 64, "raw public key must be 64 bytes"); + let digest = keccak256(pubkey); + Address::from_slice(&digest[12..]) +} + +/// Converts an public key, in compressed or uncompressed form to an Ethereum +/// address +pub fn public_key_to_address(pubkey: &VerifyingKey) -> Address { + let affine: &AffinePoint = pubkey.as_ref(); + let encoded = affine.to_encoded_point(false); + raw_public_key_to_address(&encoded.as_bytes()[1..]) +} + +/// Converts a K256 SigningKey to an Ethereum Address +pub fn secret_key_to_address(secret_key: &SigningKey) -> Address { + let public_key = secret_key.verifying_key(); + + public_key_to_address(public_key) +} + +/// Encodes an Ethereum address to its [EIP-55] checksum. +/// +/// You can optionally specify an [EIP-155 chain ID] to encode the address using the [EIP-1191] +/// extension. +/// +/// [EIP-55]: https://eips.ethereum.org/EIPS/eip-55 +/// [EIP-155 chain ID]: https://eips.ethereum.org/EIPS/eip-155 +/// [EIP-1191]: https://eips.ethereum.org/EIPS/eip-1191 +pub fn to_checksum(addr: &Address, chain_id: Option) -> String { + let prefixed_addr = match chain_id { + Some(chain_id) => format!("{chain_id}0x{addr:x}"), + None => format!("{addr:x}"), + }; + let hash = hex::encode(keccak256(prefixed_addr)); + let hash = hash.as_bytes(); + + let addr_hex = hex::encode(addr.as_bytes()); + let addr_hex = addr_hex.as_bytes(); + + addr_hex.iter().zip(hash).fold("0x".to_owned(), |mut encoded, (addr, hash)| { + encoded.push(if *hash >= 56 { + addr.to_ascii_uppercase() as char + } else { + addr.to_ascii_lowercase() as char + }); + encoded + }) +} + +/// Parses an [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191) checksum address. +/// +/// Returns `Ok(address)` if the checksummed address is valid, `Err()` otherwise. +/// If `chain_id` is `None`, falls back to [EIP-55](https://eips.ethereum.org/EIPS/eip-55) address checksum method +pub fn parse_checksummed(addr: &str, chain_id: Option) -> Result { + let addr = addr.strip_prefix("0x").unwrap_or(addr); + let address: Address = addr.parse().map_err(ConversionError::FromHexError)?; + let checksum_addr = to_checksum(&address, chain_id); + + if checksum_addr.strip_prefix("0x").unwrap_or(&checksum_addr) == addr { + Ok(address) + } else { + Err(ConversionError::InvalidAddressChecksum) + } +} + +/// Returns a bytes32 string representation of text. If the length of text exceeds 32 bytes, +/// an error is returned. +pub fn format_bytes32_string(text: &str) -> Result<[u8; 32], ConversionError> { + let str_bytes: &[u8] = text.as_bytes(); + if str_bytes.len() > 32 { + return Err(ConversionError::TextTooLong); + } + + let mut bytes32: [u8; 32] = [0u8; 32]; + bytes32[..str_bytes.len()].copy_from_slice(str_bytes); + + Ok(bytes32) +} + +/// Returns the decoded string represented by the bytes32 encoded data. +pub fn parse_bytes32_string(bytes: &[u8; 32]) -> Result<&str, ConversionError> { + let mut length = 0; + while length < 32 && bytes[length] != 0 { + length += 1; + } + + Ok(std::str::from_utf8(&bytes[..length])?) +} + +/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts) +pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec>) -> (U256, U256) { + let max_priority_fee_per_gas = + if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) { + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) + } else { + std::cmp::max( + estimate_priority_fee(rewards), + U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE), + ) + }; + let potential_max_fee = base_fee_surged(base_fee_per_gas); + let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee { + max_priority_fee_per_gas + potential_max_fee + } else { + potential_max_fee + }; + (max_fee_per_gas, max_priority_fee_per_gas) +} + +/// Converts a Bytes value into a H256, accepting inputs that are less than 32 bytes long. These +/// inputs will be left padded with zeros. +pub fn from_bytes_to_h256<'de, D>(bytes: Bytes) -> Result +where + D: Deserializer<'de>, +{ + if bytes.0.len() > 32 { + return Err(serde::de::Error::custom("input too long to be a H256")); + } + + // left pad with zeros to 32 bytes + let mut padded = [0u8; 32]; + padded[32 - bytes.0.len()..].copy_from_slice(&bytes.0); + + // then convert to H256 without a panic + Ok(H256::from_slice(&padded)) +} + +/// Deserializes the input into an Option>, using from_unformatted_hex to +/// deserialize the keys and values. +pub fn from_unformatted_hex_map<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let map = Option::>::deserialize(deserializer)?; + match map { + Some(mut map) => { + let mut res_map = HashMap::new(); + for (k, v) in map.drain() { + let k_deserialized = from_bytes_to_h256::<'de, D>(k)?; + let v_deserialized = from_bytes_to_h256::<'de, D>(v)?; + res_map.insert(k_deserialized, v_deserialized); + } + Ok(Some(res_map)) + } + None => Ok(None), + } +} + +fn estimate_priority_fee(rewards: Vec>) -> U256 { + let mut rewards: Vec = + rewards.iter().map(|r| r[0]).filter(|r| *r > U256::zero()).collect(); + if rewards.is_empty() { + return U256::zero(); + } + if rewards.len() == 1 { + return rewards[0]; + } + // Sort the rewards as we will eventually take the median. + rewards.sort(); + + // A copy of the same vector is created for convenience to calculate percentage change + // between subsequent fee values. + let mut rewards_copy = rewards.clone(); + rewards_copy.rotate_left(1); + + let mut percentage_change: Vec = rewards + .iter() + .zip(rewards_copy.iter()) + .map(|(a, b)| { + let a = I256::try_from(*a).expect("priority fee overflow"); + let b = I256::try_from(*b).expect("priority fee overflow"); + ((b - a) * 100) / a + }) + .collect(); + percentage_change.pop(); + + // Fetch the max of the percentage change, and that element's index. + let max_change = percentage_change.iter().max().unwrap(); + let max_change_index = percentage_change.iter().position(|&c| c == *max_change).unwrap(); + + // If we encountered a big change in fees at a certain position, then consider only + // the values >= it. + let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE.into() + && (max_change_index >= (rewards.len() / 2)) + { + rewards[max_change_index..].to_vec() + } else { + rewards + }; + + // Return the median. + values[values.len() / 2] +} + +fn base_fee_surged(base_fee_per_gas: U256) -> U256 { + if base_fee_per_gas <= U256::from(40_000_000_000u64) { + base_fee_per_gas * 2 + } else if base_fee_per_gas <= U256::from(100_000_000_000u64) { + base_fee_per_gas * 16 / 10 + } else if base_fee_per_gas <= U256::from(200_000_000_000u64) { + base_fee_per_gas * 14 / 10 + } else { + base_fee_per_gas * 12 / 10 + } +} + +/// A bit of hack to find an unused TCP port. +/// +/// Does not guarantee that the given port is unused after the function exists, just that it was +/// unused before the function started (i.e., it does not reserve a port). +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn unused_port() -> u16 { + let listener = std::net::TcpListener::bind("127.0.0.1:0") + .expect("Failed to create TCP listener to find unused port"); + + let local_addr = + listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port"); + local_addr.port() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::serde_helpers::deserialize_stringified_numeric; + use hex_literal::hex; + + #[test] + fn wei_in_ether() { + assert_eq!(WEI_IN_ETHER.as_u64(), 1e18 as u64); + } + + #[test] + fn test_format_ether_unsigned() { + let eth = format_ether(WEI_IN_ETHER); + assert_eq!(eth.parse::().unwrap() as u64, 1); + + let eth = format_ether(1395633240123456000_u128); + assert_eq!(eth.parse::().unwrap(), 1.395633240123456); + + let eth = format_ether(U256::from_dec_str("1395633240123456000").unwrap()); + assert_eq!(eth.parse::().unwrap(), 1.395633240123456); + + let eth = format_ether(U256::from_dec_str("1395633240123456789").unwrap()); + assert_eq!(eth, "1.395633240123456789"); + + let eth = format_ether(U256::from_dec_str("1005633240123456789").unwrap()); + assert_eq!(eth, "1.005633240123456789"); + + let eth = format_ether(u16::MAX); + assert_eq!(eth, "0.000000000000065535"); + + // Note: This covers usize on 32 bit systems. + let eth = format_ether(u32::MAX); + assert_eq!(eth, "0.000000004294967295"); + + // Note: This covers usize on 64 bit systems. + let eth = format_ether(u64::MAX); + assert_eq!(eth, "18.446744073709551615"); + } + + #[test] + fn test_format_ether_signed() { + let eth = format_ether(I256::from_dec_str("-1395633240123456000").unwrap()); + assert_eq!(eth.parse::().unwrap(), -1.395633240123456); + + let eth = format_ether(I256::from_dec_str("-1395633240123456789").unwrap()); + assert_eq!(eth, "-1.395633240123456789"); + + let eth = format_ether(I256::from_dec_str("1005633240123456789").unwrap()); + assert_eq!(eth, "1.005633240123456789"); + + let eth = format_ether(i8::MIN); + assert_eq!(eth, "-0.000000000000000128"); + + let eth = format_ether(i8::MAX); + assert_eq!(eth, "0.000000000000000127"); + + let eth = format_ether(i16::MIN); + assert_eq!(eth, "-0.000000000000032768"); + + // Note: This covers isize on 32 bit systems. + let eth = format_ether(i32::MIN); + assert_eq!(eth, "-0.000000002147483648"); + + // Note: This covers isize on 64 bit systems. + let eth = format_ether(i64::MIN); + assert_eq!(eth, "-9.223372036854775808"); + } + + #[test] + fn test_format_units_unsigned() { + let gwei_in_ether = format_units(WEI_IN_ETHER, 9).unwrap(); + assert_eq!(gwei_in_ether.parse::().unwrap() as u64, 1e9 as u64); + + let eth = format_units(WEI_IN_ETHER, "ether").unwrap(); + assert_eq!(eth.parse::().unwrap() as u64, 1); + + let eth = format_units(1395633240123456000_u128, "ether").unwrap(); + assert_eq!(eth.parse::().unwrap(), 1.395633240123456); + + let eth = + format_units(U256::from_dec_str("1395633240123456000").unwrap(), "ether").unwrap(); + assert_eq!(eth.parse::().unwrap(), 1.395633240123456); + + let eth = + format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether").unwrap(); + assert_eq!(eth, "1.395633240123456789"); + + let eth = + format_units(U256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap(); + assert_eq!(eth, "1.005633240123456789"); + + let eth = format_units(u8::MAX, 4).unwrap(); + assert_eq!(eth, "0.0255"); + + let eth = format_units(u16::MAX, "ether").unwrap(); + assert_eq!(eth, "0.000000000000065535"); + + // Note: This covers usize on 32 bit systems. + let eth = format_units(u32::MAX, 18).unwrap(); + assert_eq!(eth, "0.000000004294967295"); + + // Note: This covers usize on 64 bit systems. + let eth = format_units(u64::MAX, "gwei").unwrap(); + assert_eq!(eth, "18446744073.709551615"); + + let eth = format_units(u128::MAX, 36).unwrap(); + assert_eq!(eth, "340.282366920938463463374607431768211455"); + + let eth = format_units(U256::MAX, 77).unwrap(); + assert_eq!( + eth, + "1.15792089237316195423570985008687907853269984665640564039457584007913129639935" + ); + + let err = format_units(U256::MAX, 78).unwrap_err(); + assert!(matches!(err, ConversionError::ParseOverflow)); + } + + #[test] + fn test_format_units_signed() { + let eth = + format_units(I256::from_dec_str("-1395633240123456000").unwrap(), "ether").unwrap(); + assert_eq!(eth.parse::().unwrap(), -1.395633240123456); + + let eth = + format_units(I256::from_dec_str("-1395633240123456789").unwrap(), "ether").unwrap(); + assert_eq!(eth, "-1.395633240123456789"); + + let eth = + format_units(I256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap(); + assert_eq!(eth, "1.005633240123456789"); + + let eth = format_units(i8::MIN, 4).unwrap(); + assert_eq!(eth, "-0.0128"); + assert_eq!(eth.parse::().unwrap(), -0.0128_f64); + + let eth = format_units(i8::MAX, 4).unwrap(); + assert_eq!(eth, "0.0127"); + assert_eq!(eth.parse::().unwrap(), 0.0127); + + let eth = format_units(i16::MIN, "ether").unwrap(); + assert_eq!(eth, "-0.000000000000032768"); + + // Note: This covers isize on 32 bit systems. + let eth = format_units(i32::MIN, 18).unwrap(); + assert_eq!(eth, "-0.000000002147483648"); + + // Note: This covers isize on 64 bit systems. + let eth = format_units(i64::MIN, "gwei").unwrap(); + assert_eq!(eth, "-9223372036.854775808"); + + let eth = format_units(i128::MIN, 36).unwrap(); + assert_eq!(eth, "-170.141183460469231731687303715884105728"); + + let eth = format_units(I256::MIN, 76).unwrap(); + assert_eq!( + eth, + "-5.7896044618658097711785492504343953926634992332820282019728792003956564819968" + ); + + let err = format_units(I256::MIN, 77).unwrap_err(); + assert!(matches!(err, ConversionError::ParseOverflow)); + } + + #[test] + fn parse_large_units() { + let decimals = 27u32; + let val = "10.55"; + + let n: U256 = parse_units(val, decimals).unwrap().into(); + assert_eq!(n.to_string(), "10550000000000000000000000000"); + } + + #[test] + fn test_parse_units() { + let gwei: U256 = parse_units(1.5, 9).unwrap().into(); + assert_eq!(gwei.as_u64(), 15e8 as u64); + + let token: U256 = parse_units(1163.56926418, 8).unwrap().into(); + assert_eq!(token.as_u64(), 116356926418); + + let eth_dec_float: U256 = parse_units(1.39563324, "ether").unwrap().into(); + assert_eq!(eth_dec_float, U256::from_dec_str("1395633240000000000").unwrap()); + + let eth_dec_string: U256 = parse_units("1.39563324", "ether").unwrap().into(); + assert_eq!(eth_dec_string, U256::from_dec_str("1395633240000000000").unwrap()); + + let eth: U256 = parse_units(1, "ether").unwrap().into(); + assert_eq!(eth, WEI_IN_ETHER); + + let val: U256 = parse_units("2.3", "ether").unwrap().into(); + assert_eq!(val, U256::from_dec_str("2300000000000000000").unwrap()); + + let n: U256 = parse_units(".2", 2).unwrap().into(); + assert_eq!(n, U256::from(20), "leading dot"); + + let n: U256 = parse_units("333.21", 2).unwrap().into(); + assert_eq!(n, U256::from(33321), "trailing dot"); + + let n: U256 = parse_units("98766", 16).unwrap().into(); + assert_eq!(n, U256::from_dec_str("987660000000000000000").unwrap(), "no dot"); + + let n: U256 = parse_units("3_3_0", 3).unwrap().into(); + assert_eq!(n, U256::from(330000), "underscore"); + + let n: U256 = parse_units("330", 0).unwrap().into(); + assert_eq!(n, U256::from(330), "zero decimals"); + + let n: U256 = parse_units(".1234", 3).unwrap().into(); + assert_eq!(n, U256::from(123), "truncate too many decimals"); + + assert!(parse_units("1", 80).is_err(), "overflow"); + assert!(parse_units("1", -1).is_err(), "neg units"); + + let two_e30 = U256::from(2) * U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0]); + let n: U256 = parse_units("2", 30).unwrap().into(); + assert_eq!(n, two_e30, "2e30"); + + let n: U256 = parse_units(".33_319_2", 0).unwrap().into(); + assert_eq!(n, U256::zero(), "mix"); + + let n: U256 = parse_units("", 3).unwrap().into(); + assert_eq!(n, U256::zero(), "empty"); + } + + #[test] + fn test_signed_parse_units() { + let gwei: I256 = parse_units(-1.5, 9).unwrap().into(); + assert_eq!(gwei.as_i64(), -15e8 as i64); + + let token: I256 = parse_units(-1163.56926418, 8).unwrap().into(); + assert_eq!(token.as_i64(), -116356926418); + + let eth_dec_float: I256 = parse_units(-1.39563324, "ether").unwrap().into(); + assert_eq!(eth_dec_float, I256::from_dec_str("-1395633240000000000").unwrap()); + + let eth_dec_string: I256 = parse_units("-1.39563324", "ether").unwrap().into(); + assert_eq!(eth_dec_string, I256::from_dec_str("-1395633240000000000").unwrap()); + + let eth: I256 = parse_units(-1, "ether").unwrap().into(); + assert_eq!(eth, I256::from_raw(WEI_IN_ETHER) * I256::minus_one()); + + let val: I256 = parse_units("-2.3", "ether").unwrap().into(); + assert_eq!(val, I256::from_dec_str("-2300000000000000000").unwrap()); + + let n: I256 = parse_units("-.2", 2).unwrap().into(); + assert_eq!(n, I256::from(-20), "leading dot"); + + let n: I256 = parse_units("-333.21", 2).unwrap().into(); + assert_eq!(n, I256::from(-33321), "trailing dot"); + + let n: I256 = parse_units("-98766", 16).unwrap().into(); + assert_eq!(n, I256::from_dec_str("-987660000000000000000").unwrap(), "no dot"); + + let n: I256 = parse_units("-3_3_0", 3).unwrap().into(); + assert_eq!(n, I256::from(-330000), "underscore"); + + let n: I256 = parse_units("-330", 0).unwrap().into(); + assert_eq!(n, I256::from(-330), "zero decimals"); + + let n: I256 = parse_units("-.1234", 3).unwrap().into(); + assert_eq!(n, I256::from(-123), "truncate too many decimals"); + + assert!(parse_units("-1", 80).is_err(), "overflow"); + + let two_e30 = + I256::from(-2) * I256::from_raw(U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0])); + let n: I256 = parse_units("-2", 30).unwrap().into(); + assert_eq!(n, two_e30, "-2e30"); + + let n: I256 = parse_units("-.33_319_2", 0).unwrap().into(); + assert_eq!(n, I256::zero(), "mix"); + + let n: I256 = parse_units("-", 3).unwrap().into(); + assert_eq!(n, I256::zero(), "empty"); + } + + #[test] + fn addr_checksum() { + let addr_list = vec![ + // mainnet + ( + None, + "27b1fdb04752bbc536007a920d24acb045561c26", + "0x27b1fdb04752bbc536007a920d24acb045561c26", + ), + ( + None, + "3599689e6292b81b2d85451025146515070129bb", + "0x3599689E6292b81B2d85451025146515070129Bb", + ), + ( + None, + "42712d45473476b98452f434e72461577d686318", + "0x42712D45473476b98452f434e72461577D686318", + ), + ( + None, + "52908400098527886e0f7030069857d2e4169ee7", + "0x52908400098527886E0F7030069857D2E4169EE7", + ), + ( + None, + "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", + "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", + ), + ( + None, + "6549f4939460de12611948b3f82b88c3c8975323", + "0x6549f4939460DE12611948b3f82b88C3C8975323", + ), + ( + None, + "66f9664f97f2b50f62d13ea064982f936de76657", + "0x66f9664f97F2b50F62D13eA064982f936dE76657", + ), + ( + None, + "88021160c5c792225e4e5452585947470010289d", + "0x88021160C5C792225E4E5452585947470010289D", + ), + // rsk mainnet + ( + Some(30), + "27b1fdb04752bbc536007a920d24acb045561c26", + "0x27b1FdB04752BBc536007A920D24ACB045561c26", + ), + ( + Some(30), + "3599689e6292b81b2d85451025146515070129bb", + "0x3599689E6292B81B2D85451025146515070129Bb", + ), + ( + Some(30), + "42712d45473476b98452f434e72461577d686318", + "0x42712D45473476B98452f434E72461577d686318", + ), + ( + Some(30), + "52908400098527886e0f7030069857d2e4169ee7", + "0x52908400098527886E0F7030069857D2E4169ee7", + ), + ( + Some(30), + "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", + "0x5aaEB6053f3e94c9b9a09f33669435E7ef1bEAeD", + ), + ( + Some(30), + "6549f4939460de12611948b3f82b88c3c8975323", + "0x6549F4939460DE12611948B3F82B88C3C8975323", + ), + ( + Some(30), + "66f9664f97f2b50f62d13ea064982f936de76657", + "0x66F9664f97f2B50F62d13EA064982F936de76657", + ), + ]; + + for (chain_id, addr, checksummed_addr) in addr_list { + let addr = addr.parse::
().unwrap(); + assert_eq!(to_checksum(&addr, chain_id), String::from(checksummed_addr)); + } + } + + #[test] + fn checksummed_parse() { + let cases = vec![ + // mainnet + // wrong case + (None, "0x27b1fdb04752bbc536007a920d24acb045561c26", true), + (None, "0x27B1fdb04752bbc536007a920d24acb045561c26", false), + // no checksummed + (None, "0x52908400098527886e0f7030069857d2e4169ee7", false), + // without 0x + (None, "0x42712D45473476b98452f434e72461577D686318", true), + (None, "42712D45473476b98452f434e72461577D686318", true), + // invalid address string + (None, "0x52908400098527886E0F7030069857D2E4169EE7", true), + (None, "0x52908400098527886E0F7030069857D2E4169EEX", false), + (None, "0x52908400098527886E0F7030069857D2E4169EE70", false), + // mistyped address + (None, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", true), + (None, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAe1", false), + // rsk mainnet + // wrong case + (Some(30), "0x27b1FdB04752BBc536007A920D24ACB045561c26", true), + (Some(30), "0x27b1FdB04752BBc536007A920D24ACB045561C26", false), + // without 0x + (Some(30), "0x3599689E6292B81B2D85451025146515070129Bb", true), + (Some(30), "3599689E6292B81B2D85451025146515070129Bb", true), + // invalid address string + (Some(30), "0x42712D45473476B98452f434E72461577d686318", true), + (Some(30), "0x42712D45473476B98452f434E72461577d686318Z", false), + // mistyped address + (Some(30), "0x52908400098527886E0F7030069857D2E4169ee7", true), + (Some(30), "0x52908400098527886E0F7030069857D2E4169ee9", false), + ]; // mainnet + + for (chain_id, addr, expected) in cases { + let result = parse_checksummed(addr, chain_id); + assert_eq!( + result.is_ok(), + expected, + "chain_id: {:?} addr: {:?} error: {:?}", + chain_id, + addr, + result.err() + ); + } + } + + #[test] + fn contract_address() { + // http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed + let from = "6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0".parse::
().unwrap(); + for (nonce, expected) in [ + "cd234a471b72ba2f1ccf0a70fcaba648a5eecd8d", + "343c43a37d37dff08ae8c4a11544c718abb4fcf8", + "f778b86fa74e846c4f0a1fbd1335fe81c00a0c91", + "fffd933a0bc612844eaf0c6fe3e5b8e9b6c1d19c", + ] + .iter() + .enumerate() + { + let address = get_contract_address(from, nonce); + assert_eq!(address, expected.parse::
().unwrap()); + } + } + + #[test] + // Test vectors from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md#examples + fn create2_address() { + for (from, salt, init_code, expected) in &[ + ( + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "00", + "4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38", + ), + ( + "deadbeef00000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "00", + "B928f69Bb1D91Cd65274e3c79d8986362984fDA3", + ), + ( + "deadbeef00000000000000000000000000000000", + "000000000000000000000000feed000000000000000000000000000000000000", + "00", + "D04116cDd17beBE565EB2422F2497E06cC1C9833", + ), + ( + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "deadbeef", + "70f2b2914A2a4b783FaEFb75f459A580616Fcb5e", + ), + ( + "00000000000000000000000000000000deadbeef", + "00000000000000000000000000000000000000000000000000000000cafebabe", + "deadbeef", + "60f3f640a8508fC6a86d45DF051962668E1e8AC7", + ), + ( + "00000000000000000000000000000000deadbeef", + "00000000000000000000000000000000000000000000000000000000cafebabe", + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "1d8bfDC5D46DC4f61D6b6115972536eBE6A8854C", + ), + ( + "0000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "", + "E33C0C7F7df4809055C3ebA6c09CFe4BaF1BD9e0", + ), + ] { + // get_create2_address() + let from = from.parse::
().unwrap(); + let salt = hex::decode(salt).unwrap(); + let init_code = hex::decode(init_code).unwrap(); + let expected = expected.parse::
().unwrap(); + assert_eq!(expected, get_create2_address(from, salt.clone(), init_code.clone())); + + // get_create2_address_from_hash() + let init_code_hash = keccak256(init_code).to_vec(); + assert_eq!(expected, get_create2_address_from_hash(from, salt, init_code_hash)) + } + } + + #[test] + fn bytes32_string_parsing() { + let text_bytes_list = vec![ + ("", hex!("0000000000000000000000000000000000000000000000000000000000000000")), + ("A", hex!("4100000000000000000000000000000000000000000000000000000000000000")), + ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", + hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"), + ), + ( + "!@#$%^&*(),./;'[]", + hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"), + ), + ]; + + for (text, bytes) in text_bytes_list { + assert_eq!(text, parse_bytes32_string(&bytes).unwrap()); + } + } + + #[test] + fn bytes32_string_formatting() { + let text_bytes_list = vec![ + ("", hex!("0000000000000000000000000000000000000000000000000000000000000000")), + ("A", hex!("4100000000000000000000000000000000000000000000000000000000000000")), + ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", + hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"), + ), + ( + "!@#$%^&*(),./;'[]", + hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"), + ), + ]; + + for (text, bytes) in text_bytes_list { + assert_eq!(bytes, format_bytes32_string(text).unwrap()); + } + } + + #[test] + fn bytes32_string_formatting_too_long() { + assert!(matches!( + format_bytes32_string("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456").unwrap_err(), + ConversionError::TextTooLong + )); + } + + #[test] + fn test_eip1559_default_estimator() { + // If the base fee is below the triggering base fee, we should get the default priority fee + // with the base fee surged. + let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) - 1; + let rewards: Vec> = vec![vec![]]; + let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards); + assert_eq!(priority_fee, U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)); + assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); + + // If the base fee is above the triggering base fee, we calculate the priority fee using + // the fee history (rewards). + let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) + 1; + let rewards: Vec> = vec![ + vec![100_000_000_000u64.into()], + vec![105_000_000_000u64.into()], + vec![102_000_000_000u64.into()], + ]; // say, last 3 blocks + let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards.clone()); + assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); + assert_eq!(priority_fee, estimate_priority_fee(rewards.clone())); + + // The median should be taken because none of the changes are big enough to ignore values. + assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into()); + + // Ensure fee estimation doesn't panic when overflowing a u32. This had been a divide by + // zero. + let overflow = U256::from(u32::MAX) + 1; + let rewards_overflow: Vec> = vec![vec![overflow], vec![overflow]]; + assert_eq!(estimate_priority_fee(rewards_overflow), overflow); + } + + #[test] + fn int_or_hex_combinations() { + // make sure we can deserialize all combinations of int and hex + // including large numbers that would overflow u64 + // + // format: (string, expected value) + let cases = vec![ + // hex strings + ("\"0x0\"", U256::from(0)), + ("\"0x1\"", U256::from(1)), + ("\"0x10\"", U256::from(16)), + ("\"0x100000000000000000000000000000000000000000000000000\"", U256::from_dec_str("1606938044258990275541962092341162602522202993782792835301376").unwrap()), + // small num, both num and str form + ("10", U256::from(10)), + ("\"10\"", U256::from(10)), + // max u256, in both num and str form + ("\"115792089237316195423570985008687907853269984665640564039457584007913129639935\"", U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639935").unwrap()) + ]; + + #[derive(Deserialize)] + struct TestUint(#[serde(deserialize_with = "deserialize_stringified_numeric")] U256); + + for (string, expected) in cases { + let test: TestUint = serde_json::from_str(string) + .unwrap_or_else(|err| panic!("failed to deserialize {string}: {err}")); + assert_eq!(test.0, expected, "expected to deserialize {}", string); + } + } + + // Only tests for correctness, no edge cases. Uses examples from https://docs.ethers.org/v5/api/utils/address/#utils-computeAddress + #[test] + fn test_public_key_to_address() { + let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); + + // Compressed + let pubkey = VerifyingKey::from_sec1_bytes( + &hex::decode("0376698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762") + .unwrap(), + ) + .unwrap(); + assert_eq!(public_key_to_address(&pubkey), addr); + + // Uncompressed + let pubkey= VerifyingKey::from_sec1_bytes(&hex::decode("0476698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap()).unwrap(); + assert_eq!(public_key_to_address(&pubkey), addr); + } + + #[test] + fn test_raw_public_key_to_address() { + let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); + + let pubkey_bytes = hex::decode("76698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap(); + + assert_eq!(raw_public_key_to_address(pubkey_bytes), addr); + } + + #[test] + #[should_panic] + fn test_raw_public_key_to_address_panics() { + let fake_pkb = vec![]; + + raw_public_key_to_address(fake_pkb); + } +} diff --git a/crates/test-utils/src/moonbeam.rs b/crates/test-utils/src/moonbeam.rs new file mode 100644 index 00000000000..63576386d36 --- /dev/null +++ b/crates/test-utils/src/moonbeam.rs @@ -0,0 +1,115 @@ +//! Moonbeam utilities + +use std::collections::BTreeMap; + +use generic_array::GenericArray; +use k256::SecretKey; + +/// Returns the private developer keys +pub fn dev_keys() -> Vec { + MoonbeamDev::default().into_keys().collect() +} + +/// Holds private developer keys with their names +#[derive(Debug, Clone)] +pub struct MoonbeamDev { + keys: BTreeMap<&'static str, SecretKey>, +} + +impl MoonbeamDev { + pub fn keys(&self) -> impl Iterator { + self.keys.values() + } + + pub fn into_keys(self) -> impl Iterator { + self.keys.into_values() + } + + /// Get a key by then, like `Alith` + pub fn get(&self, name: impl AsRef) -> Option<&SecretKey> { + self.keys.get(name.as_ref()) + } + + pub fn alith(&self) -> &SecretKey { + self.get("Alith").unwrap() + } + + pub fn baltathar(&self) -> &SecretKey { + self.get("Baltathar").unwrap() + } + + pub fn charleth(&self) -> &SecretKey { + self.get("Charleth").unwrap() + } + + pub fn ethan(&self) -> &SecretKey { + self.get("Ethan").unwrap() + } +} + +fn to_secret_key(s: &str) -> SecretKey { + SecretKey::from_bytes(&GenericArray::clone_from_slice(&hex::decode(s).unwrap())).unwrap() +} + +impl Default for MoonbeamDev { + fn default() -> Self { + Self { + keys: BTreeMap::from([ + ( + "Alith", + to_secret_key( + "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133", + ), + ), + ( + "Baltathar", + to_secret_key( + "8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b", + ), + ), + ( + "Charleth", + to_secret_key( + "0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b", + ), + ), + ( + "Dorothy", + to_secret_key( + "39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68", + ), + ), + ( + "Faith", + to_secret_key( + "b9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df", + ), + ), + ( + "Goliath", + to_secret_key( + "96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18", + ), + ), + ( + "Heath", + to_secret_key( + "0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab", + ), + ), + ( + "Ida", + to_secret_key( + "4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8", + ), + ), + ( + "Judith", + to_secret_key( + "94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b", + ), + ), + ]), + } + } +} diff --git a/crates/test-utils/src/units.rs b/crates/test-utils/src/units.rs new file mode 100644 index 00000000000..82c55476f0c --- /dev/null +++ b/crates/test-utils/src/units.rs @@ -0,0 +1,173 @@ +use super::ConversionError; +use std::{convert::TryFrom, fmt, str::FromStr}; + +/// Common Ethereum unit types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Units { + /// Wei is equivalent to 1 wei. + Wei, + /// Kwei is equivalent to 1e3 wei. + Kwei, + /// Mwei is equivalent to 1e6 wei. + Mwei, + /// Gwei is equivalent to 1e9 wei. + Gwei, + /// Twei is equivalent to 1e12 wei. + Twei, + /// Pwei is equivalent to 1e15 wei. + Pwei, + /// Ether is equivalent to 1e18 wei. + Ether, + /// Other less frequent unit sizes, equivalent to 1e{0} wei. + Other(u32), +} + +impl fmt::Display for Units { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad(self.as_num().to_string().as_str()) + } +} + +impl TryFrom for Units { + type Error = ConversionError; + + fn try_from(value: u32) -> Result { + Ok(Units::Other(value)) + } +} + +impl TryFrom for Units { + type Error = ConversionError; + + fn try_from(value: i32) -> Result { + Ok(Units::Other(value as u32)) + } +} + +impl TryFrom for Units { + type Error = ConversionError; + + fn try_from(value: usize) -> Result { + Ok(Units::Other(value as u32)) + } +} + +impl TryFrom for Units { + type Error = ConversionError; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +impl<'a> TryFrom<&'a String> for Units { + type Error = ConversionError; + + fn try_from(value: &'a String) -> Result { + Self::from_str(value) + } +} + +impl TryFrom<&str> for Units { + type Error = ConversionError; + + fn try_from(value: &str) -> Result { + Self::from_str(value) + } +} + +impl FromStr for Units { + type Err = ConversionError; + + fn from_str(s: &str) -> Result { + Ok(match s.to_lowercase().as_str() { + "eth" | "ether" => Units::Ether, + "pwei" | "milli" | "milliether" | "finney" => Units::Pwei, + "twei" | "micro" | "microether" | "szabo" => Units::Twei, + "gwei" | "nano" | "nanoether" | "shannon" => Units::Gwei, + "mwei" | "pico" | "picoether" | "lovelace" => Units::Mwei, + "kwei" | "femto" | "femtoether" | "babbage" => Units::Kwei, + "wei" => Units::Wei, + _ => return Err(ConversionError::UnrecognizedUnits(s.to_string())), + }) + } +} + +impl From for u32 { + fn from(units: Units) -> Self { + units.as_num() + } +} + +impl From for i32 { + fn from(units: Units) -> Self { + units.as_num() as i32 + } +} + +impl From for usize { + fn from(units: Units) -> Self { + units.as_num() as usize + } +} + +impl Units { + pub fn as_num(&self) -> u32 { + match self { + Units::Wei => 0, + Units::Kwei => 3, + Units::Mwei => 6, + Units::Gwei => 9, + Units::Twei => 12, + Units::Pwei => 15, + Units::Ether => 18, + Units::Other(inner) => *inner, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use Units::*; + + #[test] + fn test_units() { + assert_eq!(Wei.as_num(), 0); + assert_eq!(Kwei.as_num(), 3); + assert_eq!(Mwei.as_num(), 6); + assert_eq!(Gwei.as_num(), 9); + assert_eq!(Twei.as_num(), 12); + assert_eq!(Pwei.as_num(), 15); + assert_eq!(Ether.as_num(), 18); + assert_eq!(Other(10).as_num(), 10); + assert_eq!(Other(20).as_num(), 20); + } + + #[test] + fn test_into() { + assert_eq!(Units::try_from("wei").unwrap(), Wei); + assert_eq!(Units::try_from("kwei").unwrap(), Kwei); + assert_eq!(Units::try_from("mwei").unwrap(), Mwei); + assert_eq!(Units::try_from("gwei").unwrap(), Gwei); + assert_eq!(Units::try_from("twei").unwrap(), Twei); + assert_eq!(Units::try_from("pwei").unwrap(), Pwei); + assert_eq!(Units::try_from("ether").unwrap(), Ether); + + assert_eq!(Units::try_from("wei".to_string()).unwrap(), Wei); + assert_eq!(Units::try_from("kwei".to_string()).unwrap(), Kwei); + assert_eq!(Units::try_from("mwei".to_string()).unwrap(), Mwei); + assert_eq!(Units::try_from("gwei".to_string()).unwrap(), Gwei); + assert_eq!(Units::try_from("twei".to_string()).unwrap(), Twei); + assert_eq!(Units::try_from("pwei".to_string()).unwrap(), Pwei); + assert_eq!(Units::try_from("ether".to_string()).unwrap(), Ether); + + assert_eq!(Units::try_from(&"wei".to_string()).unwrap(), Wei); + assert_eq!(Units::try_from(&"kwei".to_string()).unwrap(), Kwei); + assert_eq!(Units::try_from(&"mwei".to_string()).unwrap(), Mwei); + assert_eq!(Units::try_from(&"gwei".to_string()).unwrap(), Gwei); + assert_eq!(Units::try_from(&"twei".to_string()).unwrap(), Twei); + assert_eq!(Units::try_from(&"pwei".to_string()).unwrap(), Pwei); + assert_eq!(Units::try_from(&"ether".to_string()).unwrap(), Ether); + } +} From 7440161d4bf233673f40697eb6b6d21e0ab47170 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:16:32 +0100 Subject: [PATCH 2/6] feat: migrate --- .github/workflows/ci.yml | 5 +- Cargo.toml | 1 + crates/providers/Cargo.toml | 2 +- crates/providers/src/provider.rs | 4 +- crates/rpc-client/Cargo.toml | 9 +- crates/rpc-client/tests/it/ipc.rs | 2 +- crates/test-utils/Cargo.toml | 8 + crates/test-utils/src/anvil.rs | 28 +- crates/test-utils/src/ganache.rs | 235 ----- crates/test-utils/src/genesis.rs | 125 ++- crates/test-utils/src/geth.rs | 47 +- crates/test-utils/src/hash.rs | 101 -- crates/test-utils/src/lib.rs | 1226 +----------------------- crates/test-utils/src/moonbeam.rs | 115 --- crates/test-utils/src/serde_helpers.rs | 68 ++ crates/test-utils/src/units.rs | 173 ---- 16 files changed, 197 insertions(+), 1952 deletions(-) delete mode 100644 crates/test-utils/src/ganache.rs delete mode 100644 crates/test-utils/src/hash.rs delete mode 100644 crates/test-utils/src/moonbeam.rs create mode 100644 crates/test-utils/src/serde_helpers.rs delete mode 100644 crates/test-utils/src/units.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9f1e5f7511..98cea9af8eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,11 +60,12 @@ jobs: - name: cargo hack run: | cargo hack check --workspace --target wasm32-unknown-unknown \ - --exclude alloy-transport-ipc \ --exclude alloy-signer \ --exclude alloy-signer-aws \ --exclude alloy-signer-ledger \ - --exclude alloy-signer-trezor + --exclude alloy-signer-trezor \ + --exclude alloy-test-utils \ + --exclude alloy-transport-ipc feature-checks: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index cc92d8e2ef9..a8442949dc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ alloy-signer = { version = "0.1.0", path = "crates/signer" } alloy-signer-aws = { version = "0.1.0", path = "crates/signer-aws" } alloy-signer-ledger = { version = "0.1.0", path = "crates/signer-ledger" } alloy-signer-trezor = { version = "0.1.0", path = "crates/signer-trezor" } +alloy-test-utils = { version = "0.1.0", path = "crates/test-utils" } alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ipc = { version = "0.1.0", path = "crates/transport-ipc" } diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 8fcdfc9369f..f3f5679209a 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -26,8 +26,8 @@ reqwest.workspace = true auto_impl = "1.1.0" [dev-dependencies] +alloy-test-utils.workspace = true tokio = { version = "1.33.0", features = ["macros"] } -ethers-core = "2.0.10" [features] anvil = [] diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index 4f175885d3b..7aa06402a60 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -516,14 +516,14 @@ impl<'a> TryFrom<&'a String> for Provider> { } #[cfg(test)] -mod providers_test { +mod tests { use crate::{ provider::{Provider, TempProvider}, utils, }; use alloy_primitives::{address, b256, bytes, U256, U64}; use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; - use ethers_core::utils::Anvil; + use alloy_test_utils::Anvil; #[tokio::test] async fn gets_block_number() { diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 1b822c6afe6..2ef60348b2c 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -35,12 +35,13 @@ alloy-transport-ipc = { workspace = true, optional = true } [dev-dependencies] alloy-primitives.workspace = true +alloy-test-utils.workspace = true +alloy-transport-ipc = { workspace = true, features = ["mock"] } alloy-transport-ws.workspace = true + +tempfile = "3" test-log = { version = "0.2.13", default-features = false, features = ["trace"] } tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } -ethers-core = "2.0.10" -alloy-transport-ipc = { workspace = true, features = ["mock"] } -tempfile = "3" [features] default = ["reqwest"] @@ -48,4 +49,4 @@ reqwest = ["dep:url", "dep:reqwest", "alloy-transport-http/reqwest"] hyper = ["dep:url", "dep:hyper", "alloy-transport-http/hyper"] pubsub = ["dep:tokio", "dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] -ipc = ["pubsub", "dep:alloy-transport-ipc"] \ No newline at end of file +ipc = ["pubsub", "dep:alloy-transport-ipc"] diff --git a/crates/rpc-client/tests/it/ipc.rs b/crates/rpc-client/tests/it/ipc.rs index 6de72a1459f..28e3f0d4c68 100644 --- a/crates/rpc-client/tests/it/ipc.rs +++ b/crates/rpc-client/tests/it/ipc.rs @@ -1,8 +1,8 @@ use alloy_primitives::U64; use alloy_pubsub::PubSubFrontend; use alloy_rpc_client::{ClientBuilder, RpcCall, RpcClient}; +use alloy_test_utils::{Geth, GethInstance}; use alloy_transport_ipc::IpcConnect; -use ethers_core::utils::{Geth, GethInstance}; use std::borrow::Cow; use tempfile::NamedTempFile; diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 245c273e527..1a9e164f05d 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -10,3 +10,11 @@ license.workspace = true homepage.workspace = true repository.workspace = true exclude.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["k256"] } +k256.workspace = true +rand.workspace = true +serde_json.workspace = true +serde.workspace = true +tempfile.workspace = true diff --git a/crates/test-utils/src/anvil.rs b/crates/test-utils/src/anvil.rs index 666c939cb06..37713cc303d 100644 --- a/crates/test-utils/src/anvil.rs +++ b/crates/test-utils/src/anvil.rs @@ -1,8 +1,7 @@ -use crate::{ - types::{Address, Chain}, - utils::{secret_key_to_address, unused_port}, -}; -use generic_array::GenericArray; +//! Utilities for launching an Anvil instance. + +use crate::unused_port; +use alloy_primitives::{hex, Address}; use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; use std::{ io::{BufRead, BufReader}, @@ -43,7 +42,8 @@ impl AnvilInstance { /// Returns the chain of the anvil instance pub fn chain_id(&self) -> u64 { - self.chain_id.unwrap_or_else(|| Chain::AnvilHardhat.into()) + const ANVIL_HARDHAT_CHAIN_ID: u64 = 31_337; + self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID) } /// Returns the HTTP endpoint of this instance @@ -72,7 +72,7 @@ impl Drop for AnvilInstance { /// # Example /// /// ```no_run -/// use ethers_core::utils::Anvil; +/// use alloy_test_utils::Anvil; /// /// let port = 8545u16; /// let url = format!("http://localhost:{}", port).to_string(); @@ -105,7 +105,7 @@ impl Anvil { /// # Example /// /// ``` - /// # use ethers_core::utils::Anvil; + /// # use alloy_test_utils::Anvil; /// fn a() { /// let anvil = Anvil::default().spawn(); /// @@ -121,7 +121,7 @@ impl Anvil { /// # Example /// /// ``` - /// # use ethers_core::utils::Anvil; + /// # use alloy_test_utils::Anvil; /// fn a() { /// let anvil = Anvil::at("~/.foundry/bin/anvil").spawn(); /// @@ -256,8 +256,8 @@ impl Anvil { let mut is_private_key = false; let mut chain_id = None; loop { - if start + Duration::from_millis(self.timeout.unwrap_or(ANVIL_STARTUP_TIMEOUT_MILLIS)) <= - Instant::now() + if start + Duration::from_millis(self.timeout.unwrap_or(ANVIL_STARTUP_TIMEOUT_MILLIS)) + <= Instant::now() { panic!("Timed out waiting for anvil to start. Is anvil installed?") } @@ -265,7 +265,7 @@ impl Anvil { let mut line = String::new(); reader.read_line(&mut line).expect("Failed to read line from anvil process"); if line.contains("Listening on") { - break + break; } if line.starts_with("Private Keys") { @@ -279,9 +279,9 @@ impl Anvil { .unwrap_or_else(|| panic!("could not parse private key: {}", line)) .trim(); let key_hex = hex::decode(key_str).expect("could not parse as hex"); - let key = K256SecretKey::from_bytes(&GenericArray::clone_from_slice(&key_hex)) + let key = K256SecretKey::from_bytes((&key_hex[..]).into()) .expect("did not get private key"); - addresses.push(secret_key_to_address(&SigningKey::from(&key))); + addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key())); private_keys.push(key); } diff --git a/crates/test-utils/src/ganache.rs b/crates/test-utils/src/ganache.rs deleted file mode 100644 index 42141225c49..00000000000 --- a/crates/test-utils/src/ganache.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::{ - types::Address, - utils::{secret_key_to_address, unused_port}, -}; -use generic_array::GenericArray; -use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; -use std::{ - io::{BufRead, BufReader}, - process::{Child, Command}, - time::{Duration, Instant}, -}; - -/// Default amount of time we will wait for ganache to indicate that it is ready. -const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; - -/// A ganache CLI instance. Will close the instance when dropped. -/// -/// Construct this using [`Ganache`]. -pub struct GanacheInstance { - pid: Child, - private_keys: Vec, - addresses: Vec
, - port: u16, -} - -impl GanacheInstance { - /// Returns the private keys used to instantiate this instance - pub fn keys(&self) -> &[K256SecretKey] { - &self.private_keys - } - - /// Returns the addresses used to instantiate this instance - pub fn addresses(&self) -> &[Address] { - &self.addresses - } - - /// Returns the port of this instance - pub fn port(&self) -> u16 { - self.port - } - - /// Returns the HTTP endpoint of this instance - pub fn endpoint(&self) -> String { - format!("http://localhost:{}", self.port) - } - - /// Returns the Websocket endpoint of this instance - pub fn ws_endpoint(&self) -> String { - format!("ws://localhost:{}", self.port) - } -} - -impl Drop for GanacheInstance { - fn drop(&mut self) { - self.pid.kill().expect("could not kill ganache"); - } -} - -/// Builder for launching `ganache-cli`. -/// -/// # Panics -/// -/// If `spawn` is called without `ganache-cli` being available in the user's $PATH -/// -/// # Example -/// -/// ```no_run -/// use ethers_core::utils::Ganache; -/// -/// let port = 8545u16; -/// let url = format!("http://localhost:{}", port).to_string(); -/// -/// let ganache = Ganache::new() -/// .port(port) -/// .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") -/// .spawn(); -/// -/// drop(ganache); // this will kill the instance -/// ``` -#[derive(Clone, Default)] -#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] -pub struct Ganache { - port: Option, - block_time: Option, - mnemonic: Option, - fork: Option, - args: Vec, - startup_timeout: Option, -} - -impl Ganache { - /// Creates an empty Ganache builder. - /// The default port is 8545. The mnemonic is chosen randomly. - pub fn new() -> Self { - Self::default() - } - - /// Sets the startup timeout which will be used when the `ganache-cli` instance is launched in - /// miliseconds. 10_000 miliseconds by default). - pub fn startup_timeout_millis>(mut self, timeout: T) -> Self { - self.startup_timeout = Some(timeout.into()); - self - } - - /// Sets the port which will be used when the `ganache-cli` instance is launched. - pub fn port>(mut self, port: T) -> Self { - self.port = Some(port.into()); - self - } - - /// Sets the mnemonic which will be used when the `ganache-cli` instance is launched. - pub fn mnemonic>(mut self, mnemonic: T) -> Self { - self.mnemonic = Some(mnemonic.into()); - self - } - - /// Sets the block-time which will be used when the `ganache-cli` instance is launched. - pub fn block_time>(mut self, block_time: T) -> Self { - self.block_time = Some(block_time.into()); - self - } - - /// Sets the `fork` argument to fork from another currently running Ethereum client - /// at a given block. Input should be the HTTP location and port of the other client, - /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from - /// using an @ sign: `http://localhost:8545@1599200` - pub fn fork>(mut self, fork: T) -> Self { - self.fork = Some(fork.into()); - self - } - - /// Adds an argument to pass to the `ganache-cli`. - pub fn arg>(mut self, arg: T) -> Self { - self.args.push(arg.into()); - self - } - - /// Adds multiple arguments to pass to the `ganache-cli`. - pub fn args(mut self, args: I) -> Self - where - I: IntoIterator, - S: Into, - { - for arg in args { - self = self.arg(arg); - } - self - } - - /// Consumes the builder and spawns `ganache-cli`. - /// - /// # Panics - /// - /// If spawning the instance fails at any point. - #[track_caller] - pub fn spawn(self) -> GanacheInstance { - let mut cmd = Command::new("ganache-cli"); - cmd.stdout(std::process::Stdio::piped()); - let port = if let Some(port) = self.port { port } else { unused_port() }; - cmd.arg("-p").arg(port.to_string()); - - if let Some(mnemonic) = self.mnemonic { - cmd.arg("-m").arg(mnemonic); - } - - if let Some(block_time) = self.block_time { - cmd.arg("-b").arg(block_time.to_string()); - } - - if let Some(fork) = self.fork { - cmd.arg("-f").arg(fork); - } - - cmd.args(self.args); - - let mut child = cmd.spawn().expect("couldnt start ganache-cli"); - - let stdout = child.stdout.expect("Unable to get stdout for ganache child process"); - - let start = Instant::now(); - let mut reader = BufReader::new(stdout); - - let mut private_keys = Vec::new(); - let mut addresses = Vec::new(); - let mut is_private_key = false; - - let startup_timeout = - Duration::from_millis(self.startup_timeout.unwrap_or(GANACHE_STARTUP_TIMEOUT_MILLIS)); - loop { - if start + startup_timeout <= Instant::now() { - panic!("Timed out waiting for ganache to start. Is ganache-cli installed?") - } - - let mut line = String::new(); - reader.read_line(&mut line).expect("Failed to read line from ganache process"); - if line.contains("Listening on") { - break - } - - if line.starts_with("Private Keys") { - is_private_key = true; - } - - if is_private_key && line.starts_with('(') { - let key_str = &line[6..line.len() - 1]; - let key_hex = hex::decode(key_str).expect("could not parse as hex"); - let key = K256SecretKey::from_bytes(&GenericArray::clone_from_slice(&key_hex)) - .expect("did not get private key"); - addresses.push(secret_key_to_address(&SigningKey::from(&key))); - private_keys.push(key); - } - } - - child.stdout = Some(reader.into_inner()); - - GanacheInstance { pid: child, private_keys, addresses, port } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[ignore] - fn configurable_startup_timeout() { - Ganache::new().startup_timeout_millis(100000_u64).spawn(); - } - - #[test] - #[ignore] - fn default_startup_works() { - Ganache::new().spawn(); - } -} diff --git a/crates/test-utils/src/genesis.rs b/crates/test-utils/src/genesis.rs index 72ae2bb0bd1..9c274259aeb 100644 --- a/crates/test-utils/src/genesis.rs +++ b/crates/test-utils/src/genesis.rs @@ -1,17 +1,9 @@ -use std::{collections::HashMap, str::FromStr}; - -use crate::{ - types::{ - serde_helpers::{ - deserialize_stringified_eth_u64, deserialize_stringified_eth_u64_opt, - deserialize_stringified_numeric, deserialize_stringified_numeric_opt, - deserialize_stringified_u64_opt, Numeric, - }, - Address, Bytes, H256, U256, U64, - }, - utils::from_unformatted_hex_map, -}; +//! Utilities for working with a `genesis.json` and other chain config structs. + +use crate::{from_bytes_to_h256, serde_helpers::deserialize_stringified_u64_opt}; +use alloy_primitives::{Address, Bytes, B256, U256, U64}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; /// This represents the chain configuration, specifying the genesis block, header fields, and hard /// fork switch blocks. @@ -23,11 +15,11 @@ pub struct Genesis { pub config: ChainConfig, /// The genesis header nonce. - #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + #[serde(default)] pub nonce: U64, /// The genesis header timestamp. - #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + #[serde(default)] pub timestamp: U64, /// The genesis header extra data. @@ -35,16 +27,15 @@ pub struct Genesis { pub extra_data: Bytes, /// The genesis header gas limit. - #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")] + #[serde(default)] pub gas_limit: U64, /// The genesis header difficulty. - #[serde(deserialize_with = "deserialize_stringified_numeric")] pub difficulty: U256, /// The genesis header mix hash. #[serde(default)] - pub mix_hash: H256, + pub mix_hash: B256, /// The genesis header coinbase address. #[serde(default)] @@ -56,31 +47,19 @@ pub struct Genesis { // The following fields are only included for tests, and should not be used in real genesis // blocks. /// The block number - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_stringified_eth_u64_opt", - default - )] + #[serde(skip_serializing_if = "Option::is_none", default)] pub number: Option, /// The block gas gasUsed - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_stringified_eth_u64_opt", - default - )] + #[serde(skip_serializing_if = "Option::is_none", default)] pub gas_used: Option, /// The block parent hash #[serde(skip_serializing_if = "Option::is_none", default)] - pub parent_hash: Option, + pub parent_hash: Option, /// The base fee - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_stringified_numeric_opt", - default - )] + #[serde(skip_serializing_if = "Option::is_none", default)] pub base_fee_per_gas: Option, } @@ -127,13 +106,13 @@ impl Genesis { // There are 65 bytes of zeros after the signer address, which is usually populated with the // proposer signature. Because the genesis does not have a proposer signature, it will be // populated with zeros. - let extra_data_bytes = [&[0u8; 32][..], signer_addr.as_bytes(), &[0u8; 65][..]].concat(); + let extra_data_bytes = [&[0u8; 32][..], signer_addr.as_slice(), &[0u8; 65][..]].concat(); let extra_data = Bytes::from(extra_data_bytes); Genesis { config, alloc, - difficulty: U256::one(), + difficulty: U256::from(1), gas_limit: U64::from(5000000), extra_data, ..Default::default() @@ -150,7 +129,6 @@ pub struct GenesisAccount { default )] pub nonce: Option, - #[serde(deserialize_with = "deserialize_stringified_numeric")] pub balance: U256, #[serde(skip_serializing_if = "Option::is_none", default)] pub code: Option, @@ -159,7 +137,7 @@ pub struct GenesisAccount { deserialize_with = "from_unformatted_hex_map", default )] - pub storage: Option>, + pub storage: Option>, } /// Represents a node's chain configuration. @@ -200,7 +178,7 @@ pub struct ChainConfig { /// The EIP-150 hard fork hash. #[serde(skip_serializing_if = "Option::is_none")] - pub eip150_hash: Option, + pub eip150_hash: Option, /// The EIP-155 hard fork block. #[serde( @@ -355,10 +333,7 @@ where None => Ok(None), Some(val) => { if let Some(num) = val.as_str() { - return Numeric::from_str(num) - .map(U256::from) - .map(Some) - .map_err(serde::de::Error::custom) + return num.parse().map(Some).map_err(serde::de::Error::custom); } if let serde_json::Value::Number(num) = val { @@ -381,13 +356,29 @@ where } } +/// Deserializes the input into an Option>, using from_unformatted_hex to +/// deserialize the keys and values. +fn from_unformatted_hex_map<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result>, D::Error> { + let map = Option::>::deserialize(deserializer)?; + match map { + Some(mut map) => { + let mut res_map = HashMap::new(); + for (k, v) in map.drain() { + let k_deserialized = from_bytes_to_h256::<'de, D>(k)?; + let v_deserialized = from_bytes_to_h256::<'de, D>(v)?; + res_map.insert(k_deserialized, v_deserialized); + } + Ok(Some(res_map)) + } + None => Ok(None), + } +} + #[cfg(test)] mod tests { - use super::{ChainConfig, Genesis, GenesisAccount, H256}; - use crate::{ - types::{Address, Bytes, H160, U256}, - utils::EthashConfig, - }; + use super::*; use std::{collections::HashMap, str::FromStr}; #[test] @@ -475,11 +466,11 @@ mod tests { let genesis: Genesis = serde_json::from_str(example_balance_json).unwrap(); // check difficulty against hex ground truth - let expected_difficulty = U256::from_str("0x2123456").unwrap(); + let expected_difficulty = U256::from(0x2123456); assert_eq!(expected_difficulty, genesis.difficulty); // check all alloc balances - let dec_balance = U256::from_dec_str("1000000000000000000000000000").unwrap(); + let dec_balance = U256::from(1000000000000000000000000000_u128); for alloc in &genesis.alloc { assert_eq!(alloc.1.balance, dec_balance); } @@ -735,10 +726,10 @@ mod tests { let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); // ensure the test fields are parsed correctly - assert_eq!(genesis.base_fee_per_gas, Some(1000000000.into())); - assert_eq!(genesis.number, Some(0.into())); - assert_eq!(genesis.gas_used, Some(0.into())); - assert_eq!(genesis.parent_hash, Some(H256::zero())); + assert_eq!(genesis.base_fee_per_gas, Some(U256::from(1000000000))); + assert_eq!(genesis.number, Some(U64::ZERO)); + assert_eq!(genesis.gas_used, Some(U64::ZERO)); + assert_eq!(genesis.parent_hash, Some(B256::ZERO)); } #[test] @@ -798,26 +789,26 @@ mod tests { let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); let alloc_entry = genesis .alloc - .get(&H160::from_str("0000000000000000000000000000000000000314").unwrap()) + .get(&Address::from_str("0000000000000000000000000000000000000314").unwrap()) .expect("missing account for parsed genesis"); let storage = alloc_entry.storage.as_ref().expect("missing storage for parsed genesis"); let expected_storage = HashMap::from_iter(vec![ ( - H256::from_str( + B256::from_str( "0x0000000000000000000000000000000000000000000000000000000000000000", ) .unwrap(), - H256::from_str( + B256::from_str( "0x0000000000000000000000000000000000000000000000000000000000001234", ) .unwrap(), ), ( - H256::from_str( + B256::from_str( "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9", ) .unwrap(), - H256::from_str( + B256::from_str( "0x0000000000000000000000000000000000000000000000000000000000000001", ) .unwrap(), @@ -889,14 +880,14 @@ mod tests { "#; let expected_genesis = Genesis { - nonce: 0x0000000000000042.into(), - difficulty: 0x2123456.into(), - mix_hash: H256::from_str("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234").unwrap(), + nonce: U64::from(0x0000000000000042), + difficulty: U256::from(0x2123456), + mix_hash: B256::from_str("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234").unwrap(), coinbase: Address::from_str("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), - timestamp: 0x123456.into(), - parent_hash: Some(H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap()), + timestamp: U64::from(0x123456), + parent_hash: Some(B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap()), extra_data: Bytes::from_str("0xfafbfcfd").unwrap(), - gas_limit: 0x2fefd8.into(), + gas_limit: U64::from(0x2fefd8), alloc: HashMap::from_iter(vec![ ( Address::from_str("0xdbdbdb2cbd23b783741e8d7fcf51e459b497e4a6").unwrap(), @@ -924,8 +915,8 @@ mod tests { code: None, storage: Some(HashMap::from_iter(vec![ ( - H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001").unwrap(), - H256::from_str("0x0000000000000000000000000000000000000000000000000000000000000022").unwrap(), + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001").unwrap(), + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000022").unwrap(), ), ])), }, diff --git a/crates/test-utils/src/geth.rs b/crates/test-utils/src/geth.rs index 6e4742c6264..9be6e6bd8f8 100644 --- a/crates/test-utils/src/geth.rs +++ b/crates/test-utils/src/geth.rs @@ -1,8 +1,7 @@ -use super::{CliqueConfig, Genesis}; -use crate::{ - types::{Bytes, H256}, - utils::{secret_key_to_address, unused_port}, -}; +//! Utilities for launching a go-ethereum dev-mode instance. + +use crate::{unused_port, CliqueConfig, Genesis}; +use alloy_primitives::{hex, Address, Bytes, B256}; use k256::ecdsa::SigningKey; use std::{ borrow::Cow, @@ -106,7 +105,7 @@ impl GethInstance { /// Blocks until geth adds the specified peer, using 20s as the timeout. /// /// Requires the stderr to be present in the `GethInstance`. - pub fn wait_to_add_peer(&mut self, id: H256) -> Result<(), GethInstanceError> { + pub fn wait_to_add_peer(&mut self, id: B256) -> Result<(), GethInstanceError> { let mut stderr = self.pid.stderr.as_mut().ok_or(GethInstanceError::NoStderr)?; let mut err_reader = BufReader::new(&mut stderr); let mut line = String::new(); @@ -119,7 +118,7 @@ impl GethInstance { // geth ids are trunated let truncated_id = hex::encode(&id.0[..8]); if line.contains("Adding p2p peer") && line.contains(&truncated_id) { - return Ok(()) + return Ok(()); } } Err(GethInstanceError::Timeout("Timed out waiting for geth to add a peer".into())) @@ -179,15 +178,12 @@ impl Default for PrivateNetOptions { /// # Example /// /// ```no_run -/// use ethers_core::utils::Geth; +/// use alloy_test_utils::Geth; /// /// let port = 8545u16; /// let url = format!("http://localhost:{}", port).to_string(); /// -/// let geth = Geth::new() -/// .port(port) -/// .block_time(5000u64) -/// .spawn(); +/// let geth = Geth::new().port(port).block_time(5000u64).spawn(); /// /// drop(geth); // this will kill the instance /// ``` @@ -219,22 +215,27 @@ impl Geth { /// # Example /// /// ``` - /// use ethers_core::utils::Geth; + /// use alloy_test_utils::Geth; /// # fn a() { - /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn(); + /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn(); /// - /// println!("Geth running at `{}`", geth.endpoint()); + /// println!("Geth running at `{}`", geth.endpoint()); /// # } /// ``` pub fn at(path: impl Into) -> Self { Self::new().path(path) } - /// Returns whether the node is launched in Clique consensus mode + /// Returns whether the node is launched in Clique consensus mode. pub fn is_clique(&self) -> bool { self.clique_private_key.is_some() } + /// Calculates the address of the Clique consensus address. + pub fn clique_address(&self) -> Option
{ + self.clique_private_key.as_ref().map(|pk| Address::from_public_key(pk.verifying_key())) + } + /// Sets the `path` to the `geth` executable /// /// By default, it's expected that `geth` is in `$PATH`, see also @@ -395,14 +396,13 @@ impl Geth { // use geth init to initialize the datadir if the genesis exists if is_clique { + let clique_addr = self.clique_address(); if let Some(genesis) = &mut self.genesis { // set up a clique config with an instant sealing period and short (8 block) epoch let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; genesis.config.clique = Some(clique_config); - let clique_addr = secret_key_to_address( - self.clique_private_key.as_ref().expect("is_clique == true"), - ); + let clique_addr = clique_addr.expect("is_clique == true"); // set the extraData field let extra_data_bytes = @@ -416,8 +416,7 @@ impl Geth { cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}")); } - let clique_addr = - secret_key_to_address(self.clique_private_key.as_ref().expect("is_clique == true")); + let clique_addr = self.clique_address().expect("is_clique == true"); self.genesis = Some(Genesis::new( self.chain_id.expect("chain id must be set in clique mode"), @@ -545,8 +544,8 @@ impl Geth { // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened" // the unauthenticated api is used for regular non-engine API requests - if line.contains("HTTP endpoint opened") || - (line.contains("HTTP server started") && !line.contains("auth=true")) + if line.contains("HTTP endpoint opened") + || (line.contains("HTTP server started") && !line.contains("auth=true")) { // Extracts the address from the output if let Some(addr) = extract_endpoint(&line) { @@ -564,7 +563,7 @@ impl Geth { } if p2p_started && http_started { - break + break; } } diff --git a/crates/test-utils/src/hash.rs b/crates/test-utils/src/hash.rs deleted file mode 100644 index 1855259049d..00000000000 --- a/crates/test-utils/src/hash.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Various utilities for manipulating Ethereum related data. - -use ethabi::ethereum_types::H256; -use tiny_keccak::{Hasher, Keccak}; - -/// Hash a message according to [EIP-191] (version `0x01`). -/// -/// The final message is a UTF-8 string, encoded as follows: -/// `"\x19Ethereum Signed Message:\n" + message.length + message` -/// -/// This message is then hashed using [Keccak-256](keccak256). -/// -/// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 -pub fn hash_message>(message: T) -> H256 { - const PREFIX: &str = "\x19Ethereum Signed Message:\n"; - - let message = message.as_ref(); - let len = message.len(); - let len_string = len.to_string(); - - let mut eth_message = Vec::with_capacity(PREFIX.len() + len_string.len() + len); - eth_message.extend_from_slice(PREFIX.as_bytes()); - eth_message.extend_from_slice(len_string.as_bytes()); - eth_message.extend_from_slice(message); - - H256(keccak256(ð_message)) -} - -/// Compute the Keccak-256 hash of input bytes. -/// -/// Note that strings are interpreted as UTF-8 bytes, -// TODO: Add Solidity Keccak256 packing support -pub fn keccak256>(bytes: T) -> [u8; 32] { - let mut output = [0u8; 32]; - - let mut hasher = Keccak::v256(); - hasher.update(bytes.as_ref()); - hasher.finalize(&mut output); - - output -} - -/// Calculate the function selector as per the contract ABI specification. This -/// is defined as the first 4 bytes of the Keccak256 hash of the function -/// signature. -pub fn id>(signature: S) -> [u8; 4] { - let mut output = [0u8; 4]; - - let mut hasher = Keccak::v256(); - hasher.update(signature.as_ref().as_bytes()); - hasher.finalize(&mut output); - - output -} - -/// Serialize a type. -/// -/// # Panics -/// -/// If the type returns an error during serialization. -pub fn serialize(t: &T) -> serde_json::Value { - serde_json::to_value(t).expect("Failed to serialize value") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - // from https://emn178.github.io/online-tools/keccak_256.html - fn test_keccak256() { - assert_eq!( - hex::encode(keccak256(b"hello")), - "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" - ); - } - - // test vector taken from: - // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#hashmessage - #[test] - fn test_hash_message() { - let hash = hash_message("Hello World"); - - assert_eq!( - hash, - "a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2".parse().unwrap() - ); - } - - #[test] - fn simple_function_signature() { - // test vector retrieved from - // https://web3js.readthedocs.io/en/v1.2.4/web3-eth-abi.html#encodefunctionsignature - assert_eq!(id("myMethod(uint256,string)"), [0x24, 0xee, 0x00, 0x97],); - } - - #[test] - fn revert_function_signature() { - assert_eq!(id("Error(string)"), [0x08, 0xc3, 0x79, 0xa0]); - } -} diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index b99467d73ad..55b7e5c2490 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,88 +1,19 @@ -/// Utilities for launching a ganache-cli testnet instance -#[cfg(not(target_arch = "wasm32"))] -mod ganache; -#[cfg(not(target_arch = "wasm32"))] -pub use ganache::{Ganache, GanacheInstance}; +use alloy_primitives::{Bytes, B256, U256}; +use serde::Deserializer; + +mod anvil; +pub use anvil::{Anvil, AnvilInstance}; -/// Utilities for launching a go-ethereum dev-mode instance -#[cfg(not(target_arch = "wasm32"))] mod geth; -#[cfg(not(target_arch = "wasm32"))] pub use geth::{Geth, GethInstance}; -/// Utilities for working with a `genesis.json` and other chain config structs. mod genesis; pub use genesis::{ChainConfig, CliqueConfig, EthashConfig, Genesis, GenesisAccount}; -/// Utilities for launching an anvil instance -#[cfg(not(target_arch = "wasm32"))] -mod anvil; -#[cfg(not(target_arch = "wasm32"))] -pub use anvil::{Anvil, AnvilInstance}; - -/// Moonbeam utils -pub mod moonbeam; - -mod hash; -pub use hash::{hash_message, id, keccak256, serialize}; - -mod units; -use serde::{Deserialize, Deserializer}; -pub use units::Units; - -/// Re-export RLP -pub use rlp; - -/// Re-export hex -pub use hex; - -use crate::types::{Address, Bytes, ParseI256Error, H256, I256, U256}; -use elliptic_curve::sec1::ToEncodedPoint; -use ethabi::ethereum_types::FromDecStrErr; -use k256::{ - ecdsa::{SigningKey, VerifyingKey}, - AffinePoint, -}; -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - fmt, -}; -use thiserror::Error; - -/// I256 overflows for numbers wider than 77 units. -const OVERFLOW_I256_UNITS: usize = 77; -/// U256 overflows for numbers wider than 78 units. -const OVERFLOW_U256_UNITS: usize = 78; - -// Re-export serde-json for macro usage -#[doc(hidden)] -pub use serde_json as __serde_json; - -#[derive(Error, Debug)] -pub enum ConversionError { - #[error("Unknown units: {0}")] - UnrecognizedUnits(String), - #[error("bytes32 strings must not exceed 32 bytes in length")] - TextTooLong, - #[error(transparent)] - Utf8Error(#[from] std::str::Utf8Error), - #[error(transparent)] - InvalidFloat(#[from] std::num::ParseFloatError), - #[error(transparent)] - FromDecStrError(#[from] FromDecStrErr), - #[error("Overflow parsing string")] - ParseOverflow, - #[error(transparent)] - ParseI256Error(#[from] ParseI256Error), - #[error("Invalid address checksum")] - InvalidAddressChecksum, - #[error(transparent)] - FromHexError(
::Err), -} +pub mod serde_helpers; /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei -pub const WEI_IN_ETHER: U256 = U256([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); +pub const WEI_IN_ETHER: U256 = U256::from_limbs([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); /// The number of blocks from the past for which the fee rewards are fetched for fee estimation. pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; @@ -97,524 +28,29 @@ pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; /// under it. pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; -/// This enum holds the numeric types that a possible to be returned by `parse_units` and -/// that are taken by `format_units`. -#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] -pub enum ParseUnits { - U256(U256), - I256(I256), -} - -impl From for U256 { - fn from(n: ParseUnits) -> Self { - match n { - ParseUnits::U256(n) => n, - ParseUnits::I256(n) => n.into_raw(), - } - } -} - -impl fmt::Display for ParseUnits { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ParseUnits::U256(val) => val.fmt(f), - ParseUnits::I256(val) => val.fmt(f), - } - } -} - -macro_rules! construct_format_units_from { - ($( $t:ty[$convert:ident] ),*) => { - $( - impl From<$t> for ParseUnits { - fn from(num: $t) -> Self { - Self::$convert(num.into()) - } - } - )* - } -} - -// Generate the From code for the given numeric types below. -construct_format_units_from! { - u8[U256], u16[U256], u32[U256], u64[U256], u128[U256], U256[U256], usize[U256], - i8[I256], i16[I256], i32[I256], i64[I256], i128[I256], I256[I256], isize[I256] -} - -/// Format the output for the user which prefer to see values -/// in ether (instead of wei) -/// -/// Divides the input by 1e18 -/// ``` -/// use ethers_core::{types::U256, utils::format_ether}; -/// -/// let eth = format_ether(1395633240123456000_u128); -/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); -/// ``` -pub fn format_ether>(amount: T) -> String { - // format_units returns Err only if units >= 77. Hense, we can safely unwrap here - format_units(amount, "ether").unwrap() -} - -/// Divides the provided amount with 10^{units} provided. -/// -/// ``` -/// use ethers_core::{types::U256, utils::format_units}; -/// -/// let eth = format_units(1395633240123456000_u128, "ether").unwrap(); -/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); -/// -/// let eth = format_units(U256::from_dec_str("1395633240123456000").unwrap(), "ether").unwrap(); -/// assert_eq!(eth.parse::().unwrap(), 1.395633240123456); -/// -/// let eth = format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether").unwrap(); -/// assert_eq!(eth, "1.395633240123456789"); -/// -/// let eth = format_units(i64::MIN, "gwei").unwrap(); -/// assert_eq!(eth, "-9223372036.854775808"); -/// -/// let eth = format_units(i128::MIN, 36).unwrap(); -/// assert_eq!(eth, "-170.141183460469231731687303715884105728"); -/// ``` -pub fn format_units(amount: T, units: K) -> Result -where - T: Into, - K: TryInto, -{ - let units: usize = units.try_into()?.into(); - let amount = amount.into(); - - match amount { - // 2**256 ~= 1.16e77 - ParseUnits::U256(_) if units >= OVERFLOW_U256_UNITS => { - return Err(ConversionError::ParseOverflow) - } - // 2**255 ~= 5.79e76 - ParseUnits::I256(_) if units >= OVERFLOW_I256_UNITS => { - return Err(ConversionError::ParseOverflow) - } - _ => {} - }; - let exp10 = U256::exp10(units); - - // `decimals` are formatted twice because U256 does not support alignment (`:0>width`). - match amount { - ParseUnits::U256(amount) => { - let integer = amount / exp10; - let decimals = (amount % exp10).to_string(); - Ok(format!("{integer}.{decimals:0>units$}")) - } - ParseUnits::I256(amount) => { - let exp10 = I256::from_raw(exp10); - let sign = if amount.is_negative() { "-" } else { "" }; - let integer = (amount / exp10).twos_complement(); - let decimals = ((amount % exp10).twos_complement()).to_string(); - Ok(format!("{sign}{integer}.{decimals:0>units$}")) - } - } -} - -/// Converts the input to a U256 and converts from Ether to Wei. -/// -/// ``` -/// use ethers_core::{ -/// types::U256, -/// utils::{parse_ether, WEI_IN_ETHER}, -/// }; -/// -/// let eth = U256::from(WEI_IN_ETHER); -/// assert_eq!(eth, parse_ether(1u8).unwrap()); -/// assert_eq!(eth, parse_ether(1usize).unwrap()); -/// assert_eq!(eth, parse_ether("1").unwrap()); -/// ``` -pub fn parse_ether(eth: S) -> Result { - Ok(parse_units(eth, "ether")?.into()) -} - -/// Multiplies the provided amount with 10^{units} provided. -/// -/// ``` -/// use ethers_core::{types::U256, utils::parse_units}; -/// let amount_in_eth = U256::from_dec_str("15230001000000000000").unwrap(); -/// let amount_in_gwei = U256::from_dec_str("15230001000").unwrap(); -/// let amount_in_wei = U256::from_dec_str("15230001000").unwrap(); -/// assert_eq!(amount_in_eth, parse_units("15.230001000000000000", "ether").unwrap().into()); -/// assert_eq!(amount_in_gwei, parse_units("15.230001000000000000", "gwei").unwrap().into()); -/// assert_eq!(amount_in_wei, parse_units("15230001000", "wei").unwrap().into()); -/// ``` -/// Example of trying to parse decimal WEI, which should fail, as WEI is the smallest -/// ETH denominator. 1 ETH = 10^18 WEI. -/// ```should_panic -/// use ethers_core::{types::U256, utils::parse_units}; -/// let amount_in_wei = U256::from_dec_str("15230001000").unwrap(); -/// assert_eq!(amount_in_wei, parse_units("15.230001000000000000", "wei").unwrap().into()); -/// ``` -pub fn parse_units(amount: S, units: K) -> Result -where - S: ToString, - K: TryInto + Copy, -{ - let exponent: u32 = units.try_into()?.as_num(); - let mut amount_str = amount.to_string().replace('_', ""); - let negative = amount_str.chars().next().unwrap_or_default() == '-'; - let dec_len = if let Some(di) = amount_str.find('.') { - amount_str.remove(di); - amount_str[di..].len() as u32 - } else { - 0 - }; - - if dec_len > exponent { - // Truncate the decimal part if it is longer than the exponent - let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)]; - if negative { - // Edge case: We have removed the entire number and only the negative sign is left. - // Return 0 as a I256 given the input was signed. - if amount_str == "-" { - Ok(ParseUnits::I256(I256::zero())) - } else { - Ok(ParseUnits::I256(I256::from_dec_str(amount_str)?)) - } - } else { - Ok(ParseUnits::U256(U256::from_dec_str(amount_str)?)) - } - } else if negative { - // Edge case: Only a negative sign was given, return 0 as a I256 given the input was signed. - if amount_str == "-" { - Ok(ParseUnits::I256(I256::zero())) - } else { - let mut n = I256::from_dec_str(&amount_str)?; - n *= I256::from(10) - .checked_pow(exponent - dec_len) - .ok_or(ConversionError::ParseOverflow)?; - Ok(ParseUnits::I256(n)) - } - } else { - let mut a_uint = U256::from_dec_str(&amount_str)?; - a_uint *= U256::from(10) - .checked_pow(U256::from(exponent - dec_len)) - .ok_or(ConversionError::ParseOverflow)?; - Ok(ParseUnits::U256(a_uint)) - } -} - -/// The address for an Ethereum contract is deterministically computed from the -/// address of its creator (sender) and how many transactions the creator has -/// sent (nonce). The sender and nonce are RLP encoded and then hashed with Keccak-256. -pub fn get_contract_address(sender: impl Into
, nonce: impl Into) -> Address { - let mut stream = rlp::RlpStream::new(); - stream.begin_list(2); - stream.append(&sender.into()); - stream.append(&nonce.into()); - - let hash = keccak256(&stream.out()); - - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&hash[12..]); - Address::from(bytes) -} - -/// Returns the CREATE2 address of a smart contract as specified in -/// [EIP1014](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md) -/// -/// keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12..] -pub fn get_create2_address( - from: impl Into
, - salt: impl AsRef<[u8]>, - init_code: impl AsRef<[u8]>, -) -> Address { - let init_code_hash = keccak256(init_code.as_ref()); - get_create2_address_from_hash(from, salt, init_code_hash) -} - -/// Returns the CREATE2 address of a smart contract as specified in -/// [EIP1014](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md), -/// taking the pre-computed hash of the init code as input. -/// -/// keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12..] -/// -/// # Example -/// -/// Calculate the address of a UniswapV3 pool. -/// -/// ``` -/// use ethers_core::{ -/// abi, -/// abi::Token, -/// types::{Address, Bytes, U256}, -/// utils::{get_create2_address_from_hash, keccak256}, -/// }; -/// -/// let init_code_hash = -/// hex::decode("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54").unwrap(); -/// let factory: Address = "0x1F98431c8aD98523631AE4a59f267346ea31F984".parse().unwrap(); -/// let token0: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap(); -/// let token1: Address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".parse().unwrap(); -/// let fee = U256::from(500_u64); -/// -/// // abi.encode(token0 as address, token1 as address, fee as uint256) -/// let input = abi::encode(&[Token::Address(token0), Token::Address(token1), Token::Uint(fee)]); -/// -/// // keccak256(abi.encode(token0, token1, fee)) -/// let salt = keccak256(&input); -/// let pool_address = get_create2_address_from_hash(factory, salt, init_code_hash); -/// -/// assert_eq!( -/// pool_address, -/// "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" // USDC/ETH pool address -/// .parse() -/// .unwrap() -/// ); -/// ``` -pub fn get_create2_address_from_hash( - from: impl Into
, - salt: impl AsRef<[u8]>, - init_code_hash: impl AsRef<[u8]>, -) -> Address { - let from = from.into(); - let salt = salt.as_ref(); - let init_code_hash = init_code_hash.as_ref(); - - let mut bytes = Vec::with_capacity(1 + 20 + salt.len() + init_code_hash.len()); - bytes.push(0xff); - bytes.extend_from_slice(from.as_bytes()); - bytes.extend_from_slice(salt); - bytes.extend_from_slice(init_code_hash); - - let hash = keccak256(bytes); - - let mut bytes = [0u8; 20]; - bytes.copy_from_slice(&hash[12..]); - Address::from(bytes) -} - -/// Convert a raw, uncompressed public key to an address. -/// -/// ### Warning -/// -/// This method **does not** verify that the public key is valid. It is the -/// caller's responsibility to pass a valid public key. Passing an invalid -/// public key will produce an unspendable output. -/// -/// ### Panics -/// -/// When the input is not EXACTLY 64 bytes. -pub fn raw_public_key_to_address>(pubkey: T) -> Address { - let pubkey = pubkey.as_ref(); - assert_eq!(pubkey.len(), 64, "raw public key must be 64 bytes"); - let digest = keccak256(pubkey); - Address::from_slice(&digest[12..]) -} - -/// Converts an public key, in compressed or uncompressed form to an Ethereum -/// address -pub fn public_key_to_address(pubkey: &VerifyingKey) -> Address { - let affine: &AffinePoint = pubkey.as_ref(); - let encoded = affine.to_encoded_point(false); - raw_public_key_to_address(&encoded.as_bytes()[1..]) -} - -/// Converts a K256 SigningKey to an Ethereum Address -pub fn secret_key_to_address(secret_key: &SigningKey) -> Address { - let public_key = secret_key.verifying_key(); - - public_key_to_address(public_key) -} - -/// Encodes an Ethereum address to its [EIP-55] checksum. -/// -/// You can optionally specify an [EIP-155 chain ID] to encode the address using the [EIP-1191] -/// extension. -/// -/// [EIP-55]: https://eips.ethereum.org/EIPS/eip-55 -/// [EIP-155 chain ID]: https://eips.ethereum.org/EIPS/eip-155 -/// [EIP-1191]: https://eips.ethereum.org/EIPS/eip-1191 -pub fn to_checksum(addr: &Address, chain_id: Option) -> String { - let prefixed_addr = match chain_id { - Some(chain_id) => format!("{chain_id}0x{addr:x}"), - None => format!("{addr:x}"), - }; - let hash = hex::encode(keccak256(prefixed_addr)); - let hash = hash.as_bytes(); - - let addr_hex = hex::encode(addr.as_bytes()); - let addr_hex = addr_hex.as_bytes(); - - addr_hex.iter().zip(hash).fold("0x".to_owned(), |mut encoded, (addr, hash)| { - encoded.push(if *hash >= 56 { - addr.to_ascii_uppercase() as char - } else { - addr.to_ascii_lowercase() as char - }); - encoded - }) -} - -/// Parses an [EIP-1191](https://eips.ethereum.org/EIPS/eip-1191) checksum address. -/// -/// Returns `Ok(address)` if the checksummed address is valid, `Err()` otherwise. -/// If `chain_id` is `None`, falls back to [EIP-55](https://eips.ethereum.org/EIPS/eip-55) address checksum method -pub fn parse_checksummed(addr: &str, chain_id: Option) -> Result { - let addr = addr.strip_prefix("0x").unwrap_or(addr); - let address: Address = addr.parse().map_err(ConversionError::FromHexError)?; - let checksum_addr = to_checksum(&address, chain_id); - - if checksum_addr.strip_prefix("0x").unwrap_or(&checksum_addr) == addr { - Ok(address) - } else { - Err(ConversionError::InvalidAddressChecksum) - } -} - -/// Returns a bytes32 string representation of text. If the length of text exceeds 32 bytes, -/// an error is returned. -pub fn format_bytes32_string(text: &str) -> Result<[u8; 32], ConversionError> { - let str_bytes: &[u8] = text.as_bytes(); - if str_bytes.len() > 32 { - return Err(ConversionError::TextTooLong); - } - - let mut bytes32: [u8; 32] = [0u8; 32]; - bytes32[..str_bytes.len()].copy_from_slice(str_bytes); - - Ok(bytes32) -} - -/// Returns the decoded string represented by the bytes32 encoded data. -pub fn parse_bytes32_string(bytes: &[u8; 32]) -> Result<&str, ConversionError> { - let mut length = 0; - while length < 32 && bytes[length] != 0 { - length += 1; - } - - Ok(std::str::from_utf8(&bytes[..length])?) -} - -/// The default EIP-1559 fee estimator which is based on the work by [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts) -pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec>) -> (U256, U256) { - let max_priority_fee_per_gas = - if base_fee_per_gas < U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) { - U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE) - } else { - std::cmp::max( - estimate_priority_fee(rewards), - U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE), - ) - }; - let potential_max_fee = base_fee_surged(base_fee_per_gas); - let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee { - max_priority_fee_per_gas + potential_max_fee - } else { - potential_max_fee - }; - (max_fee_per_gas, max_priority_fee_per_gas) -} - -/// Converts a Bytes value into a H256, accepting inputs that are less than 32 bytes long. These +/// Converts a Bytes value into a B256, accepting inputs that are less than 32 bytes long. These /// inputs will be left padded with zeros. -pub fn from_bytes_to_h256<'de, D>(bytes: Bytes) -> Result +pub fn from_bytes_to_h256<'de, D>(bytes: Bytes) -> Result where D: Deserializer<'de>, { if bytes.0.len() > 32 { - return Err(serde::de::Error::custom("input too long to be a H256")); + return Err(serde::de::Error::custom("input too long to be a B256")); } // left pad with zeros to 32 bytes let mut padded = [0u8; 32]; padded[32 - bytes.0.len()..].copy_from_slice(&bytes.0); - // then convert to H256 without a panic - Ok(H256::from_slice(&padded)) -} - -/// Deserializes the input into an Option>, using from_unformatted_hex to -/// deserialize the keys and values. -pub fn from_unformatted_hex_map<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let map = Option::>::deserialize(deserializer)?; - match map { - Some(mut map) => { - let mut res_map = HashMap::new(); - for (k, v) in map.drain() { - let k_deserialized = from_bytes_to_h256::<'de, D>(k)?; - let v_deserialized = from_bytes_to_h256::<'de, D>(v)?; - res_map.insert(k_deserialized, v_deserialized); - } - Ok(Some(res_map)) - } - None => Ok(None), - } -} - -fn estimate_priority_fee(rewards: Vec>) -> U256 { - let mut rewards: Vec = - rewards.iter().map(|r| r[0]).filter(|r| *r > U256::zero()).collect(); - if rewards.is_empty() { - return U256::zero(); - } - if rewards.len() == 1 { - return rewards[0]; - } - // Sort the rewards as we will eventually take the median. - rewards.sort(); - - // A copy of the same vector is created for convenience to calculate percentage change - // between subsequent fee values. - let mut rewards_copy = rewards.clone(); - rewards_copy.rotate_left(1); - - let mut percentage_change: Vec = rewards - .iter() - .zip(rewards_copy.iter()) - .map(|(a, b)| { - let a = I256::try_from(*a).expect("priority fee overflow"); - let b = I256::try_from(*b).expect("priority fee overflow"); - ((b - a) * 100) / a - }) - .collect(); - percentage_change.pop(); - - // Fetch the max of the percentage change, and that element's index. - let max_change = percentage_change.iter().max().unwrap(); - let max_change_index = percentage_change.iter().position(|&c| c == *max_change).unwrap(); - - // If we encountered a big change in fees at a certain position, then consider only - // the values >= it. - let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE.into() - && (max_change_index >= (rewards.len() / 2)) - { - rewards[max_change_index..].to_vec() - } else { - rewards - }; - - // Return the median. - values[values.len() / 2] -} - -fn base_fee_surged(base_fee_per_gas: U256) -> U256 { - if base_fee_per_gas <= U256::from(40_000_000_000u64) { - base_fee_per_gas * 2 - } else if base_fee_per_gas <= U256::from(100_000_000_000u64) { - base_fee_per_gas * 16 / 10 - } else if base_fee_per_gas <= U256::from(200_000_000_000u64) { - base_fee_per_gas * 14 / 10 - } else { - base_fee_per_gas * 12 / 10 - } + // then convert to B256 without a panic + Ok(B256::from_slice(&padded)) } /// A bit of hack to find an unused TCP port. /// /// Does not guarantee that the given port is unused after the function exists, just that it was /// unused before the function started (i.e., it does not reserve a port). -#[cfg(not(target_arch = "wasm32"))] -pub(crate) fn unused_port() -> u16 { +fn unused_port() -> u16 { let listener = std::net::TcpListener::bind("127.0.0.1:0") .expect("Failed to create TCP listener to find unused port"); @@ -622,639 +58,3 @@ pub(crate) fn unused_port() -> u16 { listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port"); local_addr.port() } - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::serde_helpers::deserialize_stringified_numeric; - use hex_literal::hex; - - #[test] - fn wei_in_ether() { - assert_eq!(WEI_IN_ETHER.as_u64(), 1e18 as u64); - } - - #[test] - fn test_format_ether_unsigned() { - let eth = format_ether(WEI_IN_ETHER); - assert_eq!(eth.parse::().unwrap() as u64, 1); - - let eth = format_ether(1395633240123456000_u128); - assert_eq!(eth.parse::().unwrap(), 1.395633240123456); - - let eth = format_ether(U256::from_dec_str("1395633240123456000").unwrap()); - assert_eq!(eth.parse::().unwrap(), 1.395633240123456); - - let eth = format_ether(U256::from_dec_str("1395633240123456789").unwrap()); - assert_eq!(eth, "1.395633240123456789"); - - let eth = format_ether(U256::from_dec_str("1005633240123456789").unwrap()); - assert_eq!(eth, "1.005633240123456789"); - - let eth = format_ether(u16::MAX); - assert_eq!(eth, "0.000000000000065535"); - - // Note: This covers usize on 32 bit systems. - let eth = format_ether(u32::MAX); - assert_eq!(eth, "0.000000004294967295"); - - // Note: This covers usize on 64 bit systems. - let eth = format_ether(u64::MAX); - assert_eq!(eth, "18.446744073709551615"); - } - - #[test] - fn test_format_ether_signed() { - let eth = format_ether(I256::from_dec_str("-1395633240123456000").unwrap()); - assert_eq!(eth.parse::().unwrap(), -1.395633240123456); - - let eth = format_ether(I256::from_dec_str("-1395633240123456789").unwrap()); - assert_eq!(eth, "-1.395633240123456789"); - - let eth = format_ether(I256::from_dec_str("1005633240123456789").unwrap()); - assert_eq!(eth, "1.005633240123456789"); - - let eth = format_ether(i8::MIN); - assert_eq!(eth, "-0.000000000000000128"); - - let eth = format_ether(i8::MAX); - assert_eq!(eth, "0.000000000000000127"); - - let eth = format_ether(i16::MIN); - assert_eq!(eth, "-0.000000000000032768"); - - // Note: This covers isize on 32 bit systems. - let eth = format_ether(i32::MIN); - assert_eq!(eth, "-0.000000002147483648"); - - // Note: This covers isize on 64 bit systems. - let eth = format_ether(i64::MIN); - assert_eq!(eth, "-9.223372036854775808"); - } - - #[test] - fn test_format_units_unsigned() { - let gwei_in_ether = format_units(WEI_IN_ETHER, 9).unwrap(); - assert_eq!(gwei_in_ether.parse::().unwrap() as u64, 1e9 as u64); - - let eth = format_units(WEI_IN_ETHER, "ether").unwrap(); - assert_eq!(eth.parse::().unwrap() as u64, 1); - - let eth = format_units(1395633240123456000_u128, "ether").unwrap(); - assert_eq!(eth.parse::().unwrap(), 1.395633240123456); - - let eth = - format_units(U256::from_dec_str("1395633240123456000").unwrap(), "ether").unwrap(); - assert_eq!(eth.parse::().unwrap(), 1.395633240123456); - - let eth = - format_units(U256::from_dec_str("1395633240123456789").unwrap(), "ether").unwrap(); - assert_eq!(eth, "1.395633240123456789"); - - let eth = - format_units(U256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap(); - assert_eq!(eth, "1.005633240123456789"); - - let eth = format_units(u8::MAX, 4).unwrap(); - assert_eq!(eth, "0.0255"); - - let eth = format_units(u16::MAX, "ether").unwrap(); - assert_eq!(eth, "0.000000000000065535"); - - // Note: This covers usize on 32 bit systems. - let eth = format_units(u32::MAX, 18).unwrap(); - assert_eq!(eth, "0.000000004294967295"); - - // Note: This covers usize on 64 bit systems. - let eth = format_units(u64::MAX, "gwei").unwrap(); - assert_eq!(eth, "18446744073.709551615"); - - let eth = format_units(u128::MAX, 36).unwrap(); - assert_eq!(eth, "340.282366920938463463374607431768211455"); - - let eth = format_units(U256::MAX, 77).unwrap(); - assert_eq!( - eth, - "1.15792089237316195423570985008687907853269984665640564039457584007913129639935" - ); - - let err = format_units(U256::MAX, 78).unwrap_err(); - assert!(matches!(err, ConversionError::ParseOverflow)); - } - - #[test] - fn test_format_units_signed() { - let eth = - format_units(I256::from_dec_str("-1395633240123456000").unwrap(), "ether").unwrap(); - assert_eq!(eth.parse::().unwrap(), -1.395633240123456); - - let eth = - format_units(I256::from_dec_str("-1395633240123456789").unwrap(), "ether").unwrap(); - assert_eq!(eth, "-1.395633240123456789"); - - let eth = - format_units(I256::from_dec_str("1005633240123456789").unwrap(), "ether").unwrap(); - assert_eq!(eth, "1.005633240123456789"); - - let eth = format_units(i8::MIN, 4).unwrap(); - assert_eq!(eth, "-0.0128"); - assert_eq!(eth.parse::().unwrap(), -0.0128_f64); - - let eth = format_units(i8::MAX, 4).unwrap(); - assert_eq!(eth, "0.0127"); - assert_eq!(eth.parse::().unwrap(), 0.0127); - - let eth = format_units(i16::MIN, "ether").unwrap(); - assert_eq!(eth, "-0.000000000000032768"); - - // Note: This covers isize on 32 bit systems. - let eth = format_units(i32::MIN, 18).unwrap(); - assert_eq!(eth, "-0.000000002147483648"); - - // Note: This covers isize on 64 bit systems. - let eth = format_units(i64::MIN, "gwei").unwrap(); - assert_eq!(eth, "-9223372036.854775808"); - - let eth = format_units(i128::MIN, 36).unwrap(); - assert_eq!(eth, "-170.141183460469231731687303715884105728"); - - let eth = format_units(I256::MIN, 76).unwrap(); - assert_eq!( - eth, - "-5.7896044618658097711785492504343953926634992332820282019728792003956564819968" - ); - - let err = format_units(I256::MIN, 77).unwrap_err(); - assert!(matches!(err, ConversionError::ParseOverflow)); - } - - #[test] - fn parse_large_units() { - let decimals = 27u32; - let val = "10.55"; - - let n: U256 = parse_units(val, decimals).unwrap().into(); - assert_eq!(n.to_string(), "10550000000000000000000000000"); - } - - #[test] - fn test_parse_units() { - let gwei: U256 = parse_units(1.5, 9).unwrap().into(); - assert_eq!(gwei.as_u64(), 15e8 as u64); - - let token: U256 = parse_units(1163.56926418, 8).unwrap().into(); - assert_eq!(token.as_u64(), 116356926418); - - let eth_dec_float: U256 = parse_units(1.39563324, "ether").unwrap().into(); - assert_eq!(eth_dec_float, U256::from_dec_str("1395633240000000000").unwrap()); - - let eth_dec_string: U256 = parse_units("1.39563324", "ether").unwrap().into(); - assert_eq!(eth_dec_string, U256::from_dec_str("1395633240000000000").unwrap()); - - let eth: U256 = parse_units(1, "ether").unwrap().into(); - assert_eq!(eth, WEI_IN_ETHER); - - let val: U256 = parse_units("2.3", "ether").unwrap().into(); - assert_eq!(val, U256::from_dec_str("2300000000000000000").unwrap()); - - let n: U256 = parse_units(".2", 2).unwrap().into(); - assert_eq!(n, U256::from(20), "leading dot"); - - let n: U256 = parse_units("333.21", 2).unwrap().into(); - assert_eq!(n, U256::from(33321), "trailing dot"); - - let n: U256 = parse_units("98766", 16).unwrap().into(); - assert_eq!(n, U256::from_dec_str("987660000000000000000").unwrap(), "no dot"); - - let n: U256 = parse_units("3_3_0", 3).unwrap().into(); - assert_eq!(n, U256::from(330000), "underscore"); - - let n: U256 = parse_units("330", 0).unwrap().into(); - assert_eq!(n, U256::from(330), "zero decimals"); - - let n: U256 = parse_units(".1234", 3).unwrap().into(); - assert_eq!(n, U256::from(123), "truncate too many decimals"); - - assert!(parse_units("1", 80).is_err(), "overflow"); - assert!(parse_units("1", -1).is_err(), "neg units"); - - let two_e30 = U256::from(2) * U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0]); - let n: U256 = parse_units("2", 30).unwrap().into(); - assert_eq!(n, two_e30, "2e30"); - - let n: U256 = parse_units(".33_319_2", 0).unwrap().into(); - assert_eq!(n, U256::zero(), "mix"); - - let n: U256 = parse_units("", 3).unwrap().into(); - assert_eq!(n, U256::zero(), "empty"); - } - - #[test] - fn test_signed_parse_units() { - let gwei: I256 = parse_units(-1.5, 9).unwrap().into(); - assert_eq!(gwei.as_i64(), -15e8 as i64); - - let token: I256 = parse_units(-1163.56926418, 8).unwrap().into(); - assert_eq!(token.as_i64(), -116356926418); - - let eth_dec_float: I256 = parse_units(-1.39563324, "ether").unwrap().into(); - assert_eq!(eth_dec_float, I256::from_dec_str("-1395633240000000000").unwrap()); - - let eth_dec_string: I256 = parse_units("-1.39563324", "ether").unwrap().into(); - assert_eq!(eth_dec_string, I256::from_dec_str("-1395633240000000000").unwrap()); - - let eth: I256 = parse_units(-1, "ether").unwrap().into(); - assert_eq!(eth, I256::from_raw(WEI_IN_ETHER) * I256::minus_one()); - - let val: I256 = parse_units("-2.3", "ether").unwrap().into(); - assert_eq!(val, I256::from_dec_str("-2300000000000000000").unwrap()); - - let n: I256 = parse_units("-.2", 2).unwrap().into(); - assert_eq!(n, I256::from(-20), "leading dot"); - - let n: I256 = parse_units("-333.21", 2).unwrap().into(); - assert_eq!(n, I256::from(-33321), "trailing dot"); - - let n: I256 = parse_units("-98766", 16).unwrap().into(); - assert_eq!(n, I256::from_dec_str("-987660000000000000000").unwrap(), "no dot"); - - let n: I256 = parse_units("-3_3_0", 3).unwrap().into(); - assert_eq!(n, I256::from(-330000), "underscore"); - - let n: I256 = parse_units("-330", 0).unwrap().into(); - assert_eq!(n, I256::from(-330), "zero decimals"); - - let n: I256 = parse_units("-.1234", 3).unwrap().into(); - assert_eq!(n, I256::from(-123), "truncate too many decimals"); - - assert!(parse_units("-1", 80).is_err(), "overflow"); - - let two_e30 = - I256::from(-2) * I256::from_raw(U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0])); - let n: I256 = parse_units("-2", 30).unwrap().into(); - assert_eq!(n, two_e30, "-2e30"); - - let n: I256 = parse_units("-.33_319_2", 0).unwrap().into(); - assert_eq!(n, I256::zero(), "mix"); - - let n: I256 = parse_units("-", 3).unwrap().into(); - assert_eq!(n, I256::zero(), "empty"); - } - - #[test] - fn addr_checksum() { - let addr_list = vec![ - // mainnet - ( - None, - "27b1fdb04752bbc536007a920d24acb045561c26", - "0x27b1fdb04752bbc536007a920d24acb045561c26", - ), - ( - None, - "3599689e6292b81b2d85451025146515070129bb", - "0x3599689E6292b81B2d85451025146515070129Bb", - ), - ( - None, - "42712d45473476b98452f434e72461577d686318", - "0x42712D45473476b98452f434e72461577D686318", - ), - ( - None, - "52908400098527886e0f7030069857d2e4169ee7", - "0x52908400098527886E0F7030069857D2E4169EE7", - ), - ( - None, - "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", - "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", - ), - ( - None, - "6549f4939460de12611948b3f82b88c3c8975323", - "0x6549f4939460DE12611948b3f82b88C3C8975323", - ), - ( - None, - "66f9664f97f2b50f62d13ea064982f936de76657", - "0x66f9664f97F2b50F62D13eA064982f936dE76657", - ), - ( - None, - "88021160c5c792225e4e5452585947470010289d", - "0x88021160C5C792225E4E5452585947470010289D", - ), - // rsk mainnet - ( - Some(30), - "27b1fdb04752bbc536007a920d24acb045561c26", - "0x27b1FdB04752BBc536007A920D24ACB045561c26", - ), - ( - Some(30), - "3599689e6292b81b2d85451025146515070129bb", - "0x3599689E6292B81B2D85451025146515070129Bb", - ), - ( - Some(30), - "42712d45473476b98452f434e72461577d686318", - "0x42712D45473476B98452f434E72461577d686318", - ), - ( - Some(30), - "52908400098527886e0f7030069857d2e4169ee7", - "0x52908400098527886E0F7030069857D2E4169ee7", - ), - ( - Some(30), - "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed", - "0x5aaEB6053f3e94c9b9a09f33669435E7ef1bEAeD", - ), - ( - Some(30), - "6549f4939460de12611948b3f82b88c3c8975323", - "0x6549F4939460DE12611948B3F82B88C3C8975323", - ), - ( - Some(30), - "66f9664f97f2b50f62d13ea064982f936de76657", - "0x66F9664f97f2B50F62d13EA064982F936de76657", - ), - ]; - - for (chain_id, addr, checksummed_addr) in addr_list { - let addr = addr.parse::
().unwrap(); - assert_eq!(to_checksum(&addr, chain_id), String::from(checksummed_addr)); - } - } - - #[test] - fn checksummed_parse() { - let cases = vec![ - // mainnet - // wrong case - (None, "0x27b1fdb04752bbc536007a920d24acb045561c26", true), - (None, "0x27B1fdb04752bbc536007a920d24acb045561c26", false), - // no checksummed - (None, "0x52908400098527886e0f7030069857d2e4169ee7", false), - // without 0x - (None, "0x42712D45473476b98452f434e72461577D686318", true), - (None, "42712D45473476b98452f434e72461577D686318", true), - // invalid address string - (None, "0x52908400098527886E0F7030069857D2E4169EE7", true), - (None, "0x52908400098527886E0F7030069857D2E4169EEX", false), - (None, "0x52908400098527886E0F7030069857D2E4169EE70", false), - // mistyped address - (None, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", true), - (None, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAe1", false), - // rsk mainnet - // wrong case - (Some(30), "0x27b1FdB04752BBc536007A920D24ACB045561c26", true), - (Some(30), "0x27b1FdB04752BBc536007A920D24ACB045561C26", false), - // without 0x - (Some(30), "0x3599689E6292B81B2D85451025146515070129Bb", true), - (Some(30), "3599689E6292B81B2D85451025146515070129Bb", true), - // invalid address string - (Some(30), "0x42712D45473476B98452f434E72461577d686318", true), - (Some(30), "0x42712D45473476B98452f434E72461577d686318Z", false), - // mistyped address - (Some(30), "0x52908400098527886E0F7030069857D2E4169ee7", true), - (Some(30), "0x52908400098527886E0F7030069857D2E4169ee9", false), - ]; // mainnet - - for (chain_id, addr, expected) in cases { - let result = parse_checksummed(addr, chain_id); - assert_eq!( - result.is_ok(), - expected, - "chain_id: {:?} addr: {:?} error: {:?}", - chain_id, - addr, - result.err() - ); - } - } - - #[test] - fn contract_address() { - // http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed - let from = "6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0".parse::
().unwrap(); - for (nonce, expected) in [ - "cd234a471b72ba2f1ccf0a70fcaba648a5eecd8d", - "343c43a37d37dff08ae8c4a11544c718abb4fcf8", - "f778b86fa74e846c4f0a1fbd1335fe81c00a0c91", - "fffd933a0bc612844eaf0c6fe3e5b8e9b6c1d19c", - ] - .iter() - .enumerate() - { - let address = get_contract_address(from, nonce); - assert_eq!(address, expected.parse::
().unwrap()); - } - } - - #[test] - // Test vectors from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1014.md#examples - fn create2_address() { - for (from, salt, init_code, expected) in &[ - ( - "0000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "00", - "4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38", - ), - ( - "deadbeef00000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "00", - "B928f69Bb1D91Cd65274e3c79d8986362984fDA3", - ), - ( - "deadbeef00000000000000000000000000000000", - "000000000000000000000000feed000000000000000000000000000000000000", - "00", - "D04116cDd17beBE565EB2422F2497E06cC1C9833", - ), - ( - "0000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "deadbeef", - "70f2b2914A2a4b783FaEFb75f459A580616Fcb5e", - ), - ( - "00000000000000000000000000000000deadbeef", - "00000000000000000000000000000000000000000000000000000000cafebabe", - "deadbeef", - "60f3f640a8508fC6a86d45DF051962668E1e8AC7", - ), - ( - "00000000000000000000000000000000deadbeef", - "00000000000000000000000000000000000000000000000000000000cafebabe", - "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", - "1d8bfDC5D46DC4f61D6b6115972536eBE6A8854C", - ), - ( - "0000000000000000000000000000000000000000", - "0000000000000000000000000000000000000000000000000000000000000000", - "", - "E33C0C7F7df4809055C3ebA6c09CFe4BaF1BD9e0", - ), - ] { - // get_create2_address() - let from = from.parse::
().unwrap(); - let salt = hex::decode(salt).unwrap(); - let init_code = hex::decode(init_code).unwrap(); - let expected = expected.parse::
().unwrap(); - assert_eq!(expected, get_create2_address(from, salt.clone(), init_code.clone())); - - // get_create2_address_from_hash() - let init_code_hash = keccak256(init_code).to_vec(); - assert_eq!(expected, get_create2_address_from_hash(from, salt, init_code_hash)) - } - } - - #[test] - fn bytes32_string_parsing() { - let text_bytes_list = vec![ - ("", hex!("0000000000000000000000000000000000000000000000000000000000000000")), - ("A", hex!("4100000000000000000000000000000000000000000000000000000000000000")), - ( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", - hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"), - ), - ( - "!@#$%^&*(),./;'[]", - hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"), - ), - ]; - - for (text, bytes) in text_bytes_list { - assert_eq!(text, parse_bytes32_string(&bytes).unwrap()); - } - } - - #[test] - fn bytes32_string_formatting() { - let text_bytes_list = vec![ - ("", hex!("0000000000000000000000000000000000000000000000000000000000000000")), - ("A", hex!("4100000000000000000000000000000000000000000000000000000000000000")), - ( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", - hex!("4142434445464748494a4b4c4d4e4f505152535455565758595a303132333435"), - ), - ( - "!@#$%^&*(),./;'[]", - hex!("21402324255e262a28292c2e2f3b275b5d000000000000000000000000000000"), - ), - ]; - - for (text, bytes) in text_bytes_list { - assert_eq!(bytes, format_bytes32_string(text).unwrap()); - } - } - - #[test] - fn bytes32_string_formatting_too_long() { - assert!(matches!( - format_bytes32_string("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456").unwrap_err(), - ConversionError::TextTooLong - )); - } - - #[test] - fn test_eip1559_default_estimator() { - // If the base fee is below the triggering base fee, we should get the default priority fee - // with the base fee surged. - let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) - 1; - let rewards: Vec> = vec![vec![]]; - let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards); - assert_eq!(priority_fee, U256::from(EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE)); - assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); - - // If the base fee is above the triggering base fee, we calculate the priority fee using - // the fee history (rewards). - let base_fee_per_gas = U256::from(EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER) + 1; - let rewards: Vec> = vec![ - vec![100_000_000_000u64.into()], - vec![105_000_000_000u64.into()], - vec![102_000_000_000u64.into()], - ]; // say, last 3 blocks - let (base_fee, priority_fee) = eip1559_default_estimator(base_fee_per_gas, rewards.clone()); - assert_eq!(base_fee, base_fee_surged(base_fee_per_gas)); - assert_eq!(priority_fee, estimate_priority_fee(rewards.clone())); - - // The median should be taken because none of the changes are big enough to ignore values. - assert_eq!(estimate_priority_fee(rewards), 102_000_000_000u64.into()); - - // Ensure fee estimation doesn't panic when overflowing a u32. This had been a divide by - // zero. - let overflow = U256::from(u32::MAX) + 1; - let rewards_overflow: Vec> = vec![vec![overflow], vec![overflow]]; - assert_eq!(estimate_priority_fee(rewards_overflow), overflow); - } - - #[test] - fn int_or_hex_combinations() { - // make sure we can deserialize all combinations of int and hex - // including large numbers that would overflow u64 - // - // format: (string, expected value) - let cases = vec![ - // hex strings - ("\"0x0\"", U256::from(0)), - ("\"0x1\"", U256::from(1)), - ("\"0x10\"", U256::from(16)), - ("\"0x100000000000000000000000000000000000000000000000000\"", U256::from_dec_str("1606938044258990275541962092341162602522202993782792835301376").unwrap()), - // small num, both num and str form - ("10", U256::from(10)), - ("\"10\"", U256::from(10)), - // max u256, in both num and str form - ("\"115792089237316195423570985008687907853269984665640564039457584007913129639935\"", U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639935").unwrap()) - ]; - - #[derive(Deserialize)] - struct TestUint(#[serde(deserialize_with = "deserialize_stringified_numeric")] U256); - - for (string, expected) in cases { - let test: TestUint = serde_json::from_str(string) - .unwrap_or_else(|err| panic!("failed to deserialize {string}: {err}")); - assert_eq!(test.0, expected, "expected to deserialize {}", string); - } - } - - // Only tests for correctness, no edge cases. Uses examples from https://docs.ethers.org/v5/api/utils/address/#utils-computeAddress - #[test] - fn test_public_key_to_address() { - let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); - - // Compressed - let pubkey = VerifyingKey::from_sec1_bytes( - &hex::decode("0376698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762") - .unwrap(), - ) - .unwrap(); - assert_eq!(public_key_to_address(&pubkey), addr); - - // Uncompressed - let pubkey= VerifyingKey::from_sec1_bytes(&hex::decode("0476698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap()).unwrap(); - assert_eq!(public_key_to_address(&pubkey), addr); - } - - #[test] - fn test_raw_public_key_to_address() { - let addr = "0Ac1dF02185025F65202660F8167210A80dD5086".parse::
().unwrap(); - - let pubkey_bytes = hex::decode("76698beebe8ee5c74d8cc50ab84ac301ee8f10af6f28d0ffd6adf4d6d3b9b762d46ca56d3dad2ce13213a6f42278dabbb53259f2d92681ea6a0b98197a719be3").unwrap(); - - assert_eq!(raw_public_key_to_address(pubkey_bytes), addr); - } - - #[test] - #[should_panic] - fn test_raw_public_key_to_address_panics() { - let fake_pkb = vec![]; - - raw_public_key_to_address(fake_pkb); - } -} diff --git a/crates/test-utils/src/moonbeam.rs b/crates/test-utils/src/moonbeam.rs deleted file mode 100644 index 63576386d36..00000000000 --- a/crates/test-utils/src/moonbeam.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! Moonbeam utilities - -use std::collections::BTreeMap; - -use generic_array::GenericArray; -use k256::SecretKey; - -/// Returns the private developer keys -pub fn dev_keys() -> Vec { - MoonbeamDev::default().into_keys().collect() -} - -/// Holds private developer keys with their names -#[derive(Debug, Clone)] -pub struct MoonbeamDev { - keys: BTreeMap<&'static str, SecretKey>, -} - -impl MoonbeamDev { - pub fn keys(&self) -> impl Iterator { - self.keys.values() - } - - pub fn into_keys(self) -> impl Iterator { - self.keys.into_values() - } - - /// Get a key by then, like `Alith` - pub fn get(&self, name: impl AsRef) -> Option<&SecretKey> { - self.keys.get(name.as_ref()) - } - - pub fn alith(&self) -> &SecretKey { - self.get("Alith").unwrap() - } - - pub fn baltathar(&self) -> &SecretKey { - self.get("Baltathar").unwrap() - } - - pub fn charleth(&self) -> &SecretKey { - self.get("Charleth").unwrap() - } - - pub fn ethan(&self) -> &SecretKey { - self.get("Ethan").unwrap() - } -} - -fn to_secret_key(s: &str) -> SecretKey { - SecretKey::from_bytes(&GenericArray::clone_from_slice(&hex::decode(s).unwrap())).unwrap() -} - -impl Default for MoonbeamDev { - fn default() -> Self { - Self { - keys: BTreeMap::from([ - ( - "Alith", - to_secret_key( - "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133", - ), - ), - ( - "Baltathar", - to_secret_key( - "8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b", - ), - ), - ( - "Charleth", - to_secret_key( - "0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b", - ), - ), - ( - "Dorothy", - to_secret_key( - "39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68", - ), - ), - ( - "Faith", - to_secret_key( - "b9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df", - ), - ), - ( - "Goliath", - to_secret_key( - "96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18", - ), - ), - ( - "Heath", - to_secret_key( - "0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab", - ), - ), - ( - "Ida", - to_secret_key( - "4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8", - ), - ), - ( - "Judith", - to_secret_key( - "94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b", - ), - ), - ]), - } - } -} diff --git a/crates/test-utils/src/serde_helpers.rs b/crates/test-utils/src/serde_helpers.rs new file mode 100644 index 00000000000..a699c80be26 --- /dev/null +++ b/crates/test-utils/src/serde_helpers.rs @@ -0,0 +1,68 @@ +//! Some convenient serde helpers + +use alloy_primitives::U256; +use serde::{Deserialize, Deserializer}; + +/// Supports parsing u64 +/// +/// See +pub fn deserialize_stringified_u64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let num = U256::deserialize(deserializer)?; + num.try_into().map_err(serde::de::Error::custom) +} + +/// Supports parsing u64 +/// +/// See +pub fn deserialize_stringified_u64_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + if let Some(num) = Option::::deserialize(deserializer)? { + num.try_into().map(Some).map_err(serde::de::Error::custom) + } else { + Ok(None) + } +} + +/// Helper type to deserialize sequence of numbers +#[derive(Deserialize)] +#[serde(untagged)] +pub enum NumericSeq { + Seq([U256; 1]), + U256(U256), + Num(u64), +} + +/// Deserializes a number from hex or int +pub fn deserialize_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + U256::deserialize(deserializer) +} + +/// Deserializes a number from hex or int, but optionally +pub fn deserialize_number_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::::deserialize(deserializer) +} + +/// Deserializes single integer params: `1, [1], ["0x01"]` +pub fn deserialize_number_seq<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let num = match NumericSeq::deserialize(deserializer)? { + NumericSeq::Seq(seq) => seq[0].into(), + NumericSeq::U256(n) => n, + NumericSeq::Num(n) => U256::from(n), + }; + + Ok(num) +} diff --git a/crates/test-utils/src/units.rs b/crates/test-utils/src/units.rs deleted file mode 100644 index 82c55476f0c..00000000000 --- a/crates/test-utils/src/units.rs +++ /dev/null @@ -1,173 +0,0 @@ -use super::ConversionError; -use std::{convert::TryFrom, fmt, str::FromStr}; - -/// Common Ethereum unit types. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Units { - /// Wei is equivalent to 1 wei. - Wei, - /// Kwei is equivalent to 1e3 wei. - Kwei, - /// Mwei is equivalent to 1e6 wei. - Mwei, - /// Gwei is equivalent to 1e9 wei. - Gwei, - /// Twei is equivalent to 1e12 wei. - Twei, - /// Pwei is equivalent to 1e15 wei. - Pwei, - /// Ether is equivalent to 1e18 wei. - Ether, - /// Other less frequent unit sizes, equivalent to 1e{0} wei. - Other(u32), -} - -impl fmt::Display for Units { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.pad(self.as_num().to_string().as_str()) - } -} - -impl TryFrom for Units { - type Error = ConversionError; - - fn try_from(value: u32) -> Result { - Ok(Units::Other(value)) - } -} - -impl TryFrom for Units { - type Error = ConversionError; - - fn try_from(value: i32) -> Result { - Ok(Units::Other(value as u32)) - } -} - -impl TryFrom for Units { - type Error = ConversionError; - - fn try_from(value: usize) -> Result { - Ok(Units::Other(value as u32)) - } -} - -impl TryFrom for Units { - type Error = ConversionError; - - fn try_from(value: String) -> Result { - Self::from_str(&value) - } -} - -impl<'a> TryFrom<&'a String> for Units { - type Error = ConversionError; - - fn try_from(value: &'a String) -> Result { - Self::from_str(value) - } -} - -impl TryFrom<&str> for Units { - type Error = ConversionError; - - fn try_from(value: &str) -> Result { - Self::from_str(value) - } -} - -impl FromStr for Units { - type Err = ConversionError; - - fn from_str(s: &str) -> Result { - Ok(match s.to_lowercase().as_str() { - "eth" | "ether" => Units::Ether, - "pwei" | "milli" | "milliether" | "finney" => Units::Pwei, - "twei" | "micro" | "microether" | "szabo" => Units::Twei, - "gwei" | "nano" | "nanoether" | "shannon" => Units::Gwei, - "mwei" | "pico" | "picoether" | "lovelace" => Units::Mwei, - "kwei" | "femto" | "femtoether" | "babbage" => Units::Kwei, - "wei" => Units::Wei, - _ => return Err(ConversionError::UnrecognizedUnits(s.to_string())), - }) - } -} - -impl From for u32 { - fn from(units: Units) -> Self { - units.as_num() - } -} - -impl From for i32 { - fn from(units: Units) -> Self { - units.as_num() as i32 - } -} - -impl From for usize { - fn from(units: Units) -> Self { - units.as_num() as usize - } -} - -impl Units { - pub fn as_num(&self) -> u32 { - match self { - Units::Wei => 0, - Units::Kwei => 3, - Units::Mwei => 6, - Units::Gwei => 9, - Units::Twei => 12, - Units::Pwei => 15, - Units::Ether => 18, - Units::Other(inner) => *inner, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use Units::*; - - #[test] - fn test_units() { - assert_eq!(Wei.as_num(), 0); - assert_eq!(Kwei.as_num(), 3); - assert_eq!(Mwei.as_num(), 6); - assert_eq!(Gwei.as_num(), 9); - assert_eq!(Twei.as_num(), 12); - assert_eq!(Pwei.as_num(), 15); - assert_eq!(Ether.as_num(), 18); - assert_eq!(Other(10).as_num(), 10); - assert_eq!(Other(20).as_num(), 20); - } - - #[test] - fn test_into() { - assert_eq!(Units::try_from("wei").unwrap(), Wei); - assert_eq!(Units::try_from("kwei").unwrap(), Kwei); - assert_eq!(Units::try_from("mwei").unwrap(), Mwei); - assert_eq!(Units::try_from("gwei").unwrap(), Gwei); - assert_eq!(Units::try_from("twei").unwrap(), Twei); - assert_eq!(Units::try_from("pwei").unwrap(), Pwei); - assert_eq!(Units::try_from("ether").unwrap(), Ether); - - assert_eq!(Units::try_from("wei".to_string()).unwrap(), Wei); - assert_eq!(Units::try_from("kwei".to_string()).unwrap(), Kwei); - assert_eq!(Units::try_from("mwei".to_string()).unwrap(), Mwei); - assert_eq!(Units::try_from("gwei".to_string()).unwrap(), Gwei); - assert_eq!(Units::try_from("twei".to_string()).unwrap(), Twei); - assert_eq!(Units::try_from("pwei".to_string()).unwrap(), Pwei); - assert_eq!(Units::try_from("ether".to_string()).unwrap(), Ether); - - assert_eq!(Units::try_from(&"wei".to_string()).unwrap(), Wei); - assert_eq!(Units::try_from(&"kwei".to_string()).unwrap(), Kwei); - assert_eq!(Units::try_from(&"mwei".to_string()).unwrap(), Mwei); - assert_eq!(Units::try_from(&"gwei".to_string()).unwrap(), Gwei); - assert_eq!(Units::try_from(&"twei".to_string()).unwrap(), Twei); - assert_eq!(Units::try_from(&"pwei".to_string()).unwrap(), Pwei); - assert_eq!(Units::try_from(&"ether".to_string()).unwrap(), Ether); - } -} From ba7b980159df27f392fc4738d22fea93c1ae8d94 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:28:15 +0100 Subject: [PATCH 3/6] chore: lints --- crates/test-utils/Cargo.toml | 4 +++- crates/test-utils/README.md | 3 +++ crates/test-utils/src/lib.rs | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 crates/test-utils/README.md diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 1a9e164f05d..23e772d326a 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -14,7 +14,9 @@ exclude.workspace = true [dependencies] alloy-primitives = { workspace = true, features = ["k256"] } k256.workspace = true -rand.workspace = true serde_json.workspace = true serde.workspace = true tempfile.workspace = true + +[dev-dependencies] +rand.workspace = true diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md new file mode 100644 index 00000000000..00e1c83f4e7 --- /dev/null +++ b/crates/test-utils/README.md @@ -0,0 +1,3 @@ +# alloy-test-utils + +Common Ethereum testing utilities. diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 55b7e5c2490..d1681a7195b 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -1,3 +1,21 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + // TODO + // missing_copy_implementations, + // missing_debug_implementations, + // missing_docs, + // unreachable_pub, + clippy::missing_const_for_fn, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + use alloy_primitives::{Bytes, B256, U256}; use serde::Deserializer; From eb6f92d21fab33f251131c813f1f5448e10bac44 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:31:54 +0100 Subject: [PATCH 4/6] chore: lints --- crates/test-utils/src/anvil.rs | 1 + crates/test-utils/src/genesis.rs | 6 ++++++ crates/test-utils/src/lib.rs | 13 ++++++------- crates/test-utils/src/serde_helpers.rs | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/test-utils/src/anvil.rs b/crates/test-utils/src/anvil.rs index 37713cc303d..f56456b01b3 100644 --- a/crates/test-utils/src/anvil.rs +++ b/crates/test-utils/src/anvil.rs @@ -16,6 +16,7 @@ const ANVIL_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; /// An anvil CLI instance. Will close the instance when dropped. /// /// Construct this using [`Anvil`]. +#[derive(Debug)] pub struct AnvilInstance { pid: Child, private_keys: Vec, diff --git a/crates/test-utils/src/genesis.rs b/crates/test-utils/src/genesis.rs index 9c274259aeb..79ebc44d728 100644 --- a/crates/test-utils/src/genesis.rs +++ b/crates/test-utils/src/genesis.rs @@ -123,15 +123,19 @@ impl Genesis { /// An account in the state of the genesis block. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GenesisAccount { + /// The nonce of the account. #[serde( skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_stringified_u64_opt", default )] pub nonce: Option, + /// The balance of the account. pub balance: U256, + /// The code of the account, if any. #[serde(skip_serializing_if = "Option::is_none", default)] pub code: Option, + /// The storage of the account, if any. #[serde( skip_serializing_if = "Option::is_none", deserialize_with = "from_unformatted_hex_map", @@ -303,10 +307,12 @@ const fn one() -> u64 { /// Empty consensus configuration for proof-of-work networks. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[allow(missing_copy_implementations)] pub struct EthashConfig {} /// Consensus configuration for Clique. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[allow(missing_copy_implementations)] pub struct CliqueConfig { /// Number of seconds between blocks to enforce. #[serde( diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index d1681a7195b..04ebfd4bcb7 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -4,11 +4,10 @@ html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" )] #![warn( - // TODO - // missing_copy_implementations, - // missing_debug_implementations, - // missing_docs, - // unreachable_pub, + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, clippy::missing_const_for_fn, rustdoc::all )] @@ -19,10 +18,10 @@ use alloy_primitives::{Bytes, B256, U256}; use serde::Deserializer; -mod anvil; +pub mod anvil; pub use anvil::{Anvil, AnvilInstance}; -mod geth; +pub mod geth; pub use geth::{Geth, GethInstance}; mod genesis; diff --git a/crates/test-utils/src/serde_helpers.rs b/crates/test-utils/src/serde_helpers.rs index a699c80be26..72444d3013e 100644 --- a/crates/test-utils/src/serde_helpers.rs +++ b/crates/test-utils/src/serde_helpers.rs @@ -31,7 +31,7 @@ where /// Helper type to deserialize sequence of numbers #[derive(Deserialize)] #[serde(untagged)] -pub enum NumericSeq { +enum NumericSeq { Seq([U256; 1]), U256(U256), Num(u64), From d62795ef57999e2a724f2f3d846eebe1643bf07e Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:48:09 +0100 Subject: [PATCH 5/6] chore: clippy --- crates/test-utils/Cargo.toml | 2 +- crates/test-utils/src/lib.rs | 1 - crates/test-utils/src/serde_helpers.rs | 9 +++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 23e772d326a..2593fa1e9ae 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-primitives = { workspace = true, features = ["k256"] } +alloy-primitives = { workspace = true, features = ["k256", "serde"] } k256.workspace = true serde_json.workspace = true serde.workspace = true diff --git a/crates/test-utils/src/lib.rs b/crates/test-utils/src/lib.rs index 04ebfd4bcb7..3947783acf9 100644 --- a/crates/test-utils/src/lib.rs +++ b/crates/test-utils/src/lib.rs @@ -8,7 +8,6 @@ missing_debug_implementations, missing_docs, unreachable_pub, - clippy::missing_const_for_fn, rustdoc::all )] #![cfg_attr(not(test), warn(unused_crate_dependencies))] diff --git a/crates/test-utils/src/serde_helpers.rs b/crates/test-utils/src/serde_helpers.rs index 72444d3013e..d647b003601 100644 --- a/crates/test-utils/src/serde_helpers.rs +++ b/crates/test-utils/src/serde_helpers.rs @@ -58,11 +58,8 @@ pub fn deserialize_number_seq<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { - let num = match NumericSeq::deserialize(deserializer)? { - NumericSeq::Seq(seq) => seq[0].into(), - NumericSeq::U256(n) => n, + Ok(match NumericSeq::deserialize(deserializer)? { + NumericSeq::Seq([n]) | NumericSeq::U256(n) => n, NumericSeq::Num(n) => U256::from(n), - }; - - Ok(num) + }) } From 4d49fbe125e17c099da49688c547943fdd656b0c Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:49:52 +0100 Subject: [PATCH 6/6] chore: rename to node-bindings --- .github/workflows/ci.yml | 2 +- Cargo.toml | 4 ++-- crates/{test-utils => node-bindings}/Cargo.toml | 4 ++-- crates/node-bindings/README.md | 3 +++ crates/{test-utils => node-bindings}/src/anvil.rs | 6 +++--- crates/{test-utils => node-bindings}/src/genesis.rs | 0 crates/{test-utils => node-bindings}/src/geth.rs | 4 ++-- crates/{test-utils => node-bindings}/src/lib.rs | 0 crates/{test-utils => node-bindings}/src/serde_helpers.rs | 0 crates/providers/Cargo.toml | 2 +- crates/providers/src/provider.rs | 2 +- crates/rpc-client/Cargo.toml | 2 +- crates/rpc-client/tests/it/ipc.rs | 2 +- crates/test-utils/README.md | 3 --- 14 files changed, 17 insertions(+), 17 deletions(-) rename crates/{test-utils => node-bindings}/Cargo.toml (83%) create mode 100644 crates/node-bindings/README.md rename crates/{test-utils => node-bindings}/src/anvil.rs (98%) rename crates/{test-utils => node-bindings}/src/genesis.rs (100%) rename crates/{test-utils => node-bindings}/src/geth.rs (99%) rename crates/{test-utils => node-bindings}/src/lib.rs (100%) rename crates/{test-utils => node-bindings}/src/serde_helpers.rs (100%) delete mode 100644 crates/test-utils/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98cea9af8eb..5ea86c2dfd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: --exclude alloy-signer-aws \ --exclude alloy-signer-ledger \ --exclude alloy-signer-trezor \ - --exclude alloy-test-utils \ + --exclude alloy-node-bindings \ --exclude alloy-transport-ipc feature-checks: diff --git a/Cargo.toml b/Cargo.toml index a8442949dc2..3d29ee2c745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,15 +21,15 @@ alloy-consensus = { version = "0.1.0", path = "crates/consensus" } alloy-eips = { version = "0.1.0", path = "crates/eips" } alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" } alloy-network = { version = "0.1.0", path = "crates/network" } +alloy-node-bindings = { version = "0.1.0", path = "crates/node-bindings" } alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } -alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } alloy-rpc-trace-types = { version = "0.1.0", path = "crates/rpc-trace-types" } +alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } alloy-signer = { version = "0.1.0", path = "crates/signer" } alloy-signer-aws = { version = "0.1.0", path = "crates/signer-aws" } alloy-signer-ledger = { version = "0.1.0", path = "crates/signer-ledger" } alloy-signer-trezor = { version = "0.1.0", path = "crates/signer-trezor" } -alloy-test-utils = { version = "0.1.0", path = "crates/test-utils" } alloy-transport = { version = "0.1.0", path = "crates/transport" } alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" } alloy-transport-ipc = { version = "0.1.0", path = "crates/transport-ipc" } diff --git a/crates/test-utils/Cargo.toml b/crates/node-bindings/Cargo.toml similarity index 83% rename from crates/test-utils/Cargo.toml rename to crates/node-bindings/Cargo.toml index 2593fa1e9ae..fc75eff14e3 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/node-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "alloy-test-utils" -description = "Common Ethereum testing utilities" +name = "alloy-node-bindings" +description = "Ethereum execution-layer client bindings" version.workspace = true edition.workspace = true diff --git a/crates/node-bindings/README.md b/crates/node-bindings/README.md new file mode 100644 index 00000000000..a9195594d68 --- /dev/null +++ b/crates/node-bindings/README.md @@ -0,0 +1,3 @@ +# alloy-node-bindings + +Ethereum execution-layer client bindings. diff --git a/crates/test-utils/src/anvil.rs b/crates/node-bindings/src/anvil.rs similarity index 98% rename from crates/test-utils/src/anvil.rs rename to crates/node-bindings/src/anvil.rs index f56456b01b3..c242d91c8c3 100644 --- a/crates/test-utils/src/anvil.rs +++ b/crates/node-bindings/src/anvil.rs @@ -73,7 +73,7 @@ impl Drop for AnvilInstance { /// # Example /// /// ```no_run -/// use alloy_test_utils::Anvil; +/// use alloy_node_bindings::Anvil; /// /// let port = 8545u16; /// let url = format!("http://localhost:{}", port).to_string(); @@ -106,7 +106,7 @@ impl Anvil { /// # Example /// /// ``` - /// # use alloy_test_utils::Anvil; + /// # use alloy_node_bindings::Anvil; /// fn a() { /// let anvil = Anvil::default().spawn(); /// @@ -122,7 +122,7 @@ impl Anvil { /// # Example /// /// ``` - /// # use alloy_test_utils::Anvil; + /// # use alloy_node_bindings::Anvil; /// fn a() { /// let anvil = Anvil::at("~/.foundry/bin/anvil").spawn(); /// diff --git a/crates/test-utils/src/genesis.rs b/crates/node-bindings/src/genesis.rs similarity index 100% rename from crates/test-utils/src/genesis.rs rename to crates/node-bindings/src/genesis.rs diff --git a/crates/test-utils/src/geth.rs b/crates/node-bindings/src/geth.rs similarity index 99% rename from crates/test-utils/src/geth.rs rename to crates/node-bindings/src/geth.rs index 9be6e6bd8f8..41d802c53bb 100644 --- a/crates/test-utils/src/geth.rs +++ b/crates/node-bindings/src/geth.rs @@ -178,7 +178,7 @@ impl Default for PrivateNetOptions { /// # Example /// /// ```no_run -/// use alloy_test_utils::Geth; +/// use alloy_node_bindings::Geth; /// /// let port = 8545u16; /// let url = format!("http://localhost:{}", port).to_string(); @@ -215,7 +215,7 @@ impl Geth { /// # Example /// /// ``` - /// use alloy_test_utils::Geth; + /// use alloy_node_bindings::Geth; /// # fn a() { /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn(); /// diff --git a/crates/test-utils/src/lib.rs b/crates/node-bindings/src/lib.rs similarity index 100% rename from crates/test-utils/src/lib.rs rename to crates/node-bindings/src/lib.rs diff --git a/crates/test-utils/src/serde_helpers.rs b/crates/node-bindings/src/serde_helpers.rs similarity index 100% rename from crates/test-utils/src/serde_helpers.rs rename to crates/node-bindings/src/serde_helpers.rs diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index f3f5679209a..24aceeaa6f1 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -26,7 +26,7 @@ reqwest.workspace = true auto_impl = "1.1.0" [dev-dependencies] -alloy-test-utils.workspace = true +alloy-node-bindings.workspace = true tokio = { version = "1.33.0", features = ["macros"] } [features] diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index 7aa06402a60..84e671f5317 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -521,9 +521,9 @@ mod tests { provider::{Provider, TempProvider}, utils, }; + use alloy_node_bindings::Anvil; use alloy_primitives::{address, b256, bytes, U256, U64}; use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; - use alloy_test_utils::Anvil; #[tokio::test] async fn gets_block_number() { diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 2ef60348b2c..5d62220fd98 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -35,7 +35,7 @@ alloy-transport-ipc = { workspace = true, optional = true } [dev-dependencies] alloy-primitives.workspace = true -alloy-test-utils.workspace = true +alloy-node-bindings.workspace = true alloy-transport-ipc = { workspace = true, features = ["mock"] } alloy-transport-ws.workspace = true diff --git a/crates/rpc-client/tests/it/ipc.rs b/crates/rpc-client/tests/it/ipc.rs index 28e3f0d4c68..53de95b909b 100644 --- a/crates/rpc-client/tests/it/ipc.rs +++ b/crates/rpc-client/tests/it/ipc.rs @@ -1,7 +1,7 @@ +use alloy_node_bindings::{Geth, GethInstance}; use alloy_primitives::U64; use alloy_pubsub::PubSubFrontend; use alloy_rpc_client::{ClientBuilder, RpcCall, RpcClient}; -use alloy_test_utils::{Geth, GethInstance}; use alloy_transport_ipc::IpcConnect; use std::borrow::Cow; use tempfile::NamedTempFile; diff --git a/crates/test-utils/README.md b/crates/test-utils/README.md deleted file mode 100644 index 00e1c83f4e7..00000000000 --- a/crates/test-utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# alloy-test-utils - -Common Ethereum testing utilities.