diff --git a/.changeset/fair-ghosts-rule.md b/.changeset/fair-ghosts-rule.md new file mode 100644 index 0000000000..56d937615a --- /dev/null +++ b/.changeset/fair-ghosts-rule.md @@ -0,0 +1,5 @@ +--- +"@tevm/test-utils": minor +--- + +Added a new BlockReader contract. BlockReader is a contract that can be used to test reading blocks from the evm. Used internally to test block overrides diff --git a/.changeset/grumpy-islands-sleep.md b/.changeset/grumpy-islands-sleep.md new file mode 100644 index 0000000000..7f219767c1 --- /dev/null +++ b/.changeset/grumpy-islands-sleep.md @@ -0,0 +1,5 @@ +--- +"@tevm/actions": patch +--- + +Fixed bug where loadState would not validate params correctly and then fail with a confusing error if state was wrong diff --git a/.changeset/mean-ghosts-wave.md b/.changeset/mean-ghosts-wave.md new file mode 100644 index 0000000000..e829db9526 --- /dev/null +++ b/.changeset/mean-ghosts-wave.md @@ -0,0 +1,6 @@ +--- +"@tevm/actions": patch +"@tevm/vm": patch +--- + +Fixed bug with block override set missing a state root diff --git a/.changeset/tiny-dolls-enjoy.md b/.changeset/tiny-dolls-enjoy.md new file mode 100644 index 0000000000..eaace2f1fd --- /dev/null +++ b/.changeset/tiny-dolls-enjoy.md @@ -0,0 +1,5 @@ +--- +"@tevm/actions": patch +--- + +Fixed bug in tevm_call json-rpc procedure where deployedBytecode, createTrace and createAccessList were not forwarded to the underlying handler. This bug only affected users using JSON-RPC directly diff --git a/.changeset/wild-bats-complain.md b/.changeset/wild-bats-complain.md new file mode 100644 index 0000000000..d825f6d010 --- /dev/null +++ b/.changeset/wild-bats-complain.md @@ -0,0 +1,5 @@ +--- +"@tevm/actions": patch +--- + +Fixed bug where client status would stay mining if an error gets thrown while emitting events after mining diff --git a/packages/actions/src/Call/callHandler.js b/packages/actions/src/Call/callHandler.js index 178ab1f0ee..d4912046fa 100644 --- a/packages/actions/src/Call/callHandler.js +++ b/packages/actions/src/Call/callHandler.js @@ -107,7 +107,6 @@ export const callHandler = const block = /** @type {import('@tevm/block').Block}*/ (evmInput.block) await handlePendingTransactionsWarning(client, params, code, deployedBytecode) - /** * ************ * 1 CLONE THE VM WITH BLOCK TAG diff --git a/packages/actions/src/Call/callHandlerOpts.js b/packages/actions/src/Call/callHandlerOpts.js index ccf5778a09..93ece765b5 100644 --- a/packages/actions/src/Call/callHandlerOpts.js +++ b/packages/actions/src/Call/callHandlerOpts.js @@ -67,6 +67,8 @@ export const callHandlerOpts = async (client, params) => { opts.block = { ...opts.block, header: { + // this isn't in the type but it needs to be here or else block overrides will fail + ...{ stateRoot: block.header.stateRoot }, coinbase: params.blockOverrideSet.coinbase !== undefined ? createAddress(params.blockOverrideSet.coinbase) diff --git a/packages/actions/src/Call/callHandlerResult.js b/packages/actions/src/Call/callHandlerResult.js index 08c65c8bac..c140973b22 100644 --- a/packages/actions/src/Call/callHandlerResult.js +++ b/packages/actions/src/Call/callHandlerResult.js @@ -96,6 +96,5 @@ export const callHandlerResult = (evmResult, txHash, trace, accessList) => { if (evmResult.createdAddress) { out.createdAddress = getAddress(evmResult.createdAddress.toString()) } - return out } diff --git a/packages/actions/src/Call/callProcedure.js b/packages/actions/src/Call/callProcedure.js index 8c7cb7e4e1..913f86a4e8 100644 --- a/packages/actions/src/Call/callProcedure.js +++ b/packages/actions/src/Call/callProcedure.js @@ -39,9 +39,12 @@ export const callProcedure = (client) => async (request) => { } : {}), ...(request.params[0].code ? { code: request.params[0].code } : {}), + ...(request.params[0].data ? { data: request.params[0].data } : {}), + ...(request.params[0].deployedBytecode ? { deployedBytecode: request.params[0].deployedBytecode } : {}), + ...(request.params[0].createTrace ? { createTrace: request.params[0].createTrace } : {}), + ...(request.params[0].createAccessList ? { createAccessList: request.params[0].createAccessList } : {}), ...(request.params[0].blobVersionedHashes ? { blobVersionedHashes: request.params[0].blobVersionedHashes } : {}), ...(request.params[0].caller ? { caller: request.params[0].caller } : {}), - ...(request.params[0].data ? { data: request.params[0].data } : {}), ...(request.params[0].depth ? { depth: request.params[0].depth } : {}), ...(request.params[0].gasPrice ? { gasPrice: hexToBigInt(request.params[0].gasPrice) } : {}), ...(request.params[0].gas ? { gas: hexToBigInt(request.params[0].gas) } : {}), @@ -59,8 +62,6 @@ export const callProcedure = (client) => async (request) => { ...(request.params[0].maxPriorityFeePerGas ? { maxPriorityFeePerGas: hexToBigInt(request.params[0].maxPriorityFeePerGas) } : {}), - // TODO add support for manually setting nonce - // ...(request.params[0].nonce ? { nonce: hexToBigInt(request.params[0].nonce) } : {}), }) if (errors.length > 0) { const error = /** @type {import('./TevmCallError.js').TevmCallError}*/ (errors[0]) diff --git a/packages/actions/src/Call/callProcedure.spec.ts b/packages/actions/src/Call/callProcedure.spec.ts index 3737e23a7a..ed092023d6 100644 --- a/packages/actions/src/Call/callProcedure.spec.ts +++ b/packages/actions/src/Call/callProcedure.spec.ts @@ -1,7 +1,11 @@ +import { createAddress } from '@tevm/address' import { ERC20 } from '@tevm/contract' import { type TevmNode, createTevmNode } from '@tevm/node' -import { encodeFunctionData, numberToHex, parseEther } from '@tevm/utils' +import { BlockReader, SimpleContract } from '@tevm/test-utils' +import { type Address, type Hex, decodeFunctionResult, encodeFunctionData, numberToHex, parseEther } from '@tevm/utils' import { beforeEach, describe, expect, it } from 'vitest' +import { deployHandler } from '../Deploy/deployHandler.js' +import { mineHandler } from '../Mine/mineHandler.js' import { setAccountHandler } from '../SetAccount/setAccountHandler.js' import type { CallJsonRpcRequest } from './CallJsonRpcRequest.js' import { callProcedure } from './callProcedure.js' @@ -77,7 +81,102 @@ describe('callProcedure', () => { expect(response.result).toMatchSnapshot() }) - it.todo('should handle a call with block override', async () => {}) + it('should handle a call with block override', async () => { + const blockReaderAddress = createAddress(1234) + await setAccountHandler(client)({ + address: blockReaderAddress.toString(), + deployedBytecode: BlockReader.deployedBytecode, + }) + + const request: CallJsonRpcRequest = { + jsonrpc: '2.0', + method: 'tevm_call', + id: 1, + params: [ + { + to: blockReaderAddress.toString(), + data: encodeFunctionData(BlockReader.read.getBlockInfo()), + }, + {}, // No state override + { + number: numberToHex(1000n), + time: numberToHex(1234567890n), + coinbase: '0x1000000000000000000000000000000000000000', + baseFee: numberToHex(1n), + }, + ], + } + + const response = await callProcedure(client)(request) + expect(response.error).toBeUndefined() + expect(response.result).toBeDefined() + expect(response.method).toBe('tevm_call') + expect(response.id).toBe(request.id as any) + + const decodedResult = decodeFunctionResult({ + abi: BlockReader.read.getBlockInfo.abi, + data: response.result?.rawData as Hex, + functionName: 'getBlockInfo', + }) + expect(decodedResult).toEqual([1000n, 1234567890n, '0x1000000000000000000000000000000000000000', 1n]) + }) + + it('should handle a call with tracing enabled', async () => { + const { createdAddress } = await deployHandler(client)(SimpleContract.deploy(0n)) + await mineHandler(client)() + + const request: CallJsonRpcRequest = { + jsonrpc: '2.0', + method: 'tevm_call', + id: 1, + params: [ + { + to: createdAddress as Address, + data: encodeFunctionData(SimpleContract.write.set(100n)), + createTransaction: true, + createTrace: true, + createAccessList: true, + }, + ], + } + + const response = await callProcedure(client)(request) + expect(response.error).toBeUndefined() + expect(response.result).toBeDefined() + expect(response.method).toBe('tevm_call') + expect(response.result?.logs).toMatchInlineSnapshot(` + [ + { + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "data": "0x0000000000000000000000000000000000000000000000000000000000000064", + "topics": [ + "0x012c78e2b84325878b1bd9d250d772cfe5bda7722d795f45036fa5e1e6e303fc", + ], + }, + ] + `) + expect(response.id).toBe(request.id as any) + expect(response.result?.trace).toBeDefined() + expect(response.result?.trace?.structLogs).toBeInstanceOf(Array) + expect(response.result?.trace?.structLogs?.length).toBeGreaterThan(0) + expect(response.result?.trace?.structLogs[0]).toMatchInlineSnapshot(` + { + "depth": 0, + "gas": "0x1c970ac", + "gasCost": "0x6", + "op": "PUSH1", + "pc": 0, + "stack": [], + } + `) + expect(response.result?.accessList).toMatchInlineSnapshot(` + { + "0x5fbdb2315678afecb367f032d93f642f64180aa3": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + ], + } + `) + }) it('should handle errors from callHandler', async () => { const request: CallJsonRpcRequest = { diff --git a/packages/actions/src/Call/handleEvmError.js b/packages/actions/src/Call/handleEvmError.js index 8a1e74ac04..9f22f74e2e 100644 --- a/packages/actions/src/Call/handleEvmError.js +++ b/packages/actions/src/Call/handleEvmError.js @@ -125,9 +125,6 @@ export const handleRunTxError = (e) => { if (e.message.includes("sender doesn't have enough funds to send tx.")) { return new InsufficientBalanceError(e.message, { cause: /** @type {any}*/ (e) }) } - if (e.message.includes("sender doesn't have enough funds to send tx. The upfront cost is")) { - return new InsufficientBalanceError(e.message, { cause: /** @type {any}*/ (e) }) - } return new InternalEvmError(e.message, { cause: /** @type {any}*/ (e) }) } if (!(e instanceof EvmError)) { diff --git a/packages/actions/src/Call/handleEvmError.spec.ts b/packages/actions/src/Call/handleEvmError.spec.ts index d0ce6c5caf..787a7f4df5 100644 --- a/packages/actions/src/Call/handleEvmError.spec.ts +++ b/packages/actions/src/Call/handleEvmError.spec.ts @@ -122,6 +122,21 @@ describe('handleRunTxError', () => { }) }) + it('should handle insufficient balance error with upfront cost', () => { + const errorMessage = "sender doesn't have enough funds to send tx. The upfront cost is 1000 wei" + const error = new Error(errorMessage) + const result = handleRunTxError(error) + expect(result).toBeInstanceOf(InsufficientBalanceError) + expect(result.cause).toBe(error) + expect(result.message).toMatchInlineSnapshot(` + "sender doesn't have enough funds to send tx. The upfront cost is 1000 wei + + Docs: https://tevm.sh/reference/tevm/errors/classes/insufficientbalanceerror/ + Details: sender doesn't have enough funds to send tx. The upfront cost is 1000 wei + Version: 1.1.0.next-73" + `) + }) + it('should handle unknown EvmError subclasses', () => { class UnknownEvmError extends EvmError { constructor(message: string) { diff --git a/packages/actions/src/Contract/contractHandler.spec.ts b/packages/actions/src/Contract/contractHandler.spec.ts index 90fd509801..ff29389615 100644 --- a/packages/actions/src/Contract/contractHandler.spec.ts +++ b/packages/actions/src/Contract/contractHandler.spec.ts @@ -363,4 +363,40 @@ describe('contractHandler', () => { expect(result.l1BlobFee).toBeGreaterThan(0n) expect(result.l1GasUsed).toBeGreaterThan(0n) }) + + it('should handle contract revert errors', async () => { + const client = createTevmNode() + // deploy contract + expect( + ( + await setAccountHandler(client)({ + address: ERC20_ADDRESS, + deployedBytecode: ERC20_BYTECODE, + }) + ).errors, + ).toBeUndefined() + + const from = `0x${'11'.repeat(20)}` as const + const to = `0x${'22'.repeat(20)}` as const + const amount = 1000n + + const result = await contractHandler(client)({ + abi: ERC20_ABI, + functionName: 'transferFrom', + args: [from, to, amount], + to: ERC20_ADDRESS, + throwOnFail: false, + }) + + expect(result.errors).toBeDefined() + expect(result.errors?.length).toBe(1) + expect(result.errors?.[0]?.name).toBe('RevertError') + expect(result.errors?.[0]).toMatchInlineSnapshot(` + [RevertError: revert + + Docs: https://tevm.sh/reference/tevm/errors/classes/reverterror/ + Details: {"error":"revert","errorType":"EvmError"} + Version: 1.1.0.next-73] + `) + }) }) diff --git a/packages/actions/src/Deploy/deployHandler.spec.ts b/packages/actions/src/Deploy/deployHandler.spec.ts index 77c1edcaf0..88da00ae38 100644 --- a/packages/actions/src/Deploy/deployHandler.spec.ts +++ b/packages/actions/src/Deploy/deployHandler.spec.ts @@ -1,3 +1,4 @@ +import { InvalidRequestError } from '@tevm/errors' import { createTevmNode } from '@tevm/node' import { EthjsAddress, type Hex, bytesToHex, parseAbi } from '@tevm/utils' import { describe, expect, it } from 'vitest' @@ -61,4 +62,27 @@ describe('deployHandler', () => { ).data, ).toBe(initialValue) }) + + it('should throw an InvalidRequestError when constructor args are incorrect', async () => { + const client = createTevmNode({ loggingLevel: 'warn' }) + const deploy = deployHandler(client) + + // Attempt to deploy with incorrect argument type (string instead of uint256) + await expect( + deploy({ + abi: simpleConstructorAbi, + bytecode: simpleConstructorBytecode, + args: ['not a number'], + }), + ).rejects.toThrow(InvalidRequestError) + + // Attempt to deploy with incorrect number of arguments + await expect( + deploy({ + abi: simpleConstructorAbi, + bytecode: simpleConstructorBytecode, + args: [420n, 'extra arg'], + }), + ).rejects.toThrow(InvalidRequestError) + }) }) diff --git a/packages/actions/src/DumpState/dumpStateHandler.js b/packages/actions/src/DumpState/dumpStateHandler.js index fe88210265..ce84b73445 100644 --- a/packages/actions/src/DumpState/dumpStateHandler.js +++ b/packages/actions/src/DumpState/dumpStateHandler.js @@ -1,4 +1,4 @@ -import { InternalError } from '@tevm/errors' +import { BaseError, InternalError } from '@tevm/errors' import { bytesToHex } from '@tevm/utils' import { getPendingClient } from '../internal/getPendingClient.js' import { maybeThrowOnFail } from '../internal/maybeThrowOnFail.js' @@ -56,6 +56,13 @@ export const dumpStateHandler = 'Unsupported state manager. Must use a TEVM state manager from `@tevm/state` package. This may indicate a bug in TEVM internal code.', ) } catch (e) { + if (/** @type {BaseError}*/ (e)._tag) { + return maybeThrowOnFail(throwOnFail ?? true, { + state: {}, + // TODO we need to strongly type errors better here + errors: [/**@type {any} */ (e)], + }) + } return maybeThrowOnFail(throwOnFail ?? true, { state: {}, errors: [e instanceof InternalError ? e : new InternalError('Unexpected error', { cause: e })], diff --git a/packages/actions/src/DumpState/dumpStateHandler.spec.ts b/packages/actions/src/DumpState/dumpStateHandler.spec.ts index 1dfb675db1..00e1f2cfc5 100644 --- a/packages/actions/src/DumpState/dumpStateHandler.spec.ts +++ b/packages/actions/src/DumpState/dumpStateHandler.spec.ts @@ -39,3 +39,18 @@ test('should dump important account info and storage', async () => { expect(accountStorage).toEqual(storageValue) }) + +test('should handle block not found', async () => { + const client = createTevmNode() + const { errors } = await dumpStateHandler(client)({ blockTag: 1n, throwOnFail: false }) + expect(errors).toBeDefined() + expect(errors).toHaveLength(1) + expect(errors).toMatchInlineSnapshot(` + [ + [UnknownBlock: Block number 1 does not exist + + Docs: https://tevm.sh/reference/tevm/errors/classes/unknownblockerror/ + Version: 1.1.0.next-73], + ] + `) +}) diff --git a/packages/actions/src/LoadState/loadStateHandler.spec.ts b/packages/actions/src/LoadState/loadStateHandler.spec.ts index ea05ad28dc..f9d3d71b4c 100644 --- a/packages/actions/src/LoadState/loadStateHandler.spec.ts +++ b/packages/actions/src/LoadState/loadStateHandler.spec.ts @@ -1,7 +1,8 @@ +import { createTevmNode } from '@tevm/node' import { createStateManager } from '@tevm/state' import { EthjsAddress } from '@tevm/utils' import { bytesToHex, hexToBytes } from '@tevm/utils' -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' import { dumpStateHandler } from '../DumpState/dumpStateHandler.js' import { loadStateHandler } from './loadStateHandler.js' @@ -36,7 +37,7 @@ test('should load state into the state manager', async () => { const client = { getVm: () => ({ stateManager }) } as any - await loadStateHandler(client)({ state }) + await loadStateHandler(client)({ state, throwOnFail: false }) accountData = await stateManager.getAccount(address) @@ -62,3 +63,68 @@ test('should load state into the state manager', async () => { }, }) }) + +describe('loadStateHandler', () => { + test('should return errors for invalid params', async () => { + const client = createTevmNode() + const handler = loadStateHandler(client) + + const result = await handler({ + state: 5 as any, + throwOnFail: false, + } as any) + + expect(result.errors).toBeDefined() + expect(result.errors?.length).toBeGreaterThan(0) + expect(result.errors?.[0]?.message).toMatchInlineSnapshot(` + "Invalid state: Expected object, received number + + Docs: https://tevm.sh/reference/tevm/errors/classes/invalidrequesterror/ + Version: 1.1.0.next-73" + `) + }) + + test('should throw error for unsupported state manager', async () => { + const client = createTevmNode() + const vm = await client.getVm() + // @ts-ignore - Intentionally removing the method for testing + delete vm.stateManager.generateCanonicalGenesis + + const handler = loadStateHandler({ getVm: () => Promise.resolve(vm) } as any) + + const result = await handler({ state: {}, throwOnFail: false }) + expect(result.errors?.[0]?.message).toMatchInlineSnapshot(` + "UnexpectedError + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Details: Unsupported state manager. Must use a Tevm state manager from \`@tevm/state\` package. This may indicate a bug in tevm internal code. + Version: 1.1.0.next-73" + `) + }) + + test('should handle error when generating genesis fails', async () => { + const client = createTevmNode() + const vm = await client.getVm() + vm.stateManager.generateCanonicalGenesis = () => { + throw new Error('Genesis generation failed') + } + + const handler = loadStateHandler({ getVm: () => Promise.resolve(vm) } as any) + + const result = await handler({ + state: {}, + throwOnFail: false, + }) + + expect(result.errors).toBeDefined() + expect(result.errors?.length).toBe(1) + expect(result.errors?.[0]?.message).toMatchInlineSnapshot(` + "UnexpectedError + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Details: Genesis generation failed + Version: 1.1.0.next-73" + `) + expect(result.errors?.[0]?.cause?.message).toMatchInlineSnapshot(`"Genesis generation failed"`) + }) +}) diff --git a/packages/actions/src/LoadState/validateLoadStateParams.js b/packages/actions/src/LoadState/validateLoadStateParams.js index f2fe3020bd..c6975c976e 100644 --- a/packages/actions/src/LoadState/validateLoadStateParams.js +++ b/packages/actions/src/LoadState/validateLoadStateParams.js @@ -39,6 +39,12 @@ export const validateLoadStateParams = (action) => { errors.push(new InvalidRequestError(error)) }) } + + if (formattedErrors.state?._errors) { + formattedErrors.state._errors.forEach((error) => { + errors.push(new InvalidRequestError(`Invalid state: ${error}`)) + }) + } } return errors } diff --git a/packages/actions/src/Mine/mineHandler.js b/packages/actions/src/Mine/mineHandler.js index ed9882bca8..4582af97bd 100644 --- a/packages/actions/src/Mine/mineHandler.js +++ b/packages/actions/src/Mine/mineHandler.js @@ -15,6 +15,33 @@ import { validateMineParams } from './validateMineParams.js' export const mineHandler = (client, options = {}) => async ({ throwOnFail = options.throwOnFail ?? true, ...params } = {}) => { + switch (client.status) { + case 'MINING': { + const err = new MisconfiguredClientError('Mining is already in progress') + return maybeThrowOnFail(throwOnFail, { errors: [err] }) + } + case 'INITIALIZING': { + await client.ready() + client.status = 'MINING' + break + } + case 'SYNCING': { + const err = new MisconfiguredClientError('Syncing not currently implemented') + return maybeThrowOnFail(throwOnFail, { errors: [err] }) + } + case 'STOPPED': { + const err = new MisconfiguredClientError('Client is stopped') + return maybeThrowOnFail(throwOnFail, { errors: [err] }) + } + case 'READY': { + client.status = 'MINING' + break + } + default: { + const err = new UnreachableCodeError(client.status) + return maybeThrowOnFail(throwOnFail, { errors: [err] }) + } + } try { client.logger.debug({ throwOnFail, ...params }, 'mineHandler called with params') const errors = validateMineParams(params) @@ -36,43 +63,6 @@ export const mineHandler = const pool = await client.getTxPool() const originalVm = await client.getVm() - switch (client.status) { - case 'MINING': { - // wait for the previous mine to finish - await new Promise((resolve) => { - client.on('newBlock', async () => { - if (client.status === 'MINING') { - return - } - client.status = 'MINING' - resolve(client) - }) - }) - break - } - case 'INITIALIZING': { - await client.ready() - client.status = 'MINING' - break - } - case 'SYNCING': { - const err = new MisconfiguredClientError('Syncing not currently implemented') - return maybeThrowOnFail(throwOnFail, { errors: [err] }) - } - case 'STOPPED': { - const err = new MisconfiguredClientError('Client is stopped') - return maybeThrowOnFail(throwOnFail, { errors: [err] }) - } - case 'READY': { - client.status = 'MINING' - break - } - default: { - const err = new UnreachableCodeError(client.status) - return maybeThrowOnFail(throwOnFail, { errors: [err] }) - } - } - const vm = await originalVm.deepCopy() const receiptsManager = await client.getReceiptsManager() @@ -121,15 +111,6 @@ export const mineHandler = const txResult = await blockBuilder.addTransaction(nextTx, { skipHardForkValidation: true, }) - if (txResult.execResult.exceptionError) { - if (txResult.execResult.exceptionError.error === 'out of gas') { - client.logger.debug(txResult.execResult.executionGasUsed, 'out of gas') - } - client.logger.debug( - txResult.execResult.exceptionError, - `There was an exception when building block for tx ${bytesToHex(nextTx.hash())}`, - ) - } receipts.push(txResult.receipt) index++ } @@ -161,8 +142,6 @@ export const mineHandler = originalVm.evm.blockchain = vm.evm.blockchain await originalVm.stateManager.setStateRoot(hexToBytes(vm.stateManager._baseState.getCurrentStateRoot())) - client.status = 'READY' - emitEvents(client, newBlocks, newReceipts) return { blockHashes: newBlocks.map((b) => bytesToHex(b.hash())) } @@ -170,5 +149,7 @@ export const mineHandler = return maybeThrowOnFail(throwOnFail, { errors: [new InternalError(/** @type {Error} */ (e).message, { cause: e })], }) + } finally { + client.status = 'READY' } } diff --git a/packages/actions/src/Mine/mineHandler.spec.ts b/packages/actions/src/Mine/mineHandler.spec.ts index 604a36bdf5..e685435b37 100644 --- a/packages/actions/src/Mine/mineHandler.spec.ts +++ b/packages/actions/src/Mine/mineHandler.spec.ts @@ -1,7 +1,7 @@ import { type TevmNode, createTevmNode } from '@tevm/node' import { type Hex, hexToBytes } from '@tevm/utils' import { http } from 'viem' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { callHandler } from '../Call/callHandler.js' import { mineHandler } from './mineHandler.js' @@ -87,4 +87,158 @@ describe(mineHandler.name, () => { // should remove tx from mempool expect(await client.getTxPool().then((pool) => [...pool.pool.keys()].length)).toBe(0) }) + + it('should throw an error for invalid params', async () => { + const client = createTevmNode() + await expect(mineHandler(client)({ interval: 'invalid' as any })).rejects.toThrowErrorMatchingInlineSnapshot(` + [InternalError: Expected number, received string + + Docs: https://tevm.sh/reference/tevm/errors/classes/invalidnonceerror/ + Version: 1.1.0.next-73 + + Docs: https://tevm.sh/reference/tevm/errors/classes/invalidnonceerror/ + Details: /reference/tevm/errors/classes/invalidnonceerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle INITIALIZING status', async () => { + const client = createTevmNode() + client.status = 'INITIALIZING' + const readyMock = vi.fn().mockResolvedValue(true) + ;(client as any).ready = readyMock + + await mineHandler(client)({}) + + expect(readyMock).toHaveBeenCalled() + expect(await getBlockNumber(client)).toBe(1n) + }) + + it('should throw error when client status is STOPPED', async () => { + const client = createTevmNode() + client.status = 'STOPPED' + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [MisconfiguredClientError: Client is stopped + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should throw error for bogus client status', async () => { + const client = createTevmNode() + client.status = 'BOGUS_STATUS' as any + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [UnreachableCodeError: Unreachable code executed with value: "BOGUS_STATUS" + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle SYNCING status', async () => { + const client = createTevmNode() + client.status = 'SYNCING' + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [MisconfiguredClientError: Syncing not currently implemented + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle errors during mining process', async () => { + const client = createTevmNode() + const errorMock = new Error('Mining error') + ;(client as any).getVm = vi.fn().mockRejectedValue(errorMock) + + const result = await mineHandler(client)({ throwOnFail: false }) + + expect(result.errors?.[0]?.message).toMatchInlineSnapshot(` + "Mining error + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Details: Mining error + Version: 1.1.0.next-73" + `) + }) + + it('should throw an error if mining is already in progress', async () => { + const client = createTevmNode() + client.status = 'MINING' + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [MisconfiguredClientError: Mining is already in progress + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle INITIALIZING status', async () => { + const client = createTevmNode() + client.status = 'INITIALIZING' + const readyMock = vi.fn().mockResolvedValue(true) + ;(client as any).ready = readyMock + + await mineHandler(client)({}) + + expect(readyMock).toHaveBeenCalled() + expect(await getBlockNumber(client)).toBe(1n) + }) + + it('should throw error when client status is STOPPED', async () => { + const client = createTevmNode() + client.status = 'STOPPED' + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [MisconfiguredClientError: Client is stopped + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should throw error for bogus client status', async () => { + const client = createTevmNode() + client.status = 'BOGUS_STATUS' as any + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [UnreachableCodeError: Unreachable code executed with value: "BOGUS_STATUS" + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle SYNCING status', async () => { + const client = createTevmNode() + client.status = 'SYNCING' + + await expect(mineHandler(client)({})).rejects.toThrowErrorMatchingInlineSnapshot(` + [MisconfiguredClientError: Syncing not currently implemented + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Version: 1.1.0.next-73] + `) + }) + + it('should handle errors during mining process', async () => { + const client = createTevmNode() + const errorMock = new Error('Mining error') + ;(client as any).getVm = vi.fn().mockRejectedValue(errorMock) + + const result = await mineHandler(client)({ throwOnFail: false }) + + expect(result.errors?.[0]?.message).toMatchInlineSnapshot(` + "Mining error + + Docs: https://tevm.sh/reference/tevm/errors/classes/internalerror/ + Details: Mining error + Version: 1.1.0.next-73" + `) + }) }) diff --git a/packages/actions/src/anvil/__snapshots__/anvilDumpStateProcedure.spec.ts.snap b/packages/actions/src/anvil/__snapshots__/anvilDumpStateProcedure.spec.ts.snap new file mode 100644 index 0000000000..bcc72db161 --- /dev/null +++ b/packages/actions/src/anvil/__snapshots__/anvilDumpStateProcedure.spec.ts.snap @@ -0,0 +1,85 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`anvilDumpStateJsonRpcProcedure > should dump state correctly 1`] = ` +{ + "state": { + "0x0000000000000000000000000000000000000000": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x90F79bf6EB2c4f870365E785982E1f101E93b906": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x976EA74026E726554dB657fA54763abd0C3a0aa9": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": { + "balance": "0x3635c9adc5dea00000", + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "storage": {}, + "storageRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + }, +} +`; diff --git a/packages/actions/src/anvil/anvilDropTransactionProcedure.spec.ts b/packages/actions/src/anvil/anvilDropTransactionProcedure.spec.ts new file mode 100644 index 0000000000..424087415e --- /dev/null +++ b/packages/actions/src/anvil/anvilDropTransactionProcedure.spec.ts @@ -0,0 +1,124 @@ +import { createTevmNode } from '@tevm/node' +import { type Hex, hexToBytes } from '@tevm/utils' +import { beforeEach, describe, expect, it } from 'vitest' +import { callHandler } from '../Call/callHandler.js' +import { anvilDropTransactionJsonRpcProcedure } from './anvilDropTransactionProcedure.js' + +describe('anvilDropTransactionJsonRpcProcedure', () => { + let node: ReturnType + + beforeEach(() => { + node = createTevmNode() + }) + + it('should successfully drop a transaction from the pool', async () => { + // Add a transaction to the pool + const to = `0x${'69'.repeat(20)}` as const + const callResult = await callHandler(node)({ + createTransaction: true, + to, + value: 420n, + skipBalance: true, + }) + + const txHash = callResult.txHash as Hex + + // Verify the transaction is in the pool + const txPool = await node.getTxPool() + expect(txPool.getByHash([hexToBytes(txHash)])).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": "0x384", + "data": "0x", + "gasLimit": "0x5a3c", + "maxFeePerGas": "0x7", + "maxPriorityFeePerGas": "0x0", + "nonce": "0x0", + "r": undefined, + "s": undefined, + "to": "0x6969696969696969696969696969696969696969", + "type": "0x2", + "v": undefined, + "value": "0x1a4", + }, + ] + `) + + const procedure = anvilDropTransactionJsonRpcProcedure(node) + const result = await procedure({ + method: 'anvil_dropTransaction', + params: [{ transactionHash: txHash }], + jsonrpc: '2.0', + }) + + expect(result).toEqual({ + method: 'anvil_dropTransaction', + jsonrpc: '2.0', + result: null, + }) + + // Verify the transaction has been removed from the pool + expect(await txPool.getByHash([hexToBytes(txHash)])).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": "0x384", + "data": "0x", + "gasLimit": "0x5a3c", + "maxFeePerGas": "0x7", + "maxPriorityFeePerGas": "0x0", + "nonce": "0x0", + "r": undefined, + "s": undefined, + "to": "0x6969696969696969696969696969696969696969", + "type": "0x2", + "v": undefined, + "value": "0x1a4", + }, + ] + `) + }) + + it('should throw an error if the transaction is not in the pool', async () => { + const nonExistentTxHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + + const procedure = anvilDropTransactionJsonRpcProcedure(node) + + await expect( + procedure({ + method: 'anvil_dropTransaction', + params: [{ transactionHash: nonExistentTxHash }], + jsonrpc: '2.0', + }), + ).rejects.toThrow('Only tx in the txpool are allowed to be dropped') + }) + + it('should handle requests with id', async () => { + // Add a transaction to the pool + const to = `0x${'69'.repeat(20)}` as const + const callResult = await callHandler(node)({ + createTransaction: true, + to, + value: 420n, + skipBalance: true, + }) + + const txHash = callResult.txHash as Hex + + const procedure = anvilDropTransactionJsonRpcProcedure(node) + const result = await procedure({ + method: 'anvil_dropTransaction', + params: [{ transactionHash: txHash }], + jsonrpc: '2.0', + id: 1, + }) + + expect(result).toEqual({ + method: 'anvil_dropTransaction', + jsonrpc: '2.0', + result: null, + id: 1, + }) + }) +}) diff --git a/packages/actions/src/anvil/anvilDumpStateProcedure.spec.ts b/packages/actions/src/anvil/anvilDumpStateProcedure.spec.ts new file mode 100644 index 0000000000..3ce551e507 --- /dev/null +++ b/packages/actions/src/anvil/anvilDumpStateProcedure.spec.ts @@ -0,0 +1,35 @@ +import { createTevmNode } from '@tevm/node' +import { describe, expect, it } from 'vitest' +import { anvilDumpStateJsonRpcProcedure } from './anvilDumpStateProcedure.js' + +describe('anvilDumpStateJsonRpcProcedure', () => { + it('should dump state correctly', async () => { + const node = createTevmNode() + const procedure = anvilDumpStateJsonRpcProcedure(node) + + const result = await procedure({ + method: 'anvil_dumpState', + // TODO this is actually a bug we need to provide params + params: [{}], + jsonrpc: '2.0', + }) + + expect(result.jsonrpc).toBe('2.0') + expect(result.method).toBe('anvil_dumpState') + expect(result.result).toMatchSnapshot() + }) + + it('should handle requests with id', async () => { + const node = createTevmNode() + const procedure = anvilDumpStateJsonRpcProcedure(node) + + const result = await procedure({ + method: 'anvil_dumpState', + params: [{}], + jsonrpc: '2.0', + id: 1, + }) + + expect(result.id).toBe(1) + }) +}) diff --git a/packages/actions/src/anvil/anvilGetAutomineProcedure.spec.ts b/packages/actions/src/anvil/anvilGetAutomineProcedure.spec.ts new file mode 100644 index 0000000000..33d6d39f5e --- /dev/null +++ b/packages/actions/src/anvil/anvilGetAutomineProcedure.spec.ts @@ -0,0 +1,48 @@ +import { createTevmNode } from '@tevm/node' +import { describe, expect, it } from 'vitest' +import { anvilGetAutomineJsonRpcProcedure } from './anvilGetAutomineProcedure.js' + +describe('anvilGetAutomineJsonRpcProcedure', () => { + it('should return true when automine is enabled', async () => { + const node = createTevmNode({ miningConfig: { type: 'auto' } }) + const procedure = anvilGetAutomineJsonRpcProcedure(node) + + const result = await procedure({ + method: 'anvil_getAutomine', + // TODO this is actually a bug we need to provide params + params: [{}], + jsonrpc: '2.0', + }) + + expect(result.jsonrpc).toBe('2.0') + expect(result.method).toBe('anvil_getAutomine') + expect(result.result).toBe(true) + }) + + it('should return false when automine is disabled', async () => { + const node = createTevmNode({ miningConfig: { type: 'interval', interval: 1000 } }) + const procedure = anvilGetAutomineJsonRpcProcedure(node) + + const result = await procedure({ + method: 'anvil_getAutomine', + params: [{}], + jsonrpc: '2.0', + }) + + expect(result.result).toBe(false) + }) + + it('should handle requests with id', async () => { + const node = createTevmNode() + const procedure = anvilGetAutomineJsonRpcProcedure(node) + + const result = await procedure({ + method: 'anvil_getAutomine', + params: [{}], + jsonrpc: '2.0', + id: 1, + }) + + expect(result.id).toBe(1) + }) +}) diff --git a/packages/actions/src/eth/ethGetLogsHandler.spec.ts b/packages/actions/src/eth/ethGetLogsHandler.spec.ts index 7e863c5d96..9ef82e225b 100644 --- a/packages/actions/src/eth/ethGetLogsHandler.spec.ts +++ b/packages/actions/src/eth/ethGetLogsHandler.spec.ts @@ -228,44 +228,48 @@ describe(ethGetLogsHandler.name, () => { }) }) - it('should work for past blocks in forked mode', async () => { - const client = createTevmNode({ - fork: { - transport: transports.optimism, - blockTag: 125985200n, - }, - }) - const logs = await ethGetLogsHandler(client)({ - filterParams: { - address: '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1', - fromBlock: 125985142n, - toBlock: 125985142n, - topics: [ - '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', - '0x0000000000000000000000007f26A7572E8B877654eeDcBc4E573657619FA3CE', - '0x0000000000000000000000007B46fFbC976db2F94C3B3CDD9EbBe4ab50E3d77d', - ], - }, - }) - expect(logs).toHaveLength(1) - expect(logs).toMatchInlineSnapshot(` - [ - { - "address": "0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1", - "blockHash": "0x6c9355482a6937e44fbfbd1c0c9cc95882e47e80c9b48772699c6a49bad1e392", - "blockNumber": 125985142n, - "data": "0x0000000000000000000000000000000000000000000b2f1069a1f95dc7180000", - "logIndex": 23n, - "removed": false, - "topics": [ - "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", - "0x0000000000000000000000007f26a7572e8b877654eedcbc4e573657619fa3ce", - "0x0000000000000000000000007b46ffbc976db2f94c3b3cdd9ebbe4ab50e3d77d", - ], - "transactionHash": "0x4f0781ec417fecaf44b248fd0b0485dca9fbe78ad836598b65c12bb13ab9ddd4", - "transactionIndex": 11n, - }, - ] - `) - }) + it( + 'should work for past blocks in forked mode', + async () => { + const client = createTevmNode({ + fork: { + transport: transports.optimism, + blockTag: 125985200n, + }, + }) + const logs = await ethGetLogsHandler(client)({ + filterParams: { + address: '0xdC6fF44d5d932Cbd77B52E5612Ba0529DC6226F1', + fromBlock: 125985142n, + toBlock: 125985142n, + topics: [ + '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925', + '0x0000000000000000000000007f26A7572E8B877654eeDcBc4E573657619FA3CE', + '0x0000000000000000000000007B46fFbC976db2F94C3B3CDD9EbBe4ab50E3d77d', + ], + }, + }) + expect(logs).toHaveLength(1) + expect(logs).toMatchInlineSnapshot(` + [ + { + "address": "0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1", + "blockHash": "0x6c9355482a6937e44fbfbd1c0c9cc95882e47e80c9b48772699c6a49bad1e392", + "blockNumber": 125985142n, + "data": "0x0000000000000000000000000000000000000000000b2f1069a1f95dc7180000", + "logIndex": 23n, + "removed": false, + "topics": [ + "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "0x0000000000000000000000007f26a7572e8b877654eedcbc4e573657619fa3ce", + "0x0000000000000000000000007b46ffbc976db2f94c3b3cdd9ebbe4ab50e3d77d", + ], + "transactionHash": "0x4f0781ec417fecaf44b248fd0b0485dca9fbe78ad836598b65c12bb13ab9ddd4", + "transactionIndex": 11n, + }, + ] + `) + }, + { timeout: 20_000 }, + ) }) diff --git a/packages/actions/src/eth/getCodeHandler.spec.ts b/packages/actions/src/eth/getCodeHandler.spec.ts index 8ac81e1b56..4527b9883e 100644 --- a/packages/actions/src/eth/getCodeHandler.spec.ts +++ b/packages/actions/src/eth/getCodeHandler.spec.ts @@ -1,4 +1,5 @@ import { createAddress } from '@tevm/address' +import { UnknownBlockError } from '@tevm/errors' import { createTevmNode } from '@tevm/node' import { SimpleContract, transports } from '@tevm/test-utils' import { describe, expect, it } from 'vitest' @@ -22,6 +23,79 @@ describe(getCodeHandler.name, () => { }), ).toBe(contract.deployedBytecode) }) + + it('should return empty bytecode for non-existent address', async () => { + const client = createTevmNode() + const nonExistentAddress = createAddress(123456).toString() + + const code = await getCodeHandler(client)({ + address: nonExistentAddress, + }) + + expect(code).toBe('0x') + }) + + it('should handle "pending" block tag', async () => { + const client = createTevmNode() + + const code = await getCodeHandler(client)({ + address: contract.address, + blockTag: 'pending', + }) + + expect(code).toBe('0x') + }) + + it('should throw UnknownBlockError for non-existent block', async () => { + const client = createTevmNode() + + await expect( + getCodeHandler(client)({ + address: contract.address, + blockTag: '0x123456', + }), + ).rejects.toThrow(UnknownBlockError) + }) + + it('should handle numeric block tag', async () => { + const client = createTevmNode() + const blockNumber = 0 // Use genesis block + + const code = await getCodeHandler(client)({ + address: contract.address, + blockTag: blockNumber as any, + }) + + expect(code).toBe('0x') + }) + + it('should handle latest block tag', async () => { + const client = createTevmNode() + + const code = await getCodeHandler(client)({ + address: contract.address, + blockTag: 'latest', + }) + + expect(code).toBe('0x') + }) + + it('should return correct code after contract deployment', async () => { + const client = createTevmNode() + + // Deploy the contract + await setAccountHandler(client)({ + address: contract.address, + deployedBytecode: contract.deployedBytecode, + }) + + // Get the code + const code = await getCodeHandler(client)({ + address: contract.address, + }) + + expect(code).toBe(contract.deployedBytecode) + }) }) describe('Forking tests', () => { diff --git a/packages/actions/src/eth/utils/parseBlockParam.spec.ts b/packages/actions/src/eth/utils/parseBlockParam.spec.ts index 9e8abe270d..eb2a04cfef 100644 --- a/packages/actions/src/eth/utils/parseBlockParam.spec.ts +++ b/packages/actions/src/eth/utils/parseBlockParam.spec.ts @@ -1,84 +1,95 @@ import { InvalidBlockError } from '@tevm/errors' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTevmNode } from '@tevm/node' +import { bytesToHex } from 'viem' +import { beforeEach, describe, expect, it } from 'vitest' import { parseBlockParam } from './parseBlockParam.js' describe('parseBlockParam', () => { - let mockBlockchain: any - - beforeEach(() => { - mockBlockchain = { - getBlock: vi.fn(), - blocksByTag: new Map(), - logger: { - error: vi.fn(), - }, - } + let node: any + let blockchain: any + + beforeEach(async () => { + node = createTevmNode() + const vm = await node.getVm() + blockchain = vm.blockchain + + // Set up a canonical head block + const headBlock = await blockchain.getCanonicalHeadBlock() + blockchain.blocksByTag.set('latest', headBlock) + blockchain.blocksByNumber.set(BigInt(headBlock.header.number), headBlock) + blockchain.blocks.set(headBlock.hash(), headBlock) }) it('should handle number input', async () => { - const result = await parseBlockParam(mockBlockchain, 123 as any) + const result = await parseBlockParam(blockchain, 123 as any) expect(result).toBe(123n) }) it('should handle bigint input', async () => { - const result = await parseBlockParam(mockBlockchain, 456n) + const result = await parseBlockParam(blockchain, 456n) expect(result).toBe(456n) }) - it('should handle hex block number', async () => { - mockBlockchain.getBlock.mockResolvedValue({ header: { number: 789 } }) - const hash = `0x${'12'.repeat(32)}` as const - const result = await parseBlockParam(mockBlockchain, hash) - expect(result).toBe(789n) + it('should handle hex block hash', async () => { + const headBlock = await blockchain.getCanonicalHeadBlock() + const blockHash = bytesToHex(headBlock.hash()) + blockchain.blocks.set(blockHash, headBlock) + const result = await parseBlockParam(blockchain, blockHash) + expect(result).toBe(BigInt(headBlock.header.number)) }) it('should handle hex string block number input', async () => { - mockBlockchain.getBlock.mockResolvedValue({ header: { number: 789 } }) - const result = await parseBlockParam(mockBlockchain, '0x123') + const result = await parseBlockParam(blockchain, '0x123') expect(result).toBe(291n) }) it('should handle "safe" tag', async () => { - mockBlockchain.blocksByTag.set('safe', { header: { number: 101n } }) - const result = await parseBlockParam(mockBlockchain, 'safe') - expect(result).toBe(101n) + const safeBlock = await blockchain.getCanonicalHeadBlock() + blockchain.blocksByTag.set('safe', safeBlock) + const result = await parseBlockParam(blockchain, 'safe') + expect(result).toBe(BigInt(safeBlock.header.number)) }) it('should throw error for unsupported "safe" tag', async () => { - await expect(parseBlockParam(mockBlockchain, 'safe')).rejects.toThrow(InvalidBlockError) + await expect(parseBlockParam(blockchain, 'safe')).rejects.toThrow(InvalidBlockError) }) it('should handle "latest" tag', async () => { - mockBlockchain.blocksByTag.set('latest', { header: { number: 202n } }) - const result = await parseBlockParam(mockBlockchain, 'latest') - expect(result).toBe(202n) + const latestBlock = await blockchain.getCanonicalHeadBlock() + const result = await parseBlockParam(blockchain, 'latest') + expect(result).toBe(BigInt(latestBlock.header.number)) }) it('should handle undefined as "latest"', async () => { - mockBlockchain.blocksByTag.set('latest', { header: { number: 303n } }) - const result = await parseBlockParam(mockBlockchain, undefined as any) - expect(result).toBe(303n) + const latestBlock = await blockchain.getCanonicalHeadBlock() + const result = await parseBlockParam(blockchain, undefined as any) + expect(result).toBe(BigInt(latestBlock.header.number)) }) it('should throw error for missing "latest" block', async () => { - await expect(parseBlockParam(mockBlockchain, 'latest')).rejects.toThrow(InvalidBlockError) + blockchain.blocksByTag.delete('latest') + await expect(parseBlockParam(blockchain, 'latest')).rejects.toThrow(InvalidBlockError) }) it('should throw error for "pending" tag', async () => { - await expect(parseBlockParam(mockBlockchain, 'pending')).rejects.toThrow(InvalidBlockError) + await expect(parseBlockParam(blockchain, 'pending')).rejects.toThrow(InvalidBlockError) }) it('should handle "earliest" tag', async () => { - const result = await parseBlockParam(mockBlockchain, 'earliest') + const result = await parseBlockParam(blockchain, 'earliest') expect(result).toBe(1n) }) it('should throw error for "finalized" tag', async () => { - await expect(parseBlockParam(mockBlockchain, 'finalized')).rejects.toThrow(InvalidBlockError) + await expect(parseBlockParam(blockchain, 'finalized')).rejects.toThrow(InvalidBlockError) }) it('should throw error for unknown block param', async () => { - await expect(parseBlockParam(mockBlockchain, 'unknown' as any)).rejects.toThrow(InvalidBlockError) - expect(mockBlockchain.logger.error).toHaveBeenCalled() + await expect(parseBlockParam(blockchain, 'unknown' as any)).rejects.toThrow(InvalidBlockError) + }) + + it('should handle non-existent block hash', async () => { + const nonExistentHash = `0x${'1234'.repeat(16)}` as const + await expect(parseBlockParam(blockchain, nonExistentHash)).rejects.toThrow('Block with hash') }) }) diff --git a/packages/actions/src/internal/maybeThrowOnFail.spec.ts b/packages/actions/src/internal/maybeThrowOnFail.spec.ts index 996783523f..a915fd8e9f 100644 --- a/packages/actions/src/internal/maybeThrowOnFail.spec.ts +++ b/packages/actions/src/internal/maybeThrowOnFail.spec.ts @@ -33,4 +33,10 @@ describe('maybeThrowOnFail', () => { const output = maybeThrowOnFail(true, result as any) expect(output).toBe(result) }) + + it('should return the result if throwOnFail is true and errors array is empty', () => { + const result = { data: 'some data', errors: [] } + const output = maybeThrowOnFail(true, result) + expect(output).toBe(result) + }) }) diff --git a/packages/actions/src/requestBulkProcedure.js b/packages/actions/src/requestBulkProcedure.js index c04307f631..2dcb7c218f 100644 --- a/packages/actions/src/requestBulkProcedure.js +++ b/packages/actions/src/requestBulkProcedure.js @@ -18,8 +18,8 @@ export const requestBulkProcedure = (client) => async (requests) => { jsonrpc: '2.0', error: { // TODO This should be added to @tevm/errors package and rexported in tevm - code: 'UnexpectedBulkRequestError', - message: 'UnexpectedBulkRequestError', + code: response.reason.code ?? -32000, + message: response.reason.message ?? 'UnexpectedBulkRequestError', }, } } diff --git a/packages/actions/src/requestBulkProcedure.spec.ts b/packages/actions/src/requestBulkProcedure.spec.ts index da095ec7d0..c40a3dd6a2 100644 --- a/packages/actions/src/requestBulkProcedure.spec.ts +++ b/packages/actions/src/requestBulkProcedure.spec.ts @@ -4,11 +4,13 @@ import { describe, expect, it } from 'vitest' import { requestBulkProcedure } from './requestBulkProcedure.js' const ERC20_ADDRESS = `0x${'3'.repeat(40)}` as const +const VALID_ADDRESS = `0x${'1'.repeat(40)}` as const +const INVALID_ADDRESS = `0x${'g'.repeat(40)}` as const // Invalid hex character const ERC20_BYTECODE = '0x608060405234801561001057600080fd5b50600436106101425760003560e01c806370a08231116100b8578063a457c2d71161007c578063a457c2d7146103b0578063a9059cbb146103dc578063bf353dbb14610408578063cd0d00961461042e578063d505accf14610436578063dd62ed3e1461048757610142565b806370a082311461030a5780637ecebe001461033057806395d89b41146103565780639c52a7f11461035e5780639dc29fac1461038457610142565b8063313ce5671161010a578063313ce5671461025c5780633644e5151461027a578063395093511461028257806340c10f19146102ae57806354fd4d50146102dc57806365fae35e146102e457610142565b806306fdde0314610147578063095ea7b3146101c457806318160ddd1461020457806323b872dd1461021e57806330adf81f14610254575b600080fd5b61014f6104b5565b6040805160208082528351818301528351919283929083019185019080838360005b83811015610189578181015183820152602001610171565b50505050905090810190601f1680156101b65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101f0600480360360408110156101da57600080fd5b506001600160a01b0381351690602001356104df565b604080519115158252519081900360200190f35b61020c610534565b60408051918252519081900360200190f35b6101f06004803603606081101561023457600080fd5b506001600160a01b0381358116916020810135909116906040013561053a565b61020c610725565b610264610749565b6040805160ff9092168252519081900360200190f35b61020c61074e565b6101f06004803603604081101561029857600080fd5b506001600160a01b0381351690602001356107ae565b6102da600480360360408110156102c457600080fd5b506001600160a01b038135169060200135610835565b005b61014f610957565b6102da600480360360208110156102fa57600080fd5b50356001600160a01b0316610974565b61020c6004803603602081101561032057600080fd5b50356001600160a01b0316610a12565b61020c6004803603602081101561034657600080fd5b50356001600160a01b0316610a24565b61014f610a36565b6102da6004803603602081101561037457600080fd5b50356001600160a01b0316610a55565b6102da6004803603604081101561039a57600080fd5b506001600160a01b038135169060200135610af2565b6101f0600480360360408110156103c657600080fd5b506001600160a01b038135169060200135610c84565b6101f0600480360360408110156103f257600080fd5b506001600160a01b038135169060200135610d55565b61020c6004803603602081101561041e57600080fd5b50356001600160a01b0316610e7a565b61020c610e8c565b6102da600480360360e081101561044c57600080fd5b506001600160a01b03813581169160208101359091169060408101359060608101359060ff6080820135169060a08101359060c00135610eb0565b61020c6004803603604081101561049d57600080fd5b506001600160a01b0381358116916020013516611134565b6040518060400160405280600e81526020016d2230b49029ba30b13632b1b7b4b760911b81525081565b3360008181526003602090815260408083206001600160a01b03871680855290835281842086905581518681529151939490939092600080516020611259833981519152928290030190a35060015b92915050565b60015481565b60006001600160a01b0383161580159061055d57506001600160a01b0383163014155b6105a4576040805162461bcd60e51b81526020600482015260136024820152724461692f696e76616c69642d6164647265737360681b604482015290519081900360640190fd5b6001600160a01b0384166000908152600260205260409020548281101561060d576040805162461bcd60e51b81526020600482015260186024820152774461692f696e73756666696369656e742d62616c616e636560401b604482015290519081900360640190fd5b6001600160a01b03851633146106c7576001600160a01b038516600090815260036020908152604080832033845290915290205460001981146106c5578381101561069c576040805162461bcd60e51b815260206004820152601a6024820152794461692f696e73756666696369656e742d616c6c6f77616e636560301b604482015290519081900360640190fd5b6001600160a01b0386166000908152600360209081526040808320338452909152902084820390555b505b6001600160a01b038086166000818152600260209081526040808320888703905593881680835291849020805488019055835187815293519193600080516020611239833981519152929081900390910190a3506001949350505050565b7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b601281565b6000467f000000000000000000000000000000000000000000000000000000000000000a81146107865761078181611151565b6107a8565b7fc7bbf40a5fb081e6759d5d0ce2447e84427793536887332b932877b94ce51bd65b91505090565b3360009081526003602090815260408083206001600160a01b038616845290915281205481906107de9084611228565b3360008181526003602090815260408083206001600160a01b038a16808552908352928190208590558051858152905194955091936000805160206112598339815191529281900390910190a35060019392505050565b3360009081526020819052604090205460011461088e576040805162461bcd60e51b815260206004820152601260248201527111185a4bdb9bdd0b585d5d1a1bdc9a5e995960721b604482015290519081900360640190fd5b6001600160a01b038216158015906108af57506001600160a01b0382163014155b6108f6576040805162461bcd60e51b81526020600482015260136024820152724461692f696e76616c69642d6164647265737360681b604482015290519081900360640190fd5b6001600160a01b03821660009081526002602052604090208054820190556001546109219082611228565b6001556040805182815290516001600160a01b038416916000916000805160206112398339815191529181900360200190a35050565b604051806040016040528060018152602001601960f91b81525081565b336000908152602081905260409020546001146109cd576040805162461bcd60e51b815260206004820152601260248201527111185a4bdb9bdd0b585d5d1a1bdc9a5e995960721b604482015290519081900360640190fd5b6001600160a01b03811660008181526020819052604080822060019055517fdd0e34038ac38b2a1ce960229778ac48a8719bc900b6c4f8d0475c6e8b385a609190a250565b60026020526000908152604090205481565b60046020526000908152604090205481565b6040518060400160405280600381526020016244414960e81b81525081565b33600090815260208190526040902054600114610aae576040805162461bcd60e51b815260206004820152601260248201527111185a4bdb9bdd0b585d5d1a1bdc9a5e995960721b604482015290519081900360640190fd5b6001600160a01b038116600081815260208190526040808220829055517f184450df2e323acec0ed3b5c7531b81f9b4cdef7914dfd4c0a4317416bb5251b9190a250565b6001600160a01b03821660009081526002602052604090205481811015610b5b576040805162461bcd60e51b81526020600482015260186024820152774461692f696e73756666696369656e742d62616c616e636560401b604482015290519081900360640190fd5b6001600160a01b0383163314801590610b84575033600090815260208190526040902054600114155b15610c33576001600160a01b03831660009081526003602090815260408083203384529091529020546000198114610c315782811015610c08576040805162461bcd60e51b815260206004820152601a6024820152794461692f696e73756666696369656e742d616c6c6f77616e636560301b604482015290519081900360640190fd5b6001600160a01b0384166000908152600360209081526040808320338452909152902083820390555b505b6001600160a01b0383166000818152600260209081526040808320868603905560018054879003905580518681529051929392600080516020611239833981519152929181900390910190a3505050565b3360009081526003602090815260408083206001600160a01b038616845290915281205482811015610cfa576040805162461bcd60e51b815260206004820152601a6024820152794461692f696e73756666696369656e742d616c6c6f77616e636560301b604482015290519081900360640190fd5b3360008181526003602090815260408083206001600160a01b03891680855290835292819020948790039485905580518581529051929392600080516020611259833981519152929181900390910190a35060019392505050565b60006001600160a01b03831615801590610d7857506001600160a01b0383163014155b610dbf576040805162461bcd60e51b81526020600482015260136024820152724461692f696e76616c69642d6164647265737360681b604482015290519081900360640190fd5b3360009081526002602052604090205482811015610e1f576040805162461bcd60e51b81526020600482015260186024820152774461692f696e73756666696369656e742d62616c616e636560401b604482015290519081900360640190fd5b33600081815260026020908152604080832087860390556001600160a01b0388168084529281902080548801905580518781529051929392600080516020611239833981519152929181900390910190a35060019392505050565b60006020819052908152604090205481565b7f000000000000000000000000000000000000000000000000000000000000000a81565b83421115610efa576040805162461bcd60e51b815260206004820152601260248201527111185a4bdc195c9b5a5d0b595e1c1a5c995960721b604482015290519081900360640190fd5b4660007f000000000000000000000000000000000000000000000000000000000000000a8214610f3257610f2d82611151565b610f54565b7fc7bbf40a5fb081e6759d5d0ce2447e84427793536887332b932877b94ce51bd65b6001600160a01b03808b1660008181526004602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981840152808401859052948e166060860152608085018d905260a085015260c08085018c90528251808603909101815260e08501835280519082012061190160f01b6101008601526101028501959095526101228085019590955281518085039095018552610142909301905282519290910191909120915015801590611098575060018186868660405160008152602001604052604051808581526020018460ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa158015611079573d6000803e3d6000fd5b505050602060405103516001600160a01b0316896001600160a01b0316145b6110de576040805162461bcd60e51b815260206004820152601260248201527111185a4bda5b9d985b1a590b5c195c9b5a5d60721b604482015290519081900360640190fd5b6001600160a01b03808a166000818152600360209081526040808320948d16808452948252918290208b905581518b815291516000805160206112598339815191529281900390910190a3505050505050505050565b600360209081526000928352604080842090915290825290205481565b604080518082018252600e81526d2230b49029ba30b13632b1b7b4b760911b6020918201528151808301835260018152601960f91b9082015281517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f818301527f0b1461ddc0c1d5ded79a1db0f74dae949050a7c0b28728c724b24958c27a328b818401527fad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5606082015260808101939093523060a0808501919091528251808503909101815260c0909301909152815191012090565b8082018281101561052e57600080fdfeddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a26469706673582212204174ca7efe9461957e50debebcf436a7f5badaf0bd4b64389fd2735d2369a5b264736f6c63430007060033' describe('requestBulkProcedure', () => { - it('should work', async () => { + it('should work for successful requests', async () => { const client = createTevmNode() await requestBulkProcedure(client)([ { @@ -83,4 +85,59 @@ describe('requestBulkProcedure', () => { storageRoot: '0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421', }) }) + + it('should handle mixed successful and failed requests', async () => { + const client = createTevmNode() + + // Set up an account with a balance + await requestBulkProcedure(client)([ + { + jsonrpc: '2.0', + method: 'tevm_setAccount', + id: 1, + params: [ + { + address: VALID_ADDRESS, + balance: numberToHex(1000n), + }, + ], + }, + ]) + + const res = await requestBulkProcedure(client)([ + { + jsonrpc: '2.0', + method: 'eth_getBalance', + id: 1, + params: [VALID_ADDRESS, 'latest'], + }, + { + jsonrpc: '2.0', + method: 'eth_getBalance', + id: 2, + params: [INVALID_ADDRESS, 'latest'], + }, + ]) + + expect(res).toHaveLength(2) + + // Check the successful request + expect(res[0].error).toBeUndefined() + expect(res[0].result).toBe(numberToHex(1000n)) + + // Check the failed request + expect(res[1].error).toBeDefined() + expect(res[1].error?.code).toBe(-32013) + expect(res[1].error?.message).toMatchInlineSnapshot(` + "Received an invalid address input: Invalid byte sequence ("gg" in "gggggggggggggggggggggggggggggggggggggggg"). + + Version: 2.21.1 + + Docs: https://tevm.sh/reference/tevm/errors/classes/invalidaddresserror/ + Details: Invalid byte sequence ("gg" in "gggggggggggggggggggggggggggggggggggggggg"). + + Version: 2.21.1 + Version: 1.1.0.next-73" + `) + }) }) diff --git a/packages/actions/src/requestProcedure.spec.ts b/packages/actions/src/requestProcedure.spec.ts index 6460472aef..50ee6a455a 100644 --- a/packages/actions/src/requestProcedure.spec.ts +++ b/packages/actions/src/requestProcedure.spec.ts @@ -1,4 +1,5 @@ import { ERC20 } from '@tevm/contract' +import { MethodNotFoundError } from '@tevm/errors' import { type TevmNode, createTevmNode } from '@tevm/node' import { type EthjsAccount, EthjsAddress, encodeDeployData, hexToBytes } from '@tevm/utils' import { bytesToHex, encodeFunctionData, keccak256, numberToHex, parseGwei } from '@tevm/utils' @@ -301,4 +302,31 @@ describe('requestProcedure', () => { ).toMatchSnapshot() }) }) + + describe('unsupported method', () => { + it('should return a MethodNotFoundError for an unsupported method', async () => { + const res = await requestProcedure(client)({ + jsonrpc: '2.0', + method: 'unsupported_method' as any, + id: 1, + params: [], + }) + + expect(res.error.code).toBe(MethodNotFoundError.code) + expect(res).toMatchInlineSnapshot(` + { + "error": { + "code": -32601, + "message": "UnsupportedMethodError: Unknown method unsupported_method + + Docs: https://tevm.sh/reference/tevm/errors/classes/methodnotfounderror/ + Version: 1.1.0.next-73", + }, + "id": 1, + "jsonrpc": "2.0", + "method": "unsupported_method", + } + `) + }) + }) }) diff --git a/packages/actions/vitest.config.ts b/packages/actions/vitest.config.ts index 31641b6b6d..e12b88bdf2 100644 --- a/packages/actions/vitest.config.ts +++ b/packages/actions/vitest.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], environment: 'node', coverage: { + reportOnFailure: true, include: ['src/**/*.js'], provider: 'v8', reporter: ['text', 'json-summary', 'json'], diff --git a/packages/vm/src/actions/runTx.ts b/packages/vm/src/actions/runTx.ts index 8909ef4f21..4d46c2ad9d 100644 --- a/packages/vm/src/actions/runTx.ts +++ b/packages/vm/src/actions/runTx.ts @@ -382,7 +382,7 @@ const _runTx = await vm.evm.journal.cleanup() // Generate the tx receipt - const gasUsed = opts.blockGasUsed !== undefined ? opts.blockGasUsed : block.header.gasUsed + const gasUsed = (opts.blockGasUsed !== undefined ? opts.blockGasUsed : block.header.gasUsed) ?? 0n const cumulativeGasUsed = gasUsed + results.totalGasSpent results.receipt = await generateTxReceipt(vm)(tx, results, cumulativeGasUsed, totalblobGas, blobGasPrice) diff --git a/test/test-utils/src/BlockReader.s.sol b/test/test-utils/src/BlockReader.s.sol new file mode 100644 index 0000000000..068ab99396 --- /dev/null +++ b/test/test-utils/src/BlockReader.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract BlockReader { + function getBlockInfo() + public + view + returns (uint256, uint256, address, uint256) + { + return (block.number, block.timestamp, block.coinbase, block.basefee); + } +} diff --git a/test/test-utils/src/BlockReader.s.sol.ts b/test/test-utils/src/BlockReader.s.sol.ts new file mode 100644 index 0000000000..4a006ea346 --- /dev/null +++ b/test/test-utils/src/BlockReader.s.sol.ts @@ -0,0 +1,13 @@ +import { createContract } from '@tevm/contract' +const _BlockReader = { + name: 'BlockReader', + humanReadableAbi: ['function getBlockInfo() view returns (uint256, uint256, address, uint256)'], + bytecode: + '0x6080604052348015600e575f5ffd5b5061011e8061001c5f395ff3fe6080604052348015600e575f5ffd5b50600436106025575f3560e01c8062819439146029575b5f5ffd5b602f6046565b604051603d949392919060ad565b60405180910390f35b5f5f5f5f43424148935093509350935090919293565b5f819050919050565b606c81605c565b82525050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6099826072565b9050919050565b60a7816091565b82525050565b5f60808201905060be5f8301876065565b60c960208301866065565b60d4604083018560a0565b60df60608301846065565b9594505050505056fea26469706673582212209b172a14e8b9968164f4af615bbcbef8959146a746babd94297fc7088fe25e8b64736f6c634300081b0033', + deployedBytecode: + '0x6080604052348015600e575f5ffd5b50600436106025575f3560e01c8062819439146029575b5f5ffd5b602f6046565b604051603d949392919060ad565b60405180910390f35b5f5f5f5f43424148935093509350935090919293565b5f819050919050565b606c81605c565b82525050565b5f73ffffffffffffffffffffffffffffffffffffffff82169050919050565b5f6099826072565b9050919050565b60a7816091565b82525050565b5f60808201905060be5f8301876065565b60c960208301866065565b60d4604083018560a0565b60df60608301846065565b9594505050505056fea26469706673582212209b172a14e8b9968164f4af615bbcbef8959146a746babd94297fc7088fe25e8b64736f6c634300081b0033', +} as const +/** + * @see [contract docs](https://tevm.sh/learn/contracts/) for more documentation + */ +export const BlockReader = createContract(_BlockReader) diff --git a/test/test-utils/src/index.ts b/test/test-utils/src/index.ts index ea336ed021..8fa45c3b7a 100644 --- a/test/test-utils/src/index.ts +++ b/test/test-utils/src/index.ts @@ -1,4 +1,5 @@ export { getAlchemyUrl } from './getAlchemyUrl.js' export { transports } from './transports.js' export { SimpleContract } from './SimpleContract.s.sol.js' +export { BlockReader } from './BlockReader.s.sol.js' export { TestERC20, TestERC721 } from './OZ.s.sol.js'