From 190268595d94d7d9ade07605754dcae6be3765cf Mon Sep 17 00:00:00 2001 From: jxom Date: Sun, 19 May 2024 18:09:43 +1000 Subject: [PATCH] feat: `stateOverride` on `estimateGas` (#2275) * feat: implement stateOverride in estimateGas action * chore: tweaks * Update sharp-spies-roll.md --------- Co-authored-by: Conway <43109407+conwayconstar@users.noreply.github.com> --- .changeset/sharp-spies-roll.md | 5 + .gitignore | 1 + bun.lockb | Bin 554996 -> 554996 bytes site/pages/docs/actions/public/estimateGas.md | 26 +++ src/actions/public/call.test.ts | 130 +---------- src/actions/public/call.ts | 99 +------- src/actions/public/estimateGas.test.ts | 28 +++ src/actions/public/estimateGas.ts | 10 +- src/types/eip1193.ts | 10 + src/utils/stateOverride.test.ts | 214 ++++++++++++++++++ src/utils/stateOverride.ts | 98 ++++++++ 11 files changed, 400 insertions(+), 221 deletions(-) create mode 100644 .changeset/sharp-spies-roll.md create mode 100644 src/utils/stateOverride.test.ts create mode 100644 src/utils/stateOverride.ts diff --git a/.changeset/sharp-spies-roll.md b/.changeset/sharp-spies-roll.md new file mode 100644 index 0000000000..a8f28561a3 --- /dev/null +++ b/.changeset/sharp-spies-roll.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `stateOverride` on `estimateGas`. diff --git a/.gitignore b/.gitignore index 5164fc9cd9..8c70f7b92b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules tsconfig*.tsbuildinfo wagmi **/trusted-setups/**/*.txt +.idea # local env files .env diff --git a/bun.lockb b/bun.lockb index 2fc3f891aaa599ea1c53ff8b91465d467d952a42..6c654277e2d7a2a45426b3322e50f3a6c5705228 100755 GIT binary patch delta 9583 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 bPTK3-Yp&b5`*pK>?|vCqUvu3)M_l<^10hLK delta 9582 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 { ).toBe('0x556f1830') }) }) - -describe('parsing overrides', () => { - test('state mapping', () => { - const stateMapping: StateMapping = [ - { - slot: `0x${fourTwenty}`, - value: `0x${fourTwenty}`, - }, - ] - expect(parseStateMapping(stateMapping)).toMatchInlineSnapshot(` - { - "0x${fourTwenty}": "0x${fourTwenty}", - } - `) - }) - - test('state mapping: undefined', () => { - expect(parseStateMapping(undefined)).toMatchInlineSnapshot('undefined') - }) - - test('state mapping: invalid key', () => { - const stateMapping: StateMapping = [ - { - // invalid bytes length - slot: `0x${fourTwenty.slice(0, -1)}`, - value: `0x${fourTwenty}`, - }, - ] - - expect(() => - parseStateMapping(stateMapping), - ).toThrowErrorMatchingInlineSnapshot(` - [InvalidBytesLengthError: Hex is expected to be 66 hex long, but is 65 hex long. - - Version: viem@1.0.2] - `) - }) - - test('state mapping: invalid value', () => { - const stateMapping: StateMapping = [ - { - slot: `0x${fourTwenty}`, - value: `0x${fourTwenty.slice(0, -1)}`, - }, - ] - - expect(() => - parseStateMapping(stateMapping), - ).toThrowErrorMatchingInlineSnapshot(` - [InvalidBytesLengthError: Hex is expected to be 66 hex long, but is 65 hex long. - - Version: viem@1.0.2] - `) - }) - - test('args: code', () => { - const stateOverride: Omit = { - code: `0x${fourTwenty}`, - } - - expect(parseAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` - { - "code": "0x${fourTwenty}", - } - `) - - const emptyStateOverride: Omit = { - code: undefined, - } - - expect( - parseAccountStateOverride(emptyStateOverride), - ).toMatchInlineSnapshot(` - {} - `) - }) - - test('args: balance', () => { - const stateOverride: Omit = { - balance: 1n, - } - - expect(parseAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` - { - "balance": "0x1", - } - `) - - const emptyStateOverride: Omit = { - balance: undefined, - } - - expect( - parseAccountStateOverride(emptyStateOverride), - ).toMatchInlineSnapshot(` - {} - `) - }) - - test('args: nonce', () => { - const stateOverride: Omit = { - nonce: 1, - } - - expect(parseAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` - { - "nonce": "0x1", - } - `) - - const emptyStateOverride: Omit = { - nonce: undefined, - } - - expect( - parseAccountStateOverride(emptyStateOverride), - ).toMatchInlineSnapshot(` - {} - `) - }) -}) diff --git a/src/actions/public/call.ts b/src/actions/public/call.ts index 6106151538..050d3e448c 100644 --- a/src/actions/public/call.ts +++ b/src/actions/public/call.ts @@ -9,10 +9,6 @@ import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import { multicall3Abi } from '../../constants/abis.js' import { aggregate3Signature } from '../../constants/contract.js' -import { - InvalidAddressError, - type InvalidAddressErrorType, -} from '../../errors/address.js' import { BaseError } from '../../errors/base.js' import { ChainDoesNotSupportContract, @@ -22,27 +18,12 @@ import { RawContractError, type RawContractErrorType, } from '../../errors/contract.js' -import { - InvalidBytesLengthError, - type InvalidBytesLengthErrorType, -} from '../../errors/data.js' -import { - AccountStateConflictError, - type AccountStateConflictErrorType, - StateAssignmentConflictError, - type StateAssignmentConflictErrorType, -} from '../../errors/stateOverride.js' import type { ErrorType } from '../../errors/utils.js' import type { BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' import type { Hex } from '../../types/misc.js' -import type { - RpcAccountStateOverride, - RpcStateMapping, - RpcStateOverride, - RpcTransactionRequest, -} from '../../types/rpc.js' -import type { StateMapping, StateOverride } from '../../types/stateOverride.js' +import type { RpcTransactionRequest } from '../../types/rpc.js' +import type { StateOverride } from '../../types/stateOverride.js' import type { TransactionRequest } from '../../types/transaction.js' import type { ExactPartial, UnionOmit } from '../../types/utils.js' import { @@ -53,7 +34,6 @@ import { type EncodeFunctionDataErrorType, encodeFunctionData, } from '../../utils/abi/encodeFunctionData.js' -import { isAddress } from '../../utils/address/isAddress.js' import type { RequestErrorType } from '../../utils/buildRequest.js' import { type GetChainContractAddressErrorType, @@ -77,6 +57,10 @@ import { type CreateBatchSchedulerErrorType, createBatchScheduler, } from '../../utils/promise/createBatchScheduler.js' +import { + type SerializeStateOverrideErrorType, + serializeStateOverride, +} from '../../utils/stateOverride.js' import { assertRequest } from '../../utils/transaction/assertRequest.js' import type { AssertRequestErrorType, @@ -113,7 +97,7 @@ export type CallReturnType = { data: Hex | undefined } export type CallErrorType = GetCallErrorReturnType< | ParseAccountErrorType - | ParseStateOverrideErrorType + | SerializeStateOverrideErrorType | AssertRequestErrorType | NumberToHexErrorType | FormatTransactionRequestErrorType @@ -177,7 +161,7 @@ export async function call( const blockNumberHex = blockNumber ? numberToHex(blockNumber) : undefined const block = blockNumberHex || blockTag - const rpcStateOverride = parseStateOverride(stateOverride) + const rpcStateOverride = serializeStateOverride(stateOverride) const chainFormat = client.chain?.formatters?.transactionRequest?.format const format = chainFormat || formatTransactionRequest @@ -368,70 +352,3 @@ export function getRevertErrorData(err: unknown) { const error = err.walk() as RawContractError return typeof error?.data === 'object' ? error.data?.data : error.data } - -export type ParseStateMappingErrorType = InvalidBytesLengthErrorType - -export function parseStateMapping( - stateMapping: StateMapping | undefined, -): RpcStateMapping | undefined { - if (!stateMapping || stateMapping.length === 0) return undefined - return stateMapping.reduce((acc, { slot, value }) => { - if (slot.length !== 66) - throw new InvalidBytesLengthError({ - size: slot.length, - targetSize: 66, - type: 'hex', - }) - if (value.length !== 66) - throw new InvalidBytesLengthError({ - size: value.length, - targetSize: 66, - type: 'hex', - }) - acc[slot] = value - return acc - }, {} as RpcStateMapping) -} - -export type ParseAccountStateOverrideErrorType = - | NumberToHexErrorType - | StateAssignmentConflictErrorType - | ParseStateMappingErrorType - -export function parseAccountStateOverride( - args: Omit, -): RpcAccountStateOverride { - const { balance, nonce, state, stateDiff, code } = args - const rpcAccountStateOverride: RpcAccountStateOverride = {} - if (code !== undefined) rpcAccountStateOverride.code = code - if (balance !== undefined) - rpcAccountStateOverride.balance = numberToHex(balance) - if (nonce !== undefined) rpcAccountStateOverride.nonce = numberToHex(nonce) - if (state !== undefined) - rpcAccountStateOverride.state = parseStateMapping(state) - if (stateDiff !== undefined) { - if (rpcAccountStateOverride.state) throw new StateAssignmentConflictError() - rpcAccountStateOverride.stateDiff = parseStateMapping(stateDiff) - } - return rpcAccountStateOverride -} - -export type ParseStateOverrideErrorType = - | InvalidAddressErrorType - | AccountStateConflictErrorType - | ParseAccountStateOverrideErrorType - -export function parseStateOverride( - args?: StateOverride | undefined, -): RpcStateOverride | undefined { - if (!args) return undefined - const rpcStateOverride: RpcStateOverride = {} - for (const { address, ...accountState } of args) { - if (!isAddress(address, { strict: false })) - throw new InvalidAddressError({ address }) - if (rpcStateOverride[address]) - throw new AccountStateConflictError({ address: address }) - rpcStateOverride[address] = parseAccountStateOverride(accountState) - } - return rpcStateOverride -} diff --git a/src/actions/public/estimateGas.test.ts b/src/actions/public/estimateGas.test.ts index f591520399..b1f3aedf6a 100644 --- a/src/actions/public/estimateGas.test.ts +++ b/src/actions/public/estimateGas.test.ts @@ -4,8 +4,10 @@ import { accounts } from '~test/src/constants.js' import { kzg } from '~test/src/kzg.js' import { anvilMainnet } from '../../../test/src/anvil.js' import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { maxUint256 } from '../../constants/number.js' import { toBlobs } from '../../utils/blob/toBlobs.js' +import { toHex } from '../../utils/encoding/toHex.js' import { parseEther } from '../../utils/unit/parseEther.js' import { parseGwei } from '../../utils/unit/parseGwei.js' import { reset } from '../test/reset.js' @@ -137,6 +139,32 @@ test('args: blobs', async () => { ).toMatchInlineSnapshot('53001n') }) +test('args: override', async () => { + const transferData = + '0xa9059cbb00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000de0b6b3a7640000' + const balanceSlot = + '0xc651ee22c6951bb8b5bd29e8210fb394645a94315fe10eff2cc73de1aa75c137' + + expect( + await estimateGas(client, { + data: transferData, + account: accounts[0].address, + to: wethContractAddress, + stateOverride: [ + { + address: wethContractAddress, + stateDiff: [ + { + slot: balanceSlot, + value: toHex(maxUint256), + }, + ], + }, + ], + }), + ).toMatchInlineSnapshot('51594n') +}) + describe('local account', () => { test('default', async () => { expect( diff --git a/src/actions/public/estimateGas.ts b/src/actions/public/estimateGas.ts index 65bbd4adbd..baea4cf326 100644 --- a/src/actions/public/estimateGas.ts +++ b/src/actions/public/estimateGas.ts @@ -9,6 +9,7 @@ import type { Transport } from '../../clients/transports/createTransport.js' import type { BaseError } from '../../errors/base.js' import type { BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' +import type { StateOverride } from '../../types/stateOverride.js' import type { TransactionRequest } from '../../types/transaction.js' import type { UnionOmit } from '../../types/utils.js' import type { RequestErrorType } from '../../utils/buildRequest.js' @@ -25,6 +26,7 @@ import { type FormattedTransactionRequest, formatTransactionRequest, } from '../../utils/formatters/transactionRequest.js' +import { serializeStateOverride } from '../../utils/stateOverride.js' import { type AssertRequestErrorType, type AssertRequestParameters, @@ -43,6 +45,7 @@ export type EstimateGasParameters< TChain extends Chain | undefined = Chain | undefined, > = UnionOmit, 'from'> & { account?: Account | Address | undefined + stateOverride?: StateOverride | undefined } & ( | { /** The balance of the account at a block number. */ @@ -119,6 +122,7 @@ export async function estimateGas< nonce, to, value, + stateOverride, ...rest } = (await prepareTransactionRequest(client, { ...args, @@ -131,6 +135,8 @@ export async function estimateGas< const blockNumberHex = blockNumber ? numberToHex(blockNumber) : undefined const block = blockNumberHex || blockTag + const rpcStateOverride = serializeStateOverride(stateOverride) + assertRequest(args as AssertRequestParameters) const chainFormat = client.chain?.formatters?.transactionRequest?.format @@ -156,7 +162,9 @@ export async function estimateGas< const balance = await client.request({ method: 'eth_estimateGas', - params: block ? [request, block] : [request], + params: rpcStateOverride + ? [request, block || 'latest', rpcStateOverride] + : [request, block || 'latest'], }) return BigInt(balance) } catch (err) { diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 578c7a9fea..60d3296762 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -331,6 +331,11 @@ export type PublicRpcSchema = [ Parameters: | [transaction: TransactionRequest] | [transaction: TransactionRequest, block: BlockNumber | BlockTag] + | [ + transaction: TransactionRequest, + block: BlockNumber | BlockTag, + RpcStateOverride, + ] ReturnType: Quantity }, /** @@ -1197,6 +1202,11 @@ export type WalletRpcSchema = [ Parameters: | [transaction: TransactionRequest] | [transaction: TransactionRequest, block: BlockNumber | BlockTag] + | [ + transaction: TransactionRequest, + block: BlockNumber | BlockTag, + RpcStateOverride, + ] ReturnType: Quantity }, /** diff --git a/src/utils/stateOverride.test.ts b/src/utils/stateOverride.test.ts new file mode 100644 index 0000000000..4f839909ac --- /dev/null +++ b/src/utils/stateOverride.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, test } from 'vitest' +import type { StateMapping, StateOverride } from '../types/stateOverride.js' +import { + serializeAccountStateOverride, + serializeStateMapping, + serializeStateOverride, +} from './stateOverride.js' + +const fourTwenty = + '00000000000000000000000000000000000000000000000000000000000001a4' + +describe('serializeStateMapping', () => { + test('default', () => { + const stateMapping: StateMapping = [ + { + slot: `0x${fourTwenty}`, + value: `0x${fourTwenty}`, + }, + ] + expect(serializeStateMapping(stateMapping)).toMatchInlineSnapshot(` + { + "0x${fourTwenty}": "0x${fourTwenty}", + } + `) + }) + + test('undefined', () => { + expect(serializeStateMapping(undefined)).toMatchInlineSnapshot('undefined') + }) + + test('error: invalid key', () => { + const stateMapping: StateMapping = [ + { + // invalid bytes length + slot: `0x${fourTwenty.slice(0, -1)}`, + value: `0x${fourTwenty}`, + }, + ] + + expect(() => + serializeStateMapping(stateMapping), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidBytesLengthError: Hex is expected to be 66 hex long, but is 65 hex long. + + Version: viem@1.0.2] + `) + }) + + test('error: invalid value', () => { + const stateMapping: StateMapping = [ + { + slot: `0x${fourTwenty}`, + value: `0x${fourTwenty.slice(0, -1)}`, + }, + ] + + expect(() => + serializeStateMapping(stateMapping), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidBytesLengthError: Hex is expected to be 66 hex long, but is 65 hex long. + + Version: viem@1.0.2] + `) + }) +}) + +describe('serializeAccountStateOverride', () => { + test('args: code', () => { + const stateOverride: Omit = { + code: `0x${fourTwenty}`, + } + + expect(serializeAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` + { + "code": "0x${fourTwenty}", + } + `) + + const emptyStateOverride: Omit = { + code: undefined, + } + + expect( + serializeAccountStateOverride(emptyStateOverride), + ).toMatchInlineSnapshot(` + {} + `) + }) + + test('args: balance', () => { + const stateOverride: Omit = { + balance: 1n, + } + + expect(serializeAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` + { + "balance": "0x1", + } + `) + + const emptyStateOverride: Omit = { + balance: undefined, + } + + expect( + serializeAccountStateOverride(emptyStateOverride), + ).toMatchInlineSnapshot(` + {} + `) + }) + + test('args: nonce', () => { + const stateOverride: Omit = { + nonce: 1, + } + + expect(serializeAccountStateOverride(stateOverride)).toMatchInlineSnapshot(` + { + "nonce": "0x1", + } + `) + + const emptyStateOverride: Omit = { + nonce: undefined, + } + + expect( + serializeAccountStateOverride(emptyStateOverride), + ).toMatchInlineSnapshot(` + {} + `) + }) + + test('args: stateDiff', () => { + expect( + serializeAccountStateOverride({ + stateDiff: [{ slot: `0x${fourTwenty}`, value: `0x${fourTwenty}` }], + }), + ).toMatchInlineSnapshot(` + { + "stateDiff": { + "0x00000000000000000000000000000000000000000000000000000000000001a4": "0x00000000000000000000000000000000000000000000000000000000000001a4", + }, + } + `) + }) + + test('error: state conflict', () => { + expect(() => + serializeAccountStateOverride({ + stateDiff: [{ slot: `0x${fourTwenty}`, value: `0x${fourTwenty}` }], + state: [{ slot: `0x${fourTwenty}`, value: `0x${fourTwenty}` }], + }), + ).toThrowErrorMatchingInlineSnapshot(` + [StateAssignmentConflictError: state and stateDiff are set on the same account. + + Version: viem@1.0.2] + `) + }) +}) + +describe('serializeStateOverride', () => { + test('default', () => { + expect( + serializeStateOverride([ + { + address: '0x0000000000000000000000000000000000000000', + }, + ]), + ).toMatchInlineSnapshot(` + { + "0x0000000000000000000000000000000000000000": {}, + } + `) + }) + + test('undefined', () => { + expect(serializeStateOverride(undefined)).toMatchInlineSnapshot('undefined') + }) + + test('error: address conflict', () => { + expect(() => + serializeStateOverride([ + { + address: '0x0000000000000000000000000000000000000000', + }, + { + address: '0x0000000000000000000000000000000000000000', + }, + ]), + ).toThrowErrorMatchingInlineSnapshot(` + [AccountStateConflictError: State for account "0x0000000000000000000000000000000000000000" is set multiple times. + + Version: viem@1.0.2] + `) + }) + + test('error: invalid address', () => { + expect(() => + serializeStateOverride([ + { + address: '0xdeadbeef', + }, + ]), + ).toThrowErrorMatchingInlineSnapshot(` + [InvalidAddressError: Address "0xdeadbeef" is invalid. + + - Address must be a hex value of 20 bytes (40 hex characters). + - Address must match its checksum counterpart. + + Version: viem@1.0.2] + `) + }) +}) diff --git a/src/utils/stateOverride.ts b/src/utils/stateOverride.ts new file mode 100644 index 0000000000..eb4c4ea46b --- /dev/null +++ b/src/utils/stateOverride.ts @@ -0,0 +1,98 @@ +import { + InvalidAddressError, + type InvalidAddressErrorType, +} from '../errors/address.js' +import { + InvalidBytesLengthError, + type InvalidBytesLengthErrorType, +} from '../errors/data.js' +import { + AccountStateConflictError, + type AccountStateConflictErrorType, + StateAssignmentConflictError, + type StateAssignmentConflictErrorType, +} from '../errors/stateOverride.js' +import type { + RpcAccountStateOverride, + RpcStateMapping, + RpcStateOverride, +} from '../types/rpc.js' +import type { StateMapping, StateOverride } from '../types/stateOverride.js' +import { isAddress } from './address/isAddress.js' +import { type NumberToHexErrorType, numberToHex } from './encoding/toHex.js' + +export type SerializeStateMappingParameters = StateMapping | undefined + +export type SerializeStateMappingErrorType = InvalidBytesLengthErrorType + +export function serializeStateMapping( + stateMapping: SerializeStateMappingParameters, +): RpcStateMapping | undefined { + if (!stateMapping || stateMapping.length === 0) return undefined + return stateMapping.reduce((acc, { slot, value }) => { + if (slot.length !== 66) + throw new InvalidBytesLengthError({ + size: slot.length, + targetSize: 66, + type: 'hex', + }) + if (value.length !== 66) + throw new InvalidBytesLengthError({ + size: value.length, + targetSize: 66, + type: 'hex', + }) + acc[slot] = value + return acc + }, {} as RpcStateMapping) +} + +export type SerializeAccountStateOverrideParameters = Omit< + StateOverride[number], + 'address' +> + +export type SerializeAccountStateOverrideErrorType = + | NumberToHexErrorType + | StateAssignmentConflictErrorType + | SerializeStateMappingErrorType + +export function serializeAccountStateOverride( + parameters: SerializeAccountStateOverrideParameters, +): RpcAccountStateOverride { + const { balance, nonce, state, stateDiff, code } = parameters + const rpcAccountStateOverride: RpcAccountStateOverride = {} + if (code !== undefined) rpcAccountStateOverride.code = code + if (balance !== undefined) + rpcAccountStateOverride.balance = numberToHex(balance) + if (nonce !== undefined) rpcAccountStateOverride.nonce = numberToHex(nonce) + if (state !== undefined) + rpcAccountStateOverride.state = serializeStateMapping(state) + if (stateDiff !== undefined) { + if (rpcAccountStateOverride.state) throw new StateAssignmentConflictError() + rpcAccountStateOverride.stateDiff = serializeStateMapping(stateDiff) + } + return rpcAccountStateOverride +} + +export type SerializeStateOverrideParameters = StateOverride | undefined + +export type SerializeStateOverrideErrorType = + | InvalidAddressErrorType + | AccountStateConflictErrorType + | SerializeAccountStateOverrideErrorType + +export function serializeStateOverride( + parameters?: SerializeStateOverrideParameters, +): RpcStateOverride | undefined { + if (!parameters) return undefined + const rpcStateOverride: RpcStateOverride = {} + for (const { address, ...accountState } of parameters) { + if (!isAddress(address, { strict: false })) + throw new InvalidAddressError({ address }) + if (rpcStateOverride[address]) + throw new AccountStateConflictError({ address: address }) + rpcStateOverride[address] = serializeAccountStateOverride(accountState) + } + return rpcStateOverride +}