Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧪 Test: Add more test coverage to actions #1467

Merged
merged 11 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-ghosts-rule.md
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions .changeset/mean-ghosts-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tevm/actions": patch
"@tevm/vm": patch
---

Fixed bug with block override set missing a state root
5 changes: 5 additions & 0 deletions .changeset/tiny-dolls-enjoy.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion packages/actions/src/Call/callHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/actions/src/Call/callHandlerOpts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Comment on lines +70 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Include stateRoot in the type definitions

The comment notes that stateRoot isn't in the type definition but is required to prevent block overrides from failing. To ensure type safety and clarity, consider updating the type definitions to include stateRoot.

Apply this change to update the type definitions accordingly.

coinbase:
params.blockOverrideSet.coinbase !== undefined
? createAddress(params.blockOverrideSet.coinbase)
Expand Down
1 change: 0 additions & 1 deletion packages/actions/src/Call/callHandlerResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,5 @@ export const callHandlerResult = (evmResult, txHash, trace, accessList) => {
if (evmResult.createdAddress) {
out.createdAddress = getAddress(evmResult.createdAddress.toString())
}

return out
}
7 changes: 4 additions & 3 deletions packages/actions/src/Call/callProcedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } : {}),
Comment on lines +42 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure proper validation and sanitization of new request parameters.

The new parameters data, deployedBytecode, createTrace, and createAccessList are being directly passed from request.params[0] into the options for callHandler. To prevent potential security vulnerabilities or unexpected behavior, please ensure that these parameters are properly validated and sanitized before use.

...(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) } : {}),
Expand All @@ -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])
Expand Down
103 changes: 101 additions & 2 deletions packages/actions/src/Call/callProcedure.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)

Comment on lines +114 to +115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid unnecessary casting to any in test assertions

Casting request.id to any in your test assertions may mask potential type inconsistencies. Consider ensuring that the types of response.id and request.id are compatible without the need for casting. This will help maintain strong type safety in your tests.

Also applies to: 146-147, 158-159

const decodedResult = decodeFunctionResult({
abi: BlockReader.read.getBlockInfo.abi,
data: response.result?.rawData as Hex,
functionName: 'getBlockInfo',
})
expect(decodedResult).toEqual([1000n, 1234567890n, '0x1000000000000000000000000000000000000000', 1n])
})
Comment on lines +84 to +122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor common test setup code to improve maintainability

The test cases it('should handle a call with block override', ...) and it('should handle a call with tracing enabled', ...) share similar setup code, such as deploying contracts and initializing requests. Consider refactoring this code into helper functions or using shared fixtures to reduce duplication and enhance maintainability.

Also applies to: 124-179


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 = {
Expand Down
15 changes: 15 additions & 0 deletions packages/actions/src/Call/handleEvmError.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
36 changes: 36 additions & 0 deletions packages/actions/src/Contract/contractHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
`)
})
})
24 changes: 24 additions & 0 deletions packages/actions/src/Deploy/deployHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`should dump state for a specific block number 1`] = `
[
[InternalError: Unexpected error

Docs: https://tevm.sh/reference/tevm/errors/classes/unknownblockerror/
Details: /reference/tevm/errors/classes/unknownblockerror/
Version: 1.1.0.next-73],
]
`;

exports[`should handle block not found 1`] = `
[
[InternalError: Unexpected error

Docs: https://tevm.sh/reference/tevm/errors/classes/unknownblockerror/
Details: /reference/tevm/errors/classes/unknownblockerror/
Version: 1.1.0.next-73],
]
`;
9 changes: 8 additions & 1 deletion packages/actions/src/DumpState/dumpStateHandler.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 })],
Expand Down
15 changes: 15 additions & 0 deletions packages/actions/src/DumpState/dumpStateHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
]
`)
})
Comment on lines +43 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Good addition of error handling test, consider these improvements:

  1. Add a comment explaining the use of 1n as a block tag for clarity.
  2. The inline snapshot includes version information, which might require frequent updates. Consider using a more flexible assertion for the error message that doesn't include version-specific details.
  3. Add a test case to verify the behavior when throwOnFail is set to true.

Here's a suggested improvement for points 1 and 2:

 test('should handle block not found', async () => {
 	const client = createTevmNode()
+	// Use 1n as a non-existent block number to trigger the error
 	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],
-		]
-	`)
+	expect(errors[0]).toBeInstanceOf(Error)
+	expect(errors[0].message).toContain('Block number 1 does not exist')
+	expect(errors[0].message).toContain('https://tevm.sh/reference/tevm/errors/classes/unknownblockerror/')
 })

For point 3, consider adding a new test case:

test('should throw when block not found and throwOnFail is true', async () => {
	const client = createTevmNode()
	await expect(dumpStateHandler(client)({ blockTag: 1n, throwOnFail: true })).rejects.toThrow('Block number 1 does not exist')
})

Loading
Loading