From b502a7271f09cb105f230c7715cef8852944b0eb Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:16:23 +1100 Subject: [PATCH 1/5] feat: support fillTransaction --- pnpm-lock.yaml | 42 +- pnpm-workspace.yaml | 4 +- src/actions/index.ts | 6 + src/actions/public/fillTransaction.test.ts | 378 +++ src/actions/public/fillTransaction.ts | 226 ++ .../wallet/prepareTransactionRequest.test.ts | 2568 +++++++++++------ .../wallet/prepareTransactionRequest.ts | 86 +- src/actions/wallet/sendTransaction.test.ts | 8 +- .../wallet/sendTransactionSync.test.ts | 8 +- src/index.ts | 5 + src/types/eip1193.ts | 30 + src/utils/transaction/assertRequest.ts | 19 +- 12 files changed, 2441 insertions(+), 939 deletions(-) create mode 100644 src/actions/public/fillTransaction.test.ts create mode 100644 src/actions/public/fillTransaction.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3289cb248b..b3a80d0ba7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,7 +7,7 @@ settings: catalogs: default: typescript: - specifier: ^5.9.2 + specifier: 5.9.2 version: 5.9.2 overrides: @@ -563,7 +563,7 @@ importers: version: link:../src vocs: specifier: ^1.1.0 - version: 1.1.0(@types/node@24.5.2)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(jiti@2.6.0)(lightningcss@1.30.2)(next@15.5.4(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.52.2)(terser@5.36.0)(typescript@5.9.3) + version: 1.1.0(@types/node@24.5.2)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(jiti@2.6.0)(lightningcss@1.30.2)(next@15.5.4(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.52.2)(terser@5.36.0)(typescript@5.9.2) src: dependencies: @@ -6688,6 +6688,14 @@ packages: typescript: optional: true + viem@2.39.2: + resolution: {integrity: sha512-EJPt+T0AkMxKvBRPFHYMLMuvcHiIhoYItkioHRGCkkm6LBSwlK6l9DNzoKA9S09LP003BiMeYddVjVso+lg2Og==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + viem@file:src: resolution: {directory: src, type: directory} peerDependencies: @@ -8409,7 +8417,7 @@ snapshots: pino-pretty: 10.3.1 prom-client: 14.2.0 type-fest: 4.39.0 - viem: 2.39.0(typescript@5.9.2)(zod@3.23.8) + viem: 2.39.2(typescript@5.9.2)(zod@3.23.8) yargs: 17.7.2 zod: 3.23.8 zod-validation-error: 1.5.0(zod@3.23.8) @@ -9352,11 +9360,11 @@ snapshots: '@shikijs/core': 1.29.2 '@shikijs/types': 1.29.2 - '@shikijs/twoslash@1.29.2(typescript@5.9.3)': + '@shikijs/twoslash@1.29.2(typescript@5.9.2)': dependencies: '@shikijs/core': 1.29.2 '@shikijs/types': 1.29.2 - twoslash: 0.2.12(typescript@5.9.3) + twoslash: 0.2.12(typescript@5.9.2) transitivePeerDependencies: - supports-color - typescript @@ -9752,10 +9760,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript/vfs@1.6.1(typescript@5.9.3)': + '@typescript/vfs@1.6.1(typescript@5.9.2)': dependencies: debug: 4.4.3 - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -13812,19 +13820,19 @@ snapshots: twoslash-protocol@0.3.4: {} - twoslash@0.2.12(typescript@5.9.3): + twoslash@0.2.12(typescript@5.9.2): dependencies: - '@typescript/vfs': 1.6.1(typescript@5.9.3) + '@typescript/vfs': 1.6.1(typescript@5.9.2) twoslash-protocol: 0.2.12 - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - twoslash@0.3.4(typescript@5.9.3): + twoslash@0.3.4(typescript@5.9.2): dependencies: - '@typescript/vfs': 1.6.1(typescript@5.9.3) + '@typescript/vfs': 1.6.1(typescript@5.9.2) twoslash-protocol: 0.3.4 - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color @@ -13994,7 +14002,7 @@ snapshots: - utf-8-validate - zod - viem@2.39.0(typescript@5.9.2)(zod@3.23.8): + viem@2.39.2(typescript@5.9.2)(zod@3.23.8): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -14136,7 +14144,7 @@ snapshots: - tsx - yaml - vocs@1.1.0(@types/node@24.5.2)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(jiti@2.6.0)(lightningcss@1.30.2)(next@15.5.4(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.52.2)(terser@5.36.0)(typescript@5.9.3): + vocs@1.1.0(@types/node@24.5.2)(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(jiti@2.6.0)(lightningcss@1.30.2)(next@15.5.4(@opentelemetry/api@1.7.0)(@playwright/test@1.55.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.52.2)(terser@5.36.0)(typescript@5.9.2): dependencies: '@floating-ui/react': 0.27.16(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@hono/node-server': 1.19.5(hono@4.10.3) @@ -14154,7 +14162,7 @@ snapshots: '@radix-ui/react-tabs': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@shikijs/rehype': 1.29.2 '@shikijs/transformers': 1.29.2 - '@shikijs/twoslash': 1.29.2(typescript@5.9.3) + '@shikijs/twoslash': 1.29.2(typescript@5.9.2) '@tailwindcss/vite': 4.1.15(vite@7.1.11(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.2)(terser@5.36.0)(yaml@2.8.1)) '@vanilla-extract/css': 1.17.4 '@vanilla-extract/dynamic': 2.1.5 @@ -14204,7 +14212,7 @@ snapshots: serve-static: 1.16.2 shiki: 1.29.2 toml: 3.0.0 - twoslash: 0.3.4(typescript@5.9.3) + twoslash: 0.3.4(typescript@5.9.2) ua-parser-js: 1.0.40 unified: 11.0.5 unist-util-visit: 5.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1f44dafa5b..c537ebf611 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,11 +7,11 @@ catalog: '@types/react-dom': ^19 react: ^19 react-dom: ^19 - typescript: ^5.9.2 + typescript: 5.9.2 minimumReleaseAge: 1440 minimumReleaseAgeExclude: - '@vitest/*' - - glob + - glob - hono - vitest - vocs diff --git a/src/actions/index.ts b/src/actions/index.ts index 6e4a4fd12e..7cbf552b88 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -87,6 +87,12 @@ export { type EstimateMaxPriorityFeePerGasReturnType, estimateMaxPriorityFeePerGas, } from './public/estimateMaxPriorityFeePerGas.js' +export { + type FillTransactionErrorType, + type FillTransactionParameters, + type FillTransactionReturnType, + fillTransaction, +} from './public/fillTransaction.js' export { type GetBalanceErrorType, type GetBalanceParameters, diff --git a/src/actions/public/fillTransaction.test.ts b/src/actions/public/fillTransaction.test.ts new file mode 100644 index 0000000000..a8d317e59a --- /dev/null +++ b/src/actions/public/fillTransaction.test.ts @@ -0,0 +1,378 @@ +import { expect, test } from 'vitest' +import { Delegation } from '../../../contracts/generated.js' +import { wagmiContractConfig } from '../../../test/src/abis.js' +import { anvilMainnet } from '../../../test/src/anvil.js' +import { accounts } from '../../../test/src/constants.js' +import { deploy } from '../../../test/src/utils.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { encodeFunctionData, nonceManager } from '../../utils/index.js' +import { signAuthorization } from '../wallet/signAuthorization.js' +import { fillTransaction } from './fillTransaction.js' + +const client = anvilMainnet.getClient({ account: true }) + +test('default', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + }) + + expect(result).toMatchInlineSnapshot(` + { + "raw": "0x02f0018203b9843b9aca00849c18478a8252a89400000000000000000000000000000000000000008084deadbeefc0808080", + "transaction": { + "accessList": [], + "chainId": 1, + "data": "0xdeadbeef", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "gas": 21160n, + "gasPrice": 3142604248n, + "hash": "0xe9612dfada2688d94ed86c7cb20d85d31880a5f7affe51f2bb08fd6641be74a1", + "input": "0xdeadbeef", + "maxFeePerGas": 3142604248n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 953, + "to": "0x0000000000000000000000000000000000000000", + "type": "eip1559", + "typeHex": "0x2", + "value": 0n, + }, + } + `) +}) + +test.skip('args: authorizationList', async () => { + const eoa = privateKeyToAccount(accounts[1].privateKey) + + const { contractAddress } = await deploy(client, { + abi: Delegation.abi, + bytecode: Delegation.bytecode.object, + }) + + const authorization = await signAuthorization(client, { + account: eoa, + contractAddress: contractAddress!, + }) + + expect( + await fillTransaction(client, { + authorizationList: [authorization], + }), + ).toBeDefined() +}) + +test('args: gas', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + gas: 50000n, + }) + + expect(result.transaction.gas).toBe(50000n) +}) + +test('args: gasPrice', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + gasPrice: 20000000000n, + type: 'legacy', + }) + + expect(result.transaction.gasPrice).toBe(20000000000n) +}) + +test('args: maxFeePerGas', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + maxFeePerGas: 50000000000n, + }) + + expect(result.transaction.maxFeePerGas).toBe(50000000000n) +}) + +test('args: maxPriorityFeePerGas', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + maxPriorityFeePerGas: 2000000000n, + maxFeePerGas: 50000000000n, + }) + + expect(result.transaction.maxPriorityFeePerGas).toBe(2000000000n) +}) + +test('args: nonce', async () => { + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonce: 1000, + }) + + expect(result.transaction.nonce).toBe(1000) +}) + +test('behavior: fee multiplier applied when maxFeePerGas not provided', async () => { + // Get baseline transaction with default multiplier (1.2) + const baseline = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + }) + + // Get transaction with custom multiplier (1.5) and no maxFeePerGas provided + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + chain: { + ...anvilMainnet.chain, + fees: { + baseFeeMultiplier: 1.5, + }, + }, + }) + + // maxFeePerGas should be multiplied by the custom multiplier + // Baseline uses multiplier 1.2, custom uses 1.5 + // So custom = baseline * (1.5 / 1.2) = baseline * 1.25 + expect(result.transaction.maxFeePerGas).toBeGreaterThan( + baseline.transaction.maxFeePerGas!, + ) + expect(result.transaction.maxFeePerGas).toBe( + (baseline.transaction.maxFeePerGas! * 15n) / 12n + 1n, + ) +}) + +test('behavior: fee multiplier not applied when maxFeePerGas provided', async () => { + const customMaxFeePerGas = 50000000000n + + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + maxFeePerGas: customMaxFeePerGas, + chain: { + ...anvilMainnet.chain, + fees: { + baseFeeMultiplier: 1.5, + }, + }, + }) + + // maxFeePerGas should be exactly what we provided, not multiplied + expect(result.transaction.maxFeePerGas).toBe(customMaxFeePerGas) +}) + +test('behavior: fee multiplier not applied when gasPrice provided', async () => { + const customGasPrice = 20000000000n + + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + gasPrice: customGasPrice, + type: 'legacy', + chain: { + ...anvilMainnet.chain, + fees: { + baseFeeMultiplier: 1.5, + }, + }, + }) + + // gasPrice should be exactly what we provided, not multiplied + expect(result.transaction.gasPrice).toBe(customGasPrice) +}) + +test('behavior: fee multiplier function (sync)', async () => { + // Get baseline transaction with default multiplier (1.2) + const baseline = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + }) + + // Get transaction with custom multiplier function (1.5) and no maxFeePerGas provided + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + chain: { + ...anvilMainnet.chain, + fees: { + baseFeeMultiplier() { + return 1.5 + }, + }, + } as typeof anvilMainnet.chain, + }) + + // maxFeePerGas should be multiplied by the custom multiplier function result + // Baseline uses multiplier 1.2, custom uses 1.5 + // So custom = baseline * (1.5 / 1.2) = baseline * 1.25 + expect(result.transaction.maxFeePerGas).toBeGreaterThan( + baseline.transaction.maxFeePerGas!, + ) + expect(result.transaction.maxFeePerGas).toBe( + (baseline.transaction.maxFeePerGas! * 15n) / 12n + 1n, + ) +}) + +test('behavior: fee multiplier function (async)', async () => { + // Get baseline transaction with default multiplier (1.2) + const baseline = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + }) + + // Get transaction with custom async multiplier function (1.5) and no maxFeePerGas provided + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + chain: { + ...anvilMainnet.chain, + fees: { + async baseFeeMultiplier() { + return 1.5 + }, + }, + } as typeof anvilMainnet.chain, + }) + + // maxFeePerGas should be multiplied by the custom multiplier function result + // Baseline uses multiplier 1.2, custom uses 1.5 + // So custom = baseline * (1.5 / 1.2) = baseline * 1.25 + expect(result.transaction.maxFeePerGas).toBeGreaterThan( + baseline.transaction.maxFeePerGas!, + ) + expect(result.transaction.maxFeePerGas).toBe( + (baseline.transaction.maxFeePerGas! * 15n) / 12n + 1n, + ) +}) + +test('behavior: error when fee multiplier less than 1', async () => { + await expect( + fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + chain: { + ...anvilMainnet.chain, + fees: { + baseFeeMultiplier: 0.8, + }, + }, + }), + ).rejects.toThrow('`baseFeeMultiplier` must be greater than 1.') +}) + +test('behavior: nonceManager', async () => { + const alice = privateKeyToAccount(accounts[5].privateKey) + const bob = privateKeyToAccount(accounts[6].privateKey) + + const [result_1, result_2, result_3, result_4, result_5] = await Promise.all([ + fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }), + fillTransaction(client, { + account: bob, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }), + fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }), + fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }), + fillTransaction(client, { + account: bob, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }), + ]) + + // Each account should have sequential nonces + expect(result_1.transaction.nonce).toBeDefined() + expect(result_2.transaction.nonce).toBeDefined() + expect(result_3.transaction.nonce).toBe(result_1.transaction.nonce + 1) + expect(result_4.transaction.nonce).toBe(result_1.transaction.nonce + 2) + expect(result_5.transaction.nonce).toBe(result_2.transaction.nonce + 1) + + const result_6 = await fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }) + const result_7 = await fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }) + + expect(result_6.transaction.nonce).toBe(result_1.transaction.nonce + 3) + expect(result_7.transaction.nonce).toBe(result_1.transaction.nonce + 4) +}) + +test('behavior: nonceManager not used when no account', async () => { + // When no account is provided, nonceManager should not be used + const result = await fillTransaction(client, { + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonceManager, + }) + + // Should use the node's nonce, not nonceManager + expect(result.transaction.nonce).toBeDefined() +}) + +test('behavior: nonceManager not used when explicit nonce provided', async () => { + const alice = privateKeyToAccount(accounts[7].privateKey) + + const explicitNonce = 999 + + const result = await fillTransaction(client, { + account: alice, + data: '0xdeadbeef', + to: '0x0000000000000000000000000000000000000000', + nonce: explicitNonce, + nonceManager, + }) + + // Should use the explicit nonce, not nonceManager + expect(result.transaction.nonce).toBe(explicitNonce) +}) + +test('error: revert', async () => { + await expect( + fillTransaction(client, { + account: accounts[0].address, + data: encodeFunctionData({ + abi: wagmiContractConfig.abi, + functionName: 'mint', + args: [420n], + }), + to: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + }), + ).rejects.toThrowErrorMatchingInlineSnapshot( + ` + [TransactionExecutionError: Execution reverted with reason: Token ID is taken. + + Request Arguments: + chain: Ethereum (Local) (id: 1) + to: 0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2 + data: 0xa0712d6800000000000000000000000000000000000000000000000000000000000001a4 + + Details: execution reverted: Token ID is taken + Version: viem@x.y.z] + `, + ) +}) diff --git a/src/actions/public/fillTransaction.ts b/src/actions/public/fillTransaction.ts new file mode 100644 index 0000000000..bef357d0b4 --- /dev/null +++ b/src/actions/public/fillTransaction.ts @@ -0,0 +1,226 @@ +import type { Address } from 'abitype' +import { parseAccount } from '../../accounts/utils/parseAccount.js' +import type { Client } from '../../clients/createClient.js' +import type { Transport } from '../../clients/transports/createTransport.js' +import type { BaseError } from '../../errors/base.js' +import { BaseFeeScalarError } from '../../errors/fee.js' +import type { ErrorType } from '../../errors/utils.js' +import type { Account, GetAccountParameter } from '../../types/account.js' +import type { + Chain, + ChainFeesFnParameters, + DeriveChain, + GetChainParameter, +} from '../../types/chain.js' +import type { Hex } from '../../types/misc.js' +import type { TransactionRequest } from '../../types/transaction.js' +import type { UnionOmit } from '../../types/utils.js' +import { + type GetTransactionErrorReturnType, + getTransactionError, +} from '../../utils/errors/getTransactionError.js' +import { extract } from '../../utils/formatters/extract.js' +import { + type FormattedTransaction, + formatTransaction, +} from '../../utils/formatters/transaction.js' +import { + type FormattedTransactionRequest, + formatTransactionRequest, +} from '../../utils/formatters/transactionRequest.js' +import { getAction } from '../../utils/getAction.js' +import type { NonceManager } from '../../utils/nonceManager.js' +import { assertRequest } from '../../utils/transaction/assertRequest.js' +import { getBlock } from './getBlock.js' +import { getChainId as getChainId_ } from './getChainId.js' + +export type FillTransactionParameters< + chain extends Chain | undefined = Chain | undefined, + account extends Account | undefined = Account | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + accountOverride extends Account | Address | undefined = + | Account + | Address + | undefined, + /// + _derivedChain extends Chain | undefined = DeriveChain, +> = UnionOmit, 'from'> & + GetAccountParameter & + GetChainParameter & { + /** + * Nonce manager to use for the transaction request. + */ + nonceManager?: NonceManager | undefined + } + +export type FillTransactionReturnType< + chain extends Chain | undefined = Chain | undefined, + chainOverride extends Chain | undefined = Chain | undefined, + /// + _derivedChain extends Chain | undefined = DeriveChain, +> = { + raw: Hex + transaction: FormattedTransaction<_derivedChain> +} + +export type FillTransactionErrorType = + | GetTransactionErrorReturnType + | ErrorType + +export async function fillTransaction< + chain extends Chain | undefined, + account extends Account | undefined, + chainOverride extends Chain | undefined = undefined, + accountOverride extends Account | Address | undefined = undefined, +>( + client: Client, + parameters: FillTransactionParameters< + chain, + account, + chainOverride, + accountOverride + >, +): Promise> { + const { + account = client.account, + accessList, + authorizationList, + chain = client.chain, + blobVersionedHashes, + blobs, + data, + gas, + gasPrice, + maxFeePerBlobGas, + maxFeePerGas, + maxPriorityFeePerGas, + nonce: nonce_, + nonceManager, + to, + type, + value, + ...rest + } = parameters + + const nonce = await (async () => { + if (!account) return nonce_ + if (!nonceManager) return nonce_ + const account_ = parseAccount(account) + const chainId = chain + ? chain.id + : await getAction(client, getChainId_, 'getChainId')({}) + return await nonceManager.consume({ + address: account_.address, + chainId, + client, + }) + })() + + assertRequest(parameters) + + const chainFormat = chain?.formatters?.transactionRequest?.format + const format = chainFormat || formatTransactionRequest + + const request = format( + { + // Pick out extra data that might exist on the chain's transaction request type. + ...extract(rest, { format: chainFormat }), + account: account ? parseAccount(account) : undefined, + accessList, + authorizationList, + blobs, + blobVersionedHashes, + data, + gas, + gasPrice, + maxFeePerBlobGas, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + to, + type, + value, + } as TransactionRequest, + 'fillTransaction', + ) + + try { + const response = await client.request({ + method: 'eth_fillTransaction', + params: [request], + }) + const format = chain?.formatters?.transaction?.format || formatTransaction + + const transaction = format(response.tx) + + // Remove unnecessary fields. + delete transaction.blockHash + delete transaction.blockNumber + delete transaction.r + delete transaction.s + delete transaction.transactionIndex + delete transaction.v + delete transaction.yParity + + // Rewrite fields. + transaction.data = transaction.input + + // Preference supplied fees (some nodes do not take these preferences). + if (transaction.gas) transaction.gas = parameters.gas ?? transaction.gas + if (transaction.gasPrice) + transaction.gasPrice = parameters.gasPrice ?? transaction.gasPrice + if (transaction.maxFeePerBlobGas) + transaction.maxFeePerBlobGas = + parameters.maxFeePerBlobGas ?? transaction.maxFeePerBlobGas + if (transaction.maxFeePerGas) + transaction.maxFeePerGas = + parameters.maxFeePerGas ?? transaction.maxFeePerGas + if (transaction.maxPriorityFeePerGas) + transaction.maxPriorityFeePerGas = + parameters.maxPriorityFeePerGas ?? transaction.maxPriorityFeePerGas + if (transaction.nonce) + transaction.nonce = parameters.nonce ?? transaction.nonce + + // Build fee multiplier function. + const feeMultiplier = await (async () => { + if (typeof chain?.fees?.baseFeeMultiplier === 'function') { + const block = await getAction(client, getBlock, 'getBlock')({}) + return chain.fees.baseFeeMultiplier({ + block, + client, + request: parameters, + } as ChainFeesFnParameters) + } + return chain?.fees?.baseFeeMultiplier ?? 1.2 + })() + if (feeMultiplier < 1) throw new BaseFeeScalarError() + + const decimals = feeMultiplier.toString().split('.')[1]?.length ?? 0 + const denominator = 10 ** decimals + const multiplyFee = (base: bigint) => + (base * BigInt(Math.ceil(feeMultiplier * denominator))) / + BigInt(denominator) + + // Apply fee multiplier. + if (transaction.maxFeePerGas && !parameters.maxFeePerGas) + transaction.maxFeePerGas = multiplyFee(transaction.maxFeePerGas) + if (transaction.gasPrice && !parameters.gasPrice) + transaction.gasPrice = multiplyFee(transaction.gasPrice) + + return { + raw: response.raw, + transaction: { + from: request.from, + ...transaction, + }, + } + } catch (err) { + throw getTransactionError( + err as BaseError, + { + ...parameters, + chain: client.chain, + } as never, + ) + } +} diff --git a/src/actions/wallet/prepareTransactionRequest.test.ts b/src/actions/wallet/prepareTransactionRequest.test.ts index 440ec00d46..53f7554029 100644 --- a/src/actions/wallet/prepareTransactionRequest.test.ts +++ b/src/actions/wallet/prepareTransactionRequest.test.ts @@ -1,4 +1,4 @@ -import { expect, test, vi } from 'vitest' +import { beforeEach, describe, expect, test, vi } from 'vitest' import { accounts } from '~test/src/constants.js' import { kzg } from '~test/src/kzg.js' @@ -8,10 +8,17 @@ import * as getBlock from '../../actions/public/getBlock.js' import { mine } from '../../actions/test/mine.js' import { setBalance } from '../../actions/test/setBalance.js' import { setNextBlockBaseFeePerGas } from '../../actions/test/setNextBlockBaseFeePerGas.js' -import { createClient, http, toBlobs } from '../../index.js' +import { + BaseError, + createClient, + http, + MethodNotFoundRpcError, + toBlobs, +} from '../../index.js' import { nonceManager } from '../../utils/index.js' import { parseEther } from '../../utils/unit/parseEther.js' import { parseGwei } from '../../utils/unit/parseGwei.js' +import * as fillTransaction from '../public/fillTransaction.js' import { eip1559NetworkCache, prepareTransactionRequest, @@ -37,958 +44,1753 @@ async function setup() { await mine(client, { blocks: 1 }) } -test('default', async () => { - await setup() +describe('without `eth_fillTransaction`', () => { + beforeEach(() => { + vi.spyOn(fillTransaction, 'fillTransaction').mockRejectedValue( + new BaseError('Method not found', { + cause: new MethodNotFoundRpcError(new Error('Method not found')), + }), + ) + }) + + test('default', async () => { + await setup() - const block = await getBlock.getBlock(client) - const { - maxFeePerGas, - maxPriorityFeePerGas, - nonce: _nonce, - ...rest - } = await prepareTransactionRequest(client, { - to: targetAccount.address, - value: parseEther('1'), + const block = await getBlock.getBlock(client) + const { + maxFeePerGas, + maxPriorityFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + to: targetAccount.address, + value: parseEther('1'), + }) + expect(maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + maxPriorityFeePerGas!, + ) + expect(rest).toMatchInlineSnapshot(` + { + "chainId": 1, + "gas": 21000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) }) - expect(maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + maxPriorityFeePerGas!, - ) - expect(rest).toMatchInlineSnapshot(` + + test('legacy fees', async () => { + await setup() + + eip1559NetworkCache.clear() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }, + ) + expect(request).toMatchInlineSnapshot(` { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "gas": 21000n, + "gasPrice": 11700000000n, "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", + "type": "legacy", "value": 1000000000000000000n, } `) -}) -test('legacy fees', async () => { - await setup() - - eip1559NetworkCache.clear() + eip1559NetworkCache.clear() + }) - vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ - baseFeePerGas: undefined, - } as any) + test('args: account', async () => { + await setup() - const { nonce: _nonce, ...request } = await prepareTransactionRequest( - client, - { + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, value: parseEther('1'), - }, - ) - expect(request).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "gasPrice": 11700000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "legacy", - "value": 1000000000000000000n, - } - `) - - eip1559NetworkCache.clear() -}) - -test('args: account', async () => { - await setup() - - const { - maxFeePerGas: _maxFeePerGas, - nonce: _nonce, - ...rest - } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) -test('args: chain', async () => { - await setup() - - const { - maxFeePerGas: _maxFeePerGas, - nonce: _nonce, - ...rest - } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + test('args: chain', async () => { + await setup() -test('args: chainId', async () => { - await setup() - - const { - maxFeePerGas: _maxFeePerGas, - nonce: _nonce, - ...rest - } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chainId: 69, - to: targetAccount.address, - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 69, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) -test('args: nonce', async () => { - await setup() + test('args: chainId', async () => { + await setup() - const { maxFeePerGas: _maxFeePerGas, ...rest } = - await prepareTransactionRequest(client, { + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { account: privateKeyToAccount(sourceAccount.privateKey), + chainId: 69, to: targetAccount.address, - nonce: 5, value: parseEther('1'), }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxPriorityFeePerGas": 1000000000n, - "nonce": 5, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 69, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) -test('args: gasPrice', async () => { - await setup() - - const { nonce: _nonce, ...request } = await prepareTransactionRequest( - client, - { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - value: parseEther('1'), - }, - ) - expect(request).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "gasPrice": 10000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "legacy", - "value": 1000000000000000000n, - } - `) -}) + test('args: nonce', async () => { + await setup() + + const { maxFeePerGas: _maxFeePerGas, ...rest } = + await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + nonce: 5, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 5, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: gasPrice', async () => { + await setup() -test('args: gasPrice (on chain w/ block.baseFeePerGas)', async () => { - await setup() - - const { nonce: _nonce, ...request } = await prepareTransactionRequest( - client, - { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - value: parseEther('1'), - }, - ) - expect(request).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + value: parseEther('1'), }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "gasPrice": 10000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "legacy", - "value": 1000000000000000000n, - } - `) -}) + ) + expect(request).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "gasPrice": 10000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", + "value": 1000000000000000000n, + } + `) + }) + + test('args: gasPrice (on chain w/ block.baseFeePerGas)', async () => { + await setup() -test('args: maxFeePerGas', async () => { - await setup() - - const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxFeePerGas: parseGwei('100'), - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + value: parseEther('1'), }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 100000000000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + ) + expect(request).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "gasPrice": 10000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", + "value": 1000000000000000000n, + } + `) + }) -test('args: maxFeePerGas (under default tip)', async () => { - await setup() + test('args: maxFeePerGas', async () => { + await setup() - await expect(() => - prepareTransactionRequest(client, { + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, - maxFeePerGas: parseGwei('0.1'), + maxFeePerGas: parseGwei('100'), value: parseEther('1'), - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 100000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxFeePerGas (under default tip)', async () => { + await setup() + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('0.1'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` [MaxFeePerGasTooLowError: \`maxFeePerGas\` cannot be less than the \`maxPriorityFeePerGas\` (1 gwei). Version: viem@x.y.z] `) -}) - -test('args: maxFeePerGas (on legacy)', async () => { - await setup() - - vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ - baseFeePerGas: undefined, - } as any) + }) - await expect(() => - prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxFeePerGas: parseGwei('10'), - value: parseEther('1'), - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + test('args: maxFeePerGas (on legacy)', async () => { + await setup() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('10'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. Version: viem@x.y.z] `) -}) - -test('args: maxPriorityFeePerGas', async () => { - await setup() - - const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxPriorityFeePerGas: parseGwei('5'), - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 17000000000n, - "maxPriorityFeePerGas": 5000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + }) -test('args: maxPriorityFeePerGas === 0', async () => { - await setup() - - const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxPriorityFeePerGas: 0n, - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 12000000000n, - "maxPriorityFeePerGas": 0n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + test('args: maxPriorityFeePerGas', async () => { + await setup() -test('args: maxPriorityFeePerGas (on legacy)', async () => { - await setup() + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxPriorityFeePerGas: parseGwei('5'), + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 17000000000n, + "maxPriorityFeePerGas": 5000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) - vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ - baseFeePerGas: undefined, - } as any) + test('args: maxPriorityFeePerGas === 0', async () => { + await setup() - await expect(() => - prepareTransactionRequest(client, { + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, - maxFeePerGas: parseGwei('5'), + maxPriorityFeePerGas: 0n, value: parseEther('1'), - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 12000000000n, + "maxPriorityFeePerGas": 0n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxPriorityFeePerGas (on legacy)', async () => { + await setup() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('5'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. Version: viem@x.y.z] `) -}) - -test('args: maxFeePerGas + maxPriorityFeePerGas', async () => { - await setup() - - const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - maxFeePerGas: parseGwei('10'), - maxPriorityFeePerGas: parseGwei('5'), - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 10000000000n, - "maxPriorityFeePerGas": 5000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) + }) -test('args: gasPrice + maxFeePerGas', async () => { - await setup() + test('args: maxFeePerGas + maxPriorityFeePerGas', async () => { + await setup() - await expect(() => - // @ts-expect-error - prepareTransactionRequest(client, { + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { account: privateKeyToAccount(sourceAccount.privateKey), to: targetAccount.address, - gasPrice: parseGwei('10'), - maxFeePerGas: parseGwei('20'), + maxFeePerGas: parseGwei('10'), + maxPriorityFeePerGas: parseGwei('5'), value: parseEther('1'), - }), - ).rejects.toThrowError( - 'Cannot specify both a `gasPrice` and a `maxFeePerGas`/`maxPriorityFeePerGas`.', - ) -}) - -test('args: gasPrice + maxPriorityFeePerGas', async () => { - await setup() + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 10000000000n, + "maxPriorityFeePerGas": 5000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) - await expect(() => - // @ts-expect-error - prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - maxPriorityFeePerGas: parseGwei('20'), - type: 'legacy', - value: parseEther('1'), - }), - ).rejects.toThrowErrorMatchingInlineSnapshot(` + test('args: gasPrice + maxPriorityFeePerGas', async () => { + await setup() + + await expect(() => + // @ts-expect-error + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + maxPriorityFeePerGas: parseGwei('20'), + type: 'legacy', + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. Version: viem@x.y.z] `) -}) - -test('args: type', async () => { - await setup() - - const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - type: 'eip1559', - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 13000000000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) -}) - -test('args: blobs', async () => { - await setup() - - const { - blobs: _blobs, - nonce: _nonce, - ...rest - } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - blobs: toBlobs({ data: '0x1234' }), - kzg, - maxFeePerBlobGas: parseGwei('20'), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(rest).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "blobVersionedHashes": [ - "0x01d34d7bd213433308d1f63023dc70fd585064cd108ee69be0637a09f4028ea3", - ], - "chainId": 1, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 53001n, - "kzg": { - "blobToKzgCommitment": [Function], - "computeBlobKzgProof": [Function], - }, - "maxFeePerBlobGas": 20000000000n, - "maxFeePerGas": 13000000000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip4844", - "value": 1000000000000000000n, - } - `) -}) - -test('args: parameters', async () => { - await setup() - - const result = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "value": 1000000000000000000n, - } - `) - - const result2 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result2).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 13000000000n, - "maxPriorityFeePerGas": 1000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) - - const result3 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees', 'nonce'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result3).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 13000000000n, - "maxPriorityFeePerGas": 1000000000n, - "nonce": 953, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) - - const result4 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees', 'nonce', 'type'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result4).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "gas": 21000n, - "maxFeePerGas": 13000000000n, - "maxPriorityFeePerGas": 1000000000n, - "nonce": 953, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "type": "eip1559", - "value": 1000000000000000000n, - } - `) - - const { - blobs: _blobs, - sidecars, - ...result5 - } = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - blobs: toBlobs({ data: '0x1234' }), - kzg, - maxFeePerBlobGas: parseGwei('20'), - parameters: ['sidecars'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect( - sidecars.map(({ blob: _blob, ...rest }) => rest), - ).toMatchInlineSnapshot(` - [ - { - "commitment": "0xae5f688fc774ce26be308660c003c9c528a85410ce7f3138e37f424b7a31f61afaff45d74996ac5a5d83d061857b8006", - "proof": "0xb0bab7126f83bd4ad1ae36a51f64fdef1bd198174c1a355660bf462b98075546960d33101ae778128a7693a2b110d218", - }, - ] - `) - expect(result5).toMatchInlineSnapshot(` - { - "account": { - "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "nonceManager": undefined, - "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", - "sign": [Function], - "signAuthorization": [Function], - "signMessage": [Function], - "signTransaction": [Function], - "signTypedData": [Function], - "source": "privateKey", - "type": "local", - }, - "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - "kzg": { - "blobToKzgCommitment": [Function], - "computeBlobKzgProof": [Function], - }, - "maxFeePerBlobGas": 20000000000n, - "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", - "value": 1000000000000000000n, - } - `) -}) - -test('behavior: chain default priority fee', async () => { - await setup() + }) - const block = await getBlock.getBlock(client) + test('args: type', async () => { + await setup() - // client chain - const client_1 = createClient({ - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: () => parseGwei('69'), - }, - }, - transport: http(), - }) - const request_1 = await prepareTransactionRequest(client_1, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_1.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // client chain (async) - const client_2 = createClient({ - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: async () => parseGwei('69'), - }, - }, - transport: http(), - }) - const request_2 = await prepareTransactionRequest(client_2, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_2.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // client chain (bigint) - const client_3 = createClient({ - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: parseGwei('69'), - }, - }, - transport: http(), - }) - const request_3 = await prepareTransactionRequest(client_3, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_3.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // chain override (bigint) - const request_4 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: () => parseGwei('69'), - }, - }, - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_4.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // chain override (async) - const request_5 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: async () => parseGwei('69'), - }, - }, - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_5.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // chain override (bigint) - const request_6 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: parseGwei('69'), - }, - }, - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_6.maxFeePerGas).toEqual( - (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), - ) - - // chain override (bigint zero base fee) - const request_7 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: 0n, - }, - }, - to: targetAccount.address, - value: parseEther('1'), - }) - expect(request_7.maxPriorityFeePerGas).toEqual(0n) - - // chain override (async zero base fee) - const request_8 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: async () => 0n, - }, - }, - to: targetAccount.address, - value: parseEther('1'), + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + type: 'eip1559', + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) }) - expect(request_8.maxPriorityFeePerGas).toEqual(0n) -}) -test('behavior: nonce manager', async () => { - await setup() + test('args: blobs', async () => { + await setup() - const account = privateKeyToAccount(sourceAccount.privateKey, { - nonceManager, + const { + blobs: _blobs, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + blobs: toBlobs({ data: '0x1234' }), + kzg, + maxFeePerBlobGas: parseGwei('20'), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "blobVersionedHashes": [ + "0x01d34d7bd213433308d1f63023dc70fd585064cd108ee69be0637a09f4028ea3", + ], + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 53001n, + "kzg": { + "blobToKzgCommitment": [Function], + "computeBlobKzgProof": [Function], + }, + "maxFeePerBlobGas": 20000000000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip4844", + "value": 1000000000000000000n, + } + `) }) - const args = { - account, - nonceManager: account.nonceManager, - to: targetAccount.address, - value: parseEther('1'), - parameters: ['nonce'], - } as const - - const request_1 = await prepareTransactionRequest(client, args) - expect(request_1.nonce).toBe(953) + test('args: parameters', async () => { + await setup() - const request_2 = await prepareTransactionRequest(client, args) - expect(request_2.nonce).toBe(954) + const result = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": 1000000000000000000n, + } + `) - const [request_3, request_4, request_5] = await Promise.all([ - prepareTransactionRequest(client, args), - prepareTransactionRequest(client, args), - prepareTransactionRequest(client, args), - ]) + const result2 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result2).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) - expect(request_3.nonce).toBe(955) - expect(request_4.nonce).toBe(956) - expect(request_5.nonce).toBe(957) + const result3 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees', 'nonce'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result3).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 953, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + + const result4 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees', 'nonce', 'type'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result4).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 953, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + + const { + blobs: _blobs, + sidecars, + ...result5 + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + blobs: toBlobs({ data: '0x1234' }), + kzg, + maxFeePerBlobGas: parseGwei('20'), + parameters: ['sidecars'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect( + sidecars.map(({ blob: _blob, ...rest }) => rest), + ).toMatchInlineSnapshot(` + [ + { + "commitment": "0xae5f688fc774ce26be308660c003c9c528a85410ce7f3138e37f424b7a31f61afaff45d74996ac5a5d83d061857b8006", + "proof": "0xb0bab7126f83bd4ad1ae36a51f64fdef1bd198174c1a355660bf462b98075546960d33101ae778128a7693a2b110d218", + }, + ] + `) + expect(result5).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "kzg": { + "blobToKzgCommitment": [Function], + "computeBlobKzgProof": [Function], + }, + "maxFeePerBlobGas": 20000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": 1000000000000000000n, + } + `) + }) + + test('behavior: chain default priority fee', async () => { + await setup() + + const block = await getBlock.getBlock(client) + + // client chain + const client_1 = createClient({ + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: () => parseGwei('69'), + }, + }, + transport: http(), + }) + const request_1 = await prepareTransactionRequest(client_1, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_1.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // client chain (async) + const client_2 = createClient({ + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: async () => parseGwei('69'), + }, + }, + transport: http(), + }) + const request_2 = await prepareTransactionRequest(client_2, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_2.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // client chain (bigint) + const client_3 = createClient({ + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: parseGwei('69'), + }, + }, + transport: http(), + }) + const request_3 = await prepareTransactionRequest(client_3, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_3.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // chain override (bigint) + const request_4 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: () => parseGwei('69'), + }, + }, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_4.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // chain override (async) + const request_5 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: async () => parseGwei('69'), + }, + }, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_5.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // chain override (bigint) + const request_6 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: parseGwei('69'), + }, + }, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_6.maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + parseGwei('69'), + ) + + // chain override (bigint zero base fee) + const request_7 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: 0n, + }, + }, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_7.maxPriorityFeePerGas).toEqual(0n) + + // chain override (async zero base fee) + const request_8 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chain: { + ...anvilMainnet.chain, + fees: { + defaultPriorityFee: async () => 0n, + }, + }, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(request_8.maxPriorityFeePerGas).toEqual(0n) + }) + + test('behavior: nonce manager', async () => { + await setup() + + const account = privateKeyToAccount(sourceAccount.privateKey, { + nonceManager, + }) + + const args = { + account, + nonceManager: account.nonceManager, + to: targetAccount.address, + value: parseEther('1'), + parameters: ['nonce'], + } as const + + const request_1 = await prepareTransactionRequest(client, args) + expect(request_1.nonce).toBe(953) + + const request_2 = await prepareTransactionRequest(client, args) + expect(request_2.nonce).toBe(954) + + const [request_3, request_4, request_5] = await Promise.all([ + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + ]) + + expect(request_3.nonce).toBe(955) + expect(request_4.nonce).toBe(956) + expect(request_5.nonce).toBe(957) + }) +}) + +describe('with `eth_fillTransaction`', () => { + test('default', async () => { + await setup() + + const block = await getBlock.getBlock(client) + const { + maxFeePerGas, + maxPriorityFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + to: targetAccount.address, + value: parseEther('1'), + }) + expect(maxFeePerGas).toEqual( + (block.baseFeePerGas! * 120n) / 100n + maxPriorityFeePerGas!, + ) + expect(rest).toMatchInlineSnapshot(` + { + "chainId": 1, + "gas": 21000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('legacy fees', async () => { + await setup() + + eip1559NetworkCache.clear() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }, + ) + expect(request).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "gasPrice": 11700000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", + "value": 1000000000000000000n, + } + `) + + eip1559NetworkCache.clear() + }) + + test('args: account', async () => { + await setup() + + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: chain', async () => { + await setup() + + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: chainId', async () => { + await setup() + + const { + maxFeePerGas: _maxFeePerGas, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + chainId: 69, + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 69, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: nonce', async () => { + await setup() + + const { maxFeePerGas: _maxFeePerGas, ...rest } = + await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + nonce: 5, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 5, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: gasPrice', async () => { + await setup() + + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + value: parseEther('1'), + }, + ) + expect(request).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "gasPrice": 10000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", + "value": 1000000000000000000n, + } + `) + }) + + test('args: gasPrice (on chain w/ block.baseFeePerGas)', async () => { + await setup() + + const { nonce: _nonce, ...request } = await prepareTransactionRequest( + client, + { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + value: parseEther('1'), + }, + ) + expect(request).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "gasPrice": 10000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "legacy", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxFeePerGas', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('100'), + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 100000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxFeePerGas (under default tip)', async () => { + await setup() + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('0.1'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [MaxFeePerGasTooLowError: \`maxFeePerGas\` cannot be less than the \`maxPriorityFeePerGas\` (1 gwei). + + Version: viem@x.y.z] + `) + }) + + test('args: maxFeePerGas (on legacy)', async () => { + await setup() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('10'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. + + Version: viem@x.y.z] + `) + }) + + test('args: maxPriorityFeePerGas', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxPriorityFeePerGas: parseGwei('5'), + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 17000000000n, + "maxPriorityFeePerGas": 5000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxPriorityFeePerGas === 0', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxPriorityFeePerGas: 0n, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 12000000000n, + "maxPriorityFeePerGas": 0n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: maxPriorityFeePerGas (on legacy)', async () => { + await setup() + + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ + baseFeePerGas: undefined, + } as any) + + await expect(() => + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('5'), + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. + + Version: viem@x.y.z] + `) + }) + + test('args: maxFeePerGas + maxPriorityFeePerGas', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + maxFeePerGas: parseGwei('10'), + maxPriorityFeePerGas: parseGwei('5'), + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 10000000000n, + "maxPriorityFeePerGas": 5000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: gasPrice + maxPriorityFeePerGas', async () => { + await setup() + + await expect(() => + // @ts-expect-error + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + maxPriorityFeePerGas: parseGwei('20'), + type: 'legacy', + value: parseEther('1'), + }), + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [Eip1559FeesNotSupportedError: Chain does not support EIP-1559 fees. + + Version: viem@x.y.z] + `) + }) + + test('args: type', async () => { + await setup() + + const { nonce: _nonce, ...rest } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + type: 'eip1559', + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + }) + + test('args: blobs', async () => { + await setup() + + const { + blobs: _blobs, + nonce: _nonce, + ...rest + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + blobs: toBlobs({ data: '0x1234' }), + kzg, + maxFeePerBlobGas: parseGwei('20'), + to: targetAccount.address, + value: parseEther('1'), + }) + expect(rest).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "blobVersionedHashes": [ + "0x01d34d7bd213433308d1f63023dc70fd585064cd108ee69be0637a09f4028ea3", + ], + "chainId": 1, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 53001n, + "kzg": { + "blobToKzgCommitment": [Function], + "computeBlobKzgProof": [Function], + }, + "maxFeePerBlobGas": 20000000000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip4844", + "value": 1000000000000000000n, + } + `) + }) + + test('args: parameters', async () => { + await setup() + + const result = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": 1000000000000000000n, + } + `) + + const result2 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result2).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + + const result3 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees', 'nonce'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result3).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 953, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + + const result4 = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + parameters: ['gas', 'fees', 'nonce', 'type'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect(result4).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "gas": 21000n, + "maxFeePerGas": 13000000000n, + "maxPriorityFeePerGas": 1000000000n, + "nonce": 953, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "type": "eip1559", + "value": 1000000000000000000n, + } + `) + + const { + blobs: _blobs, + sidecars, + ...result5 + } = await prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + blobs: toBlobs({ data: '0x1234' }), + kzg, + maxFeePerBlobGas: parseGwei('20'), + parameters: ['sidecars'], + to: targetAccount.address, + value: parseEther('1'), + }) + expect( + sidecars.map(({ blob: _blob, ...rest }) => rest), + ).toMatchInlineSnapshot(` + [ + { + "commitment": "0xae5f688fc774ce26be308660c003c9c528a85410ce7f3138e37f424b7a31f61afaff45d74996ac5a5d83d061857b8006", + "proof": "0xb0bab7126f83bd4ad1ae36a51f64fdef1bd198174c1a355660bf462b98075546960d33101ae778128a7693a2b110d218", + }, + ] + `) + expect(result5).toMatchInlineSnapshot(` + { + "account": { + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, + "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", + "sign": [Function], + "signAuthorization": [Function], + "signMessage": [Function], + "signTransaction": [Function], + "signTypedData": [Function], + "source": "privateKey", + "type": "local", + }, + "from": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "kzg": { + "blobToKzgCommitment": [Function], + "computeBlobKzgProof": [Function], + }, + "maxFeePerBlobGas": 20000000000n, + "to": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", + "value": 1000000000000000000n, + } + `) + }) + + test('behavior: nonce manager', async () => { + await setup() + + const account = privateKeyToAccount(sourceAccount.privateKey, { + nonceManager, + }) + + const args = { + account, + nonceManager: account.nonceManager, + to: targetAccount.address, + value: parseEther('1'), + parameters: ['nonce'], + } as const + + const request_1 = await prepareTransactionRequest(client, args) + expect(request_1.nonce).toBe(958) + + const request_2 = await prepareTransactionRequest(client, args) + expect(request_2.nonce).toBe(959) + + const [request_3, request_4, request_5] = await Promise.all([ + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + ]) + + expect(request_3.nonce).toBe(960) + expect(request_4.nonce).toBe(961) + expect(request_5.nonce).toBe(962) + }) }) diff --git a/src/actions/wallet/prepareTransactionRequest.ts b/src/actions/wallet/prepareTransactionRequest.ts index e6e7aafb28..ba789fa801 100644 --- a/src/actions/wallet/prepareTransactionRequest.ts +++ b/src/actions/wallet/prepareTransactionRequest.ts @@ -24,6 +24,7 @@ import { import type { Client } from '../../clients/createClient.js' import type { Transport } from '../../clients/transports/createTransport.js' import type { AccountNotFoundErrorType } from '../../errors/account.js' +import type { BaseError } from '../../errors/base.js' import { Eip1559FeesNotSupportedError, MaxFeePerGasTooLowError, @@ -58,6 +59,7 @@ import { commitmentsToVersionedHashes } from '../../utils/blob/commitmentsToVers import { toBlobSidecars } from '../../utils/blob/toBlobSidecars.js' import type { FormattedTransactionRequest } from '../../utils/formatters/transactionRequest.js' import { getAction } from '../../utils/getAction.js' +import { LruMap } from '../../utils/lru.js' import type { NonceManager } from '../../utils/nonceManager.js' import { type AssertRequestErrorType, @@ -68,6 +70,10 @@ import { type GetTransactionType, getTransactionType, } from '../../utils/transaction/getTransactionType.js' +import { + type FillTransactionParameters, + fillTransaction, +} from '../public/fillTransaction.js' import { getChainId as getChainId_ } from '../public/getChainId.js' export const defaultParameters = [ @@ -82,6 +88,8 @@ export const defaultParameters = [ /** @internal */ export const eip1559NetworkCache = /*#__PURE__*/ new Map() +const supportsFillTransaction = /*#__PURE__*/ new LruMap(128) + export type PrepareTransactionRequestParameterType = | 'blobVersionedHashes' | 'chainId' @@ -241,7 +249,7 @@ export async function prepareTransactionRequest< chainOverride extends Chain | undefined = undefined, >( client: Client, - args: PrepareTransactionRequestParameters< + args_: PrepareTransactionRequestParameters< chain, account, chainOverride, @@ -257,6 +265,60 @@ export async function prepareTransactionRequest< request > > { + const attemptFill = + // Do not attempt if `eth_fillTransaction` is not supported. + supportsFillTransaction.get(client.uid) !== false && + // Should attempt `eth_fillTransaction` if "fees" or "gas" are required to be populated, + // otherwise, can just use the other individual calls. + ['fees', 'gas'].some((parameter) => + args_.parameters?.includes( + parameter as PrepareTransactionRequestParameterType, + ), + ) + + const fillResult = attemptFill + ? await getAction( + client, + fillTransaction, + 'fillTransaction', + )(args_ as FillTransactionParameters) + .then((result) => { + const { + chainId, + from, + gas, + gasPrice, + nonce, + maxFeePerBlobGas, + maxFeePerGas, + maxPriorityFeePerGas, + type, + } = result.transaction + supportsFillTransaction.set(client.uid, true) + return { + ...args_, + chainId, + from, + gas, + gasPrice, + nonce, + maxFeePerBlobGas, + maxFeePerGas, + maxPriorityFeePerGas, + type, + } + }) + .catch((e) => { + const error = e as BaseError & { cause: BaseError } + if ( + error.cause.name === 'MethodNotFoundRpcError' || + error.cause.name === 'MethodNotSupportedRpcError' + ) + supportsFillTransaction.set(client.uid, false) + return args_ + }) + : args_ + const { account: account_ = client.account, blobs, @@ -267,10 +329,14 @@ export async function prepareTransactionRequest< nonceManager, parameters = defaultParameters, type, - } = args + } = fillResult + const account = account_ ? parseAccount(account_) : account_ - const request = { ...args, ...(account ? { from: account?.address } : {}) } + const request = { + ...fillResult, + ...(account ? { from: account?.address } : {}), + } let block: Block | undefined async function getBlock(): Promise { @@ -287,7 +353,7 @@ export async function prepareTransactionRequest< async function getChainId(): Promise { if (chainId) return chainId if (chain) return chain.id - if (typeof args.chainId !== 'undefined') return args.chainId + if (typeof request.chainId !== 'undefined') return request.chainId const chainId_ = await getAction(client, getChainId_, 'getChainId')({}) chainId = chainId_ return chainId @@ -379,9 +445,9 @@ export async function prepareTransactionRequest< }) if ( - typeof args.maxPriorityFeePerGas === 'undefined' && - args.maxFeePerGas && - args.maxFeePerGas < maxPriorityFeePerGas + typeof request.maxPriorityFeePerGas === 'undefined' && + request.maxFeePerGas && + request.maxFeePerGas < maxPriorityFeePerGas ) throw new MaxFeePerGasTooLowError({ maxPriorityFeePerGas, @@ -393,12 +459,12 @@ export async function prepareTransactionRequest< } else { // Legacy fees if ( - typeof args.maxFeePerGas !== 'undefined' || - typeof args.maxPriorityFeePerGas !== 'undefined' + typeof request.maxFeePerGas !== 'undefined' || + typeof request.maxPriorityFeePerGas !== 'undefined' ) throw new Eip1559FeesNotSupportedError() - if (typeof args.gasPrice === 'undefined') { + if (typeof request.gasPrice === 'undefined') { const block = await getBlock() const { gasPrice: gasPrice_ } = await internal_estimateFeesPerGas( client, diff --git a/src/actions/wallet/sendTransaction.test.ts b/src/actions/wallet/sendTransaction.test.ts index 913df26ad4..081ea43729 100644 --- a/src/actions/wallet/sendTransaction.test.ts +++ b/src/actions/wallet/sendTransaction.test.ts @@ -195,7 +195,7 @@ test('sends transaction (w/ serializer)', async () => { ).rejects.toThrowError() expect(serializer).toReturnWith( - '0x08f2018203b9843b9aca008469126a1c825208809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0', + '0x08f3018203b9843b9aca00850300e66100825208809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0', ) }) @@ -650,8 +650,6 @@ describe('local account', () => { test('default', async () => { await setup() - const fees = await estimateFeesPerGas(client) - expect( await getBalance(client, { address: targetAccount.address }), ).toMatchInlineSnapshot('10000000000000000000000n') @@ -676,8 +674,8 @@ describe('local account', () => { ).toBeLessThan(sourceAccount.balance) const transaction = await getTransaction(client, { hash }) - expect(transaction.maxFeePerGas).toBe(fees.maxFeePerGas) - expect(transaction.maxPriorityFeePerGas).toBe(fees.maxPriorityFeePerGas) + expect(transaction.maxFeePerGas).toBe(12900000000n) + expect(transaction.maxPriorityFeePerGas).toBe(1000000000n) expect(transaction.gas).toBe(21000n) }) diff --git a/src/actions/wallet/sendTransactionSync.test.ts b/src/actions/wallet/sendTransactionSync.test.ts index 545c9428cc..f461c0e8a8 100644 --- a/src/actions/wallet/sendTransactionSync.test.ts +++ b/src/actions/wallet/sendTransactionSync.test.ts @@ -261,7 +261,7 @@ test('sends transaction (w/ serializer)', async () => { ).rejects.toThrowError() expect(serializer).toReturnWith( - '0x08f2018203b9843b9aca008469126a1c825208809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0', + '0x08f3018203b9843b9aca00850300e66100825208809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0', ) }) @@ -809,8 +809,6 @@ describe('local account', () => { test('default', async () => { await setup() - const fees = await estimateFeesPerGas(client) - expect( await getBalance(client, { address: targetAccount.address }), ).toMatchInlineSnapshot('10000000000000000000000n') @@ -858,8 +856,8 @@ describe('local account', () => { const transaction = await getTransaction(client, { hash: receipt.transactionHash, }) - expect(transaction.maxFeePerGas).toBe(fees.maxFeePerGas) - expect(transaction.maxPriorityFeePerGas).toBe(fees.maxPriorityFeePerGas) + expect(transaction.maxFeePerGas).toBe(12900000000n) + expect(transaction.maxPriorityFeePerGas).toBe(1000000000n) expect(transaction.gas).toBe(21000n) }) diff --git a/src/index.ts b/src/index.ts index a86ad500ae..5803bdd153 100644 --- a/src/index.ts +++ b/src/index.ts @@ -135,6 +135,11 @@ export type { EstimateMaxPriorityFeePerGasParameters, EstimateMaxPriorityFeePerGasReturnType, } from './actions/public/estimateMaxPriorityFeePerGas.js' +export type { + FillTransactionErrorType, + FillTransactionParameters, + FillTransactionReturnType, +} from './actions/public/fillTransaction.js' export type { GetBalanceErrorType, GetBalanceParameters, diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 37bd651ec7..9dc31c49b6 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -753,6 +753,21 @@ export type PublicRpcSchema = [ ] ReturnType: Quantity }, + /** + * @description Fills a transaction with the necessary data to be signed. + * + * @example + * provider.request({ method: 'eth_fillTransaction', params: [{ from: '0x...', to: '0x...', value: '0x...' }] }) + * // => '0x...' + */ + { + Method: 'eth_fillTransaction' + Parameters: [transaction: TransactionRequest] + ReturnType: { + raw: Hex + tx: Transaction + } + }, /** * @description Returns a collection of historical gas information * @@ -1701,6 +1716,21 @@ export type WalletRpcSchema = [ ] ReturnType: Quantity }, + /** + * @description Fills a transaction with the necessary data to be signed. + * + * @example + * provider.request({ method: 'eth_fillTransaction', params: [{ from: '0x...', to: '0x...', value: '0x...' }] }) + * // => '0x...' + */ + { + Method: 'eth_fillTransaction' + Parameters: [transaction: TransactionRequest] + ReturnType: { + raw: Hex + tx: Transaction + } + }, /** * @description Requests that the user provides an Ethereum address to be identified by. Typically causes a browser extension popup to appear. * @link https://eips.ethereum.org/EIPS/eip-1102 diff --git a/src/utils/transaction/assertRequest.ts b/src/utils/transaction/assertRequest.ts index b6331e787c..f6afa0fd0a 100644 --- a/src/utils/transaction/assertRequest.ts +++ b/src/utils/transaction/assertRequest.ts @@ -14,10 +14,7 @@ import { TipAboveFeeCapError, type TipAboveFeeCapErrorType, } from '../../errors/node.js' -import { - FeeConflictError, - type FeeConflictErrorType, -} from '../../errors/transaction.js' +import type { FeeConflictErrorType } from '../../errors/transaction.js' import type { ErrorType } from '../../errors/utils.js' import type { Chain } from '../../types/chain.js' import type { ExactPartial } from '../../types/utils.js' @@ -36,23 +33,11 @@ export type AssertRequestErrorType = | ErrorType export function assertRequest(args: AssertRequestParameters) { - const { - account: account_, - gasPrice, - maxFeePerGas, - maxPriorityFeePerGas, - to, - } = args + const { account: account_, maxFeePerGas, maxPriorityFeePerGas, to } = args const account = account_ ? parseAccount(account_) : undefined if (account && !isAddress(account.address)) throw new InvalidAddressError({ address: account.address }) if (to && !isAddress(to)) throw new InvalidAddressError({ address: to }) - if ( - typeof gasPrice !== 'undefined' && - (typeof maxFeePerGas !== 'undefined' || - typeof maxPriorityFeePerGas !== 'undefined') - ) - throw new FeeConflictError() if (maxFeePerGas && maxFeePerGas > maxUint256) throw new FeeCapTooHighError({ maxFeePerGas }) From 372c00955648626ab5696e89cee243ed91d7faad Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:23:48 +1100 Subject: [PATCH 2/5] w --- src/utils/transaction/assertRequest.test.ts | 42 --------------------- 1 file changed, 42 deletions(-) diff --git a/src/utils/transaction/assertRequest.test.ts b/src/utils/transaction/assertRequest.test.ts index c36d7a93f7..3e69a054ac 100644 --- a/src/utils/transaction/assertRequest.test.ts +++ b/src/utils/transaction/assertRequest.test.ts @@ -64,45 +64,3 @@ test('tip higher than fee cap', () => { Version: viem@x.y.z] `) }) - -test('fee conflict', () => { - expect(() => - // @ts-expect-error - assertRequest({ - gasPrice: parseGwei('8'), - maxFeePerGas: parseGwei('10'), - maxPriorityFeePerGas: parseGwei('11'), - }), - ).toThrowErrorMatchingInlineSnapshot(` - [FeeConflictError: Cannot specify both a \`gasPrice\` and a \`maxFeePerGas\`/\`maxPriorityFeePerGas\`. - Use \`maxFeePerGas\`/\`maxPriorityFeePerGas\` for EIP-1559 compatible networks, and \`gasPrice\` for others. - - Version: viem@x.y.z] - `) - - expect(() => - // @ts-expect-error - assertRequest({ - gasPrice: parseGwei('8'), - maxFeePerGas: parseGwei('10'), - }), - ).toThrowErrorMatchingInlineSnapshot(` - [FeeConflictError: Cannot specify both a \`gasPrice\` and a \`maxFeePerGas\`/\`maxPriorityFeePerGas\`. - Use \`maxFeePerGas\`/\`maxPriorityFeePerGas\` for EIP-1559 compatible networks, and \`gasPrice\` for others. - - Version: viem@x.y.z] - `) - - expect(() => - // @ts-expect-error - assertRequest({ - gasPrice: parseGwei('8'), - maxPriorityFeePerGas: parseGwei('11'), - }), - ).toThrowErrorMatchingInlineSnapshot(` - [FeeConflictError: Cannot specify both a \`gasPrice\` and a \`maxFeePerGas\`/\`maxPriorityFeePerGas\`. - Use \`maxFeePerGas\`/\`maxPriorityFeePerGas\` for EIP-1559 compatible networks, and \`gasPrice\` for others. - - Version: viem@x.y.z] - `) -}) From d7456623c40c93346ce7256b49aab25db07604d6 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:50:43 +1100 Subject: [PATCH 3/5] decorator --- src/actions/index.test.ts | 1 + src/actions/public/fillTransaction.ts | 24 ++++++++++++++++ src/clients/createClient.test.ts | 1 + src/clients/createPublicClient.test.ts | 5 ++++ src/clients/createTestClient.test.ts | 1 + src/clients/createWalletClient.test.ts | 7 +++++ src/clients/decorators/public.test.ts | 1 + src/clients/decorators/public.ts | 40 ++++++++++++++++++++++++++ src/clients/decorators/wallet.test.ts | 1 + src/clients/decorators/wallet.ts | 40 ++++++++++++++++++++++++++ 10 files changed, 121 insertions(+) diff --git a/src/actions/index.test.ts b/src/actions/index.test.ts index a1e246f775..b6351ab500 100644 --- a/src/actions/index.test.ts +++ b/src/actions/index.test.ts @@ -27,6 +27,7 @@ test('exports actions', () => { "estimateFeesPerGas": [Function], "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getAutomine": [Function], "getBalance": [Function], diff --git a/src/actions/public/fillTransaction.ts b/src/actions/public/fillTransaction.ts index bef357d0b4..af2cd9226d 100644 --- a/src/actions/public/fillTransaction.ts +++ b/src/actions/public/fillTransaction.ts @@ -67,6 +67,30 @@ export type FillTransactionErrorType = | GetTransactionErrorReturnType | ErrorType +/** + * Fills a transaction request with the necessary fields to be signed over. + * + * - Docs: https://viem.sh/docs/actions/public/fillTransaction + * + * @param client - Client to use + * @param parameters - {@link FillTransactionParameters} + * @returns The filled transaction. {@link FillTransactionReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * import { fillTransaction } from 'viem/public' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * const result = await fillTransaction(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: parseEther('1'), + * }) + */ export async function fillTransaction< chain extends Chain | undefined, account extends Account | undefined, diff --git a/src/clients/createClient.test.ts b/src/clients/createClient.test.ts index f42ebf1c97..c1bc66e2dc 100644 --- a/src/clients/createClient.test.ts +++ b/src/clients/createClient.test.ts @@ -586,6 +586,7 @@ describe('extends', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], diff --git a/src/clients/createPublicClient.test.ts b/src/clients/createPublicClient.test.ts index 6085351c42..5b518381d8 100644 --- a/src/clients/createPublicClient.test.ts +++ b/src/clients/createPublicClient.test.ts @@ -46,6 +46,7 @@ test('creates', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], @@ -191,6 +192,7 @@ describe('transports', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], @@ -301,6 +303,7 @@ describe('transports', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], @@ -436,6 +439,7 @@ describe('transports', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], @@ -550,6 +554,7 @@ test('extend', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getAutomine": [Function], "getBalance": [Function], diff --git a/src/clients/createTestClient.test.ts b/src/clients/createTestClient.test.ts index fda48033b5..c549b8eee3 100644 --- a/src/clients/createTestClient.test.ts +++ b/src/clients/createTestClient.test.ts @@ -332,6 +332,7 @@ test('extend', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getAutomine": [Function], "getBalance": [Function], diff --git a/src/clients/createWalletClient.test.ts b/src/clients/createWalletClient.test.ts index 85bc660bd9..1b724b5ffe 100644 --- a/src/clients/createWalletClient.test.ts +++ b/src/clients/createWalletClient.test.ts @@ -42,6 +42,7 @@ test('creates', () => { "chain": undefined, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -109,6 +110,7 @@ describe('args: account', () => { "chain": undefined, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -183,6 +185,7 @@ describe('args: account', () => { "chain": undefined, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -245,6 +248,7 @@ describe('args: transport', () => { "chain": undefined, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -305,6 +309,7 @@ describe('args: transport', () => { "chain": undefined, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -386,6 +391,7 @@ describe('args: transport', () => { }, "deployContract": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], @@ -486,6 +492,7 @@ test('extend', () => { "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], "extend": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getAutomine": [Function], "getBalance": [Function], diff --git a/src/clients/decorators/public.test.ts b/src/clients/decorators/public.test.ts index 3ed91285a5..091da835ed 100644 --- a/src/clients/decorators/public.test.ts +++ b/src/clients/decorators/public.test.ts @@ -43,6 +43,7 @@ test('default', async () => { "estimateFeesPerGas": [Function], "estimateGas": [Function], "estimateMaxPriorityFeePerGas": [Function], + "fillTransaction": [Function], "getBalance": [Function], "getBlobBaseFee": [Function], "getBlock": [Function], diff --git a/src/clients/decorators/public.ts b/src/clients/decorators/public.ts index 76d7f4a1fb..67fb5b28fa 100644 --- a/src/clients/decorators/public.ts +++ b/src/clients/decorators/public.ts @@ -35,6 +35,11 @@ import { type CreateAccessListReturnType, createAccessList, } from '../../actions/public/createAccessList.js' +import { + type FillTransactionParameters, + type FillTransactionReturnType, + fillTransaction, +} from '../../actions/public/fillTransaction.js' import { type CreateBlockFilterReturnType, createBlockFilter, @@ -543,6 +548,40 @@ export type PublicActions< estimateGas: ( args: EstimateGasParameters, ) => Promise + /** + * Fills a transaction request with the necessary fields to be signed over. + * + * - Docs: https://viem.sh/docs/actions/public/fillTransaction + * + * @param client - Client to use + * @param parameters - {@link FillTransactionParameters} + * @returns The filled transaction. {@link FillTransactionReturnType} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createPublicClient({ + * chain: mainnet, + * transport: http(), + * }) + * const result = await client.fillTransaction({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: parseEther('1'), + * }) + */ + fillTransaction: < + chainOverride extends Chain | undefined = undefined, + accountOverride extends Account | Address | undefined = undefined, + >( + args: FillTransactionParameters< + chain, + account, + chainOverride, + accountOverride + >, + ) => Promise> /** * Returns the balance of an address in wei. * @@ -2039,6 +2078,7 @@ export function publicActions< getProof: (args) => getProof(client, args), estimateMaxPriorityFeePerGas: (args) => estimateMaxPriorityFeePerGas(client, args), + fillTransaction: (args) => fillTransaction(client, args), getStorageAt: (args) => getStorageAt(client, args), getTransaction: (args) => getTransaction(client, args), getTransactionConfirmations: (args) => diff --git a/src/clients/decorators/wallet.test.ts b/src/clients/decorators/wallet.test.ts index 7e18316956..32355427e9 100644 --- a/src/clients/decorators/wallet.test.ts +++ b/src/clients/decorators/wallet.test.ts @@ -20,6 +20,7 @@ test('default', async () => { { "addChain": [Function], "deployContract": [Function], + "fillTransaction": [Function], "getAddresses": [Function], "getCallsStatus": [Function], "getCapabilities": [Function], diff --git a/src/clients/decorators/wallet.ts b/src/clients/decorators/wallet.ts index 40580d3c56..665560db7b 100644 --- a/src/clients/decorators/wallet.ts +++ b/src/clients/decorators/wallet.ts @@ -141,6 +141,11 @@ import type { } from '../../types/contract.js' import type { Client } from '../createClient.js' import type { Transport } from '../transports/createTransport.js' +import { + fillTransaction, + type FillTransactionParameters, + type FillTransactionReturnType, +} from '../../actions/public/fillTransaction.js' export type WalletActions< chain extends Chain | undefined = Chain | undefined, @@ -195,6 +200,40 @@ export type WalletActions< >( args: DeployContractParameters, ) => Promise + /** + * Fills a transaction request with the necessary fields to be signed over. + * + * - Docs: https://viem.sh/docs/actions/public/fillTransaction + * + * @param client - Client to use + * @param parameters - {@link FillTransactionParameters} + * @returns The filled transaction. {@link FillTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const result = await client.fillTransaction({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: parseEther('1'), + * }) + */ + fillTransaction: < + chainOverride extends Chain | undefined = undefined, + accountOverride extends Account | Address | undefined = undefined, + >( + args: FillTransactionParameters< + chain, + account, + chainOverride, + accountOverride + >, + ) => Promise> /** * Returns a list of account addresses owned by the wallet or client. * @@ -1146,6 +1185,7 @@ export function walletActions< return { addChain: (args) => addChain(client, args), deployContract: (args) => deployContract(client, args), + fillTransaction: (args) => fillTransaction(client, args), getAddresses: () => getAddresses(client), getCallsStatus: (args) => getCallsStatus(client, args), getCapabilities: (args) => getCapabilities(client, args), From 68143cd321c8741cac0ec4e7614f0d19994dbcf7 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:50:50 +1100 Subject: [PATCH 4/5] decorator --- src/clients/decorators/public.ts | 10 +++++----- src/clients/decorators/wallet.ts | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/clients/decorators/public.ts b/src/clients/decorators/public.ts index 67fb5b28fa..40a69cc286 100644 --- a/src/clients/decorators/public.ts +++ b/src/clients/decorators/public.ts @@ -35,11 +35,6 @@ import { type CreateAccessListReturnType, createAccessList, } from '../../actions/public/createAccessList.js' -import { - type FillTransactionParameters, - type FillTransactionReturnType, - fillTransaction, -} from '../../actions/public/fillTransaction.js' import { type CreateBlockFilterReturnType, createBlockFilter, @@ -78,6 +73,11 @@ import { type EstimateMaxPriorityFeePerGasReturnType, estimateMaxPriorityFeePerGas, } from '../../actions/public/estimateMaxPriorityFeePerGas.js' +import { + type FillTransactionParameters, + type FillTransactionReturnType, + fillTransaction, +} from '../../actions/public/fillTransaction.js' import { type GetBalanceParameters, type GetBalanceReturnType, diff --git a/src/clients/decorators/wallet.ts b/src/clients/decorators/wallet.ts index 665560db7b..f918df0620 100644 --- a/src/clients/decorators/wallet.ts +++ b/src/clients/decorators/wallet.ts @@ -1,6 +1,11 @@ import type { Abi, Address, TypedData } from 'abitype' import type { Account } from '../../accounts/types.js' +import { + type FillTransactionParameters, + type FillTransactionReturnType, + fillTransaction, +} from '../../actions/public/fillTransaction.js' import { type GetChainIdReturnType, getChainId, @@ -141,11 +146,6 @@ import type { } from '../../types/contract.js' import type { Client } from '../createClient.js' import type { Transport } from '../transports/createTransport.js' -import { - fillTransaction, - type FillTransactionParameters, - type FillTransactionReturnType, -} from '../../actions/public/fillTransaction.js' export type WalletActions< chain extends Chain | undefined = Chain | undefined, From 8f7ca400910427030890bbd5706d1cd775640dfd Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:58:26 +1100 Subject: [PATCH 5/5] chore: changeset --- .changeset/beige-vans-move.md | 5 +++++ .changeset/modern-eggs-prove.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/beige-vans-move.md create mode 100644 .changeset/modern-eggs-prove.md diff --git a/.changeset/beige-vans-move.md b/.changeset/beige-vans-move.md new file mode 100644 index 0000000000..c2460ce1ed --- /dev/null +++ b/.changeset/beige-vans-move.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Hooked up `eth_fillTransaction` routing to `prepareTransactionRequest`, to reduce the RPC calls required to prepare a local transaction from 3-4, to 1 (if `eth_fillTransaction` is supported by the execution node). diff --git a/.changeset/modern-eggs-prove.md b/.changeset/modern-eggs-prove.md new file mode 100644 index 0000000000..9c6b15cd81 --- /dev/null +++ b/.changeset/modern-eggs-prove.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added `fillTransaction` action for `eth_fillTransaction` support.