diff --git a/.changeset/eip-1898-block-identifier.md b/.changeset/eip-1898-block-identifier.md new file mode 100644 index 0000000000..174f4176f2 --- /dev/null +++ b/.changeset/eip-1898-block-identifier.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added EIP-1898 block identifier support (`blockHash` and `requireCanonical` parameters) for `call`, `getBalance`, `getCode`, `getProof`, `getStorageAt`, and `getTransactionCount` actions. diff --git a/site/pages/docs/actions/public/call.md b/site/pages/docs/actions/public/call.md index 71a04c28f0..9131625257 100644 --- a/site/pages/docs/actions/public/call.md +++ b/site/pages/docs/actions/public/call.md @@ -247,6 +247,41 @@ const data = await publicClient.call({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +The block hash to perform the call against. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const data = await publicClient.call({ + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', // [!code focus] + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const data = await publicClient.call({ + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true, // [!code focus] + account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + data: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', +}) +``` + ### code (optional) - **Type:** diff --git a/site/pages/docs/actions/public/getBalance.md b/site/pages/docs/actions/public/getBalance.md index 5fe4a7f655..5772e38de8 100644 --- a/site/pages/docs/actions/public/getBalance.md +++ b/site/pages/docs/actions/public/getBalance.md @@ -77,6 +77,37 @@ const balance = await publicClient.getBalance({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +The balance of the account at a block hash. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const balance = await publicClient.getBalance({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d' // [!code focus] +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const balance = await publicClient.getBalance({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true // [!code focus] +}) +``` + ## Tips - You can convert the balance to ether units with [`formatEther`](/docs/utilities/formatEther). diff --git a/site/pages/docs/actions/public/getProof.md b/site/pages/docs/actions/public/getProof.md index b7c823e7f5..9998883a9e 100644 --- a/site/pages/docs/actions/public/getProof.md +++ b/site/pages/docs/actions/public/getProof.md @@ -110,6 +110,39 @@ const proof = await publicClient.getProof({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +Proof at a given block hash. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const proof = await publicClient.getProof({ + address: '0x4200000000000000000000000000000000000016', + storageKeys: [ + '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', + ], + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d' // [!code focus] +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const proof = await publicClient.getProof({ + address: '0x4200000000000000000000000000000000000016', + storageKeys: [ + '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', + ], + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true // [!code focus] +}) +``` + ## JSON-RPC Method - Calls [`eth_getProof`](https://eips.ethereum.org/EIPS/eip-1186). diff --git a/site/pages/docs/actions/public/getTransactionCount.md b/site/pages/docs/actions/public/getTransactionCount.md index 53947005e3..5a0da9d3c4 100644 --- a/site/pages/docs/actions/public/getTransactionCount.md +++ b/site/pages/docs/actions/public/getTransactionCount.md @@ -77,6 +77,37 @@ const transactionCount = await publicClient.getTransactionCount({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +Get the count at a block hash. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const transactionCount = await publicClient.getTransactionCount({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d' // [!code focus] +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const transactionCount = await publicClient.getTransactionCount({ + address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true // [!code focus] +}) +``` + ## Notes - The transaction count of an account can also be used as a nonce. diff --git a/site/pages/docs/contract/getCode.md b/site/pages/docs/contract/getCode.md index 223472bb0d..fc6439fe80 100644 --- a/site/pages/docs/contract/getCode.md +++ b/site/pages/docs/contract/getCode.md @@ -77,6 +77,33 @@ const bytecode = await publicClient.getCode({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +The block hash to perform the bytecode read against. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const bytecode = await publicClient.getCode({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', // [!code focus] +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const bytecode = await publicClient.getCode({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true, // [!code focus] +}) +``` + ## JSON-RPC Method [`eth_getCode`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getcode) diff --git a/site/pages/docs/contract/getStorageAt.md b/site/pages/docs/contract/getStorageAt.md index b8a5f631ce..e59f64b7b8 100644 --- a/site/pages/docs/contract/getStorageAt.md +++ b/site/pages/docs/contract/getStorageAt.md @@ -96,6 +96,35 @@ const bytecode = await publicClient.getStorageAt({ }) ``` +### blockHash (optional) + +- **Type:** `Hash` + +The block hash to perform the storage slot read against. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const bytecode = await publicClient.getStorageAt({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + slot: toHex(0), + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', // [!code focus] +}) +``` + +### requireCanonical (optional) + +- **Type:** `boolean` + +Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. Implements [EIP-1898](https://eips.ethereum.org/EIPS/eip-1898). + +```ts +const bytecode = await publicClient.getStorageAt({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + slot: toHex(0), + blockHash: '0x89644bbd5c8d682a2e9611170e6c1f02573d866d286f006cbf517eec7254ec2d', + requireCanonical: true, // [!code focus] +}) +``` + ## JSON-RPC Method [`eth_getStorageAt`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat) \ No newline at end of file diff --git a/src/actions/public/call.test.ts b/src/actions/public/call.test.ts index c22422f879..ca1d776692 100644 --- a/src/actions/public/call.test.ts +++ b/src/actions/public/call.test.ts @@ -47,6 +47,7 @@ import { parseGwei } from '../../utils/unit/parseGwei.js' import { wait } from '../../utils/wait.js' import { signAuthorization } from '../wallet/signAuthorization.js' import { call, getRevertErrorData } from './call.js' +import { getBlock } from './getBlock.js' import { readContract } from './readContract.js' const client = anvilMainnet.getClient({ account: accounts[0].address }) @@ -200,6 +201,41 @@ test.skip('args: blockNumber', async () => { expect(data).toMatchInlineSnapshot('undefined') }) +test('args: blockHash (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + const { data } = await call(client, { + blockHash: block.hash!, + data: name4bytes, + account: sourceAccount.address, + to: wagmiContractAddress, + }) + + expect(data).toMatchInlineSnapshot( + '"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000"', + ) +}) + +test('args: blockHash + requireCanonical (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + const { data } = await call(client, { + blockHash: block.hash!, + requireCanonical: true, + data: name4bytes, + account: sourceAccount.address, + to: wagmiContractAddress, + }) + + expect(data).toMatchInlineSnapshot( + '"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000"', + ) +}) + test('args: override', async () => { const fakeName = 'NotWagmi' diff --git a/src/actions/public/call.ts b/src/actions/public/call.ts index 1f3ede9878..8d7ef934b1 100644 --- a/src/actions/public/call.ts +++ b/src/actions/public/call.ts @@ -28,7 +28,7 @@ import { 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 { Hash, Hex } from '../../types/misc.js' import type { RpcTransactionRequest } from '../../types/rpc.js' import type { StateOverride } from '../../types/stateOverride.js' import type { TransactionRequest } from '../../types/transaction.js' @@ -45,15 +45,15 @@ import { type EncodeFunctionDataErrorType, encodeFunctionData, } from '../../utils/abi/encodeFunctionData.js' +import { + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' import type { RequestErrorType } from '../../utils/buildRequest.js' import { type GetChainContractAddressErrorType, getChainContractAddress, } from '../../utils/chain/getChainContractAddress.js' -import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' import { type GetCallErrorReturnType, getCallError, @@ -97,17 +97,29 @@ export type CallParameters< stateOverride?: StateOverride | undefined } & ( | { - /** The balance of the account at a block number. */ + /** The block number to perform the call against. */ blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: undefined /** - * The balance of the account at a block tag. + * The block tag to perform the call against. * @default 'latest' */ blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The block hash to perform the call against. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) type FormattedCall = @@ -119,7 +131,7 @@ export type CallErrorType = GetCallErrorReturnType< | ParseAccountErrorType | SerializeStateOverrideErrorType | AssertRequestErrorType - | NumberToHexErrorType + | FormatBlockParameterErrorType | FormatTransactionRequestErrorType | ScheduleMulticallErrorType | RequestErrorType @@ -160,8 +172,10 @@ export async function call( account: account_ = client.account, authorizationList, batch = Boolean(client.batch?.multicall), + blockHash, blockNumber, blockTag = client.experimental_blockTag ?? 'latest', + requireCanonical, accessList, blobs, blockOverrides, @@ -214,9 +228,12 @@ export async function call( try { assertRequest(args as AssertRequestParameters) - const blockNumberHex = - typeof blockNumber === 'bigint' ? numberToHex(blockNumber) : undefined - const block = blockNumberHex || blockTag + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const rpcBlockOverrides = blockOverrides ? BlockOverrides.toRpc(blockOverrides) @@ -251,13 +268,16 @@ export async function call( batch && shouldPerformMulticall({ request }) && !rpcStateOverride && - !rpcBlockOverrides + !rpcBlockOverrides && + blockHash === undefined ) { try { return await scheduleMulticall(client, { ...request, + blockHash, blockNumber, blockTag, + requireCanonical, } as unknown as ScheduleMulticallParameters) } catch (err) { if ( @@ -331,7 +351,7 @@ function shouldPerformMulticall({ request }: { request: TransactionRequest }) { type ScheduleMulticallParameters = Pick< CallParameters, - 'blockNumber' | 'blockTag' + 'blockHash' | 'blockNumber' | 'blockTag' | 'requireCanonical' > & { data: Hex multicallAddress?: Address | undefined @@ -340,7 +360,7 @@ type ScheduleMulticallParameters = Pick< type ScheduleMulticallErrorType = | GetChainContractAddressErrorType - | NumberToHexErrorType + | FormatBlockParameterErrorType | CreateBatchSchedulerErrorType | EncodeFunctionDataErrorType | DecodeFunctionResultErrorType @@ -357,8 +377,10 @@ async function scheduleMulticall( wait = 0, } = typeof client.batch?.multicall === 'object' ? client.batch.multicall : {} const { + blockHash, blockNumber, blockTag = client.experimental_blockTag ?? 'latest', + requireCanonical, data, to, } = args @@ -376,12 +398,16 @@ async function scheduleMulticall( throw new ClientChainNotConfiguredError() })() - const blockNumberHex = - typeof blockNumber === 'bigint' ? numberToHex(blockNumber) : undefined - const block = blockNumberHex || blockTag + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) + const blockId = JSON.stringify(block) const { schedule } = createBatchScheduler({ - id: `${client.uid}.${block}`, + id: `${client.uid}.${blockId}`, wait, shouldSplitBatch(args) { const size = args.reduce((size, { data }) => size + (data.length - 2), 0) diff --git a/src/actions/public/getBalance.test.ts b/src/actions/public/getBalance.test.ts index 10d0bd7f22..143f8b95ac 100644 --- a/src/actions/public/getBalance.test.ts +++ b/src/actions/public/getBalance.test.ts @@ -7,6 +7,7 @@ import { setBalance } from '../test/setBalance.js' import { sendTransaction } from '../wallet/sendTransaction.js' import { getBalance } from './getBalance.js' +import { getBlock } from './getBlock.js' import { getBlockNumber } from './getBlockNumber.js' const client = anvilMainnet.getClient() @@ -85,3 +86,56 @@ test('gets balance at block number', async () => { }), ).toMatchInlineSnapshot('10000000000000000000000n') }) + +test('gets balance at block hash (EIP-1898)', async () => { + await setup() + const currentBlockNumber = await getBlockNumber(client) + const block = await getBlock(client, { blockNumber: currentBlockNumber }) + const prevBlock = await getBlock(client, { + blockNumber: currentBlockNumber - 1n, + }) + + expect( + await getBalance(client, { + address: targetAccount.address, + blockHash: block.hash!, + }), + ).toMatchInlineSnapshot('10006000000000000000000n') + + expect( + await getBalance(client, { + address: targetAccount.address, + blockHash: prevBlock.hash!, + }), + ).toMatchInlineSnapshot('10003000000000000000000n') +}) + +test('gets balance at block hash with requireCanonical (EIP-1898)', async () => { + await setup() + const currentBlockNumber = await getBlockNumber(client) + const block = await getBlock(client, { blockNumber: currentBlockNumber }) + + expect( + await getBalance(client, { + address: targetAccount.address, + blockHash: block.hash!, + requireCanonical: true, + }), + ).toMatchInlineSnapshot('10006000000000000000000n') +}) + +test('error: requireCanonical without blockHash', async () => { + expect( + getBalance(client, { + address: targetAccount.address, + blockTag: 'latest', + requireCanonical: true, + } as never), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [BaseError: \`requireCanonical\` can only be provided when \`blockHash\` is set. + + Version: viem@x.y.z] + `, + ) +}) diff --git a/src/actions/public/getBalance.ts b/src/actions/public/getBalance.ts index ca7e845c0d..dd14573f9c 100644 --- a/src/actions/public/getBalance.ts +++ b/src/actions/public/getBalance.ts @@ -5,11 +5,12 @@ import type { Transport } from '../../clients/transports/createTransport.js' import type { ErrorType } from '../../errors/utils.js' import type { BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' -import type { RequestErrorType } from '../../utils/buildRequest.js' +import type { Hash } from '../../types/misc.js' import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' export type GetBalanceParameters = { /** The address of the account. */ @@ -19,18 +20,30 @@ export type GetBalanceParameters = { /** The balance of the account at a block number. */ blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: undefined /** The balance of the account at a block tag. */ blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The balance of the account at a block specified by block hash. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) export type GetBalanceReturnType = bigint export type GetBalanceErrorType = - | NumberToHexErrorType + | FormatBlockParameterErrorType | RequestErrorType | ErrorType @@ -73,16 +86,22 @@ export async function getBalance( client: Client, { address, + blockHash, blockNumber, blockTag = client.experimental_blockTag ?? 'latest', + requireCanonical, }: GetBalanceParameters, ): Promise { - const blockNumberHex = - typeof blockNumber === 'bigint' ? numberToHex(blockNumber) : undefined + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const balance = await client.request({ method: 'eth_getBalance', - params: [address, blockNumberHex || blockTag], + params: [address, block], }) return BigInt(balance) } diff --git a/src/actions/public/getCode.test.ts b/src/actions/public/getCode.test.ts index 6829c59ed6..940c6a684f 100644 --- a/src/actions/public/getCode.test.ts +++ b/src/actions/public/getCode.test.ts @@ -3,6 +3,7 @@ import { expect, test } from 'vitest' import { wagmiContractConfig } from '~test/abis.js' import { anvilMainnet } from '~test/anvil.js' +import { getBlock } from './getBlock.js' import { getCode } from './getCode.js' const client = anvilMainnet.getClient() @@ -23,3 +24,30 @@ test('default', async () => { }), ).toBeDefined() }) + +test('args: blockHash (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getCode(client, { + address: wagmiContractConfig.address, + blockHash: block.hash!, + }), + ).toBeDefined() +}) + +test('args: blockHash + requireCanonical (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getCode(client, { + address: wagmiContractConfig.address, + blockHash: block.hash!, + requireCanonical: true, + }), + ).toBeDefined() +}) diff --git a/src/actions/public/getCode.ts b/src/actions/public/getCode.ts index 8c85a24e82..23f17f3386 100644 --- a/src/actions/public/getCode.ts +++ b/src/actions/public/getCode.ts @@ -5,12 +5,12 @@ import type { Transport } from '../../clients/transports/createTransport.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 { RequestErrorType } from '../../utils/buildRequest.js' +import type { Hash, Hex } from '../../types/misc.js' import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' export type GetCodeParameters = { address: Address @@ -18,17 +18,29 @@ export type GetCodeParameters = { | { blockNumber?: undefined blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The bytecode at a block specified by block hash. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) export type GetCodeReturnType = Hex | undefined export type GetCodeErrorType = - | NumberToHexErrorType + | FormatBlockParameterErrorType | RequestErrorType | ErrorType @@ -57,16 +69,28 @@ export type GetCodeErrorType = */ export async function getCode( client: Client, - { address, blockNumber, blockTag = 'latest' }: GetCodeParameters, + { + address, + blockHash, + blockNumber, + blockTag = 'latest', + requireCanonical, + }: GetCodeParameters, ): Promise { - const blockNumberHex = - blockNumber !== undefined ? numberToHex(blockNumber) : undefined + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const hex = await client.request( { method: 'eth_getCode', - params: [address, blockNumberHex || blockTag], + params: [address, block], + }, + { + dedupe: typeof blockNumber === 'bigint' || blockHash !== undefined, }, - { dedupe: Boolean(blockNumberHex) }, ) if (hex === '0x') return undefined return hex diff --git a/src/actions/public/getProof.test.ts b/src/actions/public/getProof.test.ts index 55740852c5..0840d5b97b 100644 --- a/src/actions/public/getProof.test.ts +++ b/src/actions/public/getProof.test.ts @@ -3,6 +3,7 @@ import { expect, test } from 'vitest' import { base } from '../../chains/index.js' import { createPublicClient } from '../../clients/createPublicClient.js' import { http } from '../../clients/transports/http.js' +import { getBlock } from './getBlock.js' import { getProof } from './getProof.js' test('default', async () => { @@ -30,3 +31,62 @@ test('default', async () => { ] `) }) + +test('args: blockHash (EIP-1898)', async () => { + const client = createPublicClient({ + chain: base, + transport: http(), + }) + + const block = await getBlock(client, { blockTag: 'latest' }) + + const result = await getProof(client, { + address: '0x4200000000000000000000000000000000000016', + storageKeys: [ + '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', + ], + blockHash: block.hash!, + }) + + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "accountProof", + "address", + "balance", + "codeHash", + "nonce", + "storageHash", + "storageProof", + ] + `) +}) + +test('args: blockHash + requireCanonical (EIP-1898)', async () => { + const client = createPublicClient({ + chain: base, + transport: http(), + }) + + const block = await getBlock(client, { blockTag: 'latest' }) + + const result = await getProof(client, { + address: '0x4200000000000000000000000000000000000016', + storageKeys: [ + '0x4a932049252365b3eedbc5190e18949f2ec11f39d3bef2d259764799a1b27d99', + ], + blockHash: block.hash!, + requireCanonical: true, + }) + + expect(Object.keys(result)).toMatchInlineSnapshot(` + [ + "accountProof", + "address", + "balance", + "codeHash", + "nonce", + "storageHash", + "storageProof", + ] + `) +}) diff --git a/src/actions/public/getProof.ts b/src/actions/public/getProof.ts index 83a3b7169e..aa41944375 100644 --- a/src/actions/public/getProof.ts +++ b/src/actions/public/getProof.ts @@ -6,11 +6,11 @@ import type { BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' import type { Hash } from '../../types/misc.js' import type { Proof } from '../../types/proof.js' -import type { RequestErrorType } from '../../utils/buildRequest.js' import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' import { type FormatProofErrorType, formatProof, @@ -26,6 +26,8 @@ export type GetProofParameters = { /** The block number. */ blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: undefined @@ -34,13 +36,23 @@ export type GetProofParameters = { * @default 'latest' */ blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The proof at a block specified by block hash. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) export type GetProofReturnType = Proof export type GetProofErrorType = - | NumberToHexErrorType + | FormatBlockParameterErrorType | FormatProofErrorType | RequestErrorType | ErrorType @@ -74,19 +86,23 @@ export async function getProof( client: Client, { address, + blockHash, blockNumber, - blockTag: blockTag_, + blockTag = 'latest', + requireCanonical, storageKeys, }: GetProofParameters, ): Promise { - const blockTag = blockTag_ ?? 'latest' - - const blockNumberHex = - blockNumber !== undefined ? numberToHex(blockNumber) : undefined + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const proof = await client.request({ method: 'eth_getProof', - params: [address, storageKeys, blockNumberHex || blockTag], + params: [address, storageKeys, block], }) return formatProof(proof) diff --git a/src/actions/public/getStorageAt.test.ts b/src/actions/public/getStorageAt.test.ts index efe1215d6f..a8ab2ec129 100644 --- a/src/actions/public/getStorageAt.test.ts +++ b/src/actions/public/getStorageAt.test.ts @@ -3,6 +3,7 @@ import { expect, test } from 'vitest' import { wagmiContractConfig } from '~test/abis.js' import { anvilMainnet } from '~test/anvil.js' +import { getBlock } from './getBlock.js' import { getStorageAt } from './getStorageAt.js' const client = anvilMainnet.getClient() @@ -31,3 +32,32 @@ test('args: blockNumber', async () => { }), ).toBe('0x7761676d6900000000000000000000000000000000000000000000000000000a') }) + +test('args: blockHash (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getStorageAt(client, { + address: wagmiContractConfig.address, + slot: '0x0', + blockHash: block.hash!, + }), + ).toBe('0x7761676d6900000000000000000000000000000000000000000000000000000a') +}) + +test('args: blockHash + requireCanonical (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getStorageAt(client, { + address: wagmiContractConfig.address, + slot: '0x0', + blockHash: block.hash!, + requireCanonical: true, + }), + ).toBe('0x7761676d6900000000000000000000000000000000000000000000000000000a') +}) diff --git a/src/actions/public/getStorageAt.ts b/src/actions/public/getStorageAt.ts index b4e350c1fa..24aa093419 100644 --- a/src/actions/public/getStorageAt.ts +++ b/src/actions/public/getStorageAt.ts @@ -5,12 +5,12 @@ import type { Transport } from '../../clients/transports/createTransport.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 { RequestErrorType } from '../../utils/buildRequest.js' +import type { Hash, Hex } from '../../types/misc.js' import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' +import type { RequestErrorType } from '../../utils/buildRequest.js' export type GetStorageAtParameters = { address: Address @@ -19,17 +19,29 @@ export type GetStorageAtParameters = { | { blockNumber?: undefined blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The storage value at a block specified by block hash. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) export type GetStorageAtReturnType = Hex | undefined export type GetStorageAtErrorType = - | NumberToHexErrorType + | FormatBlockParameterErrorType | RequestErrorType | ErrorType @@ -59,13 +71,24 @@ export type GetStorageAtErrorType = */ export async function getStorageAt( client: Client, - { address, blockNumber, blockTag = 'latest', slot }: GetStorageAtParameters, + { + address, + blockHash, + blockNumber, + blockTag = 'latest', + requireCanonical, + slot, + }: GetStorageAtParameters, ): Promise { - const blockNumberHex = - blockNumber !== undefined ? numberToHex(blockNumber) : undefined + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const data = await client.request({ method: 'eth_getStorageAt', - params: [address, slot, blockNumberHex || blockTag], + params: [address, slot, block], }) return data } diff --git a/src/actions/public/getTransactionCount.test.ts b/src/actions/public/getTransactionCount.test.ts index cabf1e1322..0d1fcaea9f 100644 --- a/src/actions/public/getTransactionCount.test.ts +++ b/src/actions/public/getTransactionCount.test.ts @@ -6,6 +6,7 @@ import { mine } from '../test/mine.js' import { setNonce } from '../test/setNonce.js' import { sendTransaction } from '../wallet/sendTransaction.js' +import { getBlock } from './getBlock.js' import { getTransactionCount } from './getTransactionCount.js' const client = anvilMainnet.getClient() @@ -59,3 +60,30 @@ test('no count', async () => { }), ).toBe(0) }) + +test('args: blockHash (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getTransactionCount(client, { + address: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', + blockHash: block.hash!, + }), + ).toBe(676) +}) + +test('args: blockHash + requireCanonical (EIP-1898)', async () => { + const block = await getBlock(client, { + blockNumber: anvilMainnet.forkBlockNumber, + }) + + expect( + await getTransactionCount(client, { + address: '0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac', + blockHash: block.hash!, + requireCanonical: true, + }), + ).toBe(676) +}) diff --git a/src/actions/public/getTransactionCount.ts b/src/actions/public/getTransactionCount.ts index f100eb5cf6..b270cb2bca 100644 --- a/src/actions/public/getTransactionCount.ts +++ b/src/actions/public/getTransactionCount.ts @@ -6,15 +6,16 @@ import type { Transport } from '../../clients/transports/createTransport.js' import type { ErrorType } from '../../errors/utils.js' import type { BlockTag } from '../../types/block.js' import type { Chain } from '../../types/chain.js' +import type { Hash } from '../../types/misc.js' +import { + type FormatBlockParameterErrorType, + formatBlockParameter, +} from '../../utils/block/formatBlockParameter.js' import type { RequestErrorType } from '../../utils/buildRequest.js' import { type HexToNumberErrorType, hexToNumber, } from '../../utils/encoding/fromHex.js' -import { - type NumberToHexErrorType, - numberToHex, -} from '../../utils/encoding/toHex.js' export type GetTransactionCountParameters = { /** The account address. */ @@ -24,18 +25,30 @@ export type GetTransactionCountParameters = { /** The block number. */ blockNumber?: bigint | undefined blockTag?: undefined + blockHash?: undefined + requireCanonical?: undefined } | { blockNumber?: undefined /** The block tag. Defaults to 'latest'. */ blockTag?: BlockTag | undefined + blockHash?: undefined + requireCanonical?: undefined + } + | { + blockNumber?: undefined + blockTag?: undefined + /** The transaction count at a block specified by block hash. */ + blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain. Only allowed in conjunction with `blockHash`. */ + requireCanonical?: boolean | undefined } ) export type GetTransactionCountReturnType = number export type GetTransactionCountErrorType = | RequestErrorType - | NumberToHexErrorType + | FormatBlockParameterErrorType | HexToNumberErrorType | ErrorType @@ -67,18 +80,27 @@ export async function getTransactionCount< account extends Account | undefined, >( client: Client, - { address, blockTag = 'latest', blockNumber }: GetTransactionCountParameters, + { + address, + blockHash, + blockNumber, + blockTag = 'latest', + requireCanonical, + }: GetTransactionCountParameters, ): Promise { + const block = formatBlockParameter({ + blockHash, + blockNumber, + blockTag, + requireCanonical, + }) const count = await client.request( { method: 'eth_getTransactionCount', - params: [ - address, - typeof blockNumber === 'bigint' ? numberToHex(blockNumber) : blockTag, - ], + params: [address, block], }, { - dedupe: Boolean(blockNumber), + dedupe: typeof blockNumber === 'bigint' || blockHash !== undefined, }, ) return hexToNumber(count) diff --git a/src/types/block.ts b/src/types/block.ts index f0164c863b..b58e23b4e5 100644 --- a/src/types/block.ts +++ b/src/types/block.ts @@ -69,10 +69,7 @@ export type Block< withdrawalsRoot?: Hex | undefined } -export type BlockIdentifier = { - /** Whether or not to throw an error if the block is not in the canonical chain as described below. Only allowed in conjunction with the blockHash tag. Defaults to false. */ - requireCanonical?: boolean | undefined -} & ( +export type BlockIdentifier = | { /** The block in the canonical chain with this number */ blockNumber: BlockNumber @@ -80,8 +77,9 @@ export type BlockIdentifier = { | { /** The block uniquely identified by this hash. The `blockNumber` and `blockHash` properties are mutually exclusive; exactly one of them must be set. */ blockHash: Hash + /** Whether or not to throw an error if the block is not in the canonical chain as described below. Only allowed in conjunction with the blockHash tag. Defaults to false. */ + requireCanonical?: boolean | undefined } -) /** Represents a block number in the blockchain. */ export type BlockNumber = quantity diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 9dc31c49b6..04ac70c7bd 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -973,7 +973,7 @@ export type PublicRpcSchema = [ address: Address, /** An array of storage-keys that should be proofed and included. */ storageKeys: Hash[], - block: BlockNumber | BlockTag, + block: BlockNumber | BlockTag | BlockIdentifier, ] ReturnType: Proof }, diff --git a/src/utils/block/formatBlockParameter.test.ts b/src/utils/block/formatBlockParameter.test.ts new file mode 100644 index 0000000000..5eaf3743dd --- /dev/null +++ b/src/utils/block/formatBlockParameter.test.ts @@ -0,0 +1,87 @@ +import { expect, test } from 'vitest' + +import { formatBlockParameter } from './formatBlockParameter.js' + +test('returns blockTag when only blockTag is provided', () => { + expect(formatBlockParameter({ blockTag: 'latest' })).toBe('latest') + expect(formatBlockParameter({ blockTag: 'earliest' })).toBe('earliest') + expect(formatBlockParameter({ blockTag: 'pending' })).toBe('pending') + expect(formatBlockParameter({ blockTag: 'safe' })).toBe('safe') + expect(formatBlockParameter({ blockTag: 'finalized' })).toBe('finalized') +}) + +test('returns latest when no parameters provided', () => { + expect(formatBlockParameter({})).toBe('latest') +}) + +test('returns hex block number when blockNumber is provided', () => { + expect(formatBlockParameter({ blockNumber: 0n })).toBe('0x0') + expect(formatBlockParameter({ blockNumber: 69420n })).toBe('0x10f2c') + expect(formatBlockParameter({ blockNumber: 21397179n })).toBe('0x1467ebb') +}) + +test('returns block identifier object when blockHash is provided', () => { + const blockHash = + '0xf65631529d476553ca5b0056d6480c3970dd5ac884fee51d5b30ca7fceab8894' + + expect(formatBlockParameter({ blockHash })).toEqual({ blockHash }) +}) + +test('returns block identifier with requireCanonical when true', () => { + const blockHash = + '0xf65631529d476553ca5b0056d6480c3970dd5ac884fee51d5b30ca7fceab8894' + + expect(formatBlockParameter({ blockHash, requireCanonical: true })).toEqual({ + blockHash, + requireCanonical: true, + }) +}) + +test('omits requireCanonical when false (default per EIP-1898)', () => { + const blockHash = + '0xf65631529d476553ca5b0056d6480c3970dd5ac884fee51d5b30ca7fceab8894' + + expect(formatBlockParameter({ blockHash, requireCanonical: false })).toEqual({ + blockHash, + }) +}) + +test('blockHash takes priority over blockNumber and blockTag', () => { + const blockHash = + '0xf65631529d476553ca5b0056d6480c3970dd5ac884fee51d5b30ca7fceab8894' + + expect( + formatBlockParameter({ + blockHash, + blockNumber: 123n, + blockTag: 'latest', + }), + ).toEqual({ blockHash }) +}) + +test('blockNumber takes priority over blockTag', () => { + expect( + formatBlockParameter({ + blockNumber: 123n, + blockTag: 'latest', + }), + ).toBe('0x7b') +}) + +test('throws error when requireCanonical is provided without blockHash', () => { + expect(() => formatBlockParameter({ requireCanonical: true })).toThrowError( + '`requireCanonical` can only be provided when `blockHash` is set.', + ) + + expect(() => + formatBlockParameter({ blockTag: 'latest', requireCanonical: true }), + ).toThrowError( + '`requireCanonical` can only be provided when `blockHash` is set.', + ) + + expect(() => + formatBlockParameter({ blockNumber: 123n, requireCanonical: true }), + ).toThrowError( + '`requireCanonical` can only be provided when `blockHash` is set.', + ) +}) diff --git a/src/utils/block/formatBlockParameter.ts b/src/utils/block/formatBlockParameter.ts new file mode 100644 index 0000000000..ffb01f1abc --- /dev/null +++ b/src/utils/block/formatBlockParameter.ts @@ -0,0 +1,72 @@ +import { BaseError, type BaseErrorType } from '../../errors/base.js' +import type { BlockTag } from '../../types/block.js' +import type { Hash, Hex } from '../../types/misc.js' +import type { RpcBlockIdentifier } from '../../types/rpc.js' +import type { NumberToHexErrorType } from '../encoding/toHex.js' +import { numberToHex } from '../encoding/toHex.js' + +export type FormatBlockParameterParameters = { + blockHash?: Hash | undefined + blockNumber?: bigint | undefined + blockTag?: BlockTag | undefined + requireCanonical?: boolean | undefined +} + +export type RpcBlockHashIdentifier = Extract< + RpcBlockIdentifier, + { blockHash: Hash } +> + +export type FormatBlockParameterReturnType = + | Hex + | BlockTag + | RpcBlockHashIdentifier + +export type FormatBlockParameterErrorType = BaseErrorType | NumberToHexErrorType + +/** + * Formats block parameters for RPC calls according to EIP-1898. + * + * @param parameters - Block parameters + * @returns Formatted block parameter for RPC call + * + * @example + * // Using block tag + * formatBlockParameter({ blockTag: 'latest' }) + * // => 'latest' + * + * @example + * // Using block number + * formatBlockParameter({ blockNumber: 69420n }) + * // => '0x10f2c' + * + * @example + * // Using block hash (EIP-1898) + * formatBlockParameter({ blockHash: '0x...' }) + * // => { blockHash: '0x...' } + * + * @example + * // Using block hash with requireCanonical (EIP-1898) + * formatBlockParameter({ blockHash: '0x...', requireCanonical: true }) + * // => { blockHash: '0x...', requireCanonical: true } + */ +export function formatBlockParameter( + parameters: FormatBlockParameterParameters, +): FormatBlockParameterReturnType { + const { blockHash, blockNumber, blockTag, requireCanonical } = parameters + + if (requireCanonical !== undefined && !blockHash) + throw new BaseError( + '`requireCanonical` can only be provided when `blockHash` is set.', + ) + + if (blockHash) { + return requireCanonical ? { blockHash, requireCanonical } : { blockHash } + } + + if (typeof blockNumber === 'bigint') { + return numberToHex(blockNumber) + } + + return blockTag ?? 'latest' +}