diff --git a/SUPPORTED_APIS.md b/SUPPORTED_APIS.md
index 7110dcdd..a76d569d 100644
--- a/SUPPORTED_APIS.md
+++ b/SUPPORTED_APIS.md
@@ -14,6 +14,8 @@ The `status` options are:
| Namespace | API |
Status
| Description |
| --- | --- | --- | --- |
+| `ANVIL` | `anvil_snapshot` | `SUPPORTED` | Snapshot the state of the blockchain at the current block |
+| `ANVIL` | `anvil_revert` | `SUPPORTED` | Revert the state of the blockchain to a previous snapshot |
| `ANVIL` | `anvil_setTime` | `SUPPORTED` | Sets the internal clock time to the given timestamp |
| `ANVIL` | `anvil_increaseTime` | `SUPPORTED` | Jump forward in time by the given amount of time, in seconds |
| `ANVIL` | `anvil_setNextBlockTimestamp` | `SUPPORTED` | Works like `anvil_increaseTime`, but takes the exact timestamp that you want in the next block, and increases the time accordingly |
diff --git a/e2e-tests/test/anvil-apis.test.ts b/e2e-tests/test/anvil-apis.test.ts
index 72d24148..648ff80b 100644
--- a/e2e-tests/test/anvil-apis.test.ts
+++ b/e2e-tests/test/anvil-apis.test.ts
@@ -10,6 +10,43 @@ import { BigNumber } from "ethers";
const provider = getTestProvider();
+describe("anvil_snapshot", function () {
+ it("Should return incrementing snapshot ids", async function () {
+ const wallet = new Wallet(RichAccounts[6].PrivateKey);
+ const deployer = new Deployer(hre, wallet);
+ const greeter = await deployContract(deployer, "Greeter", ["Hi"]);
+ expect(await greeter.greet()).to.eq("Hi");
+
+ // Act
+ const snapshotId1: string = await provider.send("anvil_snapshot", []);
+ const snapshotId2: string = await provider.send("anvil_snapshot", []);
+
+ // Assert
+ expect(await greeter.greet()).to.eq("Hi");
+ expect(BigNumber.from(snapshotId2).toString()).to.eq(BigNumber.from(snapshotId1).add(1).toString());
+ });
+});
+
+describe("anvil_revert", function () {
+ it("Should revert with correct snapshot id", async function () {
+ const wallet = new Wallet(RichAccounts[6].PrivateKey);
+ const deployer = new Deployer(hre, wallet);
+ const greeter = await deployContract(deployer, "Greeter", ["Hi"]);
+ expect(await greeter.greet()).to.eq("Hi");
+ const snapshotId = await provider.send("anvil_snapshot", []);
+ const setGreetingTx = await greeter.setGreeting("Hola, mundo!");
+ await setGreetingTx.wait();
+ expect(await greeter.greet()).to.equal("Hola, mundo!");
+
+ // Act
+ const reverted: boolean = await provider.send("anvil_revert", [snapshotId]);
+
+ // Assert
+ expect(await greeter.greet()).to.eq("Hi");
+ expect(reverted).to.be.true;
+ });
+});
+
describe("anvil_increaseTime", function () {
it("Should increase current timestamp of the node", async function () {
// Arrange
diff --git a/src/config/cli.rs b/src/config/cli.rs
index 7f7600d5..a314acf1 100644
--- a/src/config/cli.rs
+++ b/src/config/cli.rs
@@ -197,6 +197,15 @@ pub struct Cli {
/// [default: m/44'/60'/0'/0/]
#[arg(long, help_heading = "Account Configuration")]
pub derivation_path: Option,
+
+ /// Enables automatic impersonation on startup. This allows any transaction sender to be
+ /// simulated as different accounts, which is useful for testing contract behavior.
+ #[arg(
+ long,
+ visible_alias = "auto-unlock",
+ help_heading = "Account Configuration"
+ )]
+ pub auto_impersonate: bool,
}
#[derive(Debug, Subcommand, Clone)]
@@ -288,6 +297,7 @@ impl Cli {
.with_log_level(self.log)
.with_log_file_path(self.log_file_path.clone())
.with_account_generator(self.account_generator())
+ .with_auto_impersonate(self.auto_impersonate)
.with_genesis_balance(genesis_balance)
.with_cache_config(self.cache.map(|cache_type| {
match cache_type {
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 458a396f..3fcebecd 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -101,6 +101,8 @@ pub struct TestNodeConfig {
pub account_generator: Option,
/// Signer accounts that can sign messages/transactions
pub signer_accounts: Vec,
+ /// Enable auto impersonation of accounts on startup
+ pub enable_auto_impersonate: bool,
/// Whether the node operates in offline mode
pub offline: bool,
/// The host the server will listen on
@@ -147,6 +149,7 @@ impl Default for TestNodeConfig {
account_generator: None,
genesis_accounts: genesis_accounts.clone(),
signer_accounts: genesis_accounts,
+ enable_auto_impersonate: false,
// 100ETH default balance
genesis_balance: U256::from(100u128 * 10u128.pow(18)),
@@ -696,6 +699,13 @@ impl TestNodeConfig {
.with_genesis_accounts(accounts)
}
+ /// Sets whether to enable autoImpersonate
+ #[must_use]
+ pub fn with_auto_impersonate(mut self, enable_auto_impersonate: bool) -> Self {
+ self.enable_auto_impersonate = enable_auto_impersonate;
+ self
+ }
+
/// Set the offline mode
#[must_use]
pub fn with_offline(mut self, offline: Option) -> Self {
diff --git a/src/main.rs b/src/main.rs
index 7840545d..186ab7a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -124,10 +124,7 @@ async fn main() -> anyhow::Result<()> {
let fork_details = match command {
Command::Run => {
if config.offline {
- tracing::warn!(
- "Running in offline mode: default fee parameters will be used. \
- To override, specify values in `config.toml` and use the `--config` flag."
- );
+ tracing::warn!("Running in offline mode: default fee parameters will be used.");
None
} else {
// Initialize the client to get the fee params
diff --git a/src/namespaces/anvil.rs b/src/namespaces/anvil.rs
index 4cf69ee9..39c802a0 100644
--- a/src/namespaces/anvil.rs
+++ b/src/namespaces/anvil.rs
@@ -6,6 +6,29 @@ use crate::utils::Numeric;
#[rpc]
pub trait AnvilNamespaceT {
+ /// Snapshot the state of the blockchain at the current block. Takes no parameters. Returns the id of the snapshot
+ /// that was created. A snapshot can only be reverted once. After a successful `anvil_revert`, the same snapshot id cannot
+ /// be used again. Consider creating a new snapshot after each `anvil_revert` if you need to revert to the same
+ /// point multiple times.
+ ///
+ /// # Returns
+ /// The `U64` identifier for this snapshot.
+ #[rpc(name = "anvil_snapshot")]
+ fn snapshot(&self) -> RpcResult;
+
+ /// Revert the state of the blockchain to a previous snapshot. Takes a single parameter,
+ /// which is the snapshot id to revert to. This deletes the given snapshot, as well as any snapshots
+ /// taken after (e.g.: reverting to id 0x1 will delete snapshots with ids 0x1, 0x2, etc.)
+ ///
+ /// # Arguments
+ ///
+ /// * `id` - The snapshot id to revert
+ ///
+ /// # Returns
+ /// `true` if a snapshot was reverted, otherwise `false`.
+ #[rpc(name = "anvil_revert")]
+ fn revert(&self, id: U64) -> RpcResult;
+
/// Set the current timestamp for the node.
/// Warning: This will allow you to move backwards in time, which may cause new blocks to appear to be
/// mined before old blocks. This will result in an invalid state.
diff --git a/src/node/anvil.rs b/src/node/anvil.rs
index c8f473bb..a3251f5a 100644
--- a/src/node/anvil.rs
+++ b/src/node/anvil.rs
@@ -12,6 +12,24 @@ use crate::{
impl AnvilNamespaceT
for InMemoryNode
{
+ fn snapshot(&self) -> RpcResult {
+ self.snapshot()
+ .map_err(|err| {
+ tracing::error!("failed creating snapshot: {:?}", err);
+ into_jsrpc_error(Web3Error::InternalError(err))
+ })
+ .into_boxed_future()
+ }
+
+ fn revert(&self, id: U64) -> RpcResult {
+ self.revert_snapshot(id)
+ .map_err(|err| {
+ tracing::error!("failed reverting snapshot: {:?}", err);
+ into_jsrpc_error(Web3Error::InternalError(err))
+ })
+ .into_boxed_future()
+ }
+
fn set_time(&self, timestamp: Numeric) -> RpcResult {
self.set_time(timestamp)
.map_err(|err| {
diff --git a/src/node/in_memory.rs b/src/node/in_memory.rs
index 95132494..c97f8d44 100644
--- a/src/node/in_memory.rs
+++ b/src/node/in_memory.rs
@@ -263,6 +263,11 @@ impl InMemoryNodeInner {
f.estimate_gas_scale_factor,
)
};
+ let impersonation_manager = ImpersonationManager::default();
+ if config.enable_auto_impersonate {
+ // Enable auto impersonation if configured
+ impersonation_manager.set_auto_impersonation(true);
+ }
InMemoryNodeInner {
time: TimestampManager::new(f.block_timestamp),
@@ -286,7 +291,7 @@ impl InMemoryNodeInner {
&updated_config.system_contracts_options,
updated_config.use_evm_emulator,
),
- impersonation: Default::default(),
+ impersonation: impersonation_manager,
rich_accounts: HashSet::new(),
previous_states: Default::default(),
observability,
@@ -299,6 +304,12 @@ impl InMemoryNodeInner {
blocks.insert(block_hash, create_genesis(NON_FORK_FIRST_BLOCK_TIMESTAMP));
let fee_input_provider = TestNodeFeeInputProvider::default();
+ let impersonation_manager = ImpersonationManager::default();
+ if config.enable_auto_impersonate {
+ // Enable auto impersonation if configured
+ impersonation_manager.set_auto_impersonation(true);
+ }
+
InMemoryNodeInner {
time: TimestampManager::new(NON_FORK_FIRST_BLOCK_TIMESTAMP),
current_batch: 0,
@@ -321,7 +332,7 @@ impl InMemoryNodeInner {
&config.system_contracts_options,
config.use_evm_emulator,
),
- impersonation: Default::default(),
+ impersonation: impersonation_manager,
rich_accounts: HashSet::new(),
previous_states: Default::default(),
observability,