Repository: gakonst/ethers-rs
Pull request #2297 (2023-03-23 20:03:47)

Jepsen/mdbook
I added some more to the documentation here

- [x] Events
- [x] Subscriptions

contributes to #2282 


0xJepsen: Thanks for the feedback, I went and made some of the easy changes. I have yet to distinguish the difference between `Event::stream` and `Event::subscribe` like you recommended but will look to getting around to it soon. 
prestwich: > Thanks for the feedback, I went and made some of the easy changes. I have yet to distinguish the difference between Event::stream and Event::subscribe like you recommended but will look to getting around to it soon.

did you intend to do this as part of this PR? or in a future one?


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aea50aa1..5533f238 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,7 +67,8 @@ jobs:
               run: |
                   cargo nextest run \
                   ${{ matrix.flags.flags }} \
-                  -E "!(deps(ethers-etherscan) & kind(test))"
+                  -E "!(deps(ethers-etherscan) & kind(test))" \
+                  --retries 3
 
     etherscan-tests:
         name: etherscan tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c2f760d..fe729f3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,16 @@ which is configured [here](./cliff.toml).
 
 Please do not manually edit this file.
 
+## [Unreleased]
+
+### Miscellaneous Tasks
+
+- Fix clippy lints
+
+### Other
+
+- Revert "temp: disable cyclical dep"
+
 ## [2.0.3] - 2023-04-12
 
 ### Bug Fixes
@@ -20,11 +30,69 @@ Please do not manually edit this file.
 - No need to multiply again with `GWEI_TO_WEI_U256` ([#2326](https://github.com/gakonst/ethers-rs/issues/2326))
 - [**breaking**] Change the `GasOracle` fields to be `f64` ([#2325](https://github.com/gakonst/ethers-rs/issues/2325))
 - Docsrs cfg_attr ([#2309](https://github.com/gakonst/ethers-rs/issues/2309))
+
+### Documentation
+
+- Update some doctest examples ([#2339](https://github.com/gakonst/ethers-rs/issues/2339))
+- Add erc20 token transfer example ([#2295](https://github.com/gakonst/ethers-rs/issues/2295))
+
+### Features
+
+- Add Base Goerli chain ([#2338](https://github.com/gakonst/ethers-rs/issues/2338))
+- Very permissive etherscan gwei deser ([#2327](https://github.com/gakonst/ethers-rs/issues/2327))
+- Syn 2.0, merge eip712 crate into ethers-contract-derive ([#2279](https://github.com/gakonst/ethers-rs/issues/2279))
+- Add Settings sanitize ([#2329](https://github.com/gakonst/ethers-rs/issues/2329))
+- `Transaction::decode()` sets the hash ([#2303](https://github.com/gakonst/ethers-rs/issues/2303))
+
+### Miscellaneous Tasks
+
+- Minify test json files ([#2341](https://github.com/gakonst/ethers-rs/issues/2341))
+- Dont retain errors ([#2332](https://github.com/gakonst/ethers-rs/issues/2332))
+- Update solang-parser ([#2334](https://github.com/gakonst/ethers-rs/issues/2334))
+- Update deny config ([#2330](https://github.com/gakonst/ethers-rs/issues/2330))
+- Ignore another rev ([#2311](https://github.com/gakonst/ethers-rs/issues/2311))
+- Use ethers 2 by default when generating bindings ([#2317](https://github.com/gakonst/ethers-rs/issues/2317))
+
+### Other
+
+- Disable cyclical dep
+- Add scroll alpha testnet ([#2316](https://github.com/gakonst/ethers-rs/issues/2316))
+- Add legacy-ws feat to meta crate ([#2328](https://github.com/gakonst/ethers-rs/issues/2328))
+
+### Performance
+
+- Early return in EthCodec derive ([#2314](https://github.com/gakonst/ethers-rs/issues/2314))
+
+## [2.0.2] - 2023-03-28
+
+### Bug Fixes
+
 - Ensure ethers-solc has tokio-rt-multithread
 - Ambiguous_glob_reexports rustc lint ([#2301](https://github.com/gakonst/ethers-rs/issues/2301))
 - Broken eip155 logic in aws signer ([#2300](https://github.com/gakonst/ethers-rs/issues/2300))
 - Enable doc_cfg feature for docsrs ([#2294](https://github.com/gakonst/ethers-rs/issues/2294))
 - Add missing feature on ethers tests
+
+### Depedencies
+
+- Bump generic-array
+
+### Miscellaneous Tasks
+
+- Add eyre dependency ([#2305](https://github.com/gakonst/ethers-rs/issues/2305))
+
+### Other
+
+- Rm celo integration test, install missing solc version ([#2292](https://github.com/gakonst/ethers-rs/issues/2292))
+
+### Testing
+
+- Ensure multithreaded tokio rt
+
+## [2.0.1] - 2023-03-21
+
+### Bug Fixes
+
 - Ethers-contract circular dep on ethers-signers ([#2291](https://github.com/gakonst/ethers-rs/issues/2291))
 - Features ([#2290](https://github.com/gakonst/ethers-rs/issues/2290))
 - Handle all struct field types ([#2289](https://github.com/gakonst/ethers-rs/issues/2289))
@@ -98,7 +166,6 @@ Please do not manually edit this file.
 
 ### Depedencies
 
-- Bump generic-array
 - Bump MSRV from 1.64 to 1.65 ([#2277](https://github.com/gakonst/ethers-rs/issues/2277))
 - Bump and use workspace dependencies ([#2222](https://github.com/gakonst/ethers-rs/issues/2222))
 - Bump crypto deps ([#2260](https://github.com/gakonst/ethers-rs/issues/2260))
@@ -168,8 +235,6 @@ Please do not manually edit this file.
 
 ### Documentation
 
-- Update some doctest examples ([#2339](https://github.com/gakonst/ethers-rs/issues/2339))
-- Add erc20 token transfer example ([#2295](https://github.com/gakonst/ethers-rs/issues/2295))
 - Fix broken links, update documentation ([#2203](https://github.com/gakonst/ethers-rs/issues/2203))
 - Update providers book and examples ([#2098](https://github.com/gakonst/ethers-rs/issues/2098))
 - Rename `event_derives` to `derives` ([#2018](https://github.com/gakonst/ethers-rs/issues/2018))
@@ -181,11 +246,6 @@ Please do not manually edit this file.
 
 ### Features
 
-- Add Base Goerli chain ([#2338](https://github.com/gakonst/ethers-rs/issues/2338))
-- Very permissive etherscan gwei deser ([#2327](https://github.com/gakonst/ethers-rs/issues/2327))
-- Syn 2.0, merge eip712 crate into ethers-contract-derive ([#2279](https://github.com/gakonst/ethers-rs/issues/2279))
-- Add Settings sanitize ([#2329](https://github.com/gakonst/ethers-rs/issues/2329))
-- `Transaction::decode()` sets the hash ([#2303](https://github.com/gakonst/ethers-rs/issues/2303))
 - Improve error diagnostic ([#2280](https://github.com/gakonst/ethers-rs/issues/2280))
 - Roundtrip serde + to/from strings ([#2270](https://github.com/gakonst/ethers-rs/issues/2270))
 - Support empty events ([#2263](https://github.com/gakonst/ethers-rs/issues/2263))
@@ -242,13 +302,6 @@ Please do not manually edit this file.
 
 ### Miscellaneous Tasks
 
-- Minify test json files ([#2341](https://github.com/gakonst/ethers-rs/issues/2341))
-- Dont retain errors ([#2332](https://github.com/gakonst/ethers-rs/issues/2332))
-- Update solang-parser ([#2334](https://github.com/gakonst/ethers-rs/issues/2334))
-- Update deny config ([#2330](https://github.com/gakonst/ethers-rs/issues/2330))
-- Ignore another rev ([#2311](https://github.com/gakonst/ethers-rs/issues/2311))
-- Use ethers 2 by default when generating bindings ([#2317](https://github.com/gakonst/ethers-rs/issues/2317))
-- Add eyre dependency ([#2305](https://github.com/gakonst/ethers-rs/issues/2305))
 - Remove redundant clone
 - Make clippy happy ([#2264](https://github.com/gakonst/ethers-rs/issues/2264))
 - Allow clippy false positive ([#2259](https://github.com/gakonst/ethers-rs/issues/2259))
@@ -282,10 +335,6 @@ Please do not manually edit this file.
 
 ### Other
 
-- Disable cyclical dep
-- Add scroll alpha testnet ([#2316](https://github.com/gakonst/ethers-rs/issues/2316))
-- Add legacy-ws feat to meta crate ([#2328](https://github.com/gakonst/ethers-rs/issues/2328))
-- Rm celo integration test, install missing solc version ([#2292](https://github.com/gakonst/ethers-rs/issues/2292))
 - Prestwich/gas escalator dangle ([#2284](https://github.com/gakonst/ethers-rs/issues/2284))
 - Contracts chapter ([#2281](https://github.com/gakonst/ethers-rs/issues/2281))
 - Revert "chore: remove redundant clone"
@@ -328,7 +377,6 @@ Please do not manually edit this file.
 
 ### Performance
 
-- Early return in EthCodec derive ([#2314](https://github.com/gakonst/ethers-rs/issues/2314))
 - Wrap source content in Arc ([#2138](https://github.com/gakonst/ethers-rs/issues/2138))
 - Avoid unnecessary allocations ([#2046](https://github.com/gakonst/ethers-rs/issues/2046))
 
@@ -360,7 +408,6 @@ Please do not manually edit this file.
 
 ### Testing
 
-- Ensure multithreaded tokio rt
 - Simplify test cleanup ([#2220](https://github.com/gakonst/ethers-rs/issues/2220))
 - Add parse pk test ([#2194](https://github.com/gakonst/ethers-rs/issues/2194))
 - Disable signer integration tests temporarily
diff --git a/Cargo.lock b/Cargo.lock
index 1c2ee748..d8f50ce8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -539,21 +539,19 @@ dependencies = [
 
 [[package]]
 name = "coins-bip39"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad2a68a46b9d8cc90484f0689adc0e4c890eb215bf698ae52e5235bb88f40be7"
+checksum = "84f4d04ee18e58356accd644896aeb2094ddeafb6a713e056cef0c0a8e468c15"
 dependencies = [
  "bitvec 0.17.4",
  "coins-bip32",
  "getrandom",
- "hex",
  "hmac 0.12.1",
  "once_cell",
  "pbkdf2 0.12.1",
  "rand",
  "sha2 0.10.6",
  "thiserror",
- "tracing",
 ]
 
 [[package]]
diff --git a/README.md b/README.md
index c5037f62..90f60f12 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ You should be able to build a wasm app that uses ethers-rs (see the [example](./
 compile in WASM, please
 [open an issue](https://github.com/gakonst/ethers-rs/issues/new/choose).
 There is currently no plan to provide an official JS/TS-accessible library
-interface. we believe [ethers.js](https://docs.ethers.io/v5/) serves that need
+interface. We believe [viem](https://viem.sh) or [ethers.js](https://docs.ethers.io/v6/) serves that need
 very well.
 
 Similarly, you should be able to build FFI bindings to ethers-rs. If ethers
diff --git a/book/SUMMARY.md b/book/SUMMARY.md
index 0bf0bae0..63ffba6f 100644
--- a/book/SUMMARY.md
+++ b/book/SUMMARY.md
@@ -38,13 +38,14 @@
   - [Events](./contracts/events.md)
   - [Events with meta](./contracts/events-with-meta.md)
   - [Methods](contracts/methods.md)
-- [Events]()
-  - [Logs and filtering]()
+- [Events](./events/events.md)
+  - [Logs and filtering](events/logs-and-filtering.md)
   - [Solidity topics]()
-- [Subscriptions]()
-  - [Watch blocks]()
-  - [Subscribe events by type]()
-  - [Subscribe logs]()
+- [Subscriptions](./subscriptions/subscriptions.md)
+  - [Watch blocks](./subscriptions/watch-blocks.md)
+  - [Subscribe events by type](./subscriptions/events-by-type.md)
+  - [Subscribe logs](./subscriptions/logs.md)
+  - [Mulitple subscriptions](./subscriptions/multiple-subscriptions.md)
 - [Queries]()
   - [Blocks]()
   - [Contracts]()
diff --git a/book/events/events.md b/book/events/events.md
new file mode 100644
index 00000000..d9bf385a
--- /dev/null
+++ b/book/events/events.md
@@ -0,0 +1,105 @@
+# Ethers-rs: Working with Events
+
+In this section we will discuss how to monitor, subscribe, and listen to events using the ethers-rs library. Events are an essential part of smart contract development, as they allow you to track specific occurrences on the blockchain, such as transactions, state changes, or function calls.
+
+## Overview
+
+ethers-rs provides a simple and efficient way to interact with events emitted by smart contracts. You can listen to events, filter them based on certain conditions, and subscribe to event streams for real-time updates. The key components you will work with are:
+
+1. Event: A struct representing an event emitted by a smart contract.
+2. EventWatcher: A struct that allows you to monitor and filter events.
+3. SubscriptionStream: A stream of events you can subscribe to for real-time updates.
+
+## Getting Started
+
+Before diving into event handling, ensure you have ethers-rs added to your project's dependencies in Cargo.toml:
+
+```toml
+[dependencies]
+ethers = { version = "2.0.0.", features = ["full"] }
+```
+
+Now, let's import the necessary components from the ethers-rs library:
+
+```rust
+use ethers::{
+prelude::contract::{Contract, EthEvent},
+};
+```
+
+### Listening to Events
+
+To listen to events, you'll need to instantiate a Contract object and use the event method to create an Event struct. You'll also need to define a struct that implements the EthEvent trait, representing the specific event you want to listen to.
+
+Consider a simple smart contract that emits an event called ValueChanged:
+
+```solidity
+pragma solidity ^0.8.0;
+
+contract SimpleStorage {
+
+    uint256 public value;
+    event ValueChanged(uint256 newValue);
+
+    function setValue(uint256 _value) public {
+        value = _value;
+        emit ValueChanged(_value);
+    }
+
+}
+```
+
+First, define a struct representing the ValueChanged event:
+
+```rust
+#[derive(Debug, Clone, EthEvent)]
+pub struct ValueChanged {
+    pub new_value: U256,
+}
+```
+
+Then, create an instance of the Contract object and listen for the ValueChanged event:
+
+```rust
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let provider = Provider::<Http>::try_from("http://localhost:8545")?;
+    let contract_address = "0xcontract_address_here".parse()?;
+    let contract = Contract::from_json(provider,
+        contract_address,
+        include_bytes!("../contracts/abis/SimpleStorage.json"))?;
+
+    let event = contract.event::<ValueChanged>()?;
+
+    // Your code to handle the event goes here.
+
+    Ok(())
+
+}
+```
+
+### Filtering Events
+
+You can filter events based on specific conditions using the EventWatcher struct. To create an EventWatcher, call the watcher method on your Event object:
+
+```rust
+let watcher = event.watcher().from_block(5).to_block(10);
+```
+
+In this example, the EventWatcher will only monitor events from block 5 to block 10.
+
+### Subscribing to Events
+
+To receive real-time updates for an event, create a SubscriptionStream by calling the subscribe method on your EventWatcher:
+
+```rust
+let mut stream = watcher.subscribe().await?;
+```
+
+You can now listen to events as they are emitted by the smart contract:
+
+```rust
+while let Some(event) = stream.next().await {
+    match event {
+        Ok(log) => {println!("New event: {:?}", log)},
+        Err(e) => {println!("Error: {:?}", e)},
+```
diff --git a/book/events/logs-and-filtering.md b/book/events/logs-and-filtering.md
new file mode 100644
index 00000000..a72cd813
--- /dev/null
+++ b/book/events/logs-and-filtering.md
@@ -0,0 +1,5 @@
+# Logs and filtering
+
+```rust
+{{#include ../../examples/events/examples/filtering.rs}}
+```
diff --git a/book/subscriptions/events-by-type.md b/book/subscriptions/events-by-type.md
new file mode 100644
index 00000000..81450a33
--- /dev/null
+++ b/book/subscriptions/events-by-type.md
@@ -0,0 +1,5 @@
+# Subscribe events by type
+
+```rust
+{{#include ../../examples/subscriptions/examples/subscribe_events_by_type.rs}}
+```
diff --git a/book/subscriptions/logs.md b/book/subscriptions/logs.md
new file mode 100644
index 00000000..0d0c091f
--- /dev/null
+++ b/book/subscriptions/logs.md
@@ -0,0 +1,39 @@
+# Subscribing to Logs
+
+To subscribe to logs, create a Filter object that specifies the criteria for the logs you want to listen to. Then, pass the filter to the Provider's subscribe_logs method:
+
+```rust
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let provider = Provider::<Http>::try_from("http://localhost:8545")?;
+
+    let filter = Filter::new().address("0xcontract_address_here".parse()?);
+
+    let mut stream = provider.subscribe_logs(filter).await?;
+
+    // Your code to handle logs goes here.
+
+    Ok(())
+
+}
+```
+
+You can now listen to logs that match your filter criteria:
+
+```rust
+while let Some(log) = stream.next().await {
+    match log {
+        Ok(log) => {
+            println!("New log: {:?}", log);
+        }
+        Err(e) => {
+            eprintln!("Error: {:?}", e);
+        }
+    }
+}
+```
+
+Here is another example of subscribing to logs:
+
+```rust
+{{#include ../../examples/subscriptions/examples/subscribe_logs.rs}}
+```
diff --git a/book/subscriptions/multiple-subscriptions.md b/book/subscriptions/multiple-subscriptions.md
new file mode 100644
index 00000000..bb963fae
--- /dev/null
+++ b/book/subscriptions/multiple-subscriptions.md
@@ -0,0 +1,58 @@
+# Mulitple Multiple Subscriptions
+
+You may need to handle multiple subscriptions simultaneously in your application. To manage multiple SubscriptionStreams, you can use the futures crate to efficiently process updates from all streams concurrently:
+
+```toml
+[dependencies]
+futures = "0.3"
+```
+
+Then, import the necessary components:
+
+```rust
+use futures::{stream, StreamExt, TryStreamExt};
+```
+
+Create multiple subscription streams and merge them into a single stream using the stream::select_all function:
+
+```rust
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    // Create multiple subscription streams.
+    let mut block_stream = provider.subscribe_blocks().await?;
+    let mut log_stream = provider.subscribe_logs(filter).await?;
+    let mut event_stream = watcher.subscribe().await?;
+
+    // Merge the streams into a single stream.
+    let mut combined_stream = stream::select_all(vec![
+        block_stream.map_ok(|block| EventType::Block(block)),
+        log_stream.map_ok(|log| EventType::Log(log)),
+        event_stream.map_ok(|event| EventType::Event(event)),
+    ]);
+
+    // Your code to handle the events goes here.
+
+    Ok(())
+
+}
+```
+
+Now, you can listen to updates from all the subscription streams concurrently:
+
+```rust
+while let Some(event) = combined_stream.next().await {
+    match event {
+        Ok(event) => match event {
+            EventType::Block(block) => println!("New block: {:?}", block),
+            EventType::Log(log) => println!("New log: {:?}", log),
+            EventType::Event(event) => println!("New event: {:?}", event),
+        },
+        Err(e) => {
+            eprintln!("Error: {:?}", e);
+        }
+    }
+}
+```
+
+This approach allows you to efficiently handle multiple subscriptions in your application and react to various network activities in a unified manner.
+
+By leveraging the powerful subscription capabilities of ethers-rs, you can create responsive and dynamic applications that stay up-to-date with the latest events on the Ethereum network. The library's flexibility and ease of use make it an ideal choice for developers looking to build robust and performant applications that interact with smart contracts and the Ethereum blockchain.
diff --git a/book/subscriptions/subscriptions.md b/book/subscriptions/subscriptions.md
new file mode 100644
index 00000000..9b25f5eb
--- /dev/null
+++ b/book/subscriptions/subscriptions.md
@@ -0,0 +1,60 @@
+## Ethers-rs: Subscriptions
+
+Here we will discuss how to use `ethers-rs` to subscribe and listen to blocks, events, and logs. Subscriptions provide a way to receive real-time updates on various activities on the Ethereum blockchain, allowing you to monitor the network and react to changes as they happen.
+
+## Overview
+
+ethers-rs offers a convenient way to work with subscriptions, enabling you to listen to new blocks, transaction receipts, and logs. The main components you will work with are:
+
+1. Provider: The main struct used to interact with the Ethereum network.
+2. SubscriptionStream: A stream of updates you can subscribe to for real-time notifications.
+
+## Getting Started
+
+Before working with subscriptions, make sure you have ethers-rs added to your project's dependencies in Cargo.toml:
+
+```toml
+[dependencies]
+ethers = { version = "2.0.0", features = ["full"] }
+```
+
+Next, import the necessary components from the ethers-rs library:
+
+```rust
+use ethers::{prelude::\*,types::H256,};
+```
+
+### Subscribing to Events
+
+As we discussed in the previous section on events, you can subscribe to specific events emitted by smart contracts using the EventWatcher struct. To create a SubscriptionStream, call the subscribe method on your EventWatcher:
+
+```rust
+let mut stream = watcher.subscribe().await?;
+```
+
+Now, you can listen to events as they are emitted by the smart contract:
+
+```rust
+while let Some(event) = stream.next().await {
+    match event {
+        Ok(log) => {
+            println!("New event: {:?}", log);
+        }
+        Err(e) => {
+            eprintln!("Error: {:?}", e);
+        }
+    }
+}
+```
+
+By using the subscription features provided by ethers-rs, you can efficiently monitor and react to various activities on the Ethereum network. Subscriptions are a powerful tool for building responsive and dynamic applications that can interact with smart contracts and stay up-to-date with the latest network events.
+
+### Unsubscribing from Subscriptions
+
+In some cases, you may want to stop listening to a subscription. To do this, simply drop the SubscriptionStream:
+
+```rust
+drop(stream);
+```
+
+This will stop the stream from receiving any further updates.
diff --git a/book/subscriptions/watch-blocks.md b/book/subscriptions/watch-blocks.md
new file mode 100644
index 00000000..8102f706
--- /dev/null
+++ b/book/subscriptions/watch-blocks.md
@@ -0,0 +1,37 @@
+# Subscribing to New Blocks
+
+To subscribe to new blocks, create a Provider instance and call the subscribe_blocks method:
+
+```rust
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+let provider = Provider::<Http>::try_from("http://localhost:8545")?;
+
+    let mut stream = provider.subscribe_blocks().await?;
+
+    // Your code to handle new blocks goes here.
+
+    Ok(())
+
+}
+```
+
+You can now listen to new blocks as they are mined:
+
+```rust
+while let Some(block) = stream.next().await {
+    match block {
+        Ok(block) => {
+            println!("New block: {:?}", block);
+        }
+        Err(e) => {
+            eprintln!("Error: {:?}", e);
+        }
+    }
+}
+```
+
+Here is another example of subscribing to new blocks:
+
+```rust
+{{#include ../../examples/subscriptions/examples/subscribe_blocks.rs}}
+```
diff --git a/cliff.toml b/cliff.toml
index 01e6475d..e1f1e8ae 100644
--- a/cliff.toml
+++ b/cliff.toml
@@ -20,7 +20,7 @@ Please do not manually edit this file.\n
 # https://tera.netlify.app/docs/#introduction
 body = """
 {% if version %}\
-    ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
+    ## [{{ version | trim_start_matches(pat="ethers-") | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
 {% else %}\
     ## [Unreleased]
 {% endif %}\
@@ -69,12 +69,12 @@ commit_parsers = [
 protect_breaking_commits = true
 # filter out the commits that are not matched by commit parsers
 filter_commits = false
-# glob pattern for matching git tags
+# *glob pattern* for matching git tags
 tag_pattern = "*"
-# regex for skipping tags
+# *regex* for skipping tagged commits entirely
 # skip_tags = "v0.1.0-beta.1"
-# regex for ignoring tags
-ignore_tags = "^ethers.*"
+# *regex* for ignoring tags, but including the commits into the next one
+ignore_tags = "^ethers-.*-.*"
 # sort the tags topologically
 topo_order = false
 # sort the commits inside sections by oldest/newest order
diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml
index 601fe1cb..fe8bcbe9 100644
--- a/ethers-contract/Cargo.toml
+++ b/ethers-contract/Cargo.toml
@@ -45,7 +45,7 @@ ethers-solc.workspace = true
 ethers-providers = { workspace = true, features = ["ws"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
-tokio = { workspace = true, features = ["macros"] }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
 
 [features]
 default = ["abigen"]
diff --git a/ethers-contract/ethers-contract-abigen/src/util.rs b/ethers-contract/ethers-contract-abigen/src/util.rs
index b4f0acfd..0c6f4756 100644
--- a/ethers-contract/ethers-contract-abigen/src/util.rs
+++ b/ethers-contract/ethers-contract-abigen/src/util.rs
@@ -232,11 +232,12 @@ fn _derive_builtin_traits_struct(
     for (field, ty) in fields.iter().zip(params) {
         match &field.ty {
             FieldType::Struct(s_ty) => {
-                // a tuple here is actually a sol struct so we skip it
-                if !matches!(ty, ParamType::Tuple(_)) {
-                    *def &= can_derive_default(ty);
-                    *others &= can_derive_builtin_traits(ty);
+                // `ty` here can only be `Tuple`, `Array(Tuple)`, or `FixedArray(Tuple(), len)`.
+                // We recurse on the Tuple's fields and check the FixedArray's length.
+                if let StructFieldType::FixedArray(_, len) = s_ty {
+                    *def &= *len <= MAX_SUPPORTED_ARRAY_LEN;
                 }
+
                 let id = s_ty.identifier();
                 // TODO: InternalStructs does not contain this field's ID if the struct and field
                 // are in 2 different modules, like in `can_generate_internal_structs_multiple`
diff --git a/ethers-contract/ethers-contract-derive/src/display.rs b/ethers-contract/ethers-contract-derive/src/display.rs
index 6410c83c..37e772f7 100644
--- a/ethers-contract/ethers-contract-derive/src/display.rs
+++ b/ethers-contract/ethers-contract-derive/src/display.rs
@@ -4,26 +4,30 @@ use crate::utils;
 use ethers_core::{abi::ParamType, macros::ethers_core_crate};
 use proc_macro2::TokenStream;
 use quote::quote;
-use syn::{parse::Error, spanned::Spanned, Data, DeriveInput, Fields, Index};
+use syn::{parse::Error, spanned::Spanned, Data, DeriveInput, Index};
 
 /// Derive `fmt::Display` for the given type
 pub(crate) fn derive_eth_display_impl(input: DeriveInput) -> Result<TokenStream, Error> {
     let fields = match input.data {
-        Data::Struct(ref data) => match data.fields {
-            Fields::Named(ref fields) => fields.named.iter().collect(),
-            Fields::Unnamed(ref fields) => fields.unnamed.iter().collect(),
-            Fields::Unit => vec![],
-        },
-        Data::Enum(_) => {
-            return Err(Error::new(input.span(), "Enum types are not supported by EthDisplay"))
+        Data::Struct(data) => data.fields.into_iter(),
+        Data::Enum(e) => {
+            return Err(Error::new(e.enum_token.span(), "Enums are not supported by EthDisplay"))
         }
-        Data::Union(_) => {
-            return Err(Error::new(input.span(), "Union types are not supported by EthDisplay"))
+        Data::Union(u) => {
+            return Err(Error::new(u.union_token.span(), "Unions are not supported by EthDisplay"))
         }
     };
 
     let mut expressions = TokenStream::new();
-    for (i, field) in fields.iter().enumerate() {
+    for (i, field) in fields.enumerate() {
+        // comma separator
+        if i > 0 {
+            let tokens = quote! {
+                ::core::fmt::Write::write_str(f, ", ")?;
+            };
+            expressions.extend(tokens);
+        }
+
         let ident = field.ident.as_ref().map(|id| quote!(#id)).unwrap_or_else(|| {
             let idx = Index::from(i);
             quote!(#idx)
@@ -36,14 +40,6 @@ pub(crate) fn derive_eth_display_impl(input: DeriveInput) -> Result<TokenStream,
             // could not detect the parameter type and rely on using debug fmt
             fmt_debug_tokens(&ident, &mut expressions);
         }
-
-        // comma separator
-        if i < fields.len() - 1 {
-            let tokens = quote! {
-                ::core::fmt::Write::write_str(f, ", ")?;
-            };
-            expressions.extend(tokens);
-        }
     }
 
     let name = &input.ident;
diff --git a/ethers-contract/ethers-contract-derive/src/eip712.rs b/ethers-contract/ethers-contract-derive/src/eip712.rs
index 50f3451a..6be3487e 100644
--- a/ethers-contract/ethers-contract-derive/src/eip712.rs
+++ b/ethers-contract/ethers-contract-derive/src/eip712.rs
@@ -2,7 +2,7 @@ use crate::utils;
 use ethers_core::{
     abi::{Address, ParamType},
     macros::ethers_core_crate,
-    types::transaction::eip712::EIP712Domain,
+    types::{transaction::eip712::EIP712Domain, H256},
     utils::keccak256,
 };
 use inflector::Inflector;
@@ -109,12 +109,20 @@ fn parse_attributes(input: &DeriveInput) -> Result<EIP712Domain> {
                 litstr.value().parse().map_err(|e| Error::new(litstr.span(), e))?;
             domain.verifying_contract = Some(addr);
         }
+        // hash string
         "salt", domain.salt => {
             meta.input.parse::<Token![=]>()?;
             let litstr: LitStr = meta.input.parse()?;
             let hash = keccak256(litstr.value());
             domain.salt = Some(hash);
         }
+        // parse string as H256
+        "raw_salt", domain.salt => {
+            meta.input.parse::<Token![=]>()?;
+            let litstr: LitStr = meta.input.parse()?;
+            let bytes = litstr.value().parse::<H256>().map_err(|e| Error::new(litstr.span(), e))?;
+            domain.salt = Some(bytes.0);
+        }
     );
     Ok(domain)
 }
diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs
index d259c12f..948e407b 100644
--- a/ethers-contract/ethers-contract-derive/src/lib.rs
+++ b/ethers-contract/ethers-contract-derive/src/lib.rs
@@ -52,8 +52,7 @@ pub(crate) mod utils;
 /// All the possible ABI sources:
 ///
 /// ```ignore
-/// use ethers_contract_derive::abigen;
-///
+/// # use ethers_contract_derive::abigen;
 /// // ABI Path
 /// abigen!(MyContract, "./MyContractABI.json");
 ///
@@ -78,6 +77,7 @@ pub(crate) mod utils;
 /// Specify additional parameters:
 ///
 /// ```ignore
+/// # use ethers_contract_derive::abigen;
 /// abigen!(
 ///     MyContract,
 ///     "path/to/MyContract.json",
@@ -120,10 +120,39 @@ pub fn abigen(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Derives the `AbiType` and all `Tokenizable` traits for the labeled type.
+/// Derives the [`AbiType`] and all `Tokenizable` traits for the labeled type.
+///
+/// This derive macro adds a type bound `field: Tokenizable` for each field type.
+///
+/// [`AbiType`]: ethers_core::abi::AbiType
+///
+/// # Examples
+///
+/// ```
+/// use ethers_contract_derive::EthAbiType;
+/// use ethers_core::types::*;
+/// use ethers_core::abi::{AbiType, ParamType};
+///
+/// #[derive(Clone, EthAbiType)]
+/// struct MyStruct {
+///     a: U256,
+///     b: Address,
+///     c: Bytes,
+///     d: String,
+///     e: H256,
+/// }
 ///
-/// This derive macro automatically adds a type bound `field: Tokenizable` for
-/// each field type.
+/// assert_eq!(
+///     MyStruct::param_type(),
+///     ParamType::Tuple(vec![
+///         ParamType::Uint(256),
+///         ParamType::Address,
+///         ParamType::Bytes,
+///         ParamType::String,
+///         ParamType::FixedBytes(32),
+///     ]),
+/// );
+/// ```
 #[proc_macro_derive(EthAbiType)]
 pub fn derive_abi_type(input: TokenStream) -> TokenStream {
     let input = parse_macro_input!(input as DeriveInput);
@@ -134,33 +163,36 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Derives the `AbiEncode`, `AbiDecode` and traits for the labeled type.
+/// Derives the [`AbiEncode`] and [`AbiDecode`] traits for the labeled type.
 ///
-/// This is an addition to `EthAbiType` that lacks the `AbiEncode`, `AbiDecode` implementation.
+/// This is separate from other derive macros because this derives a generic codec implementation
+/// for structs, while [`EthEvent`] and others derive a specialized implementation.
 ///
-/// The reason why this is a separate macro is the `AbiEncode` / `AbiDecode` are `ethers`
-/// generalized codec traits used for types, calls, etc. However, encoding/decoding a call differs
-/// from the basic encoding/decoding, (`[selector + encode(self)]`)
+/// [`AbiEncode`]: ethers_core::abi::AbiEncode
+/// [`AbiDecode`]: ethers_core::abi::AbiDecode
 ///
 /// Note that this macro requires the `EthAbiType` macro to be derived or for the type to implement
 /// `AbiType` and `Tokenizable`. The type returned by the `AbiType` implementation must be a
 /// `Token::Tuple`, otherwise this macro's implementation of `AbiDecode` will panic at runtime.
 ///
-/// # Example
+/// # Examples
 ///
-/// ```ignore
-/// use ethers_contract::{EthAbiCodec, EthAbiType};
-/// use ethers_core::types::*;
+/// ```
+/// use ethers_contract_derive::{EthAbiCodec, EthAbiType};
+/// use ethers_core::types::Address;
+/// use ethers_core::abi::{AbiDecode, AbiEncode};
 ///
-/// #[derive(Debug, Clone, EthAbiType, EthAbiCodec)]
+/// #[derive(Clone, Debug, Default, PartialEq, EthAbiType, EthAbiCodec)]
 /// struct MyStruct {
 ///     addr: Address,
 ///     old_value: String,
 ///     new_value: String,
 /// }
-/// let val = MyStruct {..};
-/// let bytes = val.encode();
-/// let val = MyStruct::decode(&bytes).unwrap();
+///
+/// let value = MyStruct::default();
+/// let encoded = value.clone().encode();
+/// let decoded = MyStruct::decode(&encoded).unwrap();
+/// assert_eq!(decoded, value);
 /// ```
 #[proc_macro_derive(EthAbiCodec)]
 pub fn derive_abi_codec(input: TokenStream) -> TokenStream {
@@ -168,19 +200,20 @@ pub fn derive_abi_codec(input: TokenStream) -> TokenStream {
     codec::derive_codec_impl(&input).into()
 }
 
-/// Derives `fmt::Display` trait and generates a convenient format for all the
-/// underlying primitive types/tokens.
+/// Derives the [`Display`] trait on structs by formatting each field based on its Ethereum type.
 ///
-/// The fields of the structure are formatted comma separated, like `self.0,
-/// self.1, self.2,...`
+/// The final output is a comma separated list of the struct's fields, formatted as follows:
+/// `self.0, self.1, self.2,...`
 ///
-/// # Example
+/// [`Display`]: std::fmt::Display
 ///
-/// ```ignore
-/// use ethers_contract::{EthDisplay, EthAbiType};
+/// # Examples
+///
+/// ```
+/// use ethers_contract_derive::{EthAbiType, EthDisplay};
 /// use ethers_core::types::*;
 ///
-/// #[derive(Debug, Clone, EthAbiType, EthDisplay)]
+/// #[derive(Clone, Default, EthAbiType, EthDisplay)]
 /// struct MyStruct {
 ///     addr: Address,
 ///     old_value: String,
@@ -190,8 +223,9 @@ pub fn derive_abi_codec(input: TokenStream) -> TokenStream {
 ///     arr_u16: [u16; 32],
 ///     v: Vec<u8>,
 /// }
-/// let val = MyStruct {..};
-/// format!("{}", val);
+///
+/// let s = MyStruct::default();
+/// assert!(!format!("{s}").is_empty());
 /// ```
 #[proc_macro_derive(EthDisplay, attributes(ethdisplay))]
 pub fn derive_eth_display(input: TokenStream) -> TokenStream {
@@ -203,41 +237,40 @@ pub fn derive_eth_display(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Derives the `EthEvent` and `Tokenizeable` trait for the labeled type.
+/// Derives the [`EthEvent`] and `Tokenizable` traits for the labeled type.
 ///
-/// Additional arguments can be specified using the `#[ethevent(...)]`
-/// attribute:
+/// Additional arguments can be specified using the `#[ethevent(...)]` attribute:
 ///
 /// For the struct:
 ///
-/// - `name`, `name = "..."`: Overrides the generated `EthEvent` name, default is the
-///  struct's name.
-/// - `signature`, `signature = "..."`: The signature as hex string to override the
-///  event's signature.
-/// - `abi`, `abi = "..."`: The ABI signature for the event this event's data corresponds to.
-///  The `abi` should be solidity event definition or a tuple of the event's
-/// types in case the  event has non elementary (other `EthAbiType`) types as
-/// members
-/// - `anonymous`: A flag to mark this as an anonymous event
+/// - `name = "..."`: Overrides the generated event name. Defaults to the struct's name;
+/// - `signature = "..."`: The signature as hex string to override the event's signature;
+/// - `abi = "..."`: The ABI signature of the event. The `abi` should be a Solidity event definition
+///   or a tuple of the event's types in case the event has non elementary (other `EthAbiType`)
+///   types as members;
+/// - `anonymous`: A flag to mark this as an anonymous event.
 ///
 /// For fields:
 ///
-/// - `indexed`: flag to mark a field as an indexed event input
-/// - `name`: override the name of an indexed event input, default is the rust field name
+/// - `indexed`: flag to mark a field as an indexed event input;
+/// - `name = "..."`: override the name of an indexed event input, default is the rust field name.
+///
+/// [`EthEvent`]: https://docs.rs/ethers/latest/ethers/contract/trait.EthEvent.html
+///
+/// # Examples
 ///
-/// # Example
 /// ```ignore
-/// use ethers_contract::EthCall;
+/// use ethers_contract_derive::{EthAbiType, EthEvent};
 /// use ethers_core::types::Address;
 ///
-/// #[derive(Debug, EthAbiType)]
+/// #[derive(EthAbiType)]
 /// struct Inner {
 ///     inner: Address,
 ///     msg: String,
 /// }
 ///
-/// #[derive(Debug, EthEvent)]
-/// #[ethevent(abi = "ValueChangedEvent((address,string),string)")]
+/// #[derive(EthEvent)]
+/// #[ethevent(abi = "ValueChangedEvent(address,string,(address,string))")]
 /// struct ValueChangedEvent {
 ///     #[ethevent(indexed, name = "_target")]
 ///     target: Address,
@@ -255,52 +288,53 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Derives the `EthCall` and `Tokenizeable` trait for the labeled type.
+/// Derives the [`EthCall`] and `Tokenizeable` trait for the labeled type.
 ///
-/// Additional arguments can be specified using the `#[ethcall(...)]`
-/// attribute:
+/// Additional arguments can be specified using the `#[ethcall(...)]` attribute:
 ///
 /// For the struct:
 ///
-/// - `name`, `name = "..."`: Overrides the generated `EthCall` function name, default is the
-///  struct's name.
-/// - `abi`, `abi = "..."`: The ABI signature for the function this call's data corresponds to.
+/// - `name = "..."`: Overrides the generated function name. Defaults to the struct's name;
+/// - `abi = "..."`: The ABI signature of the function.
 ///
-///  NOTE: in order to successfully parse the `abi` (`<name>(<args>,...)`) the `<name`>
-///   must match either the struct name or the name attribute: `#[ethcall(name ="<name>"]`
+/// NOTE: in order to successfully parse the `abi` (`<name>(<args>,...)`), `<name>` must match
+/// either the struct's name or the name attribute: `#[ethcall(name = "<name>"]`
 ///
-/// # Example
+/// [`EthCall`]: https://docs.rs/ethers/latest/ethers/contract/trait.EthCall.html
+///
+/// # Examples
 ///
 /// ```ignore
-/// use ethers_contract::EthCall;
+/// use ethers_contract_derive::EthCall;
+/// use ethers_core::abi::{Address, FunctionExt};
 ///
-/// #[derive(Debug, Clone, EthCall)]
-/// #[ethcall(name ="my_call")]
+/// #[derive(EthCall)]
+/// #[ethcall(name = "my_call")]
 /// struct MyCall {
 ///     addr: Address,
 ///     old_value: String,
 ///     new_value: String,
 /// }
+///
 /// assert_eq!(
-///     MyCall::abi_signature().as_ref(),
+///     MyCall::abi_signature(),
 ///     "my_call(address,string,string)"
 /// );
 /// ```
 ///
-/// # Example
-///
 /// Call with struct inputs
 ///
 /// ```ignore
-/// use ethers_core::abi::Address;
+/// use ethers_core::abi::{Address, EventExt};
+/// use ethers_contract_derive::EthCall;
 ///
-/// #[derive(Debug, Clone, PartialEq, EthAbiType)]
+/// #[derive(Clone, PartialEq, EthAbiType)]
 /// struct SomeType {
 ///     inner: Address,
 ///     msg: String,
 /// }
 ///
-/// #[derive(Debug, PartialEq, EthCall)]
+/// #[derive(PartialEq, EthCall)]
 /// #[ethcall(name = "foo", abi = "foo(address,(address,string),string)")]
 /// struct FooCall {
 ///     old_author: Address,
@@ -309,7 +343,7 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream {
 /// }
 ///
 /// assert_eq!(
-///     FooCall::abi_signature().as_ref(),
+///     FooCall::abi_signature(),
 ///     "foo(address,(address,string),string)"
 /// );
 /// ```
@@ -323,34 +357,36 @@ pub fn derive_abi_call(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// Derives the `EthError` and `Tokenizeable` trait for the labeled type.
+/// Derives the [`EthError`] and `Tokenizeable` trait for the labeled type.
 ///
-/// Additional arguments can be specified using the `#[etherror(...)]`
-/// attribute:
+/// Additional arguments can be specified using the `#[etherror(...)]` attribute:
 ///
 /// For the struct:
 ///
-/// - `name`, `name = "..."`: Overrides the generated `EthCall` function name, default is the
-///  struct's name.
-/// - `abi`, `abi = "..."`: The ABI signature for the function this call's data corresponds to.
+/// - `name = "..."`: Overrides the generated error name. Defaults to the struct's name;
+/// - `abi = "..."`: The ABI signature of the error.
+///
+/// NOTE: in order to successfully parse the `abi` (`<name>(<args>,...)`), `<name>` must match
+/// either the struct's name or the name attribute: `#[ethcall(name = "<name>"]`
 ///
-///  NOTE: in order to successfully parse the `abi` (`<name>(<args>,...)`) the `<name`>
-///   must match either the struct name or the name attribute: `#[ethcall(name ="<name>"]`
+/// [`EthError`]: https://docs.rs/ethers/latest/ethers/contract/trait.EthError.html
 ///
-/// # Example
+/// # Examples
 ///
 /// ```ignore
-/// use ethers_contract::EthError;
+/// use ethers_core::abi::{Address, ErrorExt};
+/// use ethers_contract_derive::EthError;
 ///
-/// #[derive(Debug, Clone, EthError)]
-/// #[etherror(name ="my_error")]
+/// #[derive(Clone, EthError)]
+/// #[etherror(name = "my_error")]
 /// struct MyError {
 ///     addr: Address,
 ///     old_value: String,
 ///     new_value: String,
 /// }
+///
 /// assert_eq!(
-///     MyCall::abi_signature().as_ref(),
+///     MyError::abi_signature(),
 ///     "my_error(address,string,string)"
 /// );
 /// ```
@@ -364,40 +400,45 @@ pub fn derive_abi_error(input: TokenStream) -> TokenStream {
     .into()
 }
 
-/// EIP-712 derive macro.
+/// Derives the [`Eip712`] trait for the labeled type.
 ///
-/// This crate provides a derive macro `Eip712` that is used to encode a rust struct
-/// into a payload hash, according to <https://eips.ethereum.org/EIPS/eip-712>
+/// Encodes a Rust struct into a payload hash, according to [eip-712](https://eips.ethereum.org/EIPS/eip-712).
 ///
-/// The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712`
-/// Both the derive macro and the trait must be in context when using
+/// The following traits are required to be implemented for the struct:
+/// - [`Clone`]
+/// - [`Tokenizable`]: can be derived with [`EthAbiType`]
 ///
-/// This derive macro requires the `#[eip712]` attributes to be included
-/// for specifying the domain separator used in encoding the hash.
+/// [`Tokenizable`]: ethers_core::abi::Tokenizable
 ///
-/// NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived.
-/// This allows the struct to be parsed into `ethers_core::abi::Token` for encoding.
+/// # Attribute parameters
 ///
-/// # Optional Eip712 Parameters
+/// Required:
 ///
-/// The only optional parameter is `salt`, which accepts a string
-/// that is hashed using keccak256 and stored as bytes.
+/// - `name = "..."`: The name of the EIP712 domain separator.
+/// - `version = "..."`: The version of the EIP712 domain separator.
+/// - `chain_id = ...`: The chain id of the EIP712 domain separator.
+/// - `verifying_contract = "..."`: The verifying contract's address of the EIP712 domain separator.
 ///
-/// # Example Usage
+/// Optional:
 ///
-/// ```ignore
-/// use ethers_contract::EthAbiType;
-/// use ethers_derive_eip712::*;
+/// - `salt = "..."` or `raw_salt = "..."`: The salt of the EIP712 domain separator;
+///   - `salt` is interpreted as UTF-8 bytes and hashed, while `raw_salt` is interpreted as a hex
+///     string.
+///
+/// # Examples
+///
+/// ```
+/// use ethers_contract_derive::{EthAbiType, Eip712};
 /// use ethers_core::types::{transaction::eip712::Eip712, H160};
 ///
-/// #[derive(Debug, Eip712, EthAbiType)]
+/// #[derive(Clone, Default, EthAbiType, Eip712)]
 /// #[eip712(
 ///     name = "Radicle",
 ///     version = "1",
 ///     chain_id = 1,
-///     verifying_contract = "0x0000000000000000000000000000000000000000"
-///     // salt is an optional parameter
-///     salt = "my-unique-spice"
+///     verifying_contract = "0x0000000000000000000000000000000000000000",
+///     // salt/raw_salt are optional parameters
+///     salt = "my-unique-spice",
 /// )]
 /// pub struct Puzzle {
 ///     pub organization: H160,
@@ -406,17 +447,7 @@ pub fn derive_abi_error(input: TokenStream) -> TokenStream {
 ///     pub project: String,
 /// }
 ///
-/// let puzzle = Puzzle {
-///     organization: "0000000000000000000000000000000000000000"
-///         .parse::<H160>()
-///         .expect("failed to parse address"),
-///     contributor: "0000000000000000000000000000000000000000"
-///         .parse::<H160>()
-///         .expect("failed to parse address"),
-///     commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
-///     project: "radicle-reward".to_string(),
-/// };
-///
+/// let puzzle = Puzzle::default();
 /// let hash = puzzle.encode_eip712().unwrap();
 /// ```
 ///
diff --git a/ethers-core/src/macros/ethers_crate.rs b/ethers-core/src/macros/ethers_crate.rs
index f30d2fe5..9b4d9b80 100644
--- a/ethers-core/src/macros/ethers_crate.rs
+++ b/ethers-core/src/macros/ethers_crate.rs
@@ -356,6 +356,7 @@ mod tests {
     }
 
     #[test]
+    #[ignore = "TODO: flaky and slow"]
     fn test_names() {
         fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
             write_manifest(s, ethers, dependencies);
@@ -424,6 +425,7 @@ mod tests {
     }
 
     #[test]
+    #[ignore = "TODO: flaky and slow"]
     fn test_lock_file() {
         let (s, _dir) = test_project();
         write_manifest(&s, true, &[]);
diff --git a/ethers-core/src/types/block.rs b/ethers-core/src/types/block.rs
index e43813ef..2dffe090 100644
--- a/ethers-core/src/types/block.rs
+++ b/ethers-core/src/types/block.rs
@@ -1,5 +1,7 @@
 // Modified from <https://github.com/tomusdrw/rust-web3/blob/master/src/types/block.rs>
 
+#[cfg(not(feature = "celo"))]
+use crate::types::Withdrawal;
 use crate::types::{Address, Bloom, Bytes, Transaction, TxHash, H256, U256, U64};
 use chrono::{DateTime, TimeZone, Utc};
 use serde::{
@@ -84,6 +86,14 @@ pub struct Block<TX> {
     /// Base fee per unit of gas (if past London)
     #[serde(rename = "baseFeePerGas")]
     pub base_fee_per_gas: Option<U256>,
+    /// Withdrawals root hash (if past Shanghai)
+    #[serde(default, skip_serializing_if = "Option::is_none", rename = "withdrawalsRoot")]
+    #[cfg(not(feature = "celo"))]
+    pub withdrawals_root: Option<H256>,
+    /// Withdrawals (if past Shanghai)
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[cfg(not(feature = "celo"))]
+    pub withdrawals: Option<Vec<Withdrawal>>,
 
     #[cfg(feature = "celo")]
     #[cfg_attr(docsrs, doc(cfg(feature = "celo")))]
@@ -218,6 +228,8 @@ impl Block<TxHash> {
                 mix_hash,
                 nonce,
                 base_fee_per_gas,
+                withdrawals_root,
+                withdrawals,
                 other,
                 ..
             } = self;
@@ -243,6 +255,8 @@ impl Block<TxHash> {
                 mix_hash,
                 nonce,
                 base_fee_per_gas,
+                withdrawals_root,
+                withdrawals,
                 transactions,
                 other,
             }
@@ -322,6 +336,8 @@ impl From<Block<Transaction>> for Block<TxHash> {
                 mix_hash,
                 nonce,
                 base_fee_per_gas,
+                withdrawals_root,
+                withdrawals,
                 other,
             } = full;
             Block {
@@ -346,6 +362,8 @@ impl From<Block<Transaction>> for Block<TxHash> {
                 mix_hash,
                 nonce,
                 base_fee_per_gas,
+                withdrawals_root,
+                withdrawals,
                 transactions: transactions.iter().map(|tx| tx.hash).collect(),
                 other,
             }
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index 16a814fd..d25118e3 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -89,6 +89,8 @@ pub enum Chain {
 
     ScrollAlphaTestnet = 534353,
 
+    Metis = 1088,
+
     #[strum(to_string = "xdai", serialize = "gnosis", serialize = "gnosis-chain")]
     #[serde(alias = "xdai", alias = "gnosis", alias = "gnosis_chain")]
     XDai = 100,
@@ -97,6 +99,12 @@ pub enum Chain {
     #[strum(to_string = "mumbai", serialize = "polygon-mumbai")]
     #[serde(alias = "mumbai")]
     PolygonMumbai = 80001,
+    #[strum(serialize = "polygon-zkevm", serialize = "zkevm")]
+    #[serde(alias = "zkevm", alias = "polygon_zkevm")]
+    PolygonZkEvm = 1101,
+    #[strum(serialize = "polygon-zkevm-testnet", serialize = "zkevm-testnet")]
+    #[serde(alias = "zkevm_testnet", alias = "polygon_zkevm_testnet")]
+    PolygonZkEvmTestnet = 1442,
 
     Fantom = 250,
     FantomTestnet = 4002,
@@ -144,6 +152,10 @@ pub enum Chain {
     Boba = 288,
 
     BaseGoerli = 84531,
+
+    #[strum(to_string = "zksync")]
+    #[serde(alias = "zksync")]
+    ZkSync = 324,
 }
 
 // === impl Chain ===
@@ -276,7 +288,8 @@ impl Chain {
             // Explicitly exhaustive. See NB above.
             Morden | Ropsten | Rinkeby | Goerli | Kovan | XDai | Chiado | Sepolia | Moonbase |
             MoonbeamDev | Optimism | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk |
-            EmeraldTestnet | Boba | BaseGoerli => return None,
+            EmeraldTestnet | Boba | BaseGoerli | ZkSync | PolygonZkEvm | PolygonZkEvmTestnet |
+            Metis => return None,
         };
 
         Some(Duration::from_millis(ms))
@@ -316,7 +329,10 @@ impl Chain {
             CeloAlfajores |
             CeloBaklava |
             Boba |
-            BaseGoerli => true,
+            ZkSync |
+            BaseGoerli |
+            PolygonZkEvm |
+            PolygonZkEvmTestnet => true,
 
             // Known EIP-1559 chains
             Mainnet |
@@ -333,7 +349,7 @@ impl Chain {
             Dev | AnvilHardhat | Morden | Ropsten | Rinkeby | Cronos | CronosTestnet | Kovan |
             Sokol | Poa | XDai | Moonbeam | MoonbeamDev | Moonriver | Moonbase | Evmos |
             EvmosTestnet | Chiado | Aurora | AuroraTestnet | Canto | CantoTestnet |
-            ScrollAlphaTestnet => false,
+            ScrollAlphaTestnet | Metis => false,
         }
     }
 
@@ -372,6 +388,14 @@ impl Chain {
                 ("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
             }
 
+            PolygonZkEvm => {
+                ("https://api-zkevm.polygonscan.com/api", "https://zkevm.polygonscan.com")
+            }
+            PolygonZkEvmTestnet => (
+                "https://api-testnet-zkevm.polygonscan.com/api",
+                "https://testnet-zkevm.polygonscan.com",
+            ),
+
             Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"),
             AvalancheFuji => {
                 ("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
@@ -422,6 +446,10 @@ impl Chain {
                 ("https://blockscout.scroll.io/api", "https://blockscout.scroll.io/")
             }
 
+            Metis => {
+                ("https://andromeda-explorer.metis.io/api", "https://andromeda-explorer.metis.io/")
+            }
+
             Chiado => {
                 ("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net")
             }
@@ -472,6 +500,10 @@ impl Chain {
 
             BaseGoerli => ("https://api-goerli.basescan.org/api", "https://goerli.basescan.org"),
 
+            ZkSync => {
+                ("https://zksync2-mainnet-explorer.zksync.io/", "https://explorer.zksync.io/")
+            }
+
             AnvilHardhat | Dev | Morden | MoonbeamDev | FilecoinMainnet => {
                 // this is explicitly exhaustive so we don't forget to add new urls when adding a
                 // new chain
@@ -522,7 +554,7 @@ impl Chain {
 
             Avalanche | AvalancheFuji => "SNOWTRACE_API_KEY",
 
-            Polygon | PolygonMumbai => "POLYGONSCAN_API_KEY",
+            Polygon | PolygonMumbai | PolygonZkEvm | PolygonZkEvmTestnet => "POLYGONSCAN_API_KEY",
 
             Fantom | FantomTestnet => "FTMSCAN_API_KEY",
 
@@ -535,6 +567,7 @@ impl Chain {
             // Explicitly exhaustive. See NB above.
             XDai |
             ScrollAlphaTestnet |
+            Metis |
             Chiado |
             Sepolia |
             Rsk |
@@ -547,6 +580,7 @@ impl Chain {
             EvmosTestnet |
             AnvilHardhat |
             Dev |
+            ZkSync |
             FilecoinMainnet |
             FilecoinHyperspaceTestnet => return None,
         };
@@ -618,8 +652,11 @@ mod tests {
             (BinanceSmartChainTestnet, &["bsc-testnet", "binance-smart-chain-testnet"]),
             (XDai, &["xdai", "gnosis", "gnosis-chain"]),
             (PolygonMumbai, &["mumbai"]),
+            (PolygonZkEvm, &["zkevm", "polygon-zkevm"]),
+            (PolygonZkEvmTestnet, &["zkevm-testnet", "polygon-zkevm-testnet"]),
             (AnvilHardhat, &["anvil", "hardhat"]),
             (AvalancheFuji, &["fuji"]),
+            (ZkSync, &["zksync"]),
         ];
 
         for &(chain, aliases) in ALIASES {
@@ -635,7 +672,7 @@ mod tests {
     fn serde_to_string_match() {
         for chain in Chain::iter() {
             let chain_serde = serde_json::to_string(&chain).unwrap();
-            let chain_string = format!("\"{}\"", chain);
+            let chain_string = format!("\"{chain}\"");
             assert_eq!(chain_serde, chain_string);
         }
     }
diff --git a/ethers-core/src/types/fee.rs b/ethers-core/src/types/fee.rs
index c6274e66..7fa6f58c 100644
--- a/ethers-core/src/types/fee.rs
+++ b/ethers-core/src/types/fee.rs
@@ -1,4 +1,4 @@
-use crate::{types::U256, utils::from_int_or_hex};
+use crate::types::{serde_helpers::deserialize_stringified_numeric, U256};
 use serde::{Deserialize, Serialize};
 
 #[derive(Deserialize, Serialize, Debug, Clone)]
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
 pub struct FeeHistory {
     pub base_fee_per_gas: Vec<U256>,
     pub gas_used_ratio: Vec<f64>,
-    #[serde(deserialize_with = "from_int_or_hex")]
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     /// oldestBlock is returned as an unsigned integer up to geth v1.10.6. From
     /// geth v1.10.7, this has been updated to return in the hex encoded form.
     /// The custom deserializer allows backward compatibility for those clients
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index ae6a85e6..13bd610e 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -81,3 +81,6 @@ pub use syncing::{SyncProgress, SyncingStatus};
 
 mod opcode;
 pub use opcode::Opcode;
+
+mod withdrawal;
+pub use withdrawal::Withdrawal;
diff --git a/ethers-core/src/types/serde_helpers.rs b/ethers-core/src/types/serde_helpers.rs
index 689c9e7f..e36a97f4 100644
--- a/ethers-core/src/types/serde_helpers.rs
+++ b/ethers-core/src/types/serde_helpers.rs
@@ -1,6 +1,6 @@
 //! Some convenient serde helpers
 
-use crate::types::{BlockNumber, U256};
+use crate::types::{BlockNumber, U256, U64};
 use serde::{Deserialize, Deserializer};
 use std::{
     convert::{TryFrom, TryInto},
@@ -44,7 +44,7 @@ impl FromStr for Numeric {
 pub enum StringifiedNumeric {
     String(String),
     U256(U256),
-    Num(u64),
+    Num(serde_json::Number),
 }
 
 impl TryFrom<StringifiedNumeric> for U256 {
@@ -53,7 +53,9 @@ impl TryFrom<StringifiedNumeric> for U256 {
     fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
         match value {
             StringifiedNumeric::U256(n) => Ok(n),
-            StringifiedNumeric::Num(n) => Ok(U256::from(n)),
+            StringifiedNumeric::Num(n) => {
+                Ok(U256::from_dec_str(&n.to_string()).map_err(|err| err.to_string())?)
+            }
             StringifiedNumeric::String(s) => {
                 if let Ok(val) = s.parse::<u128>() {
                     Ok(val.into())
@@ -67,6 +69,18 @@ impl TryFrom<StringifiedNumeric> for U256 {
     }
 }
 
+impl TryFrom<StringifiedNumeric> for U64 {
+    type Error = String;
+
+    fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
+        let value = U256::try_from(value)?;
+        let mut be_bytes = [0u8; 32];
+        value.to_big_endian(&mut be_bytes);
+        U64::try_from(&be_bytes[value.leading_zeros() as usize / 8..])
+            .map_err(|err| err.to_string())
+    }
+}
+
 /// Supports parsing numbers as strings
 ///
 /// See <https://github.com/gakonst/ethers-rs/issues/1507>
@@ -94,6 +108,32 @@ where
     }
 }
 
+/// Supports parsing ethereum-types U64
+///
+/// See <https://github.com/gakonst/ethers-rs/issues/1507>
+pub fn deserialize_stringified_eth_u64<'de, D>(deserializer: D) -> Result<U64, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let num = StringifiedNumeric::deserialize(deserializer)?;
+    num.try_into().map_err(serde::de::Error::custom)
+}
+
+/// Supports parsing ethereum-types `Option<U64>`
+///
+/// See <https://github.com/gakonst/ethers-rs/issues/1507>
+pub fn deserialize_stringified_eth_u64_opt<'de, D>(deserializer: D) -> Result<Option<U64>, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    if let Some(num) = Option::<StringifiedNumeric>::deserialize(deserializer)? {
+        let num: U64 = num.try_into().map_err(serde::de::Error::custom)?;
+        Ok(Some(num))
+    } else {
+        Ok(None)
+    }
+}
+
 /// Supports parsing u64
 ///
 /// See <https://github.com/gakonst/ethers-rs/issues/1507>
@@ -257,6 +297,8 @@ where
 
 #[cfg(test)]
 mod tests {
+    use super::*;
+    use crate::types::U256;
 
     #[test]
     #[cfg(feature = "eip712")]
@@ -275,4 +317,38 @@ mod tests {
         let domain: EIP712Domain = serde_json::from_value(val).unwrap();
         assert_eq!(domain.chain_id, Some(137u64.into()));
     }
+
+    // <https://github.com/gakonst/ethers-rs/issues/2353>
+    #[test]
+    fn deserialize_stringified() {
+        #[derive(Debug, Deserialize, Eq, PartialEq)]
+        struct TestValues {
+            #[serde(deserialize_with = "deserialize_stringified_numeric")]
+            value_1: U256,
+            #[serde(deserialize_with = "deserialize_stringified_numeric")]
+            value_2: U256,
+            #[serde(deserialize_with = "deserialize_stringified_numeric")]
+            value_3: U256,
+            #[serde(deserialize_with = "deserialize_stringified_numeric")]
+            value_4: U256,
+        }
+
+        let data = r#"
+        {
+            "value_1": "750000000000000000",
+            "value_2": "21000000000000000",
+            "value_3": "0",
+            "value_4": "1"
+        }
+    "#;
+
+        let deserialized: TestValues = serde_json::from_str(data).unwrap();
+        let expected = TestValues {
+            value_1: U256::from(750_000_000_000_000_000u64),
+            value_2: U256::from(21_000_000_000_000_000u64),
+            value_3: U256::from(0u64),
+            value_4: U256::from(1u64),
+        };
+        assert_eq!(deserialized, expected);
+    }
 }
diff --git a/ethers-core/src/types/trace/geth.rs b/ethers-core/src/types/trace/geth.rs
index 49a5fe7f..a3337e7a 100644
--- a/ethers-core/src/types/trace/geth.rs
+++ b/ethers-core/src/types/trace/geth.rs
@@ -9,10 +9,7 @@ pub use self::{
     noop::NoopFrame,
     pre_state::{PreStateConfig, PreStateFrame},
 };
-use crate::{
-    types::{Bytes, H256, U256},
-    utils::from_int_or_hex,
-};
+use crate::types::{serde_helpers::deserialize_stringified_numeric, Bytes, H256, U256};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
 use std::collections::BTreeMap;
@@ -21,7 +18,7 @@ use std::collections::BTreeMap;
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct DefaultFrame {
     pub failed: bool,
-    #[serde(deserialize_with = "from_int_or_hex")]
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     pub gas: U256,
     #[serde(rename = "returnValue")]
     pub return_value: Bytes,
diff --git a/ethers-core/src/types/trace/geth/call.rs b/ethers-core/src/types/trace/geth/call.rs
index e0854a71..e235ff58 100644
--- a/ethers-core/src/types/trace/geth/call.rs
+++ b/ethers-core/src/types/trace/geth/call.rs
@@ -1,6 +1,6 @@
-use crate::{
-    types::{Address, Bytes, NameOrAddress, H256, U256},
-    utils::from_int_or_hex,
+use crate::types::{
+    serde_helpers::{deserialize_stringified_numeric, deserialize_stringified_numeric_opt},
+    Address, Bytes, NameOrAddress, H256, U256,
 };
 use serde::{Deserialize, Serialize};
 
@@ -12,11 +12,15 @@ pub struct CallFrame {
     pub from: Address,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub to: Option<NameOrAddress>,
-    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(
+        default,
+        deserialize_with = "deserialize_stringified_numeric_opt",
+        skip_serializing_if = "Option::is_none"
+    )]
     pub value: Option<U256>,
-    #[serde(default, deserialize_with = "from_int_or_hex")]
+    #[serde(default, deserialize_with = "deserialize_stringified_numeric")]
     pub gas: U256,
-    #[serde(default, deserialize_with = "from_int_or_hex", rename = "gasUsed")]
+    #[serde(default, deserialize_with = "deserialize_stringified_numeric", rename = "gasUsed")]
     pub gas_used: U256,
     pub input: Bytes,
     #[serde(default, skip_serializing_if = "Option::is_none")]
diff --git a/ethers-core/src/types/trace/geth/pre_state.rs b/ethers-core/src/types/trace/geth/pre_state.rs
index b87dbbd0..062e4407 100644
--- a/ethers-core/src/types/trace/geth/pre_state.rs
+++ b/ethers-core/src/types/trace/geth/pre_state.rs
@@ -1,7 +1,4 @@
-use crate::{
-    types::{Address, H256, U256},
-    utils::from_int_or_hex_opt,
-};
+use crate::types::{serde_helpers::deserialize_stringified_numeric_opt, Address, H256, U256};
 use serde::{Deserialize, Serialize};
 use std::collections::BTreeMap;
 
@@ -26,7 +23,7 @@ pub struct DiffMode {
 pub struct AccountState {
     #[serde(
         default,
-        deserialize_with = "from_int_or_hex_opt",
+        deserialize_with = "deserialize_stringified_numeric_opt",
         skip_serializing_if = "Option::is_none"
     )]
     pub balance: Option<U256>,
@@ -34,7 +31,7 @@ pub struct AccountState {
     pub code: Option<String>,
     #[serde(
         default,
-        deserialize_with = "from_int_or_hex_opt",
+        deserialize_with = "deserialize_stringified_numeric_opt",
         skip_serializing_if = "Option::is_none"
     )]
     pub nonce: Option<U256>,
diff --git a/ethers-core/src/types/withdrawal.rs b/ethers-core/src/types/withdrawal.rs
new file mode 100644
index 00000000..b63ada3d
--- /dev/null
+++ b/ethers-core/src/types/withdrawal.rs
@@ -0,0 +1,30 @@
+use crate::types::{Address, U256, U64};
+use serde::{Deserialize, Serialize};
+
+/// A validator withdrawal from the consensus layer.
+/// See EIP-4895: Beacon chain push withdrawals as operations.
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Withdrawal {
+    /// Monotonically increasing identifier issued by consensus layer
+    pub index: U64,
+
+    /// Index of validator associated with withdrawal
+    #[serde(rename = "validatorIndex")]
+    pub validator_index: U64,
+
+    /// Target address for withdrawn ether
+    pub address: Address,
+
+    /// Value of withdrawal (in wei)
+    pub amount: U256,
+}
+
+impl rlp::Encodable for Withdrawal {
+    fn rlp_append(&self, s: &mut rlp::RlpStream) {
+        s.begin_list(4);
+        s.append(&self.index);
+        s.append(&self.validator_index);
+        s.append(&self.address);
+        s.append(&self.amount);
+    }
+}
diff --git a/ethers-core/src/utils/anvil.rs b/ethers-core/src/utils/anvil.rs
index 26612aac..1fe5baf7 100644
--- a/ethers-core/src/utils/anvil.rs
+++ b/ethers-core/src/utils/anvil.rs
@@ -85,6 +85,7 @@ impl Drop for AnvilInstance {
 /// 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<PathBuf>,
     port: Option<u16>,
@@ -135,35 +136,30 @@ impl Anvil {
     ///
     /// By default, it's expected that `anvil` is in `$PATH`, see also
     /// [`std::process::Command::new()`]
-    #[must_use]
     pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
         self.program = Some(path.into());
         self
     }
 
     /// Sets the port which will be used when the `anvil` instance is launched.
-    #[must_use]
     pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
         self.port = Some(port.into());
         self
     }
 
     /// Sets the chain_id the `anvil` instance will use.
-    #[must_use]
     pub fn chain_id<T: Into<u64>>(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.
-    #[must_use]
     pub fn mnemonic<T: Into<String>>(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.
-    #[must_use]
     pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
         self.block_time = Some(block_time.into());
         self
@@ -172,7 +168,6 @@ impl Anvil {
     /// 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
-    #[must_use]
     pub fn fork_block_number<T: Into<u64>>(mut self, fork_block_number: T) -> Self {
         self.fork_block_number = Some(fork_block_number.into());
         self
@@ -182,21 +177,18 @@ impl Anvil {
     /// 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`
-    #[must_use]
     pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
         self.fork = Some(fork.into());
         self
     }
 
     /// Adds an argument to pass to the `anvil`.
-    #[must_use]
     pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
         self.args.push(arg.into());
         self
     }
 
     /// Adds multiple arguments to pass to the `anvil`.
-    #[must_use]
     pub fn args<I, S>(mut self, args: I) -> Self
     where
         I: IntoIterator<Item = S>,
@@ -209,14 +201,17 @@ impl Anvil {
     }
 
     /// Sets the timeout which will be used when the `anvil` instance is launched.
-    #[must_use]
     pub fn timeout<T: Into<u64>>(mut self, timeout: T) -> Self {
         self.timeout = Some(timeout.into());
         self
     }
 
-    /// Consumes the builder and spawns `anvil` with stdout redirected
-    /// to /dev/null.
+    /// 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)
diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs
index d1300e78..d040eb73 100644
--- a/ethers-core/src/utils/ganache.rs
+++ b/ethers-core/src/utils/ganache.rs
@@ -78,6 +78,7 @@ impl Drop for GanacheInstance {
 /// 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<u16>,
     block_time: Option<u64>,
@@ -102,21 +103,18 @@ impl Ganache {
     }
 
     /// Sets the port which will be used when the `ganache-cli` instance is launched.
-    #[must_use]
     pub fn port<T: Into<u16>>(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.
-    #[must_use]
     pub fn mnemonic<T: Into<String>>(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.
-    #[must_use]
     pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
         self.block_time = Some(block_time.into());
         self
@@ -126,21 +124,18 @@ impl Ganache {
     /// 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`
-    #[must_use]
     pub fn fork<T: Into<String>>(mut self, fork: T) -> Self {
         self.fork = Some(fork.into());
         self
     }
 
     /// Adds an argument to pass to the `ganache-cli`.
-    #[must_use]
     pub fn arg<T: Into<String>>(mut self, arg: T) -> Self {
         self.args.push(arg.into());
         self
     }
 
     /// Adds multiple arguments to pass to the `ganache-cli`.
-    #[must_use]
     pub fn args<I, S>(mut self, args: I) -> Self
     where
         I: IntoIterator<Item = S>,
@@ -152,9 +147,12 @@ impl Ganache {
         self
     }
 
-    /// Consumes the builder and spawns `ganache-cli` with stdout redirected
-    /// to /dev/null. This takes ~2 seconds to execute as it blocks while
-    /// waiting for `ganache-cli` to launch.
+    /// 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());
diff --git a/ethers-core/src/utils/genesis.rs b/ethers-core/src/utils/genesis.rs
index 58725f90..8465de39 100644
--- a/ethers-core/src/utils/genesis.rs
+++ b/ethers-core/src/utils/genesis.rs
@@ -1,8 +1,15 @@
 use std::collections::HashMap;
 
 use crate::{
-    types::{Address, Bytes, H256, U256, U64},
-    utils::{from_int_or_hex, from_int_or_hex_opt, from_u64_or_hex_opt, from_unformatted_hex_map},
+    types::{
+        serde_helpers::{
+            deserialize_stringified_eth_u64, deserialize_stringified_eth_u64_opt,
+            deserialize_stringified_numeric, deserialize_stringified_numeric_opt,
+            deserialize_stringified_u64_opt,
+        },
+        Address, Bytes, H256, U256, U64,
+    },
+    utils::from_unformatted_hex_map,
 };
 use serde::{Deserialize, Serialize};
 
@@ -16,11 +23,11 @@ pub struct Genesis {
     pub config: ChainConfig,
 
     /// The genesis header nonce.
-    #[serde(default)]
+    #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")]
     pub nonce: U64,
 
     /// The genesis header timestamp.
-    #[serde(default)]
+    #[serde(default, deserialize_with = "deserialize_stringified_eth_u64")]
     pub timestamp: U64,
 
     /// The genesis header extra data.
@@ -28,10 +35,11 @@ pub struct Genesis {
     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 = "from_int_or_hex")]
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     pub difficulty: U256,
 
     /// The genesis header mix hash.
@@ -48,11 +56,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", default)]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_eth_u64_opt",
+        default
+    )]
     pub number: Option<U64>,
 
     /// The block gas gasUsed
-    #[serde(skip_serializing_if = "Option::is_none", default)]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_eth_u64_opt",
+        default
+    )]
     pub gas_used: Option<U64>,
 
     /// The block parent hash
@@ -60,7 +76,11 @@ pub struct Genesis {
     pub parent_hash: Option<H256>,
 
     /// The base fee
-    #[serde(skip_serializing_if = "Option::is_none", default)]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_numeric_opt",
+        default
+    )]
     pub base_fee_per_gas: Option<U256>,
 }
 
@@ -126,10 +146,11 @@ impl Genesis {
 pub struct GenesisAccount {
     #[serde(
         skip_serializing_if = "Option::is_none",
-        deserialize_with = "from_u64_or_hex_opt",
+        deserialize_with = "deserialize_stringified_u64_opt",
         default
     )]
     pub nonce: Option<u64>,
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     pub balance: U256,
     #[serde(skip_serializing_if = "Option::is_none", default)]
     pub code: Option<Bytes>,
@@ -154,18 +175,27 @@ pub struct ChainConfig {
     pub chain_id: u64,
 
     /// The homestead switch block (None = no fork, 0 = already homestead).
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub homestead_block: Option<u64>,
 
     /// The DAO fork switch block (None = no fork).
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub dao_fork_block: Option<u64>,
 
     /// 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")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub eip150_block: Option<u64>,
 
     /// The EIP-150 hard fork hash.
@@ -173,63 +203,108 @@ pub struct ChainConfig {
     pub eip150_hash: Option<H256>,
 
     /// The EIP-155 hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub eip155_block: Option<u64>,
 
     /// The EIP-158 hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub eip158_block: Option<u64>,
 
     /// The Byzantium hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub byzantium_block: Option<u64>,
 
     /// The Constantinople hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub constantinople_block: Option<u64>,
 
     /// The Petersburg hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub petersburg_block: Option<u64>,
 
     /// The Istanbul hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub istanbul_block: Option<u64>,
 
     /// The Muir Glacier hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub muir_glacier_block: Option<u64>,
 
     /// The Berlin hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub berlin_block: Option<u64>,
 
     /// The London hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub london_block: Option<u64>,
 
     /// The Arrow Glacier hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub arrow_glacier_block: Option<u64>,
 
     /// The Gray Glacier hard fork block.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub gray_glacier_block: Option<u64>,
 
     /// Virtual fork after the merge to use as a network splitter.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub merge_netsplit_block: Option<u64>,
 
     /// Shanghai switch time.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub shanghai_time: Option<u64>,
 
     /// Cancun switch time.
-    #[serde(skip_serializing_if = "Option::is_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub cancun_time: Option<u64>,
 
     /// Total difficulty reached that triggers the merge consensus upgrade.
-    #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "from_int_or_hex_opt")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_numeric_opt"
+    )]
     pub terminal_total_difficulty: Option<U256>,
 
     /// A flag specifying that the network already passed the terminal total difficulty. Its
@@ -259,11 +334,19 @@ pub struct EthashConfig {}
 #[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")]
+    #[serde(
+        default,
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub period: Option<u64>,
 
     /// Epoch length to reset votes and checkpoints.
-    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(
+        default,
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "deserialize_stringified_u64_opt"
+    )]
     pub epoch: Option<u64>,
 }
 
@@ -310,6 +393,67 @@ mod tests {
         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#"
diff --git a/ethers-core/src/utils/geth.rs b/ethers-core/src/utils/geth.rs
index bf9b0b2b..5e156788 100644
--- a/ethers-core/src/utils/geth.rs
+++ b/ethers-core/src/utils/geth.rs
@@ -40,7 +40,8 @@ pub enum GethInstanceError {
 
 /// A geth instance. Will close the instance when dropped.
 ///
-/// Construct this using [`Geth`](crate::utils::Geth)
+/// Construct this using [`Geth`](crate::utils::Geth).
+#[derive(Debug)]
 pub struct GethInstance {
     pid: Child,
     port: u16,
@@ -82,7 +83,7 @@ impl GethInstance {
         &self.data_dir
     }
 
-    /// Returns the genesis configuration used to conifugre this instance
+    /// Returns the genesis configuration used to configure this instance
     pub fn genesis(&self) -> &Option<Genesis> {
         &self.genesis
     }
@@ -130,7 +131,7 @@ impl Drop for GethInstance {
 }
 
 /// Whether or not geth is in `dev` mode and configuration options that depend on the mode.
-#[derive(Debug, Clone)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub enum GethMode {
     /// Options that can be set in dev mode
     Dev(DevOptions),
@@ -145,14 +146,14 @@ impl Default for GethMode {
 }
 
 /// Configuration options that can be set in dev mode.
-#[derive(Debug, Clone, Default)]
+#[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<u64>,
 }
 
 /// Configuration options that cannot be set in dev mode.
-#[derive(Debug, Clone)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct PrivateNetOptions {
     /// The p2p port to use.
     pub p2p_port: Option<u16>,
@@ -188,7 +189,8 @@ impl Default for PrivateNetOptions {
 ///
 /// drop(geth); // this will kill the instance
 /// ```
-#[derive(Clone, Default)]
+#[derive(Clone, Debug, Default)]
+#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
 pub struct Geth {
     program: Option<PathBuf>,
     port: Option<u16>,
@@ -204,7 +206,8 @@ pub struct Geth {
 
 impl Geth {
     /// Creates an empty Geth builder.
-    /// The default port is 8545. The mnemonic is chosen randomly.
+    ///
+    /// The mnemonic is chosen randomly.
     pub fn new() -> Self {
         Self::default()
     }
@@ -234,7 +237,6 @@ impl Geth {
     ///
     /// By default, it's expected that `geth` is in `$PATH`, see also
     /// [`std::process::Command::new()`]
-    #[must_use]
     pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
         self.program = Some(path.into());
         self
@@ -245,14 +247,12 @@ impl Geth {
     ///
     /// The address derived from this private key will be used to set the `miner.etherbase` field
     /// on the node.
-    #[must_use]
     pub fn set_clique_private_key<T: Into<SigningKey>>(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.
-    #[must_use]
     pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
         self.port = Some(port.into());
         self
@@ -262,7 +262,6 @@ impl Geth {
     ///
     /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
     /// options.
-    #[must_use]
     pub fn p2p_port(mut self, port: u16) -> Self {
         match self.mode {
             GethMode::Dev(_) => {
@@ -280,21 +279,18 @@ impl Geth {
     ///
     /// This will put the geth instance in `dev` mode, discarding any previously set options that
     /// cannot be used in dev mode.
-    #[must_use]
     pub fn block_time<T: Into<u64>>(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.
-    #[must_use]
     pub fn chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
         self.chain_id = Some(chain_id.into());
         self
     }
 
     /// Allow geth to unlock accounts when rpc apis are open.
-    #[must_use]
     pub fn insecure_unlock(mut self) -> Self {
         self.insecure_unlock = true;
         self
@@ -304,7 +300,6 @@ impl Geth {
     ///
     /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
     /// options.
-    #[must_use]
     pub fn disable_discovery(mut self) -> Self {
         self.inner_disable_discovery();
         self
@@ -321,14 +316,12 @@ impl Geth {
     }
 
     /// Manually sets the IPC path for the socket manually.
-    #[must_use]
     pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
         self.ipc_path = Some(path.into());
         self
     }
 
     /// Sets the data directory for geth.
-    #[must_use]
     pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
         self.data_dir = Some(path.into());
         self
@@ -340,21 +333,22 @@ impl Geth {
     /// set to the same value as `data_dir`.
     ///
     /// This is destructive and will overwrite any existing data in the data directory.
-    #[must_use]
     pub fn genesis(mut self, genesis: Genesis) -> Self {
         self.genesis = Some(genesis);
         self
     }
 
     /// Sets the port for authenticated RPC connections.
-    #[must_use]
     pub fn authrpc_port(mut self, port: u16) -> Self {
         self.authrpc_port = Some(port);
         self
     }
 
-    /// Consumes the builder and spawns `geth` with stdout redirected to /dev/null.
-    #[must_use]
+    /// 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() {
@@ -368,17 +362,18 @@ impl Geth {
 
         let mut unused_ports = unused_ports::<3>().into_iter();
         let mut unused_port = || unused_ports.next().unwrap();
-        let port = if let Some(port) = self.port { port } else { unused_port() };
-        let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() };
+
+        let port = self.port.unwrap_or_else(&mut unused_port);
+        let port_s = port.to_string();
 
         // Open the HTTP API
         cmd.arg("--http");
-        cmd.arg("--http.port").arg(port.to_string());
+        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.to_string());
+        cmd.arg("--ws.port").arg(port_s);
         cmd.arg("--ws.api").arg(API);
 
         // pass insecure unlock flag if set
@@ -392,11 +387,12 @@ impl Geth {
         }
 
         // 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 let Some(ref mut genesis) = self.genesis {
-            if is_clique {
+        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);
@@ -416,7 +412,7 @@ impl Geth {
                 // the entire address
                 cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
             }
-        } else if is_clique {
+
             let clique_addr =
                 secret_key_to_address(self.clique_private_key.as_ref().expect("is_clique == true"));
 
@@ -455,11 +451,14 @@ impl Geth {
             init_cmd.stderr(Stdio::null());
 
             init_cmd.arg("init").arg(temp_genesis_path);
-            init_cmd
+            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)
@@ -485,7 +484,7 @@ impl Geth {
                 None
             }
             GethMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => {
-                let port = if let Some(port) = p2p_port { port } else { unused_port() };
+                let port = p2p_port.unwrap_or_else(unused_port);
                 cmd.arg("--port").arg(port.to_string());
 
                 // disable discovery if the flag is set
@@ -562,86 +561,64 @@ impl Geth {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use std::path::Path;
 
-    #[test]
-    fn p2p_port() {
-        let temp_dir = tempfile::tempdir().unwrap();
-        let temp_dir_path = temp_dir.path().to_path_buf();
-
-        // disabling discovery should put the geth instance into non-dev mode, and it should have a
-        // p2p port.
-        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() {
+    /// 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().to_path_buf();
-
-        // 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));
+        let temp_dir_path = temp_dir.path();
+        f(temp_dir_path);
+        #[cfg(not(windows))]
+        temp_dir.close().unwrap();
     }
 
     #[test]
-    fn dev_mode() {
-        let temp_dir = tempfile::tempdir().unwrap();
-        let temp_dir_path = temp_dir.path().to_path_buf();
-
-        // 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());
+    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 clique_private_key_configured() {
-        let temp_dir = tempfile::tempdir().unwrap();
-        let temp_dir_path = temp_dir.path().to_path_buf();
-
-        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();
-
-        let clique_private_key = geth.clique_private_key().clone();
-        assert!(clique_private_key.is_some());
+    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 clique_genesis_configured() {
-        let temp_dir = tempfile::tempdir().unwrap();
-        let temp_dir_path = temp_dir.path().to_path_buf();
-
-        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();
-
-        let genesis = geth.genesis().clone();
-        assert!(genesis.is_some());
+    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());
+        })
     }
 
     #[test]
-    fn clique_p2p_configured() {
-        let temp_dir = tempfile::tempdir().unwrap();
-        let temp_dir_path = temp_dir.path().to_path_buf();
-
-        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();
-
-        let p2p_port = geth.p2p_port();
-        assert!(p2p_port.is_some());
+    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/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 7ec7e9ba..9c406de0 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -36,14 +36,13 @@ pub use rlp;
 /// Re-export hex
 pub use hex;
 
-use crate::types::{Address, Bytes, ParseI256Error, H256, I256, U256, U64};
+use crate::types::{Address, Bytes, ParseI256Error, H256, I256, U256};
 use ethabi::ethereum_types::FromDecStrErr;
 use k256::ecdsa::SigningKey;
 use std::{
     collections::HashMap,
     convert::{TryFrom, TryInto},
     fmt,
-    str::FromStr,
 };
 use thiserror::Error;
 
@@ -507,62 +506,6 @@ where
     }
 }
 
-/// Deserializes the input into a U256, accepting both 0x-prefixed hex and decimal strings with
-/// arbitrary precision, defined by serde_json's [`Number`](serde_json::Number).
-pub fn from_int_or_hex<'de, D>(deserializer: D) -> Result<U256, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    #[serde(untagged)]
-    enum IntOrHex {
-        Int(serde_json::Number),
-        Hex(String),
-    }
-
-    match IntOrHex::deserialize(deserializer)? {
-        IntOrHex::Hex(s) => U256::from_str(s.as_str()).map_err(serde::de::Error::custom),
-        IntOrHex::Int(n) => U256::from_dec_str(&n.to_string()).map_err(serde::de::Error::custom),
-    }
-}
-
-/// Deserializes the input into a U64, accepting both 0x-prefixed hex and decimal strings with
-/// arbitrary precision, defined by serde_json's [`Number`](serde_json::Number).
-pub fn from_u64_or_hex<'de, D>(deserializer: D) -> Result<U64, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    #[serde(untagged)]
-    enum IntOrHex {
-        Int(serde_json::Number),
-        Hex(String),
-    }
-
-    match IntOrHex::deserialize(deserializer)? {
-        IntOrHex::Hex(s) => U64::from_str(s.as_str()).map_err(serde::de::Error::custom),
-        IntOrHex::Int(n) => U64::from_dec_str(&n.to_string()).map_err(serde::de::Error::custom),
-    }
-}
-
-/// Deserializes the input into an `Option<U256>`, using [`from_int_or_hex`] to deserialize the
-/// inner value.
-pub fn from_int_or_hex_opt<'de, D>(deserializer: D) -> Result<Option<U256>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    Ok(Some(from_int_or_hex(deserializer)?))
-}
-
-/// Deserializes the input into an `Option<u64>`, using [`from_u64_or_hex`] to deserialize the
-/// inner value.
-pub fn from_u64_or_hex_opt<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    Ok(Some(from_u64_or_hex(deserializer)?.as_u64()))
-}
-
 fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
     let mut rewards: Vec<U256> =
         rewards.iter().map(|r| r[0]).filter(|r| *r > U256::zero()).collect();
@@ -644,6 +587,7 @@ pub(crate) fn unused_ports<const N: usize>() -> [u16; N] {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::types::serde_helpers::deserialize_stringified_numeric;
     use hex_literal::hex;
 
     #[test]
@@ -1105,4 +1049,34 @@ mod tests {
         let rewards_overflow: Vec<Vec<U256>> = 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()),
+            ("\"115792089237316195423570985008687907853269984665640564039457584007913129639935\"", U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639935").unwrap())
+        ];
+
+        #[derive(Deserialize)]
+        struct TestUint(#[serde(deserialize_with = "deserialize_stringified_numeric")] U256);
+
+        for (string, expected) in cases {
+            println!("testing {}", string);
+            let test: TestUint = serde_json::from_str(string).unwrap();
+            assert_eq!(test.0, expected);
+        }
+    }
 }
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index a8710206..a6f2799e 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -60,7 +60,7 @@ once_cell.workspace = true
 reqwest = { workspace = true, features = ["json", "rustls"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
-tokio = { workspace = true, features = ["rt", "macros", "time"] }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time"] }
 
 [features]
 default = ["rustls"]
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 82f7b344..138ef0ee 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -74,7 +74,7 @@ web-sys = { version = "0.3", features = ["console"] }
 getrandom.workspace = true
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
-tokio = { workspace = true, features = ["rt", "macros", "time"] }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time"] }
 tempfile = "3.5.0"
 
 [features]
diff --git a/ethers-providers/src/ext/admin.rs b/ethers-providers/src/ext/admin.rs
index 2c99664d..f4abed3f 100644
--- a/ethers-providers/src/ext/admin.rs
+++ b/ethers-providers/src/ext/admin.rs
@@ -1,7 +1,7 @@
 use enr::{k256::ecdsa::SigningKey, Enr};
 use ethers_core::{
-    types::{H256, U256},
-    utils::{from_int_or_hex, ChainConfig},
+    types::{serde_helpers::deserialize_stringified_numeric, H256, U256},
+    utils::ChainConfig,
 };
 use serde::{Deserialize, Serialize};
 use std::net::{IpAddr, SocketAddr};
@@ -71,7 +71,7 @@ pub struct EthProtocolInfo {
     pub network: u64,
 
     /// The total difficulty of the host's blockchain.
-    #[serde(deserialize_with = "from_int_or_hex")]
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     pub difficulty: U256,
 
     /// The Keccak hash of the host's genesis block.
@@ -131,7 +131,7 @@ pub struct EthInfo {
     pub version: u64,
 
     /// The total difficulty of the peer's blockchain.
-    #[serde(deserialize_with = "from_int_or_hex")]
+    #[serde(deserialize_with = "deserialize_stringified_numeric")]
     pub difficulty: U256,
 
     /// The hash of the peer's best known block.
diff --git a/ethers-providers/src/rpc/provider.rs b/ethers-providers/src/rpc/provider.rs
index 5478ae13..13ec96c8 100644
--- a/ethers-providers/src/rpc/provider.rs
+++ b/ethers-providers/src/rpc/provider.rs
@@ -13,6 +13,7 @@ use crate::{
 
 #[cfg(not(target_arch = "wasm32"))]
 use crate::{HttpRateLimitRetryPolicy, RetryClient};
+use std::net::Ipv4Addr;
 
 #[cfg(feature = "celo")]
 pub use crate::CeloMiddleware;
@@ -40,7 +41,7 @@ use std::{
 };
 use tracing::trace;
 use tracing_futures::Instrument;
-use url::{ParseError, Url};
+use url::{Host, ParseError, Url};
 
 /// Node Clients
 #[derive(Copy, Clone)]
@@ -1443,18 +1444,35 @@ impl ProviderExt for Provider<HttpProvider> {
 /// ```
 /// use ethers_providers::is_local_endpoint;
 /// assert!(is_local_endpoint("http://localhost:8545"));
+/// assert!(is_local_endpoint("http://169.254.0.0:8545"));
 /// assert!(is_local_endpoint("http://127.0.0.1:8545"));
+/// assert!(!is_local_endpoint("http://206.71.50.230:8545"));
+/// assert!(!is_local_endpoint("http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"));
+/// assert!(is_local_endpoint("http://[::1]"));
+/// assert!(!is_local_endpoint("havenofearlucishere"));
 /// ```
 #[inline]
-pub fn is_local_endpoint(url: &str) -> bool {
-    url.contains("127.0.0.1") || url.contains("localhost")
+pub fn is_local_endpoint(endpoint: &str) -> bool {
+    if let Ok(url) = Url::parse(endpoint) {
+        if let Some(host) = url.host() {
+            match host {
+                Host::Domain(domain) => return domain.contains("localhost"),
+                Host::Ipv4(ipv4) => {
+                    return ipv4 == Ipv4Addr::LOCALHOST ||
+                        ipv4.is_link_local() ||
+                        ipv4.is_loopback() ||
+                        ipv4.is_private()
+                }
+                Host::Ipv6(ipv6) => return ipv6.is_loopback(),
+            }
+        }
+    }
+    false
 }
 
 #[cfg(test)]
 #[cfg(not(target_arch = "wasm32"))]
 mod tests {
-    use std::path::PathBuf;
-
     use super::*;
     use crate::Http;
     use ethers_core::{
@@ -1464,6 +1482,7 @@ mod tests {
         utils::{Anvil, Genesis, Geth, GethInstance},
     };
     use futures_util::StreamExt;
+    use std::path::PathBuf;
 
     #[test]
     fn convert_h256_u256_quantity() {
@@ -1481,8 +1500,8 @@ mod tests {
         assert_eq!(params, r#"["0x295a70b2de5e3953354a6a8344e616ed314d7251","0x0","latest"]"#);
     }
 
-    #[tokio::test]
     // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
+    #[tokio::test]
     async fn mainnet_resolve_name() {
         let provider = crate::test_provider::MAINNET.provider();
 
@@ -1496,8 +1515,8 @@ mod tests {
         provider.resolve_name("asdfasdf.registrar.firefly.eth").await.unwrap_err();
     }
 
-    #[tokio::test]
     // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
+    #[tokio::test]
     async fn mainnet_lookup_address() {
         let provider = crate::MAINNET.provider();
 
@@ -1560,7 +1579,6 @@ mod tests {
     }
 
     #[tokio::test]
-    #[cfg_attr(feature = "celo", ignore)]
     async fn test_is_signer() {
         use ethers_core::utils::Anvil;
         use std::str::FromStr;
@@ -1637,20 +1655,6 @@ mod tests {
         assert!(!receipts.is_empty());
     }
 
-    #[tokio::test]
-    // Celo blocks can not get parsed when used with Ganache
-    #[cfg(not(feature = "celo"))]
-    async fn block_subscribe() {
-        use ethers_core::utils::Anvil;
-        use futures_util::StreamExt;
-        let anvil = Anvil::new().block_time(2u64).spawn();
-        let provider = Provider::connect(anvil.ws_endpoint()).await.unwrap();
-
-        let stream = provider.subscribe_blocks().await.unwrap();
-        let blocks = stream.take(3).map(|x| x.number.unwrap().as_u64()).collect::<Vec<_>>().await;
-        assert_eq!(blocks, vec![1, 2, 3]);
-    }
-
     #[tokio::test]
     #[cfg_attr(feature = "celo", ignore)]
     async fn fee_history() {
@@ -1831,9 +1835,10 @@ mod tests {
     async fn geth_admin_nodeinfo() {
         // we can't use the test provider because infura does not expose admin endpoints
         let network = 1337u64;
-        let temp_dir = tempfile::tempdir().unwrap().into_path();
+        let dir = tempfile::tempdir().unwrap();
 
-        let (geth, provider) = spawn_geth_and_create_provider(network, Some(temp_dir), None);
+        let (geth, provider) =
+            spawn_geth_and_create_provider(network, Some(dir.path().into()), None);
 
         let info = provider.node_info().await.unwrap();
         drop(geth);
@@ -1843,9 +1848,12 @@ mod tests {
 
         // check that the network id is correct
         assert_eq!(info.protocols.eth.unwrap().network, network);
+
+        #[cfg(not(windows))]
+        dir.close().unwrap();
     }
 
-    /// Spawn a new `GethInstance` without discovery and crate a `Provider` for it.
+    /// Spawn a new `GethInstance` without discovery and create a `Provider` for it.
     ///
     /// These will all use the same genesis config.
     fn spawn_geth_and_create_provider(
@@ -1873,36 +1881,27 @@ mod tests {
     /// Spawn a set of [`GethInstance`]s with the list of given data directories and [`Provider`]s
     /// for those [`GethInstance`]s without discovery, setting sequential ports for their p2p, rpc,
     /// and authrpc ports.
-    fn spawn_geth_instances(
-        datadirs: Vec<PathBuf>,
+    fn spawn_geth_instances<const N: usize>(
+        datadirs: [PathBuf; N],
         chain_id: u64,
         genesis: Option<Genesis>,
-    ) -> Vec<(GethInstance, Provider<HttpProvider>)> {
-        let mut geths = Vec::with_capacity(datadirs.len());
-
-        for dir in datadirs {
-            let (geth, provider) =
-                spawn_geth_and_create_provider(chain_id, Some(dir), genesis.clone());
-
-            geths.push((geth, provider));
-        }
-
-        geths
+    ) -> [(GethInstance, Provider<HttpProvider>); N] {
+        datadirs.map(|dir| spawn_geth_and_create_provider(chain_id, Some(dir), genesis.clone()))
     }
 
     #[tokio::test]
+    #[cfg_attr(windows, ignore = "cannot spawn multiple geth instances")]
     async fn add_second_geth_peer() {
         // init each geth directory
-        let dir1 = tempfile::tempdir().unwrap().into_path();
-        let dir2 = tempfile::tempdir().unwrap().into_path();
+        let dir1 = tempfile::tempdir().unwrap();
+        let dir2 = tempfile::tempdir().unwrap();
 
         // use the default genesis
         let genesis = utils::Genesis::default();
 
         // spawn the geths
-        let mut geths = spawn_geth_instances(vec![dir1.clone(), dir2.clone()], 1337, Some(genesis));
-        let (mut first_geth, first_peer) = geths.pop().unwrap();
-        let (second_geth, second_peer) = geths.pop().unwrap();
+        let [(mut first_geth, first_peer), (second_geth, second_peer)] =
+            spawn_geth_instances([dir1.path().into(), dir2.path().into()], 1337, Some(genesis));
 
         // get nodeinfo for each geth instance
         let first_info = first_peer.node_info().await.unwrap();
@@ -1935,7 +1934,10 @@ mod tests {
         assert_eq!(H256::from_str(&peer.id).unwrap(), second_info.id);
 
         // remove directories
-        std::fs::remove_dir_all(dir1).unwrap();
-        std::fs::remove_dir_all(dir2).unwrap();
+        #[cfg(not(windows))]
+        {
+            dir1.close().unwrap();
+            dir2.close().unwrap();
+        }
     }
 }
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index e6a77af1..16884638 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -28,7 +28,7 @@ ethers-core.workspace = true
 
 # crypto
 coins-bip32 = "0.8.3"
-coins-bip39 = "0.8.5"
+coins-bip39 = "0.8.6"
 elliptic-curve.workspace = true
 sha2.workspace = true
 rand.workspace = true
@@ -73,7 +73,7 @@ tracing-subscriber.workspace = true
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 yubihsm = { version = "0.42.0-pre.0", features = ["secp256k1", "usb", "mockhsm"] }
-tokio = { workspace = true, features = ["macros", "rt"] }
+tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
 
 [features]
 futures = ["futures-util", "futures-executor"]
diff --git a/ethers-signers/src/trezor/app.rs b/ethers-signers/src/trezor/app.rs
index 471e4e58..9f7ae67f 100644
--- a/ethers-signers/src/trezor/app.rs
+++ b/ethers-signers/src/trezor/app.rs
@@ -35,7 +35,9 @@ pub struct TrezorEthereum {
     pub(crate) address: Address,
 }
 
-const FIRMWARE_MIN_VERSION: &str = ">=2.4.2";
+// we need firmware that supports EIP-1559 and EIP-712
+const FIRMWARE_1_MIN_VERSION: &str = ">=1.11.1";
+const FIRMWARE_2_MIN_VERSION: &str = ">=2.5.1";
 
 // https://docs.trezor.io/trezor-firmware/common/communication/sessions.html
 const SESSION_ID_LENGTH: usize = 32;
@@ -73,12 +75,20 @@ impl TrezorEthereum {
     }
 
     fn check_version(version: String) -> Result<(), TrezorError> {
-        let req = semver::VersionReq::parse(FIRMWARE_MIN_VERSION)?;
         let version = semver::Version::parse(&version)?;
 
-        // Enforce firmware version is greater than FIRMWARE_MIN_VERSION
+        let min_version = match version.major {
+            1 => FIRMWARE_1_MIN_VERSION,
+            2 => FIRMWARE_2_MIN_VERSION,
+            // unknown major version, possibly newer models that we don't know about yet
+            // it's probably safe to assume they support EIP-1559 and EIP-712
+            _ => return Ok(()),
+        };
+
+        let req = semver::VersionReq::parse(min_version)?;
+        // Enforce firmware version is greater than "min_version"
         if !req.matches(&version) {
-            return Err(TrezorError::UnsupportedFirmwareVersion(FIRMWARE_MIN_VERSION.to_string()))
+            return Err(TrezorError::UnsupportedFirmwareVersion(min_version.to_string()))
         }
 
         Ok(())
diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs
index 1d9823a2..e9057850 100644
--- a/ethers-signers/src/wallet/private_key.rs
+++ b/ethers-signers/src/wallet/private_key.rs
@@ -132,6 +132,11 @@ impl FromStr for Wallet<SigningKey> {
     fn from_str(src: &str) -> Result<Self, Self::Err> {
         let src = src.strip_prefix("0x").or_else(|| src.strip_prefix("0X")).unwrap_or(src);
         let src = hex::decode(src)?;
+
+        if src.len() != 32 {
+            return Err(WalletError::HexError(hex::FromHexError::InvalidStringLength))
+        }
+
         let sk = SigningKey::from_bytes(src.as_slice().into())?;
         Ok(sk.into())
     }
@@ -157,7 +162,7 @@ impl TryFrom<String> for Wallet<SigningKey> {
 #[cfg(not(target_arch = "wasm32"))]
 mod tests {
     use super::*;
-    use crate::Signer;
+    use crate::{LocalWallet, Signer};
     use ethers_core::types::Address;
     use tempfile::tempdir;
 
@@ -167,6 +172,17 @@ mod tests {
         let _pk: Wallet<SigningKey> = s.parse().unwrap();
     }
 
+    #[test]
+    fn parse_short_key() {
+        let s = "6f142508b4eea641e33cb2a0161221105086a84584c74245ca463a49effea3";
+        assert!(s.len() < 64);
+        let pk = s.parse::<LocalWallet>().unwrap_err();
+        match pk {
+            WalletError::HexError(hex::FromHexError::InvalidStringLength) => {}
+            _ => panic!("Unexpected error"),
+        }
+    }
+
     #[tokio::test]
     async fn encrypted_json_keystore() {
         // create and store a random encrypted JSON keystore in this directory
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 6a20d7ed..01cce77f 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -9,12 +9,13 @@ use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
 use std::{
     collections::{BTreeMap, HashSet},
     fmt, fs,
+    ops::Range,
     path::{Path, PathBuf},
     str::FromStr,
     sync::Arc,
 };
 use tracing::warn;
-use yansi::Paint;
+use yansi::{Color, Paint, Style};
 
 pub mod ast;
 pub use ast::*;
@@ -757,7 +758,7 @@ impl EvmVersion {
     pub fn normalize_version(self, version: &Version) -> Option<EvmVersion> {
         // the EVM version flag was only added at 0.4.21
         // we work our way backwards
-        if version >= &CONSTANTINOPLE_SOLC {
+        if version >= &BYZANTIUM_SOLC {
             // If the Solc is at least at london, it supports all EVM versions
             Some(if version >= &LONDON_SOLC {
                 self
@@ -769,8 +770,10 @@ impl EvmVersion {
                 EvmVersion::Istanbul
             } else if version >= &PETERSBURG_SOLC && self >= EvmVersion::Petersburg {
                 EvmVersion::Petersburg
-            } else if self >= EvmVersion::Constantinople {
+            } else if version >= &CONSTANTINOPLE_SOLC && self >= EvmVersion::Constantinople {
                 EvmVersion::Constantinople
+            } else if self >= EvmVersion::Byzantium {
+                EvmVersion::Byzantium
             } else {
                 self
             })
@@ -1837,31 +1840,211 @@ pub struct Error {
     pub formatted_message: Option<String>,
 }
 
+/// Tries to mimic Solidity's own error formatting.
+///
+/// <https://github.com/ethereum/solidity/blob/a297a687261a1c634551b1dac0e36d4573c19afe/liblangutil/SourceReferenceFormatter.cpp#L105>
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if !Paint::is_enabled() {
+            let msg = self.formatted_message.as_ref().unwrap_or(&self.message);
+            self.fmt_severity(f)?;
+            f.write_str(": ")?;
+            return f.write_str(msg)
+        }
+
+        // Error (XXXX): Error Message
+        styled(f, self.severity.color().style().bold(), |f| self.fmt_severity(f))?;
+        fmt_msg(f, &self.message)?;
+
         if let Some(msg) = &self.formatted_message {
-            match self.severity {
-                Severity::Error => {
-                    if let Some(code) = self.error_code {
-                        Paint::red(format!("error[{code}]: ")).fmt(f)?;
-                    }
-                    Paint::red(msg).fmt(f)
-                }
-                Severity::Warning | Severity::Info => {
-                    if let Some(code) = self.error_code {
-                        Paint::yellow(format!("warning[{code}]: ")).fmt(f)?;
-                    }
-                    Paint::yellow(msg).fmt(f)
+            let mut lines = msg.lines();
+
+            // skip first line, it should be similar to the error message we wrote above
+            lines.next();
+
+            // format the main source location
+            fmt_source_location(f, &mut lines)?;
+
+            // format remaining lines as secondary locations
+            while let Some(line) = lines.next() {
+                f.write_str("\n")?;
+
+                if let Some((note, msg)) = line.split_once(':') {
+                    styled(f, Self::secondary_style(), |f| f.write_str(note))?;
+                    fmt_msg(f, msg)?;
+                } else {
+                    f.write_str(line)?;
                 }
+
+                fmt_source_location(f, &mut lines)?;
             }
+        }
+
+        Ok(())
+    }
+}
+
+impl Error {
+    /// The style of the diagnostic severity.
+    pub fn error_style(&self) -> Style {
+        self.severity.color().style().bold()
+    }
+
+    /// The style of the diagnostic message.
+    pub fn message_style() -> Style {
+        Color::White.style().bold()
+    }
+
+    /// The style of the secondary source location.
+    pub fn secondary_style() -> Style {
+        Color::Cyan.style().bold()
+    }
+
+    /// The style of the source location highlight.
+    pub fn highlight_style() -> Style {
+        Color::Yellow.style()
+    }
+
+    /// The style of the diagnostics.
+    pub fn diag_style() -> Style {
+        Color::Yellow.style().bold()
+    }
+
+    /// The style of the source location frame.
+    pub fn frame_style() -> Style {
+        Color::Blue.style()
+    }
+
+    /// Formats the diagnostic severity:
+    ///
+    /// ```text
+    /// Error (XXXX)
+    /// ```
+    fn fmt_severity(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.write_str(self.severity.as_str())?;
+        if let Some(code) = self.error_code {
+            write!(f, " ({code})")?;
+        }
+        Ok(())
+    }
+}
+
+/// Calls `fun` in between [`Style::fmt_prefix`] and [`Style::fmt_suffix`].
+fn styled<F>(f: &mut fmt::Formatter, style: Style, fun: F) -> fmt::Result
+where
+    F: FnOnce(&mut fmt::Formatter) -> fmt::Result,
+{
+    style.fmt_prefix(f)?;
+    fun(f)?;
+    style.fmt_suffix(f)
+}
+
+/// Formats the diagnostic message.
+fn fmt_msg(f: &mut fmt::Formatter, msg: &str) -> fmt::Result {
+    styled(f, Error::message_style(), |f| {
+        f.write_str(": ")?;
+        f.write_str(msg.trim_start())
+    })
+}
+
+/// Colors a Solidity source location:
+///
+/// ```text
+/// --> /home/user/contract.sol:420:69:
+///     |
+/// 420 |       bad_code()
+///     |                ^
+/// ```
+fn fmt_source_location(f: &mut fmt::Formatter, lines: &mut std::str::Lines) -> fmt::Result {
+    // --> source
+    if let Some(line) = lines.next() {
+        f.write_str("\n")?;
+
+        let arrow = "-->";
+        if let Some((left, loc)) = line.split_once(arrow) {
+            f.write_str(left)?;
+            styled(f, Error::frame_style(), |f| f.write_str(arrow))?;
+            f.write_str(loc)?;
         } else {
-            self.severity.fmt(f)?;
-            writeln!(f, ": {}", self.message)
+            f.write_str(line)?;
         }
     }
+
+    // get the next 3 lines
+    // FIXME: Somehow do this without allocating
+    let next_3 = lines.take(3).collect::<Vec<_>>();
+    let [line1, line2, line3] = next_3[..] else {
+        for line in next_3 {
+            f.write_str("\n")?;
+            f.write_str(line)?;
+        }
+        return Ok(())
+    };
+
+    // line 1, just a frame
+    fmt_framed_location(f, line1, None)?;
+
+    // line 2, frame and code; highlight the text based on line 3's carets
+    let hl_start = line3.find('^');
+    let highlight = hl_start.map(|start| {
+        let end = if line3.contains("^ (") {
+            // highlight the entire line because of "spans across multiple lines" diagnostic
+            line2.len()
+        } else if let Some(carets) = line3[start..].find(|c: char| c != '^') {
+            // highlight the text that the carets point to
+            start + carets
+        } else {
+            // the carets span the entire third line
+            line3.len()
+        }
+        // bound in case carets span longer than the code they point to
+        .min(line2.len());
+        (start.min(end)..end, Error::highlight_style())
+    });
+    fmt_framed_location(f, line2, highlight)?;
+
+    // line 3, frame and maybe highlight, this time till the end unconditionally
+    let highlight = hl_start.map(|i| (i..line3.len(), Error::diag_style()));
+    fmt_framed_location(f, line3, highlight)
+}
+
+/// Colors a single Solidity framed source location line. Part of [`fmt_source_location`].
+fn fmt_framed_location(
+    f: &mut fmt::Formatter,
+    line: &str,
+    highlight: Option<(Range<usize>, Style)>,
+) -> fmt::Result {
+    f.write_str("\n")?;
+
+    if let Some((space_or_line_number, rest)) = line.split_once('|') {
+        // if the potential frame is not just whitespace or numbers, don't color it
+        if !space_or_line_number.chars().all(|c| c.is_whitespace() || c.is_numeric()) {
+            return f.write_str(line)
+        }
+
+        styled(f, Error::frame_style(), |f| {
+            f.write_str(space_or_line_number)?;
+            f.write_str("|")
+        })?;
+
+        if let Some((range, style)) = highlight {
+            let Range { start, end } = range;
+            let rest_start = line.len() - rest.len();
+            f.write_str(&line[rest_start..start])?;
+            styled(f, style, |f| f.write_str(&line[range]))?;
+            f.write_str(&line[end..])
+        } else {
+            f.write_str(rest)
+        }
+    } else {
+        f.write_str(line)
+    }
 }
 
-#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)]
+#[derive(
+    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
+)]
+#[serde(rename_all = "lowercase")]
 pub enum Severity {
     #[default]
     Error,
@@ -1871,25 +2054,7 @@ pub enum Severity {
 
 impl fmt::Display for Severity {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Severity::Error => Paint::red("Error").fmt(f),
-            Severity::Warning => Paint::yellow("Warning").fmt(f),
-            Severity::Info => f.write_str("Info"),
-        }
-    }
-}
-
-impl Severity {
-    pub fn is_error(&self) -> bool {
-        matches!(self, Severity::Error)
-    }
-
-    pub fn is_warning(&self) -> bool {
-        matches!(self, Severity::Warning)
-    }
-
-    pub fn is_info(&self) -> bool {
-        matches!(self, Severity::Info)
+        f.write_str(self.as_str())
     }
 }
 
@@ -1898,50 +2063,46 @@ impl FromStr for Severity {
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
-            "error" => Ok(Severity::Error),
-            "warning" => Ok(Severity::Warning),
-            "info" => Ok(Severity::Info),
+            "Error" | "error" => Ok(Self::Error),
+            "Warning" | "warning" => Ok(Self::Warning),
+            "Info" | "info" => Ok(Self::Info),
             s => Err(format!("Invalid severity: {s}")),
         }
     }
 }
 
-impl Serialize for Severity {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        match self {
-            Severity::Error => serializer.serialize_str("error"),
-            Severity::Warning => serializer.serialize_str("warning"),
-            Severity::Info => serializer.serialize_str("info"),
-        }
+impl Severity {
+    /// Returns `true` if the severity is `Error`.
+    pub const fn is_error(&self) -> bool {
+        matches!(self, Self::Error)
     }
-}
-
-impl<'de> Deserialize<'de> for Severity {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct SeverityVisitor;
 
-        impl<'de> Visitor<'de> for SeverityVisitor {
-            type Value = Severity;
+    /// Returns `true` if the severity is `Warning`.
+    pub const fn is_warning(&self) -> bool {
+        matches!(self, Self::Warning)
+    }
 
-            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-                write!(formatter, "severity string")
-            }
+    /// Returns `true` if the severity is `Info`.
+    pub const fn is_info(&self) -> bool {
+        matches!(self, Self::Info)
+    }
 
-            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                value.parse().map_err(serde::de::Error::custom)
-            }
+    /// Returns the string representation of the severity.
+    pub const fn as_str(&self) -> &'static str {
+        match self {
+            Self::Error => "Error",
+            Self::Warning => "Warning",
+            Self::Info => "Info",
         }
+    }
 
-        deserializer.deserialize_str(SeverityVisitor)
+    /// Returns the color to format the severity with.
+    pub const fn color(&self) -> Color {
+        match self {
+            Self::Error => Color::Red,
+            Self::Warning => Color::Yellow,
+            Self::Info => Color::White,
+        }
     }
 }
 
@@ -2151,12 +2312,16 @@ mod tests {
     #[test]
     fn test_evm_version_normalization() {
         for (solc_version, evm_version, expected) in &[
-            // Ensure 0.4.21 it always returns None
+            // Ensure before 0.4.21 it always returns None
             ("0.4.20", EvmVersion::Homestead, None),
-            // Constantinople clipping
+            // Byzantium clipping
             ("0.4.21", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
-            ("0.4.21", EvmVersion::Constantinople, Some(EvmVersion::Constantinople)),
-            ("0.4.21", EvmVersion::London, Some(EvmVersion::Constantinople)),
+            ("0.4.21", EvmVersion::Constantinople, Some(EvmVersion::Byzantium)),
+            ("0.4.21", EvmVersion::London, Some(EvmVersion::Byzantium)),
+            // Constantinople bug fix
+            ("0.4.22", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
+            ("0.4.22", EvmVersion::Constantinople, Some(EvmVersion::Constantinople)),
+            ("0.4.22", EvmVersion::London, Some(EvmVersion::Constantinople)),
             // Petersburg
             ("0.5.5", EvmVersion::Homestead, Some(EvmVersion::Homestead)),
             ("0.5.5", EvmVersion::Petersburg, Some(EvmVersion::Petersburg)),
diff --git a/ethers-solc/src/compile/mod.rs b/ethers-solc/src/compile/mod.rs
index f2259006..4ffef34d 100644
--- a/ethers-solc/src/compile/mod.rs
+++ b/ethers-solc/src/compile/mod.rs
@@ -22,7 +22,11 @@ pub const SOLC: &str = "solc";
 
 /// Support for configuring the EVM version
 /// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
-pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 21);
+pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
+
+/// Bug fix for configuring the EVM version with Constantinople
+/// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
+pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
 
 /// Petersburg support
 /// <https://blog.soliditylang.org/2019/03/05/solidity-0.5.5-release-announcement/>
diff --git a/ethers-solc/src/compile/output/mod.rs b/ethers-solc/src/compile/output/mod.rs
index 15ffaed3..3f9d29fc 100644
--- a/ethers-solc/src/compile/output/mod.rs
+++ b/ethers-solc/src/compile/output/mod.rs
@@ -14,6 +14,7 @@ use contracts::{VersionedContract, VersionedContracts};
 use semver::Version;
 use std::{collections::BTreeMap, fmt, path::Path};
 use tracing::trace;
+use yansi::Paint;
 
 pub mod contracts;
 pub mod info;
@@ -401,7 +402,7 @@ impl<T: ArtifactOutput> fmt::Display for ProjectCompileOutput<T> {
             f.write_str("Nothing to compile")
         } else {
             self.compiler_output
-                .diagnostics(&self.ignored_error_codes, self.compiler_severity_filter.clone())
+                .diagnostics(&self.ignored_error_codes, self.compiler_severity_filter)
                 .fmt(f)
         }
     }
@@ -751,35 +752,38 @@ impl<'a> OutputDiagnostics<'a> {
 
 impl<'a> fmt::Display for OutputDiagnostics<'a> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("Compiler run ")?;
         if self.has_error() {
-            f.write_str("Compiler run failed")?;
+            Paint::red("failed:")
         } else if self.has_warning() {
-            f.write_str("Compiler run successful (with warnings)")?;
+            Paint::yellow("successful with warnings:")
         } else {
-            f.write_str("Compiler run successful")?;
+            Paint::green("successful!")
         }
+        .fmt(f)?;
+
         for err in &self.compiler_output.errors {
+            let mut ignored = false;
             if err.severity.is_warning() {
-                let is_ignored = err.error_code.as_ref().map_or(false, |code| {
+                if let Some(code) = err.error_code {
                     if let Some(source_location) = &err.source_location {
                         // we ignore spdx and contract size warnings in test
                         // files. if we are looking at one of these warnings
                         // from a test file we skip
-                        if self.is_test(&source_location.file) && (*code == 1878 || *code == 5574) {
-                            return true
-                        }
+                        ignored =
+                            self.is_test(&source_location.file) && (code == 1878 || code == 5574);
                     }
 
-                    self.ignored_error_codes.contains(code)
-                });
-
-                if !is_ignored {
-                    writeln!(f, "\n{err}")?;
+                    ignored |= self.ignored_error_codes.contains(&code);
                 }
-            } else {
-                writeln!(f, "\n{err}")?;
+            }
+
+            if !ignored {
+                f.write_str("\n")?;
+                err.fmt(f)?;
             }
         }
+
         Ok(())
     }
 }
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 2010f5fb..1ea58ef0 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -361,7 +361,7 @@ impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> {
         let ArtifactsState { output, cache, compiled_artifacts } = self;
         let project = cache.project();
         let ignored_error_codes = project.ignored_error_codes.clone();
-        let compiler_severity_filter = project.compiler_severity_filter.clone();
+        let compiler_severity_filter = project.compiler_severity_filter;
         let has_error = output.has_error(&ignored_error_codes, &compiler_severity_filter);
         let skip_write_to_disk = project.no_artifacts || has_error;
         trace!(has_error, project.no_artifacts, skip_write_to_disk, cache_path=?project.cache_path(),"prepare writing cache file");
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index a1c1401e..e400e231 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -989,6 +989,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn can_find_library_ancestor() {
         let mut config = ProjectPathsConfig::builder().lib("lib").build().unwrap();
         config.root = "/root/".into();
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index b3eaee2a..bc403bb8 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -805,6 +805,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn find_remapping_dapptools() {
         let tmp_dir = tempdir("lib").unwrap();
         let tmp_dir_path = tmp_dir.path();
@@ -821,6 +822,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn can_resolve_contract_dir_combinations() {
         let tmp_dir = tempdir("demo").unwrap();
         let paths =
@@ -837,6 +839,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn can_resolve_geb_remappings() {
         let tmp_dir = tempdir("geb").unwrap();
         let paths = [
@@ -1050,6 +1053,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn recursive_remappings() {
         let tmp_dir = tempdir("lib").unwrap();
         let tmp_dir_path = tmp_dir.path();
@@ -1151,6 +1155,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn simple_dapptools_remappings() {
         let tmp_dir = tempdir("lib").unwrap();
         let tmp_dir_path = tmp_dir.path();
@@ -1189,6 +1194,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn hardhat_remappings() {
         let tmp_dir = tempdir("node_modules").unwrap();
         let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
@@ -1245,6 +1251,7 @@ mod tests {
     }
 
     #[test]
+    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
     fn find_openzeppelin_remapping() {
         let tmp_dir = tempdir("lib").unwrap();
         let tmp_dir_path = tmp_dir.path();


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 348e3790..7a1cd79b 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -766,6 +766,15 @@ fn can_generate_large_output_struct() {
     let _r = GetByIdReturn(Info::default());
 }
 
+#[test]
+fn can_generate_large_structs() {
+    abigen!(LargeStructs, "ethers-contract/tests/solidity-contracts/LargeStructs.json");
+
+    assert_struct::<PoolStorage>();
+    assert_struct::<AssetStorage>();
+    assert_struct::<ChainStorage>();
+}
+
 #[test]
 fn can_generate_complex_function() {
     abigen!(
diff --git a/ethers-contract/tests/it/eip712.rs b/ethers-contract/tests/it/eip712.rs
index d89d9b26..799f6de9 100644
--- a/ethers-contract/tests/it/eip712.rs
+++ b/ethers-contract/tests/it/eip712.rs
@@ -17,7 +17,8 @@ fn derive_eip712() {
         name = "Radicle",
         version = "1",
         chain_id = 1,
-        verifying_contract = "0x0000000000000000000000000000000000000001"
+        verifying_contract = "0x0000000000000000000000000000000000000001",
+        raw_salt = "0x3000000000000000000000000000000000000000000000000000000000000000"
     )]
     pub struct Puzzle {
         pub organization: H160,
diff --git a/ethers-contract/tests/solidity-contracts/LargeStructs.json b/ethers-contract/tests/solidity-contracts/LargeStructs.json
new file mode 100644
index 00000000..3e46d7d1
--- /dev/null
+++ b/ethers-contract/tests/solidity-contracts/LargeStructs.json
@@ -0,0 +1,1 @@
+[{"inputs":[],"name":"getChainStorage","outputs":[{"components":[{"components":[{"internalType":"uint32","name":"shortFundingBaseRate8H","type":"uint32"},{"internalType":"uint32","name":"shortFundingLimitRate8H","type":"uint32"},{"internalType":"uint32","name":"fundingInterval","type":"uint32"},{"internalType":"uint32","name":"liquidityBaseFeeRate","type":"uint32"},{"internalType":"uint32","name":"liquidityDynamicFeeRate","type":"uint32"},{"internalType":"uint96","name":"mlpPriceLowerBound","type":"uint96"},{"internalType":"uint96","name":"mlpPriceUpperBound","type":"uint96"},{"internalType":"uint32","name":"lastFundingTime","type":"uint32"},{"internalType":"uint32","name":"sequence","type":"uint32"},{"internalType":"uint32","name":"strictStableDeviation","type":"uint32"}],"internalType":"struct IReader.PoolStorage","name":"pool","type":"tuple"},{"components":[{"internalType":"bytes32","name":"symbol","type":"bytes32"},{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"address","name":"muxTokenAddress","type":"address"},{"internalType":"uint8","name":"id","type":"uint8"},{"internalType":"uint8","name":"decimals","type":"uint8"},{"internalType":"uint56","name":"flags","type":"uint56"},{"internalType":"uint32","name":"initialMarginRate","type":"uint32"},{"internalType":"uint32","name":"maintenanceMarginRate","type":"uint32"},{"internalType":"uint32","name":"positionFeeRate","type":"uint32"},{"internalType":"uint32","name":"liquidationFeeRate","type":"uint32"},{"internalType":"uint32","name":"minProfitRate","type":"uint32"},{"internalType":"uint32","name":"minProfitTime","type":"uint32"},{"internalType":"uint96","name":"maxLongPositionSize","type":"uint96"},{"internalType":"uint96","name":"maxShortPositionSize","type":"uint96"},{"internalType":"uint32","name":"spotWeight","type":"uint32"},{"internalType":"uint32","name":"longFundingBaseRate8H","type":"uint32"},{"internalType":"uint32","name":"longFundingLimitRate8H","type":"uint32"},{"internalType":"uint8","name":"referenceOracleType","type":"uint8"},{"internalType":"address","name":"referenceOracle","type":"address"},{"internalType":"uint32","name":"referenceDeviation","type":"uint32"},{"internalType":"uint32","name":"halfSpread","type":"uint32"},{"internalType":"uint128","name":"longCumulativeFundingRate","type":"uint128"},{"internalType":"uint128","name":"shortCumulativeFunding","type":"uint128"},{"internalType":"uint96","name":"spotLiquidity","type":"uint96"},{"internalType":"uint96","name":"credit","type":"uint96"},{"internalType":"uint96","name":"totalLongPosition","type":"uint96"},{"internalType":"uint96","name":"totalShortPosition","type":"uint96"},{"internalType":"uint96","name":"averageLongPrice","type":"uint96"},{"internalType":"uint96","name":"averageShortPrice","type":"uint96"},{"internalType":"uint128","name":"collectedFee","type":"uint128"},{"internalType":"uint256","name":"deduct","type":"uint256"}],"internalType":"struct IReader.AssetStorage[]","name":"assets","type":"tuple[]"},{"components":[{"internalType":"uint8","name":"dexId","type":"uint8"},{"internalType":"uint8","name":"dexType","type":"uint8"},{"internalType":"uint8[]","name":"assetIds","type":"uint8[]"},{"internalType":"uint32[]","name":"assetWeightInDEX","type":"uint32[]"},{"internalType":"uint256[]","name":"totalSpotInDEX","type":"uint256[]"},{"internalType":"uint32","name":"dexWeight","type":"uint32"},{"internalType":"uint256","name":"dexLPBalance","type":"uint256"},{"internalType":"uint256[]","name":"liquidityBalance","type":"uint256[]"}],"internalType":"struct IReader.DexStorage[]","name":"dexes","type":"tuple[]"},{"internalType":"uint32","name":"liquidityLockPeriod","type":"uint32"},{"internalType":"uint32","name":"marketOrderTimeout","type":"uint32"},{"internalType":"uint32","name":"maxLimitOrderTimeout","type":"uint32"},{"internalType":"uint256","name":"lpDeduct","type":"uint256"},{"internalType":"uint256","name":"stableDeduct","type":"uint256"},{"internalType":"bool","name":"isPositionOrderPaused","type":"bool"},{"internalType":"bool","name":"isLiquidityOrderPaused","type":"bool"}],"internalType":"struct IReader.ChainStorage","name":"chain","type":"tuple"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64[]","name":"orderIds","type":"uint64[]"}],"name":"getOrders","outputs":[{"internalType":"bytes32[3][]","name":"orders","type":"bytes32[3][]"},{"internalType":"bool[]","name":"isExist","type":"bool[]"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"subAccountIds","type":"bytes32[]"}],"name":"getSubAccounts","outputs":[{"components":[{"internalType":"uint96","name":"collateral","type":"uint96"},{"internalType":"uint96","name":"size","type":"uint96"},{"internalType":"uint32","name":"lastIncreasedTime","type":"uint32"},{"internalType":"uint96","name":"entryPrice","type":"uint96"},{"internalType":"uint128","name":"entryFunding","type":"uint128"}],"internalType":"struct IReader.SubAccountState[]","name":"subAccounts","type":"tuple[]"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"subAccountIds","type":"bytes32[]"},{"internalType":"uint64[]","name":"orderIds","type":"uint64[]"}],"name":"getSubAccountsAndOrders","outputs":[{"components":[{"internalType":"uint96","name":"collateral","type":"uint96"},{"internalType":"uint96","name":"size","type":"uint96"},{"internalType":"uint32","name":"lastIncreasedTime","type":"uint32"},{"internalType":"uint96","name":"entryPrice","type":"uint96"},{"internalType":"uint128","name":"entryFunding","type":"uint128"}],"internalType":"struct IReader.SubAccountState[]","name":"subAccounts","type":"tuple[]"},{"internalType":"bytes32[3][]","name":"orders","type":"bytes32[3][]"},{"internalType":"bool[]","name":"isOrderExist","type":"bool[]"}],"stateMutability":"pure","type":"function"}]