diff --git a/package-lock.json b/package-lock.json index 59c8e033a2..10dffc87c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -80,7 +80,8 @@ "node_modules/@adraffy/ens-normalize": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==" + "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", + "dev": true }, "node_modules/@ampproject/remapping": { "version": "2.2.1", @@ -2057,6 +2058,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "dev": true, "funding": [ { "type": "individual", @@ -3752,7 +3754,8 @@ "node_modules/aes-js": { "version": "4.0.0-beta.5", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true }, "node_modules/agent-base": { "version": "6.0.2", @@ -7371,6 +7374,7 @@ "version": "6.6.5", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.6.5.tgz", "integrity": "sha512-Tc3HXzI0UJ9EhPp6E0fntkgMIA2//rhcB0UsmiRMCR+Bii5iu0RjtwJV55IhlLJ4K39pd0ku+eE4WRgqrLLW2Q==", + "dev": true, "funding": [ { "type": "individual", @@ -7398,6 +7402,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "dev": true, "funding": [ { "type": "individual", @@ -7408,7 +7413,8 @@ "node_modules/ethers/node_modules/@types/node": { "version": "18.15.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "dev": true }, "node_modules/events": { "version": "3.3.0", @@ -14214,7 +14220,8 @@ "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -16064,7 +16071,6 @@ "@ethereumjs/util": "^9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.2", - "ethers": "^6.4.0", "js-sdsl": "^4.1.4", "lru-cache": "^10.0.0" }, diff --git a/packages/statemanager/README.md b/packages/statemanager/README.md index ba422a68d8..38fdde128b 100644 --- a/packages/statemanager/README.md +++ b/packages/statemanager/README.md @@ -25,7 +25,7 @@ Note: this library was part of the [@ethereumjs/vm](../vm/) package up till VM ` The `StateManager` provides high-level access and manipulation methods to and for the Ethereum state, thinking in terms of accounts or contract code rather then the storage operations of the underlying data structure (e.g. a [Trie](../trie/)). -The library includes a TypeScript interface `StateManager` to ensure a unified interface (e.g. when passed to the VM) as well as a concrete Trie-based implementation `DefaultStateManager` as well as an `EthersStateManager` implementation that sources state and history data from an external `ethers` provider. +The library includes a TypeScript interface `StateManager` to ensure a unified interface (e.g. when passed to the VM) as well as a concrete Trie-based implementation `DefaultStateManager` as well as an `RPCStateManager` implementation that sources state and history data from an external JSON-RPC provider. It also includes a checkpoint/revert/commit mechanism to either persist or revert state changes and provides a sophisticated caching mechanism under the hood to reduce the need for direct state accesses. @@ -55,31 +55,45 @@ Caches now "survive" a flush operation and especially long-lived usage scenarios Have a loot at the extended `CacheOptions` on how to use and leverage the new cache system. -### `EthersStateManager` +### `RPCStateManager` First, a simple example of usage: ```typescript import { Account, Address } from '@ethereumjs/util' -import { EthersStateManager } from '@ethereumjs/statemanager' -import { ethers } from 'ethers' +import { RPCStateManager } from '@ethereumjs/statemanager' -const provider = new ethers.providers.JsonRpcProvider('https://path.to.my.provider.com') -const stateManager = new EthersStateManager({ provider, blockTag: 500000n }) +const provider = 'https://path.to.my.provider.com' +const stateManager = new RPCStateManager({ provider, blockTag: 500000n }) const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') const account = await stateManager.getAccount(vitalikDotEth) console.log('Vitalik has a current ETH balance of ', account.balance) ``` -The `EthersStateManager` can be be used with an `ethers` `JsonRpcProvider` or one of its subclasses. Instantiate the `VM` and pass in an `EthersStateManager` to run transactions against accounts sourced from the provider or to run blocks pulled from the provider at any specified block height. +The `RPCStateManager` can be be used with any JSON-RPC provider that supports the `eth` namespace. Instantiate the `VM` and pass in an `RPCStateManager` to run transactions against accounts sourced from the provider or to run blocks pulled from the provider at any specified block height. -**Note:** Usage of this StateManager can cause a heavy load regarding state request API calls, so be careful (or at least: aware) if used in combination with an Ethers provider connecting to a third-party API service like Infura! +**Note:** Usage of this StateManager can cause a heavy load regarding state request API calls, so be careful (or at least: aware) if used in combination with a JSON-RPC provider connecting to a third-party API service like Infura! ### Points on usage: +#### Instantiating the EVM + +In order to have an EVM instance that supports the BLOCKHASH opcode (which requires access to block history), you must instantiate both the `RPCStateManager` and the `RpcBlockChain` and use that when initalizing your EVM instance as below: + +```js +import { RPCStateManager, RPCBlockChain } from '../src/rpcStateManager.js' +import { EVM } from '@ethereumjs/evm' + +const blockchain = new RPCBlockChain({}, provider) +const blockTag = 1n +const state = new RPCStateManager({ provider, blockTag }) +const evm = new EVM({ blockchain, stateManager: state }) +``` + +Note: Failing to provide the `RPCBlockChain` instance when instantiating the EVM means that the `BLOCKHASH` opcode will fail to work correctly during EVM execution. + #### Provider selection -- If you don't have access to a provider, you can use the `CloudFlareProvider` from the `@ethersproject/providers` module to get a quickstart. - The provider you select must support the `eth_getProof`, `eth_getCode`, and `eth_getStorageAt` RPC methods. - Not all providers support retrieving state from all block heights so refer to your provider's documentation. Trying to use a block height not supported by your provider (e.g. any block older than the last 256 for CloudFlare) will result in RPC errors when using the state manager. @@ -92,12 +106,12 @@ The `EthersStateManager` can be be used with an `ethers` `JsonRpcProvider` or on #### Potential gotchas -- The Ethers State Manager cannot compute valid state roots when running blocks as it does not have access to the entire Ethereum state trie so can not compute correct state roots, either for the account trie or for storage tries. +- The RPC State Manager cannot compute valid state roots when running blocks as it does not have access to the entire Ethereum state trie so can not compute correct state roots, either for the account trie or for storage tries. - If you are replaying mainnet transactions and an account or account storage is touched by multiple transactions in a block, you must replay those transactions in order (with regard to their position in that block) or calculated gas will likely be different than actual gas consumed. #### Further reference -Refer to [this test script](./test/ethersStateManager.spec.ts) for complete examples of running transactions and blocks in the `vm` with data sourced from a provider. +Refer to [this test script](./test/rpcStateManager.spec.ts) for complete examples of running transactions and blocks in the `vm` with data sourced from a provider. ## Browser diff --git a/packages/statemanager/package.json b/packages/statemanager/package.json index 25ead1e932..881eca1d1b 100644 --- a/packages/statemanager/package.json +++ b/packages/statemanager/package.json @@ -54,7 +54,6 @@ "@ethereumjs/util": "^9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "^2.1.2", - "ethers": "^6.4.0", "js-sdsl": "^4.1.4", "lru-cache": "^10.0.0" }, diff --git a/packages/statemanager/src/index.ts b/packages/statemanager/src/index.ts index 5c20d4fabe..1e7a6ee9fb 100644 --- a/packages/statemanager/src/index.ts +++ b/packages/statemanager/src/index.ts @@ -1,3 +1,3 @@ export * from './cache/index.js' -export * from './ethersStateManager.js' +export * from './rpcStateManager.js' export * from './stateManager.js' diff --git a/packages/statemanager/src/ethersStateManager.ts b/packages/statemanager/src/rpcStateManager.ts similarity index 83% rename from packages/statemanager/src/ethersStateManager.ts rename to packages/statemanager/src/rpcStateManager.ts index ec7d8f2f89..976d01e3f6 100644 --- a/packages/statemanager/src/ethersStateManager.ts +++ b/packages/statemanager/src/rpcStateManager.ts @@ -1,25 +1,36 @@ import { Trie } from '@ethereumjs/trie' -import { Account, bigIntToHex, bytesToBigInt, bytesToHex, toBytes } from '@ethereumjs/util' +import { + Account, + bigIntToHex, + bytesToHex, + fetchFromProvider, + hexToBytes, + intToHex, + toBytes, +} from '@ethereumjs/util' import debugDefault from 'debug' import { keccak256 } from 'ethereum-cryptography/keccak.js' -import { ethers } from 'ethers' import { AccountCache, CacheType, OriginalStorageCache, StorageCache } from './cache/index.js' import type { Proof } from './index.js' -import type { AccountFields, EVMStateManagerInterface, StorageDump } from '@ethereumjs/common' -import type { StorageRange } from '@ethereumjs/common/src' +import type { + AccountFields, + EVMStateManagerInterface, + StorageDump, + StorageRange, +} from '@ethereumjs/common' import type { Address } from '@ethereumjs/util' import type { Debugger } from 'debug' const { debug: createDebugLogger } = debugDefault -export interface EthersStateManagerOpts { - provider: string | ethers.JsonRpcProvider +export interface RPCStateManagerOpts { + provider: string blockTag: bigint | 'earliest' } -export class EthersStateManager implements EVMStateManagerInterface { - protected _provider: ethers.JsonRpcProvider +export class RPCStateManager implements EVMStateManagerInterface { + protected _provider: string protected _contractCache: Map protected _storageCache: StorageCache protected _blockTag: string @@ -27,19 +38,17 @@ export class EthersStateManager implements EVMStateManagerInterface { originalStorageCache: OriginalStorageCache protected _debug: Debugger protected DEBUG: boolean - constructor(opts: EthersStateManagerOpts) { + constructor(opts: RPCStateManagerOpts) { // Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables // Additional window check is to prevent vite browser bundling (and potentially other) to break this.DEBUG = typeof window === 'undefined' ? process?.env?.DEBUG?.includes('ethjs') ?? false : false - this._debug = createDebugLogger('statemanager:ethersStateManager') - if (typeof opts.provider === 'string') { - this._provider = new ethers.JsonRpcProvider(opts.provider) - } else if (opts.provider instanceof ethers.JsonRpcProvider) { + this._debug = createDebugLogger('statemanager:rpcStateManager') + if (typeof opts.provider === 'string' && opts.provider.startsWith('http')) { this._provider = opts.provider } else { - throw new Error(`valid JsonRpcProvider or url required; got ${opts.provider}`) + throw new Error(`valid RPC provider url required; got ${opts.provider}`) } this._blockTag = opts.blockTag === 'earliest' ? opts.blockTag : bigIntToHex(opts.blockTag) @@ -54,10 +63,10 @@ export class EthersStateManager implements EVMStateManagerInterface { /** * Note that the returned statemanager will share the same JsonRpcProvider as the original * - * @returns EthersStateManager + * @returns RPCStateManager */ - shallowCopy(): EthersStateManager { - const newState = new EthersStateManager({ + shallowCopy(): RPCStateManager { + const newState = new RPCStateManager({ provider: this._provider, blockTag: BigInt(this._blockTag), }) @@ -103,7 +112,10 @@ export class EthersStateManager implements EVMStateManagerInterface { async getContractCode(address: Address): Promise { let codeBytes = this._contractCache.get(address.toString()) if (codeBytes !== undefined) return codeBytes - const code = await this._provider.getCode(address.toString(), this._blockTag) + const code = await fetchFromProvider(this._provider, { + method: 'eth_getCode', + params: [address.toString(), this._blockTag], + }) codeBytes = toBytes(code) this._contractCache.set(address.toString(), codeBytes) return codeBytes @@ -141,11 +153,10 @@ export class EthersStateManager implements EVMStateManagerInterface { } // Retrieve storage slot from provider if not found in cache - const storage = await this._provider.getStorage( - address.toString(), - bytesToBigInt(key), - this._blockTag - ) + const storage = await fetchFromProvider(this._provider, { + method: 'eth_getStorageAt', + params: [address.toString(), bytesToHex(key), this._blockTag], + }) value = toBytes(storage) await this.putContractStorage(address, key, value) @@ -206,11 +217,10 @@ export class EthersStateManager implements EVMStateManagerInterface { const localAccount = this._accountCache.get(address) if (localAccount !== undefined) return true // Get merkle proof for `address` from provider - const proof = await this._provider.send('eth_getProof', [ - address.toString(), - [], - this._blockTag, - ]) + const proof = await fetchFromProvider(this._provider, { + method: 'eth_getProof', + params: [address.toString(), [] as any, this._blockTag], + }) const proofBuf = proof.accountProof.map((proofNode: string) => toBytes(proofNode)) @@ -247,11 +257,10 @@ export class EthersStateManager implements EVMStateManagerInterface { */ async getAccountFromProvider(address: Address): Promise { if (this.DEBUG) this._debug(`retrieving account data from ${address.toString()} from provider`) - const accountData = await this._provider.send('eth_getProof', [ - address.toString(), - [], - this._blockTag, - ]) + const accountData = await fetchFromProvider(this._provider, { + method: 'eth_getProof', + params: [address.toString(), [] as any, this._blockTag], + }) const account = Account.fromAccountData({ balance: BigInt(accountData.balance), nonce: BigInt(accountData.nonce), @@ -334,11 +343,14 @@ export class EthersStateManager implements EVMStateManagerInterface { */ async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise { if (this.DEBUG) this._debug(`retrieving proof from provider for ${address.toString()}`) - const proof = await this._provider.send('eth_getProof', [ - address.toString(), - [storageSlots.map((slot) => bytesToHex(slot))], - this._blockTag, - ]) + const proof = await fetchFromProvider(this._provider, { + method: 'eth_getProof', + params: [ + address.toString(), + [storageSlots.map((slot) => bytesToHex(slot))], + this._blockTag, + ] as any, + }) return proof } @@ -383,19 +395,19 @@ export class EthersStateManager implements EVMStateManagerInterface { } /** - * @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface + * @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface */ getStateRoot = async () => { return new Uint8Array(32) } /** - * @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface + * @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface */ setStateRoot = async (_root: Uint8Array) => {} /** - * @deprecated This method is not used by the Ethers State Manager and is a stub required by the State Manager interface + * @deprecated This method is not used by the RPC State Manager and is a stub required by the State Manager interface */ hasStateRoot = () => { throw new Error('function not implemented') @@ -405,3 +417,24 @@ export class EthersStateManager implements EVMStateManagerInterface { return Promise.resolve() } } + +export class RPCBlockChain { + readonly provider: string + constructor(provider: string) { + if (provider === undefined || provider === '') throw new Error('provider URL is required') + this.provider = provider + } + async getBlock(blockId: number) { + const block = await fetchFromProvider(this.provider, { + method: 'eth_getBlockByNumber', + params: [intToHex(blockId), false], + }) + return { + hash: () => hexToBytes(block.hash), + } + } + + shallowCopy() { + return this + } +} diff --git a/packages/statemanager/test/ethersStateManager.spec.ts b/packages/statemanager/test/ethersStateManager.spec.ts deleted file mode 100644 index 90558c5d4b..0000000000 --- a/packages/statemanager/test/ethersStateManager.spec.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { Block } from '@ethereumjs/block' -import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { FeeMarketEIP1559Transaction, TransactionFactory } from '@ethereumjs/tx' -import { - Account, - Address, - bigIntToBytes, - bytesToHex, - bytesToUnprefixedHex, - equalsBytes, - hexToBytes, - setLengthLeft, - utf8ToBytes, -} from '@ethereumjs/util' -import { VM } from '@ethereumjs/vm' -import { ethers } from 'ethers' -import { assert, describe, expect, it } from 'vitest' - -import { EthersStateManager } from '../src/ethersStateManager.js' - -import * as blockData from './testdata/providerData/blocks/block0x7a120.json' -import { MockProvider } from './testdata/providerData/mockProvider.js' -import * as txData from './testdata/providerData/transactions/0xed1960aa7d0d7b567c946d94331dddb37a1c67f51f30bf51f256ea40db88cfb0.json' - -// Hack to detect if running in browser or not -const isBrowser = new Function('try {return this===window;}catch(e){ return false;}') - -// To run the tests with a live provider, set the PROVIDER environmental variable with a valid provider url -// from Infura/Alchemy or your favorite web3 provider when running the test. Below is an example command: -// `PROVIDER=https://mainnet.infura.io/v3/[mySuperS3cretproviderKey] npm run tape -- 'test/ethersStateManager.spec.ts' -describe('Ethers State Manager initialization tests', () => { - it('should work', () => { - const provider = new MockProvider() - let state = new EthersStateManager({ provider, blockTag: 1n }) - assert.ok( - state instanceof EthersStateManager, - 'was able to instantiate state manager with JsonRpcProvider subclass' - ) - assert.equal( - (state as any)._blockTag, - '0x1', - 'State manager starts with default block tag of 1' - ) - - state = new EthersStateManager({ provider, blockTag: 1n }) - assert.equal( - (state as any)._blockTag, - '0x1', - 'State Manager instantiated with predefined blocktag' - ) - - state = new EthersStateManager({ provider: 'https://google.com', blockTag: 1n }) - assert.ok( - state instanceof EthersStateManager, - 'was able to instantiate state manager with valid url' - ) - - const invalidProvider = new ethers.SocketProvider('mainnet') - assert.throws( - () => new EthersStateManager({ provider: invalidProvider as any, blockTag: 1n }), - undefined, - undefined, - 'cannot instantiate state manager with invalid provider' - ) - }) -}) - -describe('Ethers State Manager API tests', () => { - it('should work', async () => { - if (isBrowser() === true) { - // The `MockProvider` is not able to load JSON files dynamically in browser so skipped in browser tests - } else { - const provider = - process.env.PROVIDER !== undefined - ? new ethers.JsonRpcProvider(process.env.PROVIDER, 1) - : new MockProvider() - const state = new EthersStateManager({ provider, blockTag: 1n }) - const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') - const account = await state.getAccount(vitalikDotEth) - assert.ok(account!.nonce > 0n, 'Vitalik.eth returned a valid nonce') - - await state.putAccount(vitalikDotEth, account!) - - const retrievedVitalikAccount = Account.fromRlpSerializedAccount( - (state as any)._accountCache.get(vitalikDotEth)!.accountRLP - ) - - assert.ok(retrievedVitalikAccount.nonce > 0n, 'Vitalik.eth is stored in cache') - const doesThisAccountExist = await state.accountExists( - Address.fromString('0xccAfdD642118E5536024675e776d32413728DD07') - ) - assert.ok(!doesThisAccountExist, 'getAccount returns undefined for non-existent account') - - assert.ok(state.getAccount(vitalikDotEth) !== undefined, 'vitalik.eth does exist') - - const UNIerc20ContractAddress = Address.fromString( - '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' - ) - const UNIContractCode = await state.getContractCode(UNIerc20ContractAddress) - assert.ok(UNIContractCode.length > 0, 'was able to retrieve UNI contract code') - - await state.putContractCode(UNIerc20ContractAddress, UNIContractCode) - assert.ok( - typeof (state as any)._contractCache.get(UNIerc20ContractAddress.toString()) !== - 'undefined', - 'UNI ERC20 contract code was found in cache' - ) - - const storageSlot = await state.getContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(1n), 32) - ) - assert.ok(storageSlot.length > 0, 'was able to retrieve storage slot 1 for the UNI contract') - - await expect(async () => { - await state.getContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(1n), 31) - ) - }).rejects.toThrowError('Storage key must be 32 bytes long') - - await state.putContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(2n), 32), - utf8ToBytes('abcd') - ) - const slotValue = await state.getContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(2n), 32) - ) - assert.ok(equalsBytes(slotValue, utf8ToBytes('abcd')), 'should retrieve slot 2 value') - - const dumpedStorage = await state.dumpStorage(UNIerc20ContractAddress) - assert.deepEqual(dumpedStorage, { - [bytesToUnprefixedHex(setLengthLeft(bigIntToBytes(1n), 32))]: '0xabcd', - [bytesToUnprefixedHex(setLengthLeft(bigIntToBytes(2n), 32))]: bytesToHex( - utf8ToBytes('abcd') - ), - }) - - // Verify that provider is not called for cached data - ;(provider as any).getStorageAt = function () { - throw new Error('should not be called!') - } - - assert.doesNotThrow( - async () => - state.getContractStorage(UNIerc20ContractAddress, setLengthLeft(bigIntToBytes(2n), 32)), - 'should not call provider.getStorageAt' - ) - await state.checkpoint() - - await state.putContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(2n), 32), - new Uint8Array(0) - ) - - await state.modifyAccountFields(vitalikDotEth, { nonce: 39n }) - assert.equal( - (await state.getAccount(vitalikDotEth))?.nonce, - 39n, - 'modified account fields successfully' - ) - - // Verify that provider is not called - ;(state as any).getAccountFromProvider = function () { - throw new Error('should not have called this!') - } - assert.doesNotThrow( - async () => state.getAccount(vitalikDotEth), - 'does not call getAccountFromProvider' - ) - - try { - await state.getAccount(Address.fromString('0x9Cef824A8f4b3Dc6B7389933E52e47F010488Fc8')) - } catch (err) { - assert.ok(true, 'calls getAccountFromProvider for non-cached account') - } - - const deletedSlot = await state.getContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(2n), 32) - ) - - assert.equal(deletedSlot.length, 0, 'deleted slot from storage cache') - - await state.deleteAccount(vitalikDotEth) - assert.ok( - (await state.getAccount(vitalikDotEth)) === undefined, - 'account should not exist after being deleted' - ) - - await state.revert() - assert.ok( - (await state.getAccount(vitalikDotEth)) !== undefined, - 'account deleted since last checkpoint should exist after revert called' - ) - - const deletedSlotAfterRevert = await state.getContractStorage( - UNIerc20ContractAddress, - setLengthLeft(bigIntToBytes(2n), 32) - ) - - assert.equal( - deletedSlotAfterRevert.length, - 4, - 'slot deleted since last checkpoint should exist in storage cache after revert' - ) - - const cacheStorage = await state.dumpStorage(UNIerc20ContractAddress) - assert.equal( - 2, - Object.keys(cacheStorage).length, - 'should have 2 storage slots in cache before clear' - ) - await state.clearContractStorage(UNIerc20ContractAddress) - const clearedStorage = await state.dumpStorage(UNIerc20ContractAddress) - assert.deepEqual({}, clearedStorage, 'storage cache should be empty after clear') - - try { - await Block.fromJsonRpcProvider(provider, 'fakeBlockTag', {} as any) - assert.fail('should have thrown') - } catch (err: any) { - assert.ok( - err.message.includes('expected blockTag to be block hash, bigint, hex prefixed string'), - 'threw with correct error when invalid blockTag provided' - ) - } - - assert.equal( - (state as any)._contractCache.get(UNIerc20ContractAddress), - undefined, - 'should not have any code for contract after cache is reverted' - ) - - assert.equal((state as any)._blockTag, '0x1', 'blockTag defaults to 1') - state.setBlockTag(5n) - assert.equal((state as any)._blockTag, '0x5', 'blockTag set to 0x5') - state.setBlockTag('earliest') - assert.equal((state as any)._blockTag, 'earliest', 'blockTag set to earliest') - - await state.checkpoint() - } - }) -}) - -describe('runTx custom transaction test', () => { - it('should work', async () => { - if (isBrowser() === true) { - // The `MockProvider` is not able to load JSON files dynamically in browser so skipped in browser tests - } else { - const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) - const provider = - process.env.PROVIDER !== undefined - ? new ethers.JsonRpcProvider(process.env.PROVIDER, 1) - : new MockProvider() - const state = new EthersStateManager({ provider, blockTag: 1n }) - const vm = await VM.create({ common, stateManager: state }) // TODO fix the type DefaultStateManager back to StateManagerInterface in VM - - const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') - const privateKey = hexToBytes( - '0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109' - ) - const tx = FeeMarketEIP1559Transaction.fromTxData( - { to: vitalikDotEth, value: '0x100', gasLimit: 500000n, maxFeePerGas: 7 }, - { common } - ).sign(privateKey) - - const result = await vm.runTx({ - skipBalance: true, - skipNonce: true, - tx, - }) - - assert.equal(result.totalGasSpent, 21000n, 'sent some ETH to vitalik.eth') - } - }) -}) - -describe('runTx test: replay mainnet transactions', () => { - it('should work', async () => { - if (isBrowser() === true) { - // The `MockProvider` is not able to load JSON files dynamically in browser so skipped in browser tests - } else { - const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) - - const provider = - process.env.PROVIDER !== undefined - ? new ethers.JsonRpcProvider(process.env.PROVIDER) - : new MockProvider() - - const blockTag = 15496077n - common.setHardforkBy({ blockNumber: blockTag }) - const tx = await TransactionFactory.fromRPC(txData, { common }) - const state = new EthersStateManager({ - provider, - // Set the state manager to look at the state of the chain before the block has been executed - blockTag: blockTag - 1n, - }) - const vm = await VM.create({ common, stateManager: state }) - const res = await vm.runTx({ tx }) - assert.equal( - res.totalGasSpent, - 21000n, - 'calculated correct total gas spent for simple transfer' - ) - } - }) -}) - -describe('runBlock test', () => { - it('should work', async () => { - if (isBrowser() === true) { - // The `MockProvider` is not able to load JSON files dynamically in browser so skipped in browser tests - } else { - const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Chainstart }) - const provider = - process.env.PROVIDER !== undefined - ? new ethers.JsonRpcProvider(process.env.PROVIDER) - : new MockProvider() - const blockTag = 500000n - const state = new EthersStateManager({ - provider, - // Set the state manager to look at the state of the chain before the block has been executed - blockTag: blockTag - 1n, - }) - - // Set the common to HF, doesn't impact this specific blockTag, but will impact much recent - // blocks, also for post merge network, ttd should also be passed - common.setHardforkBy({ blockNumber: blockTag - 1n }) - - const vm = await VM.create({ common, stateManager: state }) - const block = Block.fromRPC(blockData, [], { common }) - try { - const res = await vm.runBlock({ - block, - generate: true, - skipHeaderValidation: true, - }) - assert.equal( - res.gasUsed, - block.header.gasUsed, - 'should compute correct cumulative gas for block' - ) - } catch (err: any) { - assert.fail(`should have successfully ran block; got error ${err.message}`) - } - } - }) -}) diff --git a/packages/statemanager/test/rpcStateManager.spec.ts b/packages/statemanager/test/rpcStateManager.spec.ts new file mode 100644 index 0000000000..5ef33aa4a2 --- /dev/null +++ b/packages/statemanager/test/rpcStateManager.spec.ts @@ -0,0 +1,340 @@ +import { Block } from '@ethereumjs/block' +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { EVM, type EVMRunCallOpts } from '@ethereumjs/evm' +import { FeeMarketEIP1559Transaction, TransactionFactory } from '@ethereumjs/tx' +import { + Account, + Address, + bigIntToBytes, + bytesToHex, + bytesToUnprefixedHex, + equalsBytes, + hexToBytes, + setLengthLeft, + utf8ToBytes, +} from '@ethereumjs/util' +import { VM } from '@ethereumjs/vm' +import { assert, describe, expect, it, vi } from 'vitest' + +import { RPCBlockChain, RPCStateManager } from '../src/rpcStateManager.js' + +import * as blockData from './testdata/providerData/blocks/block0x7a120.json' +import { getValues } from './testdata/providerData/mockProvider.js' +import * as txData from './testdata/providerData/transactions/0xed1960aa7d0d7b567c946d94331dddb37a1c67f51f30bf51f256ea40db88cfb0.json' + +const provider = process.env.PROVIDER ?? 'http://cheese' +// To run the tests with a live provider, set the PROVIDER environmental variable with a valid provider url +// from Infura/Alchemy or your favorite web3 provider when running the test. Below is an example command: +// `PROVIDER=https://mainnet.infura.io/v3/[mySuperS3cretproviderKey] npx vitest run test/rpcStateManager.spec.ts + +describe('RPC State Manager initialization tests', async () => { + vi.mock('@ethereumjs/util', async () => { + const util = (await vi.importActual('@ethereumjs/util')) as any + return { + ...util, + fetchFromProvider: vi.fn().mockImplementation(async (url, { method, params }: any) => { + const res = await getValues(method, 1, params) + return res.result + }), + } + }) + await import('@ethereumjs/util') + + it('should work', () => { + let state = new RPCStateManager({ provider, blockTag: 1n }) + assert.ok(state instanceof RPCStateManager, 'was able to instantiate state manager') + assert.equal( + (state as any)._blockTag, + '0x1', + 'State manager starts with default block tag of 1' + ) + + state = new RPCStateManager({ provider, blockTag: 1n }) + assert.equal( + (state as any)._blockTag, + '0x1', + 'State Manager instantiated with predefined blocktag' + ) + + state = new RPCStateManager({ provider: 'https://google.com', blockTag: 1n }) + assert.ok( + state instanceof RPCStateManager, + 'was able to instantiate state manager with valid url' + ) + + const invalidProvider = 'google.com' + assert.throws( + () => new RPCStateManager({ provider: invalidProvider as any, blockTag: 1n }), + undefined, + undefined, + 'cannot instantiate state manager with invalid provider' + ) + }) +}) + +describe('RPC State Manager API tests', () => { + it('should work', async () => { + const state = new RPCStateManager({ provider, blockTag: 1n }) + const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') + const account = await state.getAccount(vitalikDotEth) + + assert.ok(account!.nonce > 0n, 'Vitalik.eth returned a valid nonce') + + await state.putAccount(vitalikDotEth, account!) + + const retrievedVitalikAccount = Account.fromRlpSerializedAccount( + (state as any)._accountCache.get(vitalikDotEth)!.accountRLP + ) + + assert.ok(retrievedVitalikAccount.nonce > 0n, 'Vitalik.eth is stored in cache') + const doesThisAccountExist = await state.accountExists( + Address.fromString('0xccAfdD642118E5536024675e776d32413728DD07') + ) + assert.ok(!doesThisAccountExist, 'getAccount returns undefined for non-existent account') + + assert.ok(state.getAccount(vitalikDotEth) !== undefined, 'vitalik.eth does exist') + + const UNIerc20ContractAddress = Address.fromString('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984') + const UNIContractCode = await state.getContractCode(UNIerc20ContractAddress) + assert.ok(UNIContractCode.length > 0, 'was able to retrieve UNI contract code') + + await state.putContractCode(UNIerc20ContractAddress, UNIContractCode) + assert.ok( + typeof (state as any)._contractCache.get(UNIerc20ContractAddress.toString()) !== 'undefined', + 'UNI ERC20 contract code was found in cache' + ) + + const storageSlot = await state.getContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(1n), 32) + ) + assert.ok(storageSlot.length > 0, 'was able to retrieve storage slot 1 for the UNI contract') + + await expect(async () => { + await state.getContractStorage(UNIerc20ContractAddress, setLengthLeft(bigIntToBytes(1n), 31)) + }).rejects.toThrowError('Storage key must be 32 bytes long') + + await state.putContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(2n), 32), + utf8ToBytes('abcd') + ) + const slotValue = await state.getContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(2n), 32) + ) + assert.ok(equalsBytes(slotValue, utf8ToBytes('abcd')), 'should retrieve slot 2 value') + + const dumpedStorage = await state.dumpStorage(UNIerc20ContractAddress) + assert.deepEqual(dumpedStorage, { + [bytesToUnprefixedHex(setLengthLeft(bigIntToBytes(1n), 32))]: '0xabcd', + [bytesToUnprefixedHex(setLengthLeft(bigIntToBytes(2n), 32))]: bytesToHex(utf8ToBytes('abcd')), + }) + + const spy = vi.spyOn(state, 'getAccountFromProvider') + spy.mockImplementation(() => { + throw new Error('shouldnt call me') + }) + + await state.checkpoint() + + await state.putContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(2n), 32), + new Uint8Array(0) + ) + + await state.modifyAccountFields(vitalikDotEth, { nonce: 39n }) + assert.equal( + (await state.getAccount(vitalikDotEth))?.nonce, + 39n, + 'modified account fields successfully' + ) + + assert.doesNotThrow( + async () => state.getAccount(vitalikDotEth), + 'does not call getAccountFromProvider' + ) + + try { + await state.getAccount(Address.fromString('0x9Cef824A8f4b3Dc6B7389933E52e47F010488Fc8')) + } catch (err) { + assert.ok(true, 'calls getAccountFromProvider for non-cached account') + } + + const deletedSlot = await state.getContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(2n), 32) + ) + + assert.equal(deletedSlot.length, 0, 'deleted slot from storage cache') + + await state.deleteAccount(vitalikDotEth) + assert.ok( + (await state.getAccount(vitalikDotEth)) === undefined, + 'account should not exist after being deleted' + ) + + await state.revert() + assert.ok( + (await state.getAccount(vitalikDotEth)) !== undefined, + 'account deleted since last checkpoint should exist after revert called' + ) + + const deletedSlotAfterRevert = await state.getContractStorage( + UNIerc20ContractAddress, + setLengthLeft(bigIntToBytes(2n), 32) + ) + + assert.equal( + deletedSlotAfterRevert.length, + 4, + 'slot deleted since last checkpoint should exist in storage cache after revert' + ) + + const cacheStorage = await state.dumpStorage(UNIerc20ContractAddress) + assert.equal( + 2, + Object.keys(cacheStorage).length, + 'should have 2 storage slots in cache before clear' + ) + await state.clearContractStorage(UNIerc20ContractAddress) + const clearedStorage = await state.dumpStorage(UNIerc20ContractAddress) + assert.deepEqual({}, clearedStorage, 'storage cache should be empty after clear') + + try { + await Block.fromJsonRpcProvider(provider, 'fakeBlockTag', {} as any) + assert.fail('should have thrown') + } catch (err: any) { + assert.ok( + err.message.includes('expected blockTag to be block hash, bigint, hex prefixed string'), + 'threw with correct error when invalid blockTag provided' + ) + } + + assert.equal( + (state as any)._contractCache.get(UNIerc20ContractAddress), + undefined, + 'should not have any code for contract after cache is reverted' + ) + + assert.equal((state as any)._blockTag, '0x1', 'blockTag defaults to 1') + state.setBlockTag(5n) + assert.equal((state as any)._blockTag, '0x5', 'blockTag set to 0x5') + state.setBlockTag('earliest') + assert.equal((state as any)._blockTag, 'earliest', 'blockTag set to earliest') + + await state.checkpoint() + }) +}) + +describe('runTx custom transaction test', () => { + it('should work', async () => { + const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) + + const state = new RPCStateManager({ provider, blockTag: 1n }) + const vm = await VM.create({ common, stateManager: state }) // TODO fix the type DefaultStateManager back to StateManagerInterface in VM + + const vitalikDotEth = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') + const privateKey = hexToBytes( + '0xe331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109' + ) + const tx = FeeMarketEIP1559Transaction.fromTxData( + { to: vitalikDotEth, value: '0x100', gasLimit: 500000n, maxFeePerGas: 7 }, + { common } + ).sign(privateKey) + + const result = await vm.runTx({ + skipBalance: true, + skipNonce: true, + tx, + }) + + assert.equal(result.totalGasSpent, 21000n, 'sent some ETH to vitalik.eth') + }) +}) + +describe('runTx test: replay mainnet transactions', () => { + it('should work', async () => { + const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London }) + + const blockTag = 15496077n + common.setHardforkBy({ blockNumber: blockTag }) + const tx = await TransactionFactory.fromRPC(txData, { common }) + const state = new RPCStateManager({ + provider, + // Set the state manager to look at the state of the chain before the block has been executed + blockTag: blockTag - 1n, + }) + const vm = await VM.create({ common, stateManager: state }) + const res = await vm.runTx({ tx }) + assert.equal( + res.totalGasSpent, + 21000n, + 'calculated correct total gas spent for simple transfer' + ) + }) +}) + +describe('runBlock test', () => { + it('should work', async () => { + const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.Chainstart }) + + const blockTag = 500000n + const state = new RPCStateManager({ + provider, + // Set the state manager to look at the state of the chain before the block has been executed + blockTag: blockTag - 1n, + }) + + // Set the common to HF, doesn't impact this specific blockTag, but will impact much recent + // blocks, also for post merge network, ttd should also be passed + common.setHardforkBy({ blockNumber: blockTag - 1n }) + + const vm = await VM.create({ common, stateManager: state }) + const block = Block.fromRPC(blockData, [], { common }) + try { + const res = await vm.runBlock({ + block, + generate: true, + skipHeaderValidation: true, + }) + assert.equal( + res.gasUsed, + block.header.gasUsed, + 'should compute correct cumulative gas for block' + ) + } catch (err: any) { + assert.fail(`should have successfully ran block; got error ${err.message}`) + } + }) +}) + +describe('blockchain', () => + it('uses blockhash', async () => { + const blockchain = new RPCBlockChain(provider) + const blockTag = 1n + const state = new RPCStateManager({ provider, blockTag }) + const evm = new EVM({ blockchain, stateManager: state }) + // Bytecode for returning the blockhash of the block previous to `blockTag` + const code = '0x600143034060005260206000F3' + const contractAddress = new Address(hexToBytes('0x00000000000000000000000000000000000000ff')) + + const caller = Address.fromString('0xd8da6bf26964af9d7eed9e03e53415d37aa96045') + await evm.stateManager.setStateRoot( + hexToBytes('0xf8506f559699a58a4724df4fcf2ad4fd242d20324db541823f128f5974feb6c7') + ) + const block = await Block.fromJsonRpcProvider(provider, 500000n, { setHardfork: true }) + await evm.stateManager.putContractCode(contractAddress, hexToBytes(code)) + const runCallArgs: Partial = { + caller, + gasLimit: BigInt(0xffffffffff), + to: contractAddress, + block, + } + const res = await evm.runCall(runCallArgs) + assert.ok( + bytesToHex(res.execResult.returnValue), + '0xd5ba853bc7151fc044b9d273a57e3f9ed35e66e0248ab4a571445650cc4fcaa6' + ) + })) diff --git a/packages/statemanager/test/testdata/providerData/mockProvider.ts b/packages/statemanager/test/testdata/providerData/mockProvider.ts index 9c2eeb64ed..7683c9a6b9 100644 --- a/packages/statemanager/test/testdata/providerData/mockProvider.ts +++ b/packages/statemanager/test/testdata/providerData/mockProvider.ts @@ -1,122 +1,94 @@ -import { JsonRpcProvider, Network } from 'ethers' +import type { JsonBlock } from '@ethereumjs/block' -import type { FetchRequest, JsonRpcPayload, JsonRpcResult } from 'ethers' +export type SupportedMethods = + | 'eth_getProof' + | 'eth_getStorageAt' + | 'eth_getCode' + | 'eth_getBlockByNumber' + | 'eth_getTransactionByHash' -export class MockProvider extends JsonRpcProvider { - constructor() { - super('localhost:8545', Network.from('mainnet'), { staticNetwork: Network.from('mainnet') }) - } - - _getConnection(): FetchRequest { - const fakeConnection = { - url: 'localhost:8545', - } - return fakeConnection as FetchRequest - } - - _send = async (payload: JsonRpcPayload | JsonRpcPayload[]): Promise => { - let method - let params - let id - - if (Array.isArray(payload)) { - const results = [] - for (const el of payload) { - ;({ method, params, id } = el) - results.push(await this.getValues(method, id, params)) +export type JsonReturnType = { + eth_getProof: { id: number; result: any } + eth_getStorageAt: { id: number; result: any } + eth_getCode: { id: number; result: string } + eth_getBlockByNumber: { id: number; result: JsonBlock } + eth_getTransactionByHash: { id: number; result: any } +} +export const getValues = async ( + method: Method, + id: number, + params: any[] +): Promise => { + switch (method) { + case 'eth_getProof': + return { + id, + result: await getProofValues(params as any), } - return results - } else { - ;({ method, params, id } = payload) - return [await this.getValues(method, id, params)] - } - } - - private getValues = async (method: string, id: number, params: any): Promise => { - switch (method) { - case 'eth_getProof': - return { - id, - result: this.getProofValues(params as any), - } - - case 'eth_getBlockByNumber': - return { - id, - result: this.getBlockValues(params as any), - } - case 'eth_chainId': // Always pretends to be mainnet - return { - id, - result: 1, - } - case 'eth_getTransactionByHash': - return { - id, - result: this.getTransactionData(params as any), - } - - case 'eth_getCode': { - let code = '0x' - if ((params as any[])[0] !== '0xd8da6bf26964af9d7eed9e03e53415d37aa96045') { - code = '0xab' - } - return { - id, - result: code, - } + case 'eth_getBlockByNumber': + return { + id, + result: await getBlockValues(params as any), } - case 'eth_getStorageAt': - return { - id, - result: '0xabcd', - } - default: - return { - id, - result: { - error: 'method not implemented', - }, - } - } - } + case 'eth_getTransactionByHash': + return { + id, + result: await getTransactionData(params as any), + } - private getProofValues = async (params: [address: string, _: [], blockTag: bigint | string]) => { - const [address, _slot, blockTag] = params - try { - const account = (await import(`./accounts/${address}.json`)).default - return account[blockTag.toString() ?? 'latest'] - } catch { + case 'eth_getCode': { + let code = '0x' + if (params[0] !== '0xd8da6bf26964af9d7eed9e03e53415d37aa96045') { + code = '0xab' + } return { - '0x1': { - address, - balance: '0x0', - codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470', - nonce: '0x0', - storageHash: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', - storageProof: [], - }, + id, + result: code, } } - } - - private getBlockValues = async (params: [blockTag: string, _: boolean]) => { - const [blockTag, _] = params - - if (blockTag.slice(0, 2) !== '0x') + case 'eth_getStorageAt': return { - number: 'latest', - stateRoot: '0x2ffb7ec5bbe8616c24a222737f0817f389d00ab9268f9574e0b7dfe251fbfa05', + id, + result: '0xabcd', } - const block = await import(`./blocks/block${blockTag.toString()}.json`) - return block + + default: + throw new Error(`${method} not supported in tests`) } +} - private getTransactionData = async (params: [txHash: string]) => { - const [txHash] = params - const txData = await import(`./transactions/${txHash}.json`) - return txData +const getProofValues = async (params: [address: string, _: [], blockTag: bigint | string]) => { + const [address, _slot, blockTag] = params + try { + const account = (await import(`./accounts/${address}.json`)).default + return account[blockTag.toString() ?? 'latest'] + } catch { + return { + address, + balance: '0x0', + codeHash: '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470', + nonce: '0x0', + storageHash: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', + storageProof: [], + } } } + +const getBlockValues = async (params: [blockTag: string, _: boolean]) => { + const [blockTag, _] = params + if (blockTag.slice(0, 2) !== '0x') + return { + number: 'latest', + stateRoot: '0x2ffb7ec5bbe8616c24a222737f0817f389d00ab9268f9574e0b7dfe251fbfa05', + } + const block = await import(`./blocks/block${blockTag}.json`) + return block.default +} + +const getTransactionData = async (params: [txHash: string]) => { + const [txHash] = params + const txData = await import(`./transactions/${txHash}.json`) + return txData +} diff --git a/packages/statemanager/vitest.config.browser.ts b/packages/statemanager/vitest.config.browser.ts index 041eb87d66..d5531f5613 100644 --- a/packages/statemanager/vitest.config.browser.ts +++ b/packages/statemanager/vitest.config.browser.ts @@ -1,11 +1,14 @@ +import topLevelAwait from 'vite-plugin-top-level-await' +import wasm from 'vite-plugin-wasm' import { configDefaults, defineConfig } from 'vitest/config' export default defineConfig({ + plugins: [wasm(), topLevelAwait()], test: { exclude: [ ...configDefaults.exclude, // Importing a module script failed. - 'test/ethersStateManager.spec.ts', + 'test/rpcStateManager.spec.ts', // undefined is not an object (evaluating 'state.reading') 'test/stateManager.storage.spec.ts', ],