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,