Skip to content

Commit

Permalink
✨ Feat: Add anvil_deal (#1492)
Browse files Browse the repository at this point in the history
## Description

_Concise description of proposed changes_

## Testing

Explain the quality checks that have been done on the code changes

## Additional Information

- [ ] I read the [contributing docs](../docs/contributing.md) (if this
is your first contribution)

Your ENS/address:



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Introduced the `anvil_deal` JSON-RPC request for handling ERC20 token
transactions.
- Added the `eth_createAccessList` JSON-RPC request to create access
lists for Ethereum transactions.
- Implemented a new `deal` method in the `TevmActionsApi` for ERC20
token distribution.

- **Bug Fixes**
- Enhanced error handling in transaction procedures to ensure robust
responses.

- **Tests**
- Added unit tests for the new `anvil_deal` and `eth_createAccessList`
procedures to validate functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
roninjin10 authored Nov 1, 2024
1 parent 1a03258 commit b99de65
Show file tree
Hide file tree
Showing 26 changed files with 468 additions and 2 deletions.
8 changes: 8 additions & 0 deletions .changeset/little-coats-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@tevm/memory-client": minor
"@tevm/decorators": minor
"@tevm/actions": minor
---

Added eth_createAccessList and anvil_deal json-rpc requests
Added MemoryClient.deal action
4 changes: 4 additions & 0 deletions packages/actions/src/anvil/AnvilHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AnvilDealParams,
AnvilDropTransactionParams,
AnvilDumpStateParams,
AnvilGetAutomineParams,
Expand All @@ -14,6 +15,7 @@ import type {
AnvilStopImpersonatingAccountParams,
} from './AnvilParams.js'
import type {
AnvilDealResult,
AnvilDropTransactionResult,
AnvilDumpStateResult,
AnvilGetAutomineResult,
Expand Down Expand Up @@ -64,3 +66,5 @@ export type AnvilDumpStateHandler = (params: AnvilDumpStateParams) => Promise<An
// TODO make this the same as our load state
// anvil_loadState
export type AnvilLoadStateHandler = (params: AnvilLoadStateParams) => Promise<AnvilLoadStateResult>
// anvil_deal
export type AnvilDealHandler = (params: AnvilDealParams) => Promise<AnvilDealResult>
7 changes: 7 additions & 0 deletions packages/actions/src/anvil/AnvilJsonRpcRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { JsonRpcRequest } from '@tevm/jsonrpc'
import type { Address, Hex } from '@tevm/utils'
import type { SerializeToJson } from '../utils/SerializeToJson.js'
import type { AnvilDealParams } from './AnvilParams.js'
import type {
AnvilDropTransactionParams,
AnvilDumpStateParams,
Expand Down Expand Up @@ -111,6 +112,11 @@ export type AnvilLoadStateJsonRpcRequest = JsonRpcRequest<
'anvil_loadState',
readonly [SerializeToJson<AnvilLoadStateParams>]
>
// anvil_deal
/**
* JSON-RPC request for `anvil_deal` method
*/
export type AnvilDealJsonRpcRequest = JsonRpcRequest<'anvil_deal', [SerializeToJson<AnvilDealParams>]>

export type AnvilJsonRpcRequest =
| AnvilImpersonateAccountJsonRpcRequest
Expand All @@ -127,3 +133,4 @@ export type AnvilJsonRpcRequest =
| AnvilDumpStateJsonRpcRequest
| AnvilLoadStateJsonRpcRequest
| AnvilSetCoinbaseJsonRpcRequest
| AnvilDealJsonRpcRequest
6 changes: 6 additions & 0 deletions packages/actions/src/anvil/AnvilJsonRpcResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { JsonRpcResponse } from '@tevm/jsonrpc'
import type { Address } from '@tevm/utils'
import type { SerializeToJson } from '../utils/SerializeToJson.js'
import type {
AnvilDealResult,
AnvilDropTransactionResult,
AnvilDumpStateResult,
AnvilGetAutomineResult,
Expand Down Expand Up @@ -144,3 +145,8 @@ export type AnvilLoadStateJsonRpcResponse = JsonRpcResponse<
SerializeToJson<AnvilLoadStateResult>,
AnvilError
>
// anvil_deal
/**
* JSON-RPC response for `anvil_deal` procedure
*/
export type AnvilDealJsonRpcResponse = JsonRpcResponse<'anvil_deal', SerializeToJson<AnvilDealResult>, AnvilError>
9 changes: 9 additions & 0 deletions packages/actions/src/anvil/AnvilParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,12 @@ export type AnvilLoadStateParams = {
*/
readonly state: Record<Hex, Hex>
}

export type AnvilDealParams = {
/** The address of the ERC20 token to deal */
erc20?: Address
/** The owner of the dealt tokens */
account: Address
/** The amount of tokens to deal */
amount: bigint
}
24 changes: 24 additions & 0 deletions packages/actions/src/anvil/AnvilProcedure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AnvilDealJsonRpcRequest,
AnvilDropTransactionJsonRpcRequest,
AnvilDumpStateJsonRpcRequest,
AnvilGetAutomineJsonRpcRequest,
Expand All @@ -15,6 +16,7 @@ import type {
AnvilStopImpersonatingAccountJsonRpcRequest,
} from './AnvilJsonRpcRequest.js'
import type {
AnvilDealJsonRpcResponse,
AnvilDropTransactionJsonRpcResponse,
AnvilDumpStateJsonRpcResponse,
AnvilGetAutomineJsonRpcResponse,
Expand Down Expand Up @@ -120,3 +122,25 @@ export type AnvilDumpStateProcedure = (request: AnvilDumpStateJsonRpcRequest) =>
* JSON-RPC procedure for `anvil_loadState`
*/
export type AnvilLoadStateProcedure = (request: AnvilLoadStateJsonRpcRequest) => Promise<AnvilLoadStateJsonRpcResponse>
// anvil_deal
/**
* JSON-RPC procedure for `anvil_deal`
*/
export type AnvilDealProcedure = (request: AnvilDealJsonRpcRequest) => Promise<AnvilDealJsonRpcResponse>

export type AnvilProcedure =
| AnvilSetCoinbaseProcedure
| AnvilImpersonateAccountProcedure
| AnvilStopImpersonatingAccountProcedure
| AnvilGetAutomineProcedure
| AnvilMineProcedure
| AnvilResetProcedure
| AnvilDropTransactionProcedure
| AnvilSetBalanceProcedure
| AnvilSetCodeProcedure
| AnvilSetNonceProcedure
| AnvilSetStorageAtProcedure
| AnvilSetChainIdProcedure
| AnvilDumpStateProcedure
| AnvilLoadStateProcedure
| AnvilDealProcedure
4 changes: 4 additions & 0 deletions packages/actions/src/anvil/AnvilResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ export type AnvilDumpStateResult = Hex
// TODO make this the same as our load state
// anvil_loadState tf
export type AnvilLoadStateResult = null
// anvil_deal
export type AnvilDealResult = {
errors?: Error[]
}
59 changes: 59 additions & 0 deletions packages/actions/src/anvil/anvilDealHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ERC20 } from '@tevm/contract'
import { numberToHex } from '@tevm/utils'
import { encodeFunctionData } from 'viem'
import { setAccountHandler } from '../SetAccount/setAccountHandler.js'
import { ethCreateAccessListProcedure } from '../eth/ethCreateAccessListProcedure.js'
import { anvilSetStorageAtJsonRpcProcedure } from './anvilSetStorageAtProcedure.js'

/**
* Deals ERC20 tokens to an account by overriding the storage of balanceOf(account)
* @param {import('@tevm/node').TevmNode} client
* @returns {import('./AnvilHandler.js').AnvilDealHandler}
*/
export const dealHandler =
(client) =>
async ({ erc20, account, amount }) => {
if (!erc20) {
return setAccountHandler(client)({
address: account,
balance: amount,
})
}

const value = numberToHex(amount, { size: 32 })

// Get storage slots accessed by balanceOf
const accessListResponse = await ethCreateAccessListProcedure(client)({
method: 'eth_createAccessList',
params: [
{
to: erc20,
data: encodeFunctionData({
abi: ERC20.abi,
functionName: 'balanceOf',
args: [account],
}),
},
],
id: 1,
jsonrpc: '2.0',
})

if (!accessListResponse.result?.accessList) {
throw new Error('Failed to get access list')
}

// Try each storage slot until we find the right one
for (const { address, storageKeys } of accessListResponse.result.accessList) {
for (const slot of storageKeys) {
await anvilSetStorageAtJsonRpcProcedure(client)({
method: 'anvil_setStorageAt',
params: [address, slot, value],
id: 1,
jsonrpc: '2.0',
})
}
}

return {}
}
55 changes: 55 additions & 0 deletions packages/actions/src/anvil/anvilDealProcedure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { hexToBigInt } from 'viem'
import { dealHandler } from './anvilDealHandler.js'

/**
* JSON-RPC procedure for anvil_deal
* Deals ERC20 tokens to an account by overriding the storage of balanceOf(account)
* @param {import('@tevm/node').TevmNode} client
* @returns {import('./AnvilProcedure.js').AnvilDealProcedure}
* @example
* ```typescript
* const response = await client.request({
* method: 'anvil_deal',
* params: [{
* erc20: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // Optional: USDC address
* account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* amount: 1000000n // 1 USDC (6 decimals)
* }],
* id: 1,
* jsonrpc: '2.0'
* })
* ```
*/
export const anvilDealJsonRpcProcedure = (client) => async (request) => {
const [{ erc20, account, amount }] = request.params

const result = await dealHandler(client)({
...(erc20 !== undefined ? { erc20 } : {}),
account,
amount: hexToBigInt(amount),
})

if ('errors' in result && result.errors) {
/**
* @type {import('./AnvilJsonRpcResponse.js').AnvilDealJsonRpcResponse}
*/
const out = {
jsonrpc: request.jsonrpc,
...(request.id !== undefined ? { id: request.id } : {}),
method: 'anvil_deal',
error: {
// @ts-expect-error being lazy here
code: (result.errors[0]?.code ?? -32000).toString(),
message: result.errors[0]?.message ?? result.errors[0]?.name ?? 'An unknown error occured',
},
}
return out
}

return {
jsonrpc: request.jsonrpc,
...(request.id !== undefined ? { id: request.id } : {}),
method: 'anvil_deal',
result: {},
}
}
62 changes: 62 additions & 0 deletions packages/actions/src/anvil/anvilDealProcedure.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { createAddress } from '@tevm/address'
import { createTevmNode } from '@tevm/node'
import { TestERC20 } from '@tevm/test-utils'
import { describe, expect, it } from 'vitest'
import { setAccountHandler } from '../SetAccount/setAccountHandler.js'
import { anvilDealJsonRpcProcedure } from './anvilDealProcedure.js'

describe('anvilDealJsonRpcProcedure', () => {
it('should deal ERC20 tokens', async () => {
const client = createTevmNode()
const erc20 = TestERC20.withAddress(createAddress('0x66a44').toString())

// Deploy contract
await setAccountHandler(client)({
address: erc20.address,
deployedBytecode: erc20.deployedBytecode,
})

const result = await anvilDealJsonRpcProcedure(client)({
method: 'anvil_deal',
params: [
{
erc20: erc20.address,
account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
amount: '0xf4240', // 1M (6 decimals)
},
],
id: 1,
jsonrpc: '2.0',
})

expect(result).toEqual({
jsonrpc: '2.0',
id: 1,
method: 'anvil_deal',
result: {},
})
})

it('should deal native tokens when no erc20 address provided', async () => {
const client = createTevmNode()

const result = await anvilDealJsonRpcProcedure(client)({
method: 'anvil_deal',
params: [
{
account: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
amount: '0xde0b6b3a7640000', // 1 ETH
},
],
id: 1,
jsonrpc: '2.0',
})

expect(result).toEqual({
jsonrpc: '2.0',
id: 1,
method: 'anvil_deal',
result: {},
})
})
})
2 changes: 2 additions & 0 deletions packages/actions/src/anvil/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export * from './anvilSetCoinbaseProcedure.js'
export * from './anvilSetNonceProcedure.js'
export * from './anvilSetStorageAtProcedure.js'
export * from './anvilStopImpersonatingAccountProcedure.js'
export * from './anvilDealHandler.js'
export * from './anvilDealProcedure.js'
4 changes: 4 additions & 0 deletions packages/actions/src/createHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAccountProcedure } from './GetAccount/getAccountProcedure.js'
import { loadStateProcedure } from './LoadState/loadStateProcedure.js'
import { mineProcedure } from './Mine/mineProcedure.js'
import { setAccountProcedure } from './SetAccount/setAccountProcedure.js'
import { anvilDealJsonRpcProcedure } from './anvil/anvilDealProcedure.js'
import { anvilDropTransactionJsonRpcProcedure } from './anvil/anvilDropTransactionProcedure.js'
import { anvilDumpStateJsonRpcProcedure } from './anvil/anvilDumpStateProcedure.js'
import { anvilGetAutomineJsonRpcProcedure } from './anvil/anvilGetAutomineProcedure.js'
Expand All @@ -25,6 +26,7 @@ import { chainIdProcedure } from './eth/chainIdProcedure.js'
import { ethBlobBaseFeeJsonRpcProcedure } from './eth/ethBlobBaseFeeProcedure.js'
import { ethCallProcedure } from './eth/ethCallProcedure.js'
import { ethCoinbaseJsonRpcProcedure } from './eth/ethCoinbaseProcedure.js'
import { ethCreateAccessListProcedure } from './eth/ethCreateAccessListProcedure.js'
import { ethEstimateGasJsonRpcProcedure } from './eth/ethEstimateGasProcedure.js'
import { ethGetBlockByHashJsonRpcProcedure } from './eth/ethGetBlockByHashProcedure.js'
import { ethGetBlockByNumberJsonRpcProcedure } from './eth/ethGetBlockByNumberProcedure.js'
Expand Down Expand Up @@ -92,6 +94,7 @@ export const createHandlers = (client) => {
eth_blockNumber: blockNumberProcedure(client),
eth_chainId: chainIdProcedure(client),
eth_call: ethCallProcedure(client),
eth_createAccessList: ethCreateAccessListProcedure(client),
eth_getCode: getCodeProcedure(client),
eth_getStorageAt: getStorageAtProcedure(client),
eth_gasPrice: gasPriceProcedure(client),
Expand Down Expand Up @@ -145,6 +148,7 @@ export const createHandlers = (client) => {
}

const anvilHandlers = {
anvil_deal: anvilDealJsonRpcProcedure(client),
anvil_setCode: anvilSetCodeJsonRpcProcedure(client),
anvil_setBalance: anvilSetBalanceJsonRpcProcedure(client),
anvil_setNonce: anvilSetNonceJsonRpcProcedure(client),
Expand Down
11 changes: 10 additions & 1 deletion packages/actions/src/eth/EthJsonRpcRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type JsonRpcTransaction = {
/**
* The address from which the transaction is sent
*/
from: Address
from?: Address
/**
* The address to which the transaction is addressed
*/
Expand Down Expand Up @@ -293,6 +293,14 @@ export type EthNewPendingTransactionFilterJsonRpcRequest = JsonRpcRequest<
* JSON-RPC request for `eth_uninstallFilter` procedure
*/
export type EthUninstallFilterJsonRpcRequest = JsonRpcRequest<'eth_uninstallFilter', readonly [filterId: Hex]>
// eth_createAccessList
/**
* JSON-RPC request for `eth_createAccessList` procedure
*/
export type EthCreateAccessListJsonRpcRequest = JsonRpcRequest<
'eth_createAccessList',
readonly [tx: JsonRpcTransaction, tag?: BlockTag | Hex]
>

export type EthJsonRpcRequest =
| EthAccountsJsonRpcRequest
Expand Down Expand Up @@ -334,3 +342,4 @@ export type EthJsonRpcRequest =
| EthNewBlockFilterJsonRpcRequest
| EthNewPendingTransactionFilterJsonRpcRequest
| EthUninstallFilterJsonRpcRequest
| EthCreateAccessListJsonRpcRequest
16 changes: 16 additions & 0 deletions packages/actions/src/eth/EthJsonRpcResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,19 @@ export type EthNewPendingTransactionFilterJsonRpcResponse = JsonRpcResponse<
* JSON-RPC response for `eth_uninstallFilter` procedure
*/
export type EthUninstallFilterJsonRpcResponse = JsonRpcResponse<'eth_uninstallFilter', boolean, string | number>

// eth_createAccessList
/**
* JSON-RPC response for `eth_createAccessList` procedure
*/
export type EthCreateAccessListJsonRpcResponse = JsonRpcResponse<
'eth_createAccessList',
{
accessList: Array<{
address: Address
storageKeys: Hex[]
}>
gasUsed: Hex
},
string | number
>
Loading

0 comments on commit b99de65

Please sign in to comment.