From 66afc48a2242201f63ab0bcdc03cafdd3f05e884 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 12:50:38 +1000 Subject: [PATCH 01/21] wip: checkpoint --- src/accounts/hdKeyToAccount.test.ts | 1 + src/accounts/hdKeyToAccount.ts | 13 +- src/accounts/mnemonicToAccount.test.ts | 1 + src/accounts/mnemonicToAccount.ts | 3 + src/accounts/privateKeyToAccount.test.ts | 1 + src/accounts/privateKeyToAccount.ts | 12 +- src/accounts/toAccount.test.ts | 1 + src/accounts/toAccount.ts | 1 + src/accounts/types.ts | 2 + src/accounts/utils/parseAccount.test.ts | 1 + .../wallet/prepareTransactionRequest.test.ts | 918 +++++++++--------- .../wallet/prepareTransactionRequest.ts | 47 +- src/actions/wallet/sendTransaction.ts | 2 +- src/actions/wallet/signMessage.ts | 2 +- src/actions/wallet/signTransaction.ts | 2 +- src/actions/wallet/signTypedData.ts | 2 +- src/actions/wallet/switchChain.ts | 2 +- src/clients/createWalletClient.test.ts | 1 + src/clients/transports/createTransport.ts | 14 +- src/clients/transports/http.test.ts | 22 +- src/experimental/eip5792/actions/sendCalls.ts | 2 +- .../erc7715/actions/issuePermissions.ts | 11 +- src/experimental/index.ts | 8 + src/experimental/utils/nonceManager.test.ts | 242 +++++ src/experimental/utils/nonceManager.ts | 134 +++ src/types/eip1193.ts | 4 + src/utils/buildRequest.ts | 192 ++-- src/utils/promise/withDedupe.test.ts | 25 + src/utils/promise/withDedupe.ts | 21 + 29 files changed, 1122 insertions(+), 565 deletions(-) create mode 100644 src/experimental/utils/nonceManager.test.ts create mode 100644 src/experimental/utils/nonceManager.ts create mode 100644 src/utils/promise/withDedupe.test.ts create mode 100644 src/utils/promise/withDedupe.ts diff --git a/src/accounts/hdKeyToAccount.test.ts b/src/accounts/hdKeyToAccount.test.ts index e676411471..6d63756ab8 100644 --- a/src/accounts/hdKeyToAccount.test.ts +++ b/src/accounts/hdKeyToAccount.test.ts @@ -21,6 +21,7 @@ test('default', () => { { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "getHdKey": [Function], + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], diff --git a/src/accounts/hdKeyToAccount.ts b/src/accounts/hdKeyToAccount.ts index 047a9dde28..a57cebc94c 100644 --- a/src/accounts/hdKeyToAccount.ts +++ b/src/accounts/hdKeyToAccount.ts @@ -4,10 +4,13 @@ import type { ErrorType } from '../errors/utils.js' import type { HDKey } from '../types/account.js' import { type PrivateKeyToAccountErrorType, + type PrivateKeyToAccountOptions, privateKeyToAccount, } from './privateKeyToAccount.js' import type { HDAccount, HDOptions } from './types.js' +export type HDKeyToAccountOptions = HDOptions & PrivateKeyToAccountOptions + export type HDKeyToAccountErrorType = | PrivateKeyToAccountErrorType | ToHexErrorType @@ -20,12 +23,18 @@ export type HDKeyToAccountErrorType = */ export function hdKeyToAccount( hdKey_: HDKey, - { accountIndex = 0, addressIndex = 0, changeIndex = 0, path }: HDOptions = {}, + { + accountIndex = 0, + addressIndex = 0, + changeIndex = 0, + path, + ...options + }: HDKeyToAccountOptions = {}, ): HDAccount { const hdKey = hdKey_.derive( path || `m/44'/60'/${accountIndex}'/${changeIndex}/${addressIndex}`, ) - const account = privateKeyToAccount(toHex(hdKey.privateKey!)) + const account = privateKeyToAccount(toHex(hdKey.privateKey!), options) return { ...account, getHdKey: () => hdKey, diff --git a/src/accounts/mnemonicToAccount.test.ts b/src/accounts/mnemonicToAccount.test.ts index 53cec3fa0b..61d620e5b0 100644 --- a/src/accounts/mnemonicToAccount.test.ts +++ b/src/accounts/mnemonicToAccount.test.ts @@ -14,6 +14,7 @@ test('default', () => { { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "getHdKey": [Function], + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], diff --git a/src/accounts/mnemonicToAccount.ts b/src/accounts/mnemonicToAccount.ts index 39da0cc955..4b8446af14 100644 --- a/src/accounts/mnemonicToAccount.ts +++ b/src/accounts/mnemonicToAccount.ts @@ -4,10 +4,13 @@ import { mnemonicToSeedSync } from '@scure/bip39' import type { ErrorType } from '../errors/utils.js' import { type HDKeyToAccountErrorType, + type HDKeyToAccountOptions, hdKeyToAccount, } from './hdKeyToAccount.js' import type { HDAccount, HDOptions } from './types.js' +export type MnemonicToAccountOptions = HDKeyToAccountOptions + export type MnemonicToAccountErrorType = HDKeyToAccountErrorType | ErrorType /** diff --git a/src/accounts/privateKeyToAccount.test.ts b/src/accounts/privateKeyToAccount.test.ts index fb9e156153..a81e1f0b78 100644 --- a/src/accounts/privateKeyToAccount.test.ts +++ b/src/accounts/privateKeyToAccount.test.ts @@ -10,6 +10,7 @@ test('default', () => { expect(privateKeyToAccount(accounts[0].privateKey)).toMatchInlineSnapshot(` { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], diff --git a/src/accounts/privateKeyToAccount.ts b/src/accounts/privateKeyToAccount.ts index 6f90ec1ac7..fbb3d33a4a 100644 --- a/src/accounts/privateKeyToAccount.ts +++ b/src/accounts/privateKeyToAccount.ts @@ -4,6 +4,7 @@ import type { Hex } from '../types/misc.js' import { type ToHexErrorType, toHex } from '../utils/encoding/toHex.js' import type { ErrorType } from '../errors/utils.js' +import type { NonceManager } from '../experimental/utils/nonceManager.js' import { type ToAccountErrorType, toAccount } from './toAccount.js' import type { PrivateKeyAccount } from './types.js' import { @@ -20,6 +21,10 @@ import { signTypedData, } from './utils/signTypedData.js' +export type PrivateKeyToAccountOptions = { + nonceManager?: NonceManager | undefined +} + export type PrivateKeyToAccountErrorType = | ToAccountErrorType | ToHexErrorType @@ -34,12 +39,17 @@ export type PrivateKeyToAccountErrorType = * * @returns A Private Key Account. */ -export function privateKeyToAccount(privateKey: Hex): PrivateKeyAccount { +export function privateKeyToAccount( + privateKey: Hex, + options: PrivateKeyToAccountOptions = {}, +): PrivateKeyAccount { + const { nonceManager } = options const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false)) const address = publicKeyToAddress(publicKey) const account = toAccount({ address, + nonceManager, async signMessage({ message }) { return signMessage({ message, privateKey }) }, diff --git a/src/accounts/toAccount.test.ts b/src/accounts/toAccount.test.ts index d47fa4cb8a..dd26bd295c 100644 --- a/src/accounts/toAccount.test.ts +++ b/src/accounts/toAccount.test.ts @@ -42,6 +42,7 @@ describe('toAccount', () => { ).toMatchInlineSnapshot(` { "address": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonceManager": undefined, "signMessage": [Function], "signTransaction": [Function], "signTypedData": [Function], diff --git a/src/accounts/toAccount.ts b/src/accounts/toAccount.ts index dc70d171c4..64bd483833 100644 --- a/src/accounts/toAccount.ts +++ b/src/accounts/toAccount.ts @@ -47,6 +47,7 @@ export function toAccount( throw new InvalidAddressError({ address: source.address }) return { address: source.address, + nonceManager: source.nonceManager, signMessage: source.signMessage, signTransaction: source.signTransaction, signTypedData: source.signTypedData, diff --git a/src/accounts/types.ts b/src/accounts/types.ts index 5f209bc4e5..7ac3a6f7ce 100644 --- a/src/accounts/types.ts +++ b/src/accounts/types.ts @@ -1,5 +1,6 @@ import type { Address, TypedData } from 'abitype' +import type { NonceManager } from '../experimental/utils/nonceManager.js' import type { HDKey } from '../types/account.js' import type { Hash, Hex, SignableMessage } from '../types/misc.js' import type { @@ -18,6 +19,7 @@ export type Account = OneOf< export type AccountSource = Address | CustomSource export type CustomSource = { address: Address + nonceManager?: NonceManager | undefined signMessage: ({ message }: { message: SignableMessage }) => Promise signTransaction: < serializer extends diff --git a/src/accounts/utils/parseAccount.test.ts b/src/accounts/utils/parseAccount.test.ts index 1bcc66dfc1..1b9ce36250 100644 --- a/src/accounts/utils/parseAccount.test.ts +++ b/src/accounts/utils/parseAccount.test.ts @@ -22,6 +22,7 @@ test('account', () => { ).toMatchInlineSnapshot(` { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], diff --git a/src/actions/wallet/prepareTransactionRequest.test.ts b/src/actions/wallet/prepareTransactionRequest.test.ts index 8f55d39f50..5d188cf597 100644 --- a/src/actions/wallet/prepareTransactionRequest.test.ts +++ b/src/actions/wallet/prepareTransactionRequest.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test, vi } from 'vitest' +import { expect, test, vi } from 'vitest' import { accounts } from '~test/src/constants.js' import { kzg } from '~test/src/kzg.js' @@ -11,6 +11,7 @@ import { parseEther } from '../../utils/unit/parseEther.js' import { parseGwei } from '../../utils/unit/parseGwei.js' import { anvilMainnet } from '../../../test/src/anvil.js' +import { nonceManager } from '../../experimental/index.js' import { http, createClient, toBlobs } from '../../index.js' import { prepareTransactionRequest } from './prepareTransactionRequest.js' @@ -34,24 +35,23 @@ async function setup() { await mine(client, { blocks: 1 }) } -describe('prepareTransactionRequest', () => { - 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(` +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, @@ -60,27 +60,28 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('legacy fees', async () => { - await setup() +test('legacy fees', async () => { + await setup() - vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({ - baseFeePerGas: undefined, - } as any) + 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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -97,24 +98,25 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) - - 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(` +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", "signMessage": [Function], "signTransaction": [Function], @@ -131,24 +133,25 @@ describe('prepareTransactionRequest', () => { "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(` +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", "signMessage": [Function], "signTransaction": [Function], @@ -165,25 +168,26 @@ describe('prepareTransactionRequest', () => { "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(` +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", "signMessage": [Function], "signTransaction": [Function], @@ -200,22 +204,23 @@ describe('prepareTransactionRequest', () => { "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(` +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", "signMessage": [Function], "signTransaction": [Function], @@ -233,26 +238,27 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: gasPrice', async () => { - await setup() +test('args: gasPrice', async () => { + await setup() - vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({} as any) + vi.spyOn(getBlock, 'getBlock').mockResolvedValueOnce({} as any) - const { nonce: _nonce, ...request } = await prepareTransactionRequest( - client, - { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - value: parseEther('1'), - }, - ) - expect(request).toMatchInlineSnapshot(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -269,24 +275,25 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: gasPrice (on chain w/ block.baseFeePerGas)', 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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -303,21 +310,22 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: maxFeePerGas', 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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -335,59 +343,60 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) + +test('args: maxFeePerGas (under default tip)', async () => { + await setup() - 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(` + 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) - 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(` + 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() +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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -405,21 +414,22 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: maxPriorityFeePerGas === 0', async () => { - await setup() +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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -437,43 +447,44 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) + +test('args: maxPriorityFeePerGas (on legacy)', async () => { + await setup() - 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(` + 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() +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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -491,58 +502,59 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: gasPrice + maxFeePerGas', async () => { - await setup() - - await expect(() => - // @ts-expect-error - prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - to: targetAccount.address, - gasPrice: parseGwei('10'), - maxFeePerGas: parseGwei('20'), - value: parseEther('1'), - }), - ).rejects.toThrowError( - 'Cannot specify both a `gasPrice` and a `maxFeePerGas`/`maxPriorityFeePerGas`.', - ) - }) +test('args: gasPrice + maxFeePerGas', async () => { + await setup() + + await expect(() => + // @ts-expect-error + prepareTransactionRequest(client, { + account: privateKeyToAccount(sourceAccount.privateKey), + to: targetAccount.address, + gasPrice: parseGwei('10'), + maxFeePerGas: parseGwei('20'), + value: parseEther('1'), + }), + ).rejects.toThrowError( + 'Cannot specify both a `gasPrice` and a `maxFeePerGas`/`maxPriorityFeePerGas`.', + ) +}) + +test('args: gasPrice + maxPriorityFeePerGas', async () => { + await setup() - 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(` + 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() +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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -560,27 +572,28 @@ describe('prepareTransactionRequest', () => { "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(` +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", "signMessage": [Function], "signTransaction": [Function], @@ -606,21 +619,22 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('args: parameters', async () => { - await setup() +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(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -635,16 +649,17 @@ describe('prepareTransactionRequest', () => { } `) - const result2 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result2).toMatchInlineSnapshot(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -662,16 +677,17 @@ describe('prepareTransactionRequest', () => { } `) - const result3 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees', 'nonce'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result3).toMatchInlineSnapshot(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -690,16 +706,17 @@ describe('prepareTransactionRequest', () => { } `) - const result4 = await prepareTransactionRequest(client, { - account: privateKeyToAccount(sourceAccount.privateKey), - parameters: ['gas', 'fees', 'nonce', 'type'], - to: targetAccount.address, - value: parseEther('1'), - }) - expect(result4).toMatchInlineSnapshot(` + 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", "signMessage": [Function], "signTransaction": [Function], @@ -718,22 +735,22 @@ describe('prepareTransactionRequest', () => { } `) - 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(` + 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", @@ -741,10 +758,11 @@ describe('prepareTransactionRequest', () => { }, ] `) - expect(result5).toMatchInlineSnapshot(` + expect(result5).toMatchInlineSnapshot(` { "account": { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], @@ -762,144 +780,174 @@ describe('prepareTransactionRequest', () => { "value": 1000000000000000000n, } `) - }) +}) - test('chain default priority fee', async () => { - await setup() +test('behavior: chain default priority fee', async () => { + await setup() - const block = await getBlock.getBlock(client) + const block = await getBlock.getBlock(client) - // client chain - const client_1 = createClient({ - chain: { - ...anvilMainnet.chain, - fees: { - defaultPriorityFee: () => parseGwei('69'), - }, + // 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_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_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'), - }, + }, + 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_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_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_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_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) + }, + 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, + to: targetAccount.address, + value: parseEther('1'), + parameters: ['nonce'], + } as const + + const request_1 = await prepareTransactionRequest(client, args) + expect(request_1.nonce).toBe(655) + + const request_2 = await prepareTransactionRequest(client, args) + expect(request_2.nonce).toBe(656) + + const [request_3, request_4, request_5] = await Promise.all([ + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + prepareTransactionRequest(client, args), + ]) + + expect(request_3.nonce).toBe(657) + expect(request_4.nonce).toBe(658) + expect(request_5.nonce).toBe(659) }) diff --git a/src/actions/wallet/prepareTransactionRequest.ts b/src/actions/wallet/prepareTransactionRequest.ts index 6fa59b0737..ffdc9e4f9a 100644 --- a/src/actions/wallet/prepareTransactionRequest.ts +++ b/src/actions/wallet/prepareTransactionRequest.ts @@ -63,7 +63,7 @@ import { type GetTransactionType, getTransactionType, } from '../../utils/transaction/getTransactionType.js' -import { getChainId } from '../public/getChainId.js' +import { getChainId as getChainId_ } from '../public/getChainId.js' export const defaultParameters = [ 'blobVersionedHashes', @@ -243,7 +243,6 @@ export async function prepareTransactionRequest< account: account_ = client.account, blobs, chain, - chainId, gas, kzg, nonce, @@ -265,6 +264,16 @@ export async function prepareTransactionRequest< return block } + let chainId: number | undefined + async function getChainId(): Promise { + if (chainId) return chainId + if (chain) return chain.id + if (typeof args.chainId !== 'undefined') return args.chainId + const chainId_ = await getAction(client, getChainId_, 'getChainId')({}) + chainId = chainId_ + return chainId + } + if ( (parameters.includes('blobVersionedHashes') || parameters.includes('sidecars')) && @@ -292,21 +301,27 @@ export async function prepareTransactionRequest< } } - if (parameters.includes('chainId')) { - if (chain) request.chainId = chain.id - else if (typeof chainId !== 'undefined') request.chainId = chainId - else request.chainId = await getAction(client, getChainId, 'getChainId')({}) - } + if (parameters.includes('chainId')) request.chainId = await getChainId() - if (parameters.includes('nonce') && typeof nonce === 'undefined' && account) - request.nonce = await getAction( - client, - getTransactionCount, - 'getTransactionCount', - )({ - address: account.address, - blockTag: 'pending', - }) + if (parameters.includes('nonce') && typeof nonce === 'undefined' && account) { + if (account.nonceManager) { + const chainId = await getChainId() + request.nonce = await account.nonceManager.consume({ + address: account.address, + chainId, + client, + }) + } else { + request.nonce = await getAction( + client, + getTransactionCount, + 'getTransactionCount', + )({ + address: account.address, + blockTag: 'pending', + }) + } + } if ( (parameters.includes('fees') || parameters.includes('type')) && diff --git a/src/actions/wallet/sendTransaction.ts b/src/actions/wallet/sendTransaction.ts index 396be922d6..3649af77f4 100644 --- a/src/actions/wallet/sendTransaction.ts +++ b/src/actions/wallet/sendTransaction.ts @@ -234,7 +234,7 @@ export async function sendTransaction< method: 'eth_sendTransaction', params: [request], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } catch (err) { throw getTransactionError(err as BaseError, { diff --git a/src/actions/wallet/signMessage.ts b/src/actions/wallet/signMessage.ts index 6820af6c4a..6bd47168e3 100644 --- a/src/actions/wallet/signMessage.ts +++ b/src/actions/wallet/signMessage.ts @@ -107,6 +107,6 @@ export async function signMessage< method: 'personal_sign', params: [message_, account.address], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } diff --git a/src/actions/wallet/signTransaction.ts b/src/actions/wallet/signTransaction.ts index ab3f67f8f1..cc44d0460e 100644 --- a/src/actions/wallet/signTransaction.ts +++ b/src/actions/wallet/signTransaction.ts @@ -176,6 +176,6 @@ export async function signTransaction< } as unknown as RpcTransactionRequest, ], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } diff --git a/src/actions/wallet/signTypedData.ts b/src/actions/wallet/signTypedData.ts index d862f5a1e5..5acc2fb4cd 100644 --- a/src/actions/wallet/signTypedData.ts +++ b/src/actions/wallet/signTypedData.ts @@ -190,6 +190,6 @@ export async function signTypedData< method: 'eth_signTypedData_v4', params: [account.address, typedData], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } diff --git a/src/actions/wallet/switchChain.ts b/src/actions/wallet/switchChain.ts index e0d781dbb4..5230583b73 100644 --- a/src/actions/wallet/switchChain.ts +++ b/src/actions/wallet/switchChain.ts @@ -52,6 +52,6 @@ export async function switchChain< }, ], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } diff --git a/src/clients/createWalletClient.test.ts b/src/clients/createWalletClient.test.ts index 632663d0f8..55d06e8fcd 100644 --- a/src/clients/createWalletClient.test.ts +++ b/src/clients/createWalletClient.test.ts @@ -142,6 +142,7 @@ describe('args: account', () => { { "account": { "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "nonceManager": undefined, "publicKey": "0x048318535b54105d4a7aae60c08fc45f9687181b4fdfc625bd1a753fa7397fed753547f11ca8696646f2f3acb08e31016afac23e630c5d11f59f61fef57b0d2aa5", "signMessage": [Function], "signTransaction": [Function], diff --git a/src/clients/transports/createTransport.ts b/src/clients/transports/createTransport.ts index 58ee3cf610..ef922ada85 100644 --- a/src/clients/transports/createTransport.ts +++ b/src/clients/transports/createTransport.ts @@ -2,6 +2,7 @@ import type { ErrorType } from '../../errors/utils.js' import type { Chain } from '../../types/chain.js' import type { EIP1193RequestFn } from '../../types/eip1193.js' import { buildRequest } from '../../utils/buildRequest.js' +import { uid as uid_ } from '../../utils/uid.js' import type { ClientConfig } from '../createClient.js' export type TransportConfig< @@ -61,9 +62,18 @@ export function createTransport< }: TransportConfig, value?: TRpcAttributes | undefined, ): ReturnType> { + const uid = uid_() return { - config: { key, name, request, retryCount, retryDelay, timeout, type }, - request: buildRequest(request, { retryCount, retryDelay }), + config: { + key, + name, + request, + retryCount, + retryDelay, + timeout, + type, + }, + request: buildRequest(request, { retryCount, retryDelay, uid }), value, } } diff --git a/src/clients/transports/http.test.ts b/src/clients/transports/http.test.ts index 0253537ab4..1998554d75 100644 --- a/src/clients/transports/http.test.ts +++ b/src/clients/transports/http.test.ts @@ -177,12 +177,12 @@ describe('request', () => { })({ chain: localhost }) const p = [] - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) await wait(1) - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) const results = await Promise.all(p) @@ -222,14 +222,14 @@ describe('request', () => { })({ chain: localhost }) const p = [] - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) await wait(1) - p.push(transport.request({ method: 'eth_blockNumber' })) - p.push(transport.request({ method: 'eth_blockNumber' })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) await wait(20) - p.push(transport.request({ method: 'eth_blockNumber' })) + p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) const results = await Promise.all(p) diff --git a/src/experimental/eip5792/actions/sendCalls.ts b/src/experimental/eip5792/actions/sendCalls.ts index 8c6d2d7652..e83887a4dc 100644 --- a/src/experimental/eip5792/actions/sendCalls.ts +++ b/src/experimental/eip5792/actions/sendCalls.ts @@ -116,7 +116,7 @@ export async function sendCalls< }, ], }, - { retryCount: 0 }, + { dedupe: false, retryCount: 0 }, ) } catch (err) { throw getTransactionError(err as BaseError, { diff --git a/src/experimental/erc7715/actions/issuePermissions.ts b/src/experimental/erc7715/actions/issuePermissions.ts index 0481c957ca..5c14664fbf 100644 --- a/src/experimental/erc7715/actions/issuePermissions.ts +++ b/src/experimental/erc7715/actions/issuePermissions.ts @@ -84,10 +84,13 @@ export async function issuePermissions( parameters: IssuePermissionsParameters, ): Promise { const { expiry, permissions, signer } = parameters - const result = await client.request({ - method: 'wallet_issuePermissions', - params: [parseParameters({ expiry, permissions, signer })], - }) + const result = await client.request( + { + method: 'wallet_issuePermissions', + params: [parseParameters({ expiry, permissions, signer })], + }, + { dedupe: false, retryCount: 0 }, + ) return parseResult(result) as IssuePermissionsReturnType } diff --git a/src/experimental/index.ts b/src/experimental/index.ts index b7111c5b09..a4c04f0411 100644 --- a/src/experimental/index.ts +++ b/src/experimental/index.ts @@ -63,3 +63,11 @@ export { type WalletActionsErc7715, walletActionsErc7715, } from './erc7715/decorators/erc7715.js' + +export { + type CreateNonceManagerParameters, + type NonceManager, + createNonceManager, + jsonRpc, + nonceManager, +} from './utils/nonceManager.js' diff --git a/src/experimental/utils/nonceManager.test.ts b/src/experimental/utils/nonceManager.test.ts new file mode 100644 index 0000000000..81eaaff015 --- /dev/null +++ b/src/experimental/utils/nonceManager.test.ts @@ -0,0 +1,242 @@ +import { expect, test } from 'vitest' +import { anvilMainnet, anvilOptimism } from '../../../test/src/anvil.js' +import { accounts } from '../../../test/src/constants.js' +import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { + dropTransaction, + getTransaction, + sendTransaction, +} from '../../actions/index.js' +import { createNonceManager, jsonRpc, nonceManager } from './nonceManager.js' + +const mainnetClient = anvilMainnet.getClient({ account: true }) +const optimismClient = anvilOptimism.getClient({ account: true }) + +const mainnetArgs = { + address: accounts[0].address, + chainId: mainnetClient.chain.id, + client: mainnetClient, +} as const +const optimismArgs = { + address: accounts[0].address, + chainId: optimismClient.chain.id, + client: optimismClient, +} as const + +test('get next', async () => { + const nonceManager = createNonceManager({ + source: jsonRpc(), + }) + + expect(await nonceManager.get(mainnetArgs)).toBe(655) + await sendTransaction(mainnetClient, { + to: accounts[0].address, + value: 0n, + }) + expect(await nonceManager.get(mainnetArgs)).toBe(656) + + expect(await nonceManager.get(optimismArgs)).toBe(66) + await sendTransaction(optimismClient, { + to: accounts[0].address, + value: 0n, + }) + expect(await nonceManager.get(optimismArgs)).toBe(67) +}) + +test('consume (sequence)', async () => { + const nonceManager = createNonceManager({ + source: jsonRpc(), + }) + + const nonce_1 = await nonceManager.consume(mainnetArgs) + expect(nonce_1).toBe(656) + const promise_1 = sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_1, + value: 0n, + }) + expect(await nonceManager.get(mainnetArgs)).toBe(657) + + const nonce_2 = await nonceManager.consume(mainnetArgs) + expect(nonce_2).toBe(657) + const promise_2 = sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_2, + value: 0n, + }) + expect(await nonceManager.get(mainnetArgs)).toBe(658) + + const nonce_3 = await nonceManager.consume(mainnetArgs) + expect(nonce_3).toBe(658) + const promise_3 = sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_3, + value: 0n, + }) + expect(await nonceManager.get(mainnetArgs)).toBe(659) + + const [hash_1, hash_2, hash_3] = await Promise.all([ + promise_1, + promise_2, + promise_3, + ]) + + expect( + (await getTransaction(mainnetClient, { hash: hash_1 })).nonce, + ).toMatchObject(656) + expect( + (await getTransaction(mainnetClient, { hash: hash_2 })).nonce, + ).toMatchObject(657) + expect( + (await getTransaction(mainnetClient, { hash: hash_3 })).nonce, + ).toMatchObject(658) + expect(await nonceManager.get(mainnetArgs)).toBe(659) + + const nonce_4 = await nonceManager.consume(mainnetArgs) + expect(nonce_4).toBe(659) + const hash_4 = await sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_4, + value: 0n, + }) + expect( + (await getTransaction(mainnetClient, { hash: hash_4 })).nonce, + ).toMatchObject(659) + + expect(await nonceManager.get(mainnetArgs)).toBe(660) +}) + +test('consume (parallel)', async () => { + const nonceManager = createNonceManager({ + source: jsonRpc(), + }) + + const [n_1, nonce_1, n_2, nonce_2, n_3, nonce_3, n_4] = await Promise.all([ + nonceManager.get(mainnetArgs), + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + ]) + + expect(n_1).toBe(660) + expect(nonce_1).toBe(660) + expect(n_2).toBe(661) + expect(nonce_2).toBe(661) + expect(n_3).toBe(662) + expect(nonce_3).toBe(662) + expect(n_4).toBe(663) + + const [hash_1, hash_2, hash_3] = await Promise.all([ + sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_1, + value: 0n, + }), + sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_2, + value: 0n, + }), + sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_3, + value: 0n, + }), + ]) + + expect( + (await getTransaction(mainnetClient, { hash: hash_1 })).nonce, + ).toMatchObject(660) + expect( + (await getTransaction(mainnetClient, { hash: hash_2 })).nonce, + ).toMatchObject(661) + expect( + (await getTransaction(mainnetClient, { hash: hash_3 })).nonce, + ).toMatchObject(662) + expect(await nonceManager.get(mainnetArgs)).toBe(663) + + const nonce_4 = await nonceManager.consume(mainnetArgs) + expect(nonce_4).toBe(663) + const hash_4 = await sendTransaction(mainnetClient, { + to: accounts[0].address, + nonce: nonce_4, + value: 0n, + }) + expect( + (await getTransaction(mainnetClient, { hash: hash_4 })).nonce, + ).toMatchObject(663) + expect(await nonceManager.get(mainnetArgs)).toBe(664) +}) + +test('nonceManager export', async () => { + expect(await nonceManager.get(mainnetArgs)).toBe(664) + expect(await nonceManager.consume(mainnetArgs)).toBe(664) + expect(await nonceManager.get(mainnetArgs)).toBe(665) + + const nonces = await Promise.all([ + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + nonceManager.consume(mainnetArgs), + nonceManager.get(mainnetArgs), + ]) + expect(nonces).toMatchInlineSnapshot(` + [ + 665, + 666, + 666, + 667, + 667, + 668, + ] + `) +}) + +test('dropped tx', async () => { + const nonceManager = createNonceManager({ + source: jsonRpc(), + }) + + const args = { + address: accounts[1].address, + chainId: mainnetClient.chain.id, + client: mainnetClient, + } + + const nonce_1 = await nonceManager.consume(args) + expect(nonce_1).toBe(105) + const hash_1 = await sendTransaction(mainnetClient, { + account: privateKeyToAccount(accounts[1].privateKey), + to: accounts[0].address, + nonce: nonce_1, + value: 0n, + }) + expect(await nonceManager.get(args)).toBe(106) + + const nonce_2 = await nonceManager.consume(args) + expect(nonce_2).toBe(106) + const hash_2 = await sendTransaction(mainnetClient, { + account: privateKeyToAccount(accounts[1].privateKey), + to: accounts[0].address, + nonce: nonce_2, + value: 0n, + }) + expect(await nonceManager.get(args)).toBe(107) + + await dropTransaction(mainnetClient, { hash: hash_1 }) + await dropTransaction(mainnetClient, { hash: hash_2 }) + + const nonce_3 = await nonceManager.consume(args) + expect(nonce_3).toBe(105) + await sendTransaction(mainnetClient, { + account: privateKeyToAccount(accounts[1].privateKey), + to: accounts[0].address, + nonce: nonce_3, + value: 0n, + }) + expect(await nonceManager.get(args)).toBe(106) +}) diff --git a/src/experimental/utils/nonceManager.ts b/src/experimental/utils/nonceManager.ts new file mode 100644 index 0000000000..10da61c2c2 --- /dev/null +++ b/src/experimental/utils/nonceManager.ts @@ -0,0 +1,134 @@ +import type { Address } from 'abitype' + +import { getTransactionCount } from '../../actions/public/getTransactionCount.js' +import type { Client } from '../../clients/createClient.js' +import type { MaybePromise } from '../../types/utils.js' +import { LruMap } from '../../utils/lru.js' + +export type CreateNonceManagerParameters = { + source: NonceManagerSource +} + +type FunctionParameters = { + address: Address + chainId: number +} + +export type NonceManager = { + /** Clear all nonces. */ + clear(): Promise + /** Get and increment a nonce. */ + consume(parameters: FunctionParameters & { client: Client }): Promise + /** Increment a nonce. */ + increase(chainId: FunctionParameters): Promise + /** Get a nonce. */ + get(chainId: FunctionParameters & { client: Client }): Promise + /** Reset a nonce. */ + reset(chainId: FunctionParameters): Promise +} + +/** + * Creates a nonce manager for auto-incrementing transaction nonces. + * + * @example + * ```ts + * const nonceManager = createNonceManager({ + * source: jsonRpc(), + * }) + * ``` + */ +export function createNonceManager( + parameters: CreateNonceManagerParameters, +): NonceManager { + const { source } = parameters + + const deltaMap = new Map() + const nonceMap = new LruMap(8192) + let promiseMap = new Map>() + + const getKey = ({ address, chainId }: FunctionParameters) => + `${address}.${chainId}` + + return { + async clear() { + deltaMap.clear() + promiseMap = new Map() + }, + async consume({ address, chainId, client }) { + const key = getKey({ address, chainId }) + const promise = this.get({ address, chainId, client }) + + await this.increase({ address, chainId }) + const nonce = await promise + + await source.set({ address, chainId }, nonce) + nonceMap.set(key, nonce) + + return nonce + }, + async increase({ address, chainId }) { + const key = getKey({ address, chainId }) + const delta = deltaMap.get(key) ?? 0 + deltaMap.set(key, delta + 1) + }, + async get({ address, chainId, client }) { + const key = getKey({ address, chainId }) + + let promise = promiseMap.get(key) + if (!promise) { + promise = (async () => { + try { + const nonce = await source.get({ address, chainId, client }) + const previousNonce = nonceMap.get(key) ?? 0 + if (nonce <= previousNonce) return previousNonce + 1 + nonceMap.delete(key) + return nonce + } finally { + this.reset({ address, chainId }) + } + })() + promiseMap.set(key, promise) + } + + const delta = deltaMap.get(key) ?? 0 + return delta + (await promise) + }, + async reset({ address, chainId }) { + const key = getKey({ address, chainId }) + deltaMap.delete(key) + promiseMap.delete(key) + }, + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Sources + +type NonceManagerSource = { + /** Get a nonce. */ + get(parameters: FunctionParameters & { client: Client }): MaybePromise + /** Set a nonce. */ + set(parameters: FunctionParameters, nonce: number): MaybePromise +} + +/** JSON-RPC source for a nonce manager. */ +export function jsonRpc(): NonceManagerSource { + return { + async get(parameters) { + const { address, client } = parameters + return getTransactionCount(client, { + address, + blockTag: 'pending', + }) + }, + set() {}, + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +// Default + +/** Default Nonce Manager with a JSON-RPC source. */ +export const nonceManager = /*#__PURE__*/ createNonceManager({ + source: jsonRpc(), +}) diff --git a/src/types/eip1193.ts b/src/types/eip1193.ts index 5a7fc58a5c..e51cd82ce4 100644 --- a/src/types/eip1193.ts +++ b/src/types/eip1193.ts @@ -1519,10 +1519,14 @@ export type EIP1193Parameters< } export type EIP1193RequestOptions = { + // Deduplicate in-flight requests. + dedupe?: boolean | undefined // The base delay (in ms) between retries. retryDelay?: number | undefined // The max number of times to retry. retryCount?: number | undefined + /** Unique identifier for the request. */ + uid?: string | undefined } type DerivedRpcSchema< diff --git a/src/utils/buildRequest.ts b/src/utils/buildRequest.ts index 48b3567625..3ea818e478 100644 --- a/src/utils/buildRequest.ts +++ b/src/utils/buildRequest.ts @@ -55,9 +55,13 @@ import type { EIP1193RequestFn, EIP1193RequestOptions, } from '../types/eip1193.js' +import { stringToHex } from './encoding/toHex.js' +import { keccak256 } from './hash/keccak256.js' import type { CreateBatchSchedulerErrorType } from './promise/createBatchScheduler.js' +import { withDedupe } from './promise/withDedupe.js' import { type WithRetryErrorType, withRetry } from './promise/withRetry.js' import type { GetSocketRpcClientErrorType } from './rpc/socket.js' +import { stringify } from './stringify.js' export type RequestErrorType = | ChainDisconnectedErrorType @@ -94,98 +98,110 @@ export function buildRequest Promise>( options: EIP1193RequestOptions = {}, ): EIP1193RequestFn { return async (args, overrideOptions = {}) => { - const { retryDelay = 150, retryCount = 3 } = { + const { + dedupe = true, + retryDelay = 150, + retryCount = 3, + uid, + } = { ...options, ...overrideOptions, } - return withRetry( - async () => { - try { - return await request(args) - } catch (err_) { - const err = err_ as unknown as RpcError< - RpcErrorCode | ProviderRpcErrorCode - > - switch (err.code) { - // -32700 - case ParseRpcError.code: - throw new ParseRpcError(err) - // -32600 - case InvalidRequestRpcError.code: - throw new InvalidRequestRpcError(err) - // -32601 - case MethodNotFoundRpcError.code: - throw new MethodNotFoundRpcError(err) - // -32602 - case InvalidParamsRpcError.code: - throw new InvalidParamsRpcError(err) - // -32603 - case InternalRpcError.code: - throw new InternalRpcError(err) - // -32000 - case InvalidInputRpcError.code: - throw new InvalidInputRpcError(err) - // -32001 - case ResourceNotFoundRpcError.code: - throw new ResourceNotFoundRpcError(err) - // -32002 - case ResourceUnavailableRpcError.code: - throw new ResourceUnavailableRpcError(err) - // -32003 - case TransactionRejectedRpcError.code: - throw new TransactionRejectedRpcError(err) - // -32004 - case MethodNotSupportedRpcError.code: - throw new MethodNotSupportedRpcError(err) - // -32005 - case LimitExceededRpcError.code: - throw new LimitExceededRpcError(err) - // -32006 - case JsonRpcVersionUnsupportedError.code: - throw new JsonRpcVersionUnsupportedError(err) - // 4001 - case UserRejectedRequestError.code: - throw new UserRejectedRequestError(err) - // 4100 - case UnauthorizedProviderError.code: - throw new UnauthorizedProviderError(err) - // 4200 - case UnsupportedProviderMethodError.code: - throw new UnsupportedProviderMethodError(err) - // 4900 - case ProviderDisconnectedError.code: - throw new ProviderDisconnectedError(err) - // 4901 - case ChainDisconnectedError.code: - throw new ChainDisconnectedError(err) - // 4902 - case SwitchChainError.code: - throw new SwitchChainError(err) - // CAIP-25: User Rejected Error - // https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes#rejected-caip-25 - case 5000: - throw new UserRejectedRequestError(err) - default: - if (err_ instanceof BaseError) throw err_ - throw new UnknownRpcError(err as Error) - } - } - }, - { - delay: ({ count, error }) => { - // If we find a Retry-After header, let's retry after the given time. - if (error && error instanceof HttpRequestError) { - const retryAfter = error?.headers?.get('Retry-After') - if (retryAfter?.match(/\d/)) - return Number.parseInt(retryAfter) * 1000 - } + const requestId = dedupe + ? keccak256(stringToHex(`${uid}.${stringify(args)}`)) + : undefined + return withDedupe( + () => + withRetry( + async () => { + try { + return await request(args) + } catch (err_) { + const err = err_ as unknown as RpcError< + RpcErrorCode | ProviderRpcErrorCode + > + switch (err.code) { + // -32700 + case ParseRpcError.code: + throw new ParseRpcError(err) + // -32600 + case InvalidRequestRpcError.code: + throw new InvalidRequestRpcError(err) + // -32601 + case MethodNotFoundRpcError.code: + throw new MethodNotFoundRpcError(err) + // -32602 + case InvalidParamsRpcError.code: + throw new InvalidParamsRpcError(err) + // -32603 + case InternalRpcError.code: + throw new InternalRpcError(err) + // -32000 + case InvalidInputRpcError.code: + throw new InvalidInputRpcError(err) + // -32001 + case ResourceNotFoundRpcError.code: + throw new ResourceNotFoundRpcError(err) + // -32002 + case ResourceUnavailableRpcError.code: + throw new ResourceUnavailableRpcError(err) + // -32003 + case TransactionRejectedRpcError.code: + throw new TransactionRejectedRpcError(err) + // -32004 + case MethodNotSupportedRpcError.code: + throw new MethodNotSupportedRpcError(err) + // -32005 + case LimitExceededRpcError.code: + throw new LimitExceededRpcError(err) + // -32006 + case JsonRpcVersionUnsupportedError.code: + throw new JsonRpcVersionUnsupportedError(err) + // 4001 + case UserRejectedRequestError.code: + throw new UserRejectedRequestError(err) + // 4100 + case UnauthorizedProviderError.code: + throw new UnauthorizedProviderError(err) + // 4200 + case UnsupportedProviderMethodError.code: + throw new UnsupportedProviderMethodError(err) + // 4900 + case ProviderDisconnectedError.code: + throw new ProviderDisconnectedError(err) + // 4901 + case ChainDisconnectedError.code: + throw new ChainDisconnectedError(err) + // 4902 + case SwitchChainError.code: + throw new SwitchChainError(err) + // CAIP-25: User Rejected Error + // https://docs.walletconnect.com/2.0/specs/clients/sign/error-codes#rejected-caip-25 + case 5000: + throw new UserRejectedRequestError(err) + default: + if (err_ instanceof BaseError) throw err_ + throw new UnknownRpcError(err as Error) + } + } + }, + { + delay: ({ count, error }) => { + // If we find a Retry-After header, let's retry after the given time. + if (error && error instanceof HttpRequestError) { + const retryAfter = error?.headers?.get('Retry-After') + if (retryAfter?.match(/\d/)) + return Number.parseInt(retryAfter) * 1000 + } - // Otherwise, let's retry with an exponential backoff. - return ~~(1 << count) * retryDelay - }, - retryCount, - shouldRetry: ({ error }) => shouldRetry(error), - }, + // Otherwise, let's retry with an exponential backoff. + return ~~(1 << count) * retryDelay + }, + retryCount, + shouldRetry: ({ error }) => shouldRetry(error), + }, + ), + { enabled: dedupe, id: requestId }, ) } } diff --git a/src/utils/promise/withDedupe.test.ts b/src/utils/promise/withDedupe.test.ts new file mode 100644 index 0000000000..713c0aa466 --- /dev/null +++ b/src/utils/promise/withDedupe.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from 'vitest' + +import { wait } from '../wait.js' + +import { promiseCache, withDedupe } from './withDedupe.js' + +test('default', async () => { + let count = 0 + async function fn() { + count++ + await wait(1000) + return 'bar' + } + + const id = 'foo' + + const promise_1 = withDedupe(fn, { id }) + const promise_2 = withDedupe(fn, { id }) + expect(promiseCache.has(id)).toBe(true) + + const results = await Promise.all([promise_1, promise_2]) + expect(results[0]).toBe(results[1]) + expect(count).toBe(1) + expect(promiseCache.has(id)).toBe(false) +}) diff --git a/src/utils/promise/withDedupe.ts b/src/utils/promise/withDedupe.ts new file mode 100644 index 0000000000..e74e9eb0ef --- /dev/null +++ b/src/utils/promise/withDedupe.ts @@ -0,0 +1,21 @@ +import { LruMap } from '../lru.js' + +/** @internal */ +export const promiseCache = /*#__PURE__*/ new LruMap>(8192) + +export type WithDedupeOptions = { + enabled?: boolean | undefined + id?: string | undefined +} + +/** Deduplicates in-flight promises. */ +export function withDedupe( + fn: () => Promise, + { enabled = true, id }: WithDedupeOptions, +): Promise { + if (!enabled || !id) return fn() + if (promiseCache.get(id)) return promiseCache.get(id)! + const promise = fn().finally(() => promiseCache.delete(id)) + promiseCache.set(id, promise) + return promise +} From de3e31c231e80af2a6b48f63a8a475aa4f07e707 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 13:59:35 +1000 Subject: [PATCH 02/21] wip: dedupe tests --- src/clients/transports/http.test.ts | 97 +++++++++++++++++++++++++---- src/utils/buildRequest.test.ts | 60 ++++++++++++++++++ 2 files changed, 146 insertions(+), 11 deletions(-) diff --git a/src/clients/transports/http.test.ts b/src/clients/transports/http.test.ts index 1998554d75..d24f3945ae 100644 --- a/src/clients/transports/http.test.ts +++ b/src/clients/transports/http.test.ts @@ -177,12 +177,16 @@ describe('request', () => { })({ chain: localhost }) const p = [] - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_a' })) + p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_c' })) + // test dedupe + p.push(transport.request({ method: 'eth_b' })) await wait(1) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_d' })) + p.push(transport.request({ method: 'eth_e' })) + // test dedupe + p.push(transport.request({ method: 'eth_d' })) const results = await Promise.all(p) @@ -191,8 +195,10 @@ describe('request', () => { "0x1", "0x2", "0x3", + "0x2", "0x1", "0x2", + "0x1", ] `) expect(count).toEqual(2) @@ -222,14 +228,19 @@ describe('request', () => { })({ chain: localhost }) const p = [] - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_a' })) + p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_c' })) + // test dedupe + p.push(transport.request({ method: 'eth_b' })) await wait(1) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_d' })) + p.push(transport.request({ method: 'eth_e' })) await wait(20) - p.push(transport.request({ method: 'eth_blockNumber' }, { dedupe: false })) + p.push(transport.request({ method: 'eth_f' })) + p.push(transport.request({ method: 'eth_g' })) + // test dedupe + p.push(transport.request({ method: 'eth_f' })) const results = await Promise.all(p) @@ -238,9 +249,12 @@ describe('request', () => { "0x1", "0x2", "0x3", + "0x2", "0x4", "0x5", "0x1", + "0x2", + "0x1", ] `) expect(count).toEqual(2) @@ -248,6 +262,67 @@ describe('request', () => { await server.close() }) + test('behavior: dedupe', async () => { + const args: string[] = [] + const server = await createHttpServer((req, res) => { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + req.on('end', () => { + args.push(body) + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: body })) + }) + }) + + const transport = http(server.url, { + key: 'mock', + })({ chain: localhost }) + + const results = await Promise.all([ + transport.request({ method: 'eth_blockNumber' }), + transport.request({ method: 'eth_blockNumber' }), + // this will not be deduped (different params). + transport.request({ method: 'eth_blockNumber', params: [1] }), + transport.request({ method: 'eth_blockNumber' }), + // this will not be deduped (different method). + transport.request({ method: 'eth_chainId' }), + transport.request({ method: 'eth_blockNumber' }), + // this will not be deduped (dedupe: false). + transport.request({ method: 'eth_blockNumber' }, { dedupe: false }), + transport.request({ method: 'eth_blockNumber' }), + ]) + + expect( + args + .map((arg) => JSON.parse(arg)) + .sort((a, b) => a.id - b.id) + .map((arg) => JSON.stringify({ ...arg, id: undefined })), + ).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","method":"eth_chainId"}", + "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + ] + `) + expect(results).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":23,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":24,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":25,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + ] + `) + }) + test('behavior: fetchOptions', async () => { let headers: IncomingHttpHeaders = {} const server = await createHttpServer((req, res) => { diff --git a/src/utils/buildRequest.test.ts b/src/utils/buildRequest.test.ts index 015cfc8e5f..920473cf76 100644 --- a/src/utils/buildRequest.test.ts +++ b/src/utils/buildRequest.test.ts @@ -709,6 +709,66 @@ describe('behavior', () => { }) }) + test('dedupes requests', async () => { + const args: string[] = [] + const server = await createHttpServer((req, res) => { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + req.on('end', () => { + args.push(body) + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: body })) + }) + }) + + const uid = 'foo' + const request_ = buildRequest(request(server.url), { uid }) + + const results = await Promise.all([ + request_({ method: 'eth_blockNumber' }), + request_({ method: 'eth_blockNumber' }), + // this will not be deduped (different params). + request_({ method: 'eth_blockNumber', params: [1] }), + request_({ method: 'eth_blockNumber' }), + // this will not be deduped (different method). + request_({ method: 'eth_chainId' }), + request_({ method: 'eth_blockNumber' }), + // this will not be deduped (dedupe: false). + request_({ method: 'eth_blockNumber' }, { dedupe: false }), + request_({ method: 'eth_blockNumber' }), + ]) + + expect( + args + .map((arg) => JSON.parse(arg)) + .sort((a, b) => a.id - b.id) + .map((arg) => JSON.stringify({ ...arg, id: undefined })), + ).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","method":"eth_chainId"}", + "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + ] + `) + expect(results).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":62,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":63,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":64,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + ] + `) + }) + describe('retry', () => { test('non-deterministic InternalRpcError', async () => { let retryCount = -1 From e9a3ebe741e7e394d1913d34174b95b27d5d6a42 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 14:01:10 +1000 Subject: [PATCH 03/21] wip: tweak --- src/experimental/utils/nonceManager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/experimental/utils/nonceManager.ts b/src/experimental/utils/nonceManager.ts index 10da61c2c2..e54009380e 100644 --- a/src/experimental/utils/nonceManager.ts +++ b/src/experimental/utils/nonceManager.ts @@ -20,7 +20,7 @@ export type NonceManager = { /** Get and increment a nonce. */ consume(parameters: FunctionParameters & { client: Client }): Promise /** Increment a nonce. */ - increase(chainId: FunctionParameters): Promise + increment(chainId: FunctionParameters): Promise /** Get a nonce. */ get(chainId: FunctionParameters & { client: Client }): Promise /** Reset a nonce. */ @@ -58,7 +58,7 @@ export function createNonceManager( const key = getKey({ address, chainId }) const promise = this.get({ address, chainId, client }) - await this.increase({ address, chainId }) + await this.increment({ address, chainId }) const nonce = await promise await source.set({ address, chainId }, nonce) @@ -66,7 +66,7 @@ export function createNonceManager( return nonce }, - async increase({ address, chainId }) { + async increment({ address, chainId }) { const key = getKey({ address, chainId }) const delta = deltaMap.get(key) ?? 0 deltaMap.set(key, delta + 1) From 1ef5db7c491dbbfb0d6d64cdde66d85cb872ab70 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 14:06:49 +1000 Subject: [PATCH 04/21] wip: tweaks --- src/clients/transports/http.test.ts | 10 +++++----- src/experimental/utils/nonceManager.ts | 8 +------- src/utils/buildRequest.test.ts | 10 +++++----- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/clients/transports/http.test.ts b/src/clients/transports/http.test.ts index d24f3945ae..f464adb39a 100644 --- a/src/clients/transports/http.test.ts +++ b/src/clients/transports/http.test.ts @@ -300,13 +300,13 @@ describe('request', () => { args .map((arg) => JSON.parse(arg)) .sort((a, b) => a.id - b.id) - .map((arg) => JSON.stringify({ ...arg, id: undefined })), + .map((arg) => JSON.stringify(arg)), ).toMatchInlineSnapshot(` [ - "{"jsonrpc":"2.0","method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","method":"eth_blockNumber","params":[1]}", - "{"jsonrpc":"2.0","method":"eth_chainId"}", - "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":22,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":23,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":24,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":25,"method":"eth_blockNumber"}", ] `) expect(results).toMatchInlineSnapshot(` diff --git a/src/experimental/utils/nonceManager.ts b/src/experimental/utils/nonceManager.ts index e54009380e..a94071102e 100644 --- a/src/experimental/utils/nonceManager.ts +++ b/src/experimental/utils/nonceManager.ts @@ -15,8 +15,6 @@ type FunctionParameters = { } export type NonceManager = { - /** Clear all nonces. */ - clear(): Promise /** Get and increment a nonce. */ consume(parameters: FunctionParameters & { client: Client }): Promise /** Increment a nonce. */ @@ -44,16 +42,12 @@ export function createNonceManager( const deltaMap = new Map() const nonceMap = new LruMap(8192) - let promiseMap = new Map>() + const promiseMap = new Map>() const getKey = ({ address, chainId }: FunctionParameters) => `${address}.${chainId}` return { - async clear() { - deltaMap.clear() - promiseMap = new Map() - }, async consume({ address, chainId, client }) { const key = getKey({ address, chainId }) const promise = this.get({ address, chainId, client }) diff --git a/src/utils/buildRequest.test.ts b/src/utils/buildRequest.test.ts index 920473cf76..f7d7a8dcf2 100644 --- a/src/utils/buildRequest.test.ts +++ b/src/utils/buildRequest.test.ts @@ -746,13 +746,13 @@ describe('behavior', () => { args .map((arg) => JSON.parse(arg)) .sort((a, b) => a.id - b.id) - .map((arg) => JSON.stringify({ ...arg, id: undefined })), + .map((arg) => JSON.stringify(arg)), ).toMatchInlineSnapshot(` [ - "{"jsonrpc":"2.0","method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","method":"eth_blockNumber","params":[1]}", - "{"jsonrpc":"2.0","method":"eth_chainId"}", - "{"jsonrpc":"2.0","method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":62,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":63,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":64,"method":"eth_blockNumber"}", ] `) expect(results).toMatchInlineSnapshot(` From ca8a6ac0f691b7fde2d582edd3c43d35c38583ba Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 14:08:15 +1000 Subject: [PATCH 05/21] wip: knip --- src/accounts/index.ts | 3 +++ src/utils/promise/withDedupe.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/accounts/index.ts b/src/accounts/index.ts index d154ad4315..40d23ab1f9 100644 --- a/src/accounts/index.ts +++ b/src/accounts/index.ts @@ -21,14 +21,17 @@ export { generatePrivateKey, } from './generatePrivateKey.js' export { + type HDKeyToAccountOptions, type HDKeyToAccountErrorType, hdKeyToAccount, } from './hdKeyToAccount.js' export { + type MnemonicToAccountOptions, type MnemonicToAccountErrorType, mnemonicToAccount, } from './mnemonicToAccount.js' export { + type PrivateKeyToAccountOptions, type PrivateKeyToAccountErrorType, privateKeyToAccount, } from './privateKeyToAccount.js' diff --git a/src/utils/promise/withDedupe.ts b/src/utils/promise/withDedupe.ts index e74e9eb0ef..d870c4c0e1 100644 --- a/src/utils/promise/withDedupe.ts +++ b/src/utils/promise/withDedupe.ts @@ -3,7 +3,7 @@ import { LruMap } from '../lru.js' /** @internal */ export const promiseCache = /*#__PURE__*/ new LruMap>(8192) -export type WithDedupeOptions = { +type WithDedupeOptions = { enabled?: boolean | undefined id?: string | undefined } From 289801747c5c17248d87d7ab7d4dea65719a78d7 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 14:52:44 +1000 Subject: [PATCH 06/21] wip: sendTransaction test --- src/actions/wallet/sendTransaction.test.ts | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/actions/wallet/sendTransaction.test.ts b/src/actions/wallet/sendTransaction.test.ts index 7f62f77409..ed30d866fc 100644 --- a/src/actions/wallet/sendTransaction.test.ts +++ b/src/actions/wallet/sendTransaction.test.ts @@ -8,6 +8,7 @@ import { celo, localhost, mainnet, optimism } from '../../chains/index.js' import { createWalletClient } from '../../clients/createWalletClient.js' import { http } from '../../clients/transports/http.js' +import { nonceManager } from '../../experimental/index.js' import type { Hex } from '../../types/misc.js' import type { TransactionSerializable } from '../../types/transaction.js' import { toBlobs } from '../../utils/blob/toBlobs.js' @@ -932,6 +933,67 @@ describe('local account', () => { expect(transaction.nonce).toBe(hexToNumber(transactionCount)) }) }) + + describe('behavior: nonceManager', async () => { + test('default', async () => { + await setup() + + const account_1 = privateKeyToAccount(sourceAccount.privateKey, { + nonceManager, + }) + const account_2 = privateKeyToAccount(targetAccount.privateKey, { + nonceManager, + }) + + const [hash_1, hash_2, hash_3, hash_4, hash_5] = await Promise.all([ + sendTransaction(client, { + account: account_1, + to: targetAccount.address, + value: parseEther('1'), + }), + sendTransaction(client, { + account: account_2, + to: targetAccount.address, + value: parseEther('1'), + }), + sendTransaction(client, { + account: account_1, + to: targetAccount.address, + value: parseEther('1'), + }), + sendTransaction(client, { + account: account_1, + to: targetAccount.address, + value: parseEther('1'), + }), + sendTransaction(client, { + account: account_2, + to: targetAccount.address, + value: parseEther('1'), + }), + ]) + + expect((await getTransaction(client, { hash: hash_1 })).nonce).toBe(671) + expect((await getTransaction(client, { hash: hash_2 })).nonce).toBe(105) + expect((await getTransaction(client, { hash: hash_3 })).nonce).toBe(672) + expect((await getTransaction(client, { hash: hash_4 })).nonce).toBe(673) + expect((await getTransaction(client, { hash: hash_5 })).nonce).toBe(106) + + const hash_6 = await sendTransaction(client, { + account: account_1, + to: targetAccount.address, + value: parseEther('1'), + }) + const hash_7 = await sendTransaction(client, { + account: account_1, + to: targetAccount.address, + value: parseEther('1'), + }) + + expect((await getTransaction(client, { hash: hash_6 })).nonce).toBe(674) + expect((await getTransaction(client, { hash: hash_7 })).nonce).toBe(675) + }) + }) }) describe('errors', () => { From 11d3ce408cf971d3e4ec602bf3227266fcaca2bf Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:14:17 +1000 Subject: [PATCH 07/21] docs --- .../pages/docs/accounts/createNonceManager.md | 104 ++++++++++++++++++ site/sidebar.ts | 13 ++- src/accounts/privateKeyToAccount.ts | 2 +- src/accounts/types.ts | 2 +- src/experimental/index.ts | 8 -- src/index.ts | 7 ++ src/utils/index.ts | 7 ++ .../utils/nonceManager.test.ts | 8 +- src/{experimental => }/utils/nonceManager.ts | 8 +- 9 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 site/pages/docs/accounts/createNonceManager.md rename src/{experimental => }/utils/nonceManager.test.ts (96%) rename src/{experimental => }/utils/nonceManager.ts (93%) diff --git a/site/pages/docs/accounts/createNonceManager.md b/site/pages/docs/accounts/createNonceManager.md new file mode 100644 index 0000000000..409e320b78 --- /dev/null +++ b/site/pages/docs/accounts/createNonceManager.md @@ -0,0 +1,104 @@ +# createNonceManager [Creates a Nonce Manager for automatic nonce generation] + +Creates a new Nonce Manager instance to be used with a [Local Account](/docs/accounts/local). The Nonce Manager is used to automatically manage & generate nonces for transactions. + +:::warning +A Nonce Manager can only be used with [Local Accounts](/docs/accounts/local) (ie. Private Key, Mnemonic, etc). + +For [JSON-RPC Accounts](/docs/accounts/jsonRpc) (ie. Browser Extension, WalletConnect, Backend, etc), the Wallet or Backend will manage the nonces. +::: + +## Import + +```ts twoslash +import { createNonceManager } from 'viem' +``` + +## Usage + +A Nonce Manager can be instantiated with the `createNonceManager` function with a provided `source`. + +The example below demonstrates how to create a Nonce Manager with a JSON-RPC source (ie. uses `eth_getTransactionCount` as the source of truth). + +```ts twoslash +import { createNonceManager, jsonRpc } from 'viem' + +const nonceManager = createNonceManager({ + source: jsonRpc() +}) +``` + +:::tip +Viem also exports a `nonceManager` helper instance that you can use directly. + +```ts twoslash +import { nonceManager } from 'viem' +``` +::: + +### Integration with Local Accounts + +A `nonceManager` can be passed as an option to [Local Accounts](/docs/accounts/local) to automatically manage nonces for transactions. + +:::code-group + +```ts twoslash [example.ts] +import { nonceManager } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' // [!code focus] +import { client } from './config' + +const account = privateKeyToAccount('0x...', { nonceManager }) // [!code focus] + +const hashes = await Promise.all([ // [!code focus] +// @log: ↓ nonce = 0 + client.sendTransaction({ // [!code focus] + account, // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: parseEther('0.1'), // [!code focus] + }), // [!code focus] +// @log: ↓ nonce = 1 + client.sendTransaction({ // [!code focus] + account, // [!code focus] + to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', // [!code focus] + value: parseEther('0.2'), // [!code focus] + }), // [!code focus] +]) // [!code focus] +``` + +```ts twoslash [config.ts] filename="config.ts" +import { createWalletClient, http } from 'viem' +import { mainnet } from 'viem/chains' + +export const client = createWalletClient({ + chain: mainnet, + transport: http(), +}) +``` + +::: + +## Return Type + +`NonceManager` + +The Nonce Manager. + +## Parameters + +### source + +- **Type:** `NonceManagerSource` + +The source of truth for the Nonce Manager. + +Available sources: + +- `jsonRpc` + +```ts twoslash +import { createNonceManager, jsonRpc } from 'viem' + +const nonceManager = createNonceManager({ + source: jsonRpc() // [!code focus] +}) +``` \ No newline at end of file diff --git a/site/sidebar.ts b/site/sidebar.ts index 8420ddf202..72bb19d273 100644 --- a/site/sidebar.ts +++ b/site/sidebar.ts @@ -450,9 +450,9 @@ export const sidebar = { text: 'Accounts', collapsed: true, items: [ - { text: 'JSON-RPC', link: '/docs/accounts/jsonRpc' }, + { text: 'JSON-RPC Account', link: '/docs/accounts/jsonRpc' }, { - text: 'Local', + text: 'Local Accounts', link: '/docs/accounts/local', items: [ { text: 'Private Key', link: '/docs/accounts/privateKey' }, @@ -462,6 +462,15 @@ export const sidebar = { link: '/docs/accounts/hd', }, { text: 'Custom', link: '/docs/accounts/custom' }, + ], + }, + { + text: 'Utilities', + items: [ + { + text: 'createNonceManager', + link: '/docs/accounts/createNonceManager', + }, { text: 'signMessage', link: '/docs/accounts/signMessage' }, { text: 'signTransaction', link: '/docs/accounts/signTransaction' }, { text: 'signTypedData', link: '/docs/accounts/signTypedData' }, diff --git a/src/accounts/privateKeyToAccount.ts b/src/accounts/privateKeyToAccount.ts index fbb3d33a4a..da17bd793f 100644 --- a/src/accounts/privateKeyToAccount.ts +++ b/src/accounts/privateKeyToAccount.ts @@ -4,7 +4,7 @@ import type { Hex } from '../types/misc.js' import { type ToHexErrorType, toHex } from '../utils/encoding/toHex.js' import type { ErrorType } from '../errors/utils.js' -import type { NonceManager } from '../experimental/utils/nonceManager.js' +import type { NonceManager } from '../utils/nonceManager.js' import { type ToAccountErrorType, toAccount } from './toAccount.js' import type { PrivateKeyAccount } from './types.js' import { diff --git a/src/accounts/types.ts b/src/accounts/types.ts index 7ac3a6f7ce..bddbc8f49b 100644 --- a/src/accounts/types.ts +++ b/src/accounts/types.ts @@ -1,6 +1,5 @@ import type { Address, TypedData } from 'abitype' -import type { NonceManager } from '../experimental/utils/nonceManager.js' import type { HDKey } from '../types/account.js' import type { Hash, Hex, SignableMessage } from '../types/misc.js' import type { @@ -9,6 +8,7 @@ import type { } from '../types/transaction.js' import type { TypedDataDefinition } from '../types/typedData.js' import type { IsNarrowable, OneOf } from '../types/utils.js' +import type { NonceManager } from '../utils/nonceManager.js' import type { GetTransactionType } from '../utils/transaction/getTransactionType.js' import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js' diff --git a/src/experimental/index.ts b/src/experimental/index.ts index a4c04f0411..b7111c5b09 100644 --- a/src/experimental/index.ts +++ b/src/experimental/index.ts @@ -63,11 +63,3 @@ export { type WalletActionsErc7715, walletActionsErc7715, } from './erc7715/decorators/erc7715.js' - -export { - type CreateNonceManagerParameters, - type NonceManager, - createNonceManager, - jsonRpc, - nonceManager, -} from './utils/nonceManager.js' diff --git a/src/index.ts b/src/index.ts index 97c33e88df..06e4568a51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1655,3 +1655,10 @@ export { domainSeparator, getTypesForEIP712Domain, } from './utils/typedData.js' +export { + type CreateNonceManagerParameters, + type NonceManager, + createNonceManager, + jsonRpc, + nonceManager, +} from './utils/nonceManager.js' diff --git a/src/utils/index.ts b/src/utils/index.ts index 7faa570c3f..a112dd2a42 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -491,3 +491,10 @@ export { type FormatUnitsErrorType, formatUnits } from './unit/formatUnits.js' export { type ParseUnitsErrorType, parseUnits } from './unit/parseUnits.js' export { type ParseEtherErrorType, parseEther } from './unit/parseEther.js' export { type ParseGweiErrorType, parseGwei } from './unit/parseGwei.js' +export { + type CreateNonceManagerParameters, + type NonceManager, + createNonceManager, + jsonRpc, + nonceManager, +} from './nonceManager.js' diff --git a/src/experimental/utils/nonceManager.test.ts b/src/utils/nonceManager.test.ts similarity index 96% rename from src/experimental/utils/nonceManager.test.ts rename to src/utils/nonceManager.test.ts index 81eaaff015..69c5f5fb51 100644 --- a/src/experimental/utils/nonceManager.test.ts +++ b/src/utils/nonceManager.test.ts @@ -1,12 +1,12 @@ import { expect, test } from 'vitest' -import { anvilMainnet, anvilOptimism } from '../../../test/src/anvil.js' -import { accounts } from '../../../test/src/constants.js' -import { privateKeyToAccount } from '../../accounts/privateKeyToAccount.js' +import { anvilMainnet, anvilOptimism } from '../../test/src/anvil.js' +import { accounts } from '../../test/src/constants.js' +import { privateKeyToAccount } from '../accounts/privateKeyToAccount.js' import { dropTransaction, getTransaction, sendTransaction, -} from '../../actions/index.js' +} from '../actions/index.js' import { createNonceManager, jsonRpc, nonceManager } from './nonceManager.js' const mainnetClient = anvilMainnet.getClient({ account: true }) diff --git a/src/experimental/utils/nonceManager.ts b/src/utils/nonceManager.ts similarity index 93% rename from src/experimental/utils/nonceManager.ts rename to src/utils/nonceManager.ts index a94071102e..2d55648efe 100644 --- a/src/experimental/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -1,9 +1,9 @@ import type { Address } from 'abitype' -import { getTransactionCount } from '../../actions/public/getTransactionCount.js' -import type { Client } from '../../clients/createClient.js' -import type { MaybePromise } from '../../types/utils.js' -import { LruMap } from '../../utils/lru.js' +import { getTransactionCount } from '../actions/public/getTransactionCount.js' +import type { Client } from '../clients/createClient.js' +import type { MaybePromise } from '../types/utils.js' +import { LruMap } from './lru.js' export type CreateNonceManagerParameters = { source: NonceManagerSource From 3fe2fb779d210ac8520de6991de64c4570c784f7 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:15:36 +1000 Subject: [PATCH 08/21] chore: changesets --- .changeset/early-emus-share.md | 5 +++++ .changeset/orange-lies-worry.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/early-emus-share.md create mode 100644 .changeset/orange-lies-worry.md diff --git a/.changeset/early-emus-share.md b/.changeset/early-emus-share.md new file mode 100644 index 0000000000..7f04594112 --- /dev/null +++ b/.changeset/early-emus-share.md @@ -0,0 +1,5 @@ +--- +"viem": minor +--- + +Added support for a Nonce Manager on Local Accounts via `nonceManager`. diff --git a/.changeset/orange-lies-worry.md b/.changeset/orange-lies-worry.md new file mode 100644 index 0000000000..be5920a2a4 --- /dev/null +++ b/.changeset/orange-lies-worry.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Implemented in-flight request deduplication for Transport JSON-RPC requests. From 37fc5517f982e6e8396df394665081ac09428138 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:18:33 +1000 Subject: [PATCH 09/21] chore: bump sizes --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3b3795f8cf..367aa658cf 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ { "name": "viem (esm)", "path": "./src/_esm/index.js", - "limit": "59.8 kB", + "limit": "60 kB", "import": "*" }, { @@ -119,7 +119,7 @@ { "name": "viem (minimal surface - tree-shaking)", "path": "./src/_esm/index.js", - "limit": "4.1 kB", + "limit": "6.3 kB", "import": "{ createClient, http }" }, { @@ -173,7 +173,7 @@ { "name": "viem/ens (tree-shaking)", "path": "./src/_esm/ens/index.js", - "limit": "22.5 kB", + "limit": "22.6 kB", "import": "{ getEnsAvatar }" }, { From 2eede8b2054df800e1d27f8d82171f9783edbb18 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:23:42 +1000 Subject: [PATCH 10/21] fix --- src/actions/wallet/sendTransaction.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/wallet/sendTransaction.test.ts b/src/actions/wallet/sendTransaction.test.ts index ed30d866fc..5794ad2128 100644 --- a/src/actions/wallet/sendTransaction.test.ts +++ b/src/actions/wallet/sendTransaction.test.ts @@ -8,7 +8,6 @@ import { celo, localhost, mainnet, optimism } from '../../chains/index.js' import { createWalletClient } from '../../clients/createWalletClient.js' import { http } from '../../clients/transports/http.js' -import { nonceManager } from '../../experimental/index.js' import type { Hex } from '../../types/misc.js' import type { TransactionSerializable } from '../../types/transaction.js' import { toBlobs } from '../../utils/blob/toBlobs.js' @@ -17,6 +16,7 @@ import { concatHex } from '../../utils/data/concat.js' import { hexToNumber } from '../../utils/encoding/fromHex.js' import { stringToHex, toHex } from '../../utils/encoding/toHex.js' import { toRlp } from '../../utils/encoding/toRlp.js' +import { nonceManager } from '../../utils/index.js' import { parseEther } from '../../utils/unit/parseEther.js' import { parseGwei } from '../../utils/unit/parseGwei.js' import { estimateFeesPerGas } from '../public/estimateFeesPerGas.js' From 85e02be517b9f9ba025a95adb79987798c2e05f9 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:24:22 +1000 Subject: [PATCH 11/21] fix --- src/actions/wallet/prepareTransactionRequest.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/wallet/prepareTransactionRequest.test.ts b/src/actions/wallet/prepareTransactionRequest.test.ts index 5d188cf597..fbe7b0c879 100644 --- a/src/actions/wallet/prepareTransactionRequest.test.ts +++ b/src/actions/wallet/prepareTransactionRequest.test.ts @@ -11,8 +11,8 @@ import { parseEther } from '../../utils/unit/parseEther.js' import { parseGwei } from '../../utils/unit/parseGwei.js' import { anvilMainnet } from '../../../test/src/anvil.js' -import { nonceManager } from '../../experimental/index.js' import { http, createClient, toBlobs } from '../../index.js' +import { nonceManager } from '../../utils/index.js' import { prepareTransactionRequest } from './prepareTransactionRequest.js' const client = anvilMainnet.getClient() From 1551ce8a99024ab5f7fb7dfb7d3d54801b11efc6 Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 16:36:24 +1000 Subject: [PATCH 12/21] exports --- src/index.test.ts | 3 +++ src/utils/index.test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index 450cce895f..6367580cd3 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -398,6 +398,9 @@ test('exports', () => { "validateTypedData", "domainSeparator", "getTypesForEIP712Domain", + "createNonceManager", + "jsonRpc", + "nonceManager", ] `) }) diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 9a367a9743..5ce521903f 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -147,6 +147,9 @@ test('exports utils', () => { "parseUnits", "parseEther", "parseGwei", + "createNonceManager", + "jsonRpc", + "nonceManager", ] `) }) From 4dd093541137fe6d906e17e60e443621580a4695 Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 06:43:40 +1000 Subject: [PATCH 13/21] Update src/utils/nonceManager.ts Co-authored-by: awkweb --- src/utils/nonceManager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index 2d55648efe..d3d48be714 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -16,13 +16,13 @@ type FunctionParameters = { export type NonceManager = { /** Get and increment a nonce. */ - consume(parameters: FunctionParameters & { client: Client }): Promise + consume: (parameters: FunctionParameters & { client: Client }) => Promise /** Increment a nonce. */ - increment(chainId: FunctionParameters): Promise + increment: (chainId: FunctionParameters) => Promise /** Get a nonce. */ - get(chainId: FunctionParameters & { client: Client }): Promise + get: (chainId: FunctionParameters & { client: Client }) => Promise /** Reset a nonce. */ - reset(chainId: FunctionParameters): Promise + reset: (chainId: FunctionParameters) => Promise } /** From 1ee040f664a33e0caf77aa9e1c18e97b451efd0d Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 06:43:50 +1000 Subject: [PATCH 14/21] Update src/utils/nonceManager.ts Co-authored-by: awkweb --- src/utils/nonceManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index d3d48be714..097b0221c0 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -28,6 +28,8 @@ export type NonceManager = { /** * Creates a nonce manager for auto-incrementing transaction nonces. * + * - Docs: https://viem.sh/docs/accounts/createNonceManager + * * @example * ```ts * const nonceManager = createNonceManager({ From 696fb4dadc6705372c7cb49e39cb622bb591a91f Mon Sep 17 00:00:00 2001 From: jxom Date: Mon, 17 Jun 2024 20:44:46 +0000 Subject: [PATCH 15/21] chore: format --- src/utils/nonceManager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index 097b0221c0..1b7c05c394 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -16,7 +16,9 @@ type FunctionParameters = { export type NonceManager = { /** Get and increment a nonce. */ - consume: (parameters: FunctionParameters & { client: Client }) => Promise + consume: ( + parameters: FunctionParameters & { client: Client }, + ) => Promise /** Increment a nonce. */ increment: (chainId: FunctionParameters) => Promise /** Get a nonce. */ From 881f409cf9d9c9c08980d82b9162efd1c82db162 Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 06:50:44 +1000 Subject: [PATCH 16/21] chore: nonce entrypoint --- site/pages/docs/accounts/createNonceManager.md | 8 ++++---- src/index.ts | 2 +- src/nonce/index.ts | 9 +++++++++ src/nonce/package.json | 6 ++++++ src/package.json | 5 +++++ src/utils/index.ts | 2 +- src/utils/nonceManager.ts | 2 +- 7 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 src/nonce/index.ts create mode 100644 src/nonce/package.json diff --git a/site/pages/docs/accounts/createNonceManager.md b/site/pages/docs/accounts/createNonceManager.md index 409e320b78..6a56bc220e 100644 --- a/site/pages/docs/accounts/createNonceManager.md +++ b/site/pages/docs/accounts/createNonceManager.md @@ -11,7 +11,7 @@ For [JSON-RPC Accounts](/docs/accounts/jsonRpc) (ie. Browser Extension, WalletCo ## Import ```ts twoslash -import { createNonceManager } from 'viem' +import { createNonceManager } from 'viem/nonce' ``` ## Usage @@ -21,7 +21,7 @@ A Nonce Manager can be instantiated with the `createNonceManager` function with The example below demonstrates how to create a Nonce Manager with a JSON-RPC source (ie. uses `eth_getTransactionCount` as the source of truth). ```ts twoslash -import { createNonceManager, jsonRpc } from 'viem' +import { createNonceManager, jsonRpc } from 'viem/nonce' const nonceManager = createNonceManager({ source: jsonRpc() @@ -29,7 +29,7 @@ const nonceManager = createNonceManager({ ``` :::tip -Viem also exports a `nonceManager` helper instance that you can use directly. +Viem also exports a default `nonceManager` instance that you can use directly. ```ts twoslash import { nonceManager } from 'viem' @@ -96,7 +96,7 @@ Available sources: - `jsonRpc` ```ts twoslash -import { createNonceManager, jsonRpc } from 'viem' +import { createNonceManager, jsonRpc } from 'viem/nonce' const nonceManager = createNonceManager({ source: jsonRpc() // [!code focus] diff --git a/src/index.ts b/src/index.ts index 06e4568a51..c54e2860d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1658,7 +1658,7 @@ export { export { type CreateNonceManagerParameters, type NonceManager, + type NonceManagerSource, createNonceManager, - jsonRpc, nonceManager, } from './utils/nonceManager.js' diff --git a/src/nonce/index.ts b/src/nonce/index.ts new file mode 100644 index 0000000000..5875dadddc --- /dev/null +++ b/src/nonce/index.ts @@ -0,0 +1,9 @@ +// biome-ignore lint/performance/noBarrelFile: entrypoint module +export { + type CreateNonceManagerParameters, + type NonceManager, + type NonceManagerSource, + createNonceManager, + jsonRpc, + nonceManager, +} from '../utils/nonceManager.js' diff --git a/src/nonce/package.json b/src/nonce/package.json new file mode 100644 index 0000000000..0443cd5081 --- /dev/null +++ b/src/nonce/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "types": "../_types/nonce/index.d.ts", + "module": "../_esm/nonce/index.js", + "main": "../_cjs/nonce/index.js" +} diff --git a/src/package.json b/src/package.json index 902cbbfe3e..19a8dc9aee 100644 --- a/src/package.json +++ b/src/package.json @@ -65,6 +65,11 @@ "import": "./_esm/node/index.js", "default": "./_cjs/node/index.js" }, + "./nonce": { + "types": "./_types/nonce/index.d.ts", + "import": "./_esm/nonce/index.js", + "default": "./_cjs/nonce/index.js" + }, "./op-stack": { "types": "./_types/op-stack/index.d.ts", "import": "./_esm/op-stack/index.js", diff --git a/src/utils/index.ts b/src/utils/index.ts index a112dd2a42..ee0d8ed7a0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -494,7 +494,7 @@ export { type ParseGweiErrorType, parseGwei } from './unit/parseGwei.js' export { type CreateNonceManagerParameters, type NonceManager, + type NonceManagerSource, createNonceManager, - jsonRpc, nonceManager, } from './nonceManager.js' diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index 1b7c05c394..5f404a8581 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -102,7 +102,7 @@ export function createNonceManager( //////////////////////////////////////////////////////////////////////////////////////////// // Sources -type NonceManagerSource = { +export type NonceManagerSource = { /** Get a nonce. */ get(parameters: FunctionParameters & { client: Client }): MaybePromise /** Set a nonce. */ From 5cf795d9d5cdb4579a272719186af59405d1eaca Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 07:00:07 +1000 Subject: [PATCH 17/21] chore: sync increment --- src/utils/nonceManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index 5f404a8581..cd6fa510af 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -20,7 +20,7 @@ export type NonceManager = { parameters: FunctionParameters & { client: Client }, ) => Promise /** Increment a nonce. */ - increment: (chainId: FunctionParameters) => Promise + increment: (chainId: FunctionParameters) => void /** Get a nonce. */ get: (chainId: FunctionParameters & { client: Client }) => Promise /** Reset a nonce. */ @@ -56,7 +56,7 @@ export function createNonceManager( const key = getKey({ address, chainId }) const promise = this.get({ address, chainId, client }) - await this.increment({ address, chainId }) + this.increment({ address, chainId }) const nonce = await promise await source.set({ address, chainId }, nonce) From 9a9d9af71a00819a3cdc9758819a2a13ebac3ef1 Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 07:00:38 +1000 Subject: [PATCH 18/21] chore: sync reset --- src/utils/nonceManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/nonceManager.ts b/src/utils/nonceManager.ts index cd6fa510af..4dc2664eab 100644 --- a/src/utils/nonceManager.ts +++ b/src/utils/nonceManager.ts @@ -24,7 +24,7 @@ export type NonceManager = { /** Get a nonce. */ get: (chainId: FunctionParameters & { client: Client }) => Promise /** Reset a nonce. */ - reset: (chainId: FunctionParameters) => Promise + reset: (chainId: FunctionParameters) => void } /** @@ -91,7 +91,7 @@ export function createNonceManager( const delta = deltaMap.get(key) ?? 0 return delta + (await promise) }, - async reset({ address, chainId }) { + reset({ address, chainId }) { const key = getKey({ address, chainId }) deltaMap.delete(key) promiseMap.delete(key) From f092f23a45e0218ac5368908995ed5e10b054208 Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 09:11:12 +1000 Subject: [PATCH 19/21] chore: opt-in deduping --- src/actions/public/getBlock.ts | 22 +++-- .../public/getBlockTransactionCount.ts | 22 +++-- src/actions/public/getChainId.ts | 9 +- src/actions/public/getCode.ts | 11 ++- src/actions/public/getFeeHistory.ts | 19 ++-- src/actions/public/getTransaction.ts | 33 ++++--- .../public/getTransactionConfirmations.ts | 2 +- src/actions/public/getTransactionCount.ts | 11 ++- src/actions/public/getTransactionReceipt.ts | 11 ++- src/actions/wallet/addChain.ts | 2 +- src/actions/wallet/getAddresses.ts | 5 +- src/actions/wallet/getPermissions.ts | 5 +- src/actions/wallet/requestAddresses.ts | 2 +- src/actions/wallet/sendTransaction.ts | 2 +- src/actions/wallet/signMessage.ts | 2 +- src/actions/wallet/signTransaction.ts | 2 +- src/actions/wallet/signTypedData.ts | 2 +- src/actions/wallet/switchChain.ts | 2 +- src/clients/transports/http.test.ts | 35 ++++--- src/experimental/eip5792/actions/sendCalls.ts | 2 +- .../erc7715/actions/issuePermissions.ts | 2 +- src/utils/buildRequest.test.ts | 99 ++++++++++++++----- src/utils/buildRequest.ts | 2 +- src/utils/promise/withDedupe.test.ts | 70 ++++++++++++- test/src/anvil.ts | 4 +- 25 files changed, 269 insertions(+), 109 deletions(-) diff --git a/src/actions/public/getBlock.ts b/src/actions/public/getBlock.ts index 152f204233..6f555248bc 100644 --- a/src/actions/public/getBlock.ts +++ b/src/actions/public/getBlock.ts @@ -109,15 +109,21 @@ export async function getBlock< let block: RpcBlock | null = null if (blockHash) { - block = await client.request({ - method: 'eth_getBlockByHash', - params: [blockHash, includeTransactions], - }) + block = await client.request( + { + method: 'eth_getBlockByHash', + params: [blockHash, includeTransactions], + }, + { dedupe: true }, + ) } else { - block = await client.request({ - method: 'eth_getBlockByNumber', - params: [blockNumberHex || blockTag, includeTransactions], - }) + block = await client.request( + { + method: 'eth_getBlockByNumber', + params: [blockNumberHex || blockTag, includeTransactions], + }, + { dedupe: Boolean(blockNumberHex) }, + ) } if (!block) throw new BlockNotFoundError({ blockHash, blockNumber }) diff --git a/src/actions/public/getBlockTransactionCount.ts b/src/actions/public/getBlockTransactionCount.ts index 3fcfcb981c..7960643acb 100644 --- a/src/actions/public/getBlockTransactionCount.ts +++ b/src/actions/public/getBlockTransactionCount.ts @@ -81,15 +81,21 @@ export async function getBlockTransactionCount< let count: Quantity if (blockHash) { - count = await client.request({ - method: 'eth_getBlockTransactionCountByHash', - params: [blockHash], - }) + count = await client.request( + { + method: 'eth_getBlockTransactionCountByHash', + params: [blockHash], + }, + { dedupe: true }, + ) } else { - count = await client.request({ - method: 'eth_getBlockTransactionCountByNumber', - params: [blockNumberHex || blockTag], - }) + count = await client.request( + { + method: 'eth_getBlockTransactionCountByNumber', + params: [blockNumberHex || blockTag], + }, + { dedupe: Boolean(blockNumberHex) }, + ) } return hexToNumber(count) diff --git a/src/actions/public/getChainId.ts b/src/actions/public/getChainId.ts index ffbabc8581..e97b2445d4 100644 --- a/src/actions/public/getChainId.ts +++ b/src/actions/public/getChainId.ts @@ -41,8 +41,11 @@ export async function getChainId< TChain extends Chain | undefined, TAccount extends Account | undefined, >(client: Client): Promise { - const chainIdHex = await client.request({ - method: 'eth_chainId', - }) + const chainIdHex = await client.request( + { + method: 'eth_chainId', + }, + { dedupe: true }, + ) return hexToNumber(chainIdHex) } diff --git a/src/actions/public/getCode.ts b/src/actions/public/getCode.ts index 2bbd3e6a5c..d367b329a6 100644 --- a/src/actions/public/getCode.ts +++ b/src/actions/public/getCode.ts @@ -61,10 +61,13 @@ export async function getCode( ): Promise { const blockNumberHex = blockNumber !== undefined ? numberToHex(blockNumber) : undefined - const hex = await client.request({ - method: 'eth_getCode', - params: [address, blockNumberHex || blockTag], - }) + const hex = await client.request( + { + method: 'eth_getCode', + params: [address, blockNumberHex || blockTag], + }, + { dedupe: Boolean(blockNumberHex) }, + ) if (hex === '0x') return undefined return hex } diff --git a/src/actions/public/getFeeHistory.ts b/src/actions/public/getFeeHistory.ts index 54e748e41c..42dca53ad9 100644 --- a/src/actions/public/getFeeHistory.ts +++ b/src/actions/public/getFeeHistory.ts @@ -78,13 +78,16 @@ export async function getFeeHistory( }: GetFeeHistoryParameters, ): Promise { const blockNumberHex = blockNumber ? numberToHex(blockNumber) : undefined - const feeHistory = await client.request({ - method: 'eth_feeHistory', - params: [ - numberToHex(blockCount), - blockNumberHex || blockTag, - rewardPercentiles, - ], - }) + const feeHistory = await client.request( + { + method: 'eth_feeHistory', + params: [ + numberToHex(blockCount), + blockNumberHex || blockTag, + rewardPercentiles, + ], + }, + { dedupe: Boolean(blockNumberHex) }, + ) return formatFeeHistory(feeHistory) } diff --git a/src/actions/public/getTransaction.ts b/src/actions/public/getTransaction.ts index de4e266d95..cb3c9eeb34 100644 --- a/src/actions/public/getTransaction.ts +++ b/src/actions/public/getTransaction.ts @@ -108,20 +108,29 @@ export async function getTransaction< let transaction: RpcTransaction | null = null if (hash) { - transaction = await client.request({ - method: 'eth_getTransactionByHash', - params: [hash], - }) + transaction = await client.request( + { + method: 'eth_getTransactionByHash', + params: [hash], + }, + { dedupe: true }, + ) } else if (blockHash) { - transaction = await client.request({ - method: 'eth_getTransactionByBlockHashAndIndex', - params: [blockHash, numberToHex(index)], - }) + transaction = await client.request( + { + method: 'eth_getTransactionByBlockHashAndIndex', + params: [blockHash, numberToHex(index)], + }, + { dedupe: true }, + ) } else if (blockNumberHex || blockTag) { - transaction = await client.request({ - method: 'eth_getTransactionByBlockNumberAndIndex', - params: [blockNumberHex || blockTag, numberToHex(index)], - }) + transaction = await client.request( + { + method: 'eth_getTransactionByBlockNumberAndIndex', + params: [blockNumberHex || blockTag, numberToHex(index)], + }, + { dedupe: Boolean(blockNumberHex) }, + ) } if (!transaction) diff --git a/src/actions/public/getTransactionConfirmations.ts b/src/actions/public/getTransactionConfirmations.ts index ed876f42f7..48c96c121d 100644 --- a/src/actions/public/getTransactionConfirmations.ts +++ b/src/actions/public/getTransactionConfirmations.ts @@ -69,7 +69,7 @@ export async function getTransactionConfirmations< const [blockNumber, transaction] = await Promise.all([ getAction(client, getBlockNumber, 'getBlockNumber')({}), hash - ? getAction(client, getTransaction, 'getBlockNumber')({ hash }) + ? getAction(client, getTransaction, 'getTransaction')({ hash }) : undefined, ]) const transactionBlockNumber = diff --git a/src/actions/public/getTransactionCount.ts b/src/actions/public/getTransactionCount.ts index ed0a5b0ddf..5fe3626771 100644 --- a/src/actions/public/getTransactionCount.ts +++ b/src/actions/public/getTransactionCount.ts @@ -69,9 +69,12 @@ export async function getTransactionCount< client: Client, { address, blockTag = 'latest', blockNumber }: GetTransactionCountParameters, ): Promise { - const count = await client.request({ - method: 'eth_getTransactionCount', - params: [address, blockNumber ? numberToHex(blockNumber) : blockTag], - }) + const count = await client.request( + { + method: 'eth_getTransactionCount', + params: [address, blockNumber ? numberToHex(blockNumber) : blockTag], + }, + { dedupe: Boolean(blockNumber) }, + ) return hexToNumber(count) } diff --git a/src/actions/public/getTransactionReceipt.ts b/src/actions/public/getTransactionReceipt.ts index dfad05cb83..c40f803f4e 100644 --- a/src/actions/public/getTransactionReceipt.ts +++ b/src/actions/public/getTransactionReceipt.ts @@ -55,10 +55,13 @@ export async function getTransactionReceipt( client: Client, { hash }: GetTransactionReceiptParameters, ) { - const receipt = await client.request({ - method: 'eth_getTransactionReceipt', - params: [hash], - }) + const receipt = await client.request( + { + method: 'eth_getTransactionReceipt', + params: [hash], + }, + { dedupe: true }, + ) if (!receipt) throw new TransactionReceiptNotFoundError({ hash }) diff --git a/src/actions/wallet/addChain.ts b/src/actions/wallet/addChain.ts index c52574922e..ba5d5839b8 100644 --- a/src/actions/wallet/addChain.ts +++ b/src/actions/wallet/addChain.ts @@ -58,6 +58,6 @@ export async function addChain< }, ], }, - { retryCount: 0 }, + { dedupe: true, retryCount: 0 }, ) } diff --git a/src/actions/wallet/getAddresses.ts b/src/actions/wallet/getAddresses.ts index 59cc0f20cb..618cf72abf 100644 --- a/src/actions/wallet/getAddresses.ts +++ b/src/actions/wallet/getAddresses.ts @@ -45,6 +45,9 @@ export async function getAddresses< client: Client, ): Promise { if (client.account?.type === 'local') return [client.account.address] - const addresses = await client.request({ method: 'eth_accounts' }) + const addresses = await client.request( + { method: 'eth_accounts' }, + { dedupe: true }, + ) return addresses.map((address) => checksumAddress(address)) } diff --git a/src/actions/wallet/getPermissions.ts b/src/actions/wallet/getPermissions.ts index 7ee7337a2a..a81ecbff7e 100644 --- a/src/actions/wallet/getPermissions.ts +++ b/src/actions/wallet/getPermissions.ts @@ -34,6 +34,9 @@ export async function getPermissions< TChain extends Chain | undefined, TAccount extends Account | undefined = undefined, >(client: Client) { - const permissions = await client.request({ method: 'wallet_getPermissions' }) + const permissions = await client.request( + { method: 'wallet_getPermissions' }, + { dedupe: true }, + ) return permissions } diff --git a/src/actions/wallet/requestAddresses.ts b/src/actions/wallet/requestAddresses.ts index f95e52bc78..c52bcff8ea 100644 --- a/src/actions/wallet/requestAddresses.ts +++ b/src/actions/wallet/requestAddresses.ts @@ -44,7 +44,7 @@ export async function requestAddresses< ): Promise { const addresses = await client.request( { method: 'eth_requestAccounts' }, - { retryCount: 0 }, + { dedupe: true, retryCount: 0 }, ) return addresses.map((address) => getAddress(address)) } diff --git a/src/actions/wallet/sendTransaction.ts b/src/actions/wallet/sendTransaction.ts index 3649af77f4..396be922d6 100644 --- a/src/actions/wallet/sendTransaction.ts +++ b/src/actions/wallet/sendTransaction.ts @@ -234,7 +234,7 @@ export async function sendTransaction< method: 'eth_sendTransaction', params: [request], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } catch (err) { throw getTransactionError(err as BaseError, { diff --git a/src/actions/wallet/signMessage.ts b/src/actions/wallet/signMessage.ts index 6bd47168e3..6820af6c4a 100644 --- a/src/actions/wallet/signMessage.ts +++ b/src/actions/wallet/signMessage.ts @@ -107,6 +107,6 @@ export async function signMessage< method: 'personal_sign', params: [message_, account.address], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } diff --git a/src/actions/wallet/signTransaction.ts b/src/actions/wallet/signTransaction.ts index cc44d0460e..ab3f67f8f1 100644 --- a/src/actions/wallet/signTransaction.ts +++ b/src/actions/wallet/signTransaction.ts @@ -176,6 +176,6 @@ export async function signTransaction< } as unknown as RpcTransactionRequest, ], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } diff --git a/src/actions/wallet/signTypedData.ts b/src/actions/wallet/signTypedData.ts index 5acc2fb4cd..d862f5a1e5 100644 --- a/src/actions/wallet/signTypedData.ts +++ b/src/actions/wallet/signTypedData.ts @@ -190,6 +190,6 @@ export async function signTypedData< method: 'eth_signTypedData_v4', params: [account.address, typedData], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } diff --git a/src/actions/wallet/switchChain.ts b/src/actions/wallet/switchChain.ts index 5230583b73..e0d781dbb4 100644 --- a/src/actions/wallet/switchChain.ts +++ b/src/actions/wallet/switchChain.ts @@ -52,6 +52,6 @@ export async function switchChain< }, ], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } diff --git a/src/clients/transports/http.test.ts b/src/clients/transports/http.test.ts index f464adb39a..b586d15020 100644 --- a/src/clients/transports/http.test.ts +++ b/src/clients/transports/http.test.ts @@ -178,15 +178,15 @@ describe('request', () => { const p = [] p.push(transport.request({ method: 'eth_a' })) - p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_b' }, { dedupe: true })) p.push(transport.request({ method: 'eth_c' })) // test dedupe - p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_b' }, { dedupe: true })) await wait(1) - p.push(transport.request({ method: 'eth_d' })) + p.push(transport.request({ method: 'eth_d' }, { dedupe: true })) p.push(transport.request({ method: 'eth_e' })) // test dedupe - p.push(transport.request({ method: 'eth_d' })) + p.push(transport.request({ method: 'eth_d' }, { dedupe: true })) const results = await Promise.all(p) @@ -229,18 +229,18 @@ describe('request', () => { const p = [] p.push(transport.request({ method: 'eth_a' })) - p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_b' }, { dedupe: true })) p.push(transport.request({ method: 'eth_c' })) // test dedupe - p.push(transport.request({ method: 'eth_b' })) + p.push(transport.request({ method: 'eth_b' }, { dedupe: true })) await wait(1) p.push(transport.request({ method: 'eth_d' })) p.push(transport.request({ method: 'eth_e' })) await wait(20) - p.push(transport.request({ method: 'eth_f' })) + p.push(transport.request({ method: 'eth_f' }, { dedupe: true })) p.push(transport.request({ method: 'eth_g' })) // test dedupe - p.push(transport.request({ method: 'eth_f' })) + p.push(transport.request({ method: 'eth_f' }, { dedupe: true })) const results = await Promise.all(p) @@ -283,17 +283,20 @@ describe('request', () => { })({ chain: localhost }) const results = await Promise.all([ - transport.request({ method: 'eth_blockNumber' }), - transport.request({ method: 'eth_blockNumber' }), + transport.request({ method: 'eth_blockNumber' }, { dedupe: true }), + transport.request({ method: 'eth_blockNumber' }, { dedupe: true }), // this will not be deduped (different params). - transport.request({ method: 'eth_blockNumber', params: [1] }), - transport.request({ method: 'eth_blockNumber' }), + transport.request( + { method: 'eth_blockNumber', params: [1] }, + { dedupe: true }, + ), + transport.request({ method: 'eth_blockNumber' }, { dedupe: true }), // this will not be deduped (different method). - transport.request({ method: 'eth_chainId' }), - transport.request({ method: 'eth_blockNumber' }), - // this will not be deduped (dedupe: false). - transport.request({ method: 'eth_blockNumber' }, { dedupe: false }), + transport.request({ method: 'eth_chainId' }, { dedupe: true }), + transport.request({ method: 'eth_blockNumber' }, { dedupe: true }), + // this will not be deduped (dedupe: undefined). transport.request({ method: 'eth_blockNumber' }), + transport.request({ method: 'eth_blockNumber' }, { dedupe: true }), ]) expect( diff --git a/src/experimental/eip5792/actions/sendCalls.ts b/src/experimental/eip5792/actions/sendCalls.ts index e83887a4dc..8c6d2d7652 100644 --- a/src/experimental/eip5792/actions/sendCalls.ts +++ b/src/experimental/eip5792/actions/sendCalls.ts @@ -116,7 +116,7 @@ export async function sendCalls< }, ], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) } catch (err) { throw getTransactionError(err as BaseError, { diff --git a/src/experimental/erc7715/actions/issuePermissions.ts b/src/experimental/erc7715/actions/issuePermissions.ts index 5c14664fbf..e638c8c799 100644 --- a/src/experimental/erc7715/actions/issuePermissions.ts +++ b/src/experimental/erc7715/actions/issuePermissions.ts @@ -89,7 +89,7 @@ export async function issuePermissions( method: 'wallet_issuePermissions', params: [parseParameters({ expiry, permissions, signer })], }, - { dedupe: false, retryCount: 0 }, + { retryCount: 0 }, ) return parseResult(result) as IssuePermissionsReturnType } diff --git a/src/utils/buildRequest.test.ts b/src/utils/buildRequest.test.ts index f7d7a8dcf2..f021ea2050 100644 --- a/src/utils/buildRequest.test.ts +++ b/src/utils/buildRequest.test.ts @@ -52,16 +52,63 @@ function request(url: string) { } test('default', async () => { - const server = await createHttpServer((_req, res) => { - res.writeHead(200, { - 'Content-Type': 'application/json', + const args: string[] = [] + const server = await createHttpServer((req, res) => { + let body = '' + req.on('data', (chunk) => { + body += chunk + }) + req.on('end', () => { + args.push(body) + res.writeHead(200, { + 'Content-Type': 'application/json', + }) + res.end(JSON.stringify({ result: body })) }) - res.end(JSON.stringify({ result: '0x1' })) }) + const request_ = buildRequest(request(server.url)) + + const results = await Promise.all([ + request_({ method: 'eth_a' }), + request_({ method: 'eth_b' }), + request_({ method: 'eth_a', params: [1] }), + request_({ method: 'eth_c' }), + request_({ method: 'eth_d' }), + request_({ method: 'eth_a', params: [2] }), + request_({ method: 'eth_a' }), + request_({ method: 'eth_a' }), + ]) + expect( - await buildRequest(request(server.url))({ method: 'eth_blockNumber' }), - ).toMatchInlineSnapshot('"0x1"') + args + .map((arg) => JSON.parse(arg)) + .sort((a, b) => a.id - b.id) + .map((arg) => JSON.stringify(arg)), + ).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","id":1,"method":"eth_a"}", + "{"jsonrpc":"2.0","id":2,"method":"eth_b"}", + "{"jsonrpc":"2.0","id":3,"method":"eth_a","params":[1]}", + "{"jsonrpc":"2.0","id":4,"method":"eth_c"}", + "{"jsonrpc":"2.0","id":5,"method":"eth_d"}", + "{"jsonrpc":"2.0","id":6,"method":"eth_a","params":[2]}", + "{"jsonrpc":"2.0","id":7,"method":"eth_a"}", + "{"jsonrpc":"2.0","id":8,"method":"eth_a"}", + ] + `) + expect(results).toMatchInlineSnapshot(` + [ + "{"jsonrpc":"2.0","id":1,"method":"eth_a"}", + "{"jsonrpc":"2.0","id":2,"method":"eth_b"}", + "{"jsonrpc":"2.0","id":3,"method":"eth_a","params":[1]}", + "{"jsonrpc":"2.0","id":4,"method":"eth_c"}", + "{"jsonrpc":"2.0","id":5,"method":"eth_d"}", + "{"jsonrpc":"2.0","id":6,"method":"eth_a","params":[2]}", + "{"jsonrpc":"2.0","id":7,"method":"eth_a"}", + "{"jsonrpc":"2.0","id":8,"method":"eth_a"}", + ] + `) }) describe('args', () => { @@ -729,17 +776,17 @@ describe('behavior', () => { const request_ = buildRequest(request(server.url), { uid }) const results = await Promise.all([ - request_({ method: 'eth_blockNumber' }), - request_({ method: 'eth_blockNumber' }), + request_({ method: 'eth_blockNumber' }, { dedupe: true }), + request_({ method: 'eth_blockNumber' }, { dedupe: true }), // this will not be deduped (different params). - request_({ method: 'eth_blockNumber', params: [1] }), - request_({ method: 'eth_blockNumber' }), + request_({ method: 'eth_blockNumber', params: [1] }, { dedupe: true }), + request_({ method: 'eth_blockNumber' }, { dedupe: true }), // this will not be deduped (different method). - request_({ method: 'eth_chainId' }), - request_({ method: 'eth_blockNumber' }), - // this will not be deduped (dedupe: false). - request_({ method: 'eth_blockNumber' }, { dedupe: false }), + request_({ method: 'eth_chainId' }, { dedupe: true }), + request_({ method: 'eth_blockNumber' }, { dedupe: true }), + // this will not be deduped (dedupe: undefined). request_({ method: 'eth_blockNumber' }), + request_({ method: 'eth_blockNumber' }, { dedupe: true }), ]) expect( @@ -749,22 +796,22 @@ describe('behavior', () => { .map((arg) => JSON.stringify(arg)), ).toMatchInlineSnapshot(` [ - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":62,"method":"eth_blockNumber","params":[1]}", - "{"jsonrpc":"2.0","id":63,"method":"eth_chainId"}", - "{"jsonrpc":"2.0","id":64,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":69,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":70,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":71,"method":"eth_blockNumber"}", ] `) expect(results).toMatchInlineSnapshot(` [ - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":62,"method":"eth_blockNumber","params":[1]}", - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":63,"method":"eth_chainId"}", - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":64,"method":"eth_blockNumber"}", - "{"jsonrpc":"2.0","id":61,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":69,"method":"eth_blockNumber","params":[1]}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":70,"method":"eth_chainId"}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":71,"method":"eth_blockNumber"}", + "{"jsonrpc":"2.0","id":68,"method":"eth_blockNumber"}", ] `) }) diff --git a/src/utils/buildRequest.ts b/src/utils/buildRequest.ts index 3ea818e478..cf7a3fbf70 100644 --- a/src/utils/buildRequest.ts +++ b/src/utils/buildRequest.ts @@ -99,7 +99,7 @@ export function buildRequest Promise>( ): EIP1193RequestFn { return async (args, overrideOptions = {}) => { const { - dedupe = true, + dedupe = false, retryDelay = 150, retryCount = 3, uid, diff --git a/src/utils/promise/withDedupe.test.ts b/src/utils/promise/withDedupe.test.ts index 713c0aa466..cefb9b2c8e 100644 --- a/src/utils/promise/withDedupe.test.ts +++ b/src/utils/promise/withDedupe.test.ts @@ -8,7 +8,7 @@ test('default', async () => { let count = 0 async function fn() { count++ - await wait(1000) + await wait(100) return 'bar' } @@ -16,6 +16,7 @@ test('default', async () => { const promise_1 = withDedupe(fn, { id }) const promise_2 = withDedupe(fn, { id }) + expect(promise_1).toBe(promise_2) expect(promiseCache.has(id)).toBe(true) const results = await Promise.all([promise_1, promise_2]) @@ -23,3 +24,70 @@ test('default', async () => { expect(count).toBe(1) expect(promiseCache.has(id)).toBe(false) }) + +test('args: enabled', async () => { + let count = 0 + async function fn() { + count++ + await wait(100) + return 'bar' + } + + const id = 'foo' + + const promise_1 = withDedupe(fn, { id }) + const promise_2 = withDedupe(fn, { id, enabled: false }) + const promise_3 = withDedupe(fn, { id }) + expect(promise_1).not.toBe(promise_2) + expect(promise_1).toBe(promise_3) + expect(promiseCache.has(id)).toBe(true) + + const results = await Promise.all([promise_1, promise_2, promise_3]) + expect(results[0]).toBe(results[1]) + expect(count).toBe(2) + expect(promiseCache.has(id)).toBe(false) +}) + +test('args: undefined id', async () => { + let count = 0 + async function fn() { + count++ + await wait(100) + return 'bar' + } + + const id = 'foo' + + const promise_1 = withDedupe(fn, { id }) + const promise_2 = withDedupe(fn, { id: undefined }) + const promise_3 = withDedupe(fn, { id }) + expect(promise_1).not.toBe(promise_2) + expect(promise_1).toBe(promise_3) + expect(promiseCache.has(id)).toBe(true) + + const results = await Promise.all([promise_1, promise_2, promise_3]) + expect(results[0]).toBe(results[1]) + expect(count).toBe(2) + expect(promiseCache.has(id)).toBe(false) +}) + +test('behavior: errors', async () => { + let count = 0 + async function fn() { + count++ + await wait(100) + throw new Error('rekt') + } + + const id = 'foo' + + const promise_1 = withDedupe(fn, { id }) + const promise_2 = withDedupe(fn, { id }) + expect(promiseCache.has(id)).toBe(true) + + await expect(() => + Promise.all([promise_1, promise_2]), + ).rejects.toThrowErrorMatchingInlineSnapshot('[Error: rekt]') + expect(count).toBe(1) + expect(promiseCache.has(id)).toBe(false) +}) diff --git a/test/src/anvil.ts b/test/src/anvil.ts index 62481e179c..ed0b8e2d4a 100644 --- a/test/src/anvil.ts +++ b/test/src/anvil.ts @@ -172,7 +172,7 @@ function defineAnvil( return { config, - async request({ method, params }: any) { + async request({ method, params }: any, opts: any = {}) { if (method === 'eth_requestAccounts') { return [accounts[0].address] as any } @@ -213,7 +213,7 @@ function defineAnvil( }, ] - return request({ method, params }) + return request({ method, params }, opts) }, value, } From 77e7dbac40865a1c680fdf5b16dd256db067faf1 Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 09:25:25 +1000 Subject: [PATCH 20/21] exports --- site/pages/docs/accounts/createNonceManager.md | 3 +-- src/accounts/index.test.ts | 2 ++ src/accounts/index.ts | 7 +++++++ src/index.test.ts | 1 - src/utils/index.test.ts | 1 - 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/site/pages/docs/accounts/createNonceManager.md b/site/pages/docs/accounts/createNonceManager.md index 6a56bc220e..8b141db955 100644 --- a/site/pages/docs/accounts/createNonceManager.md +++ b/site/pages/docs/accounts/createNonceManager.md @@ -43,8 +43,7 @@ A `nonceManager` can be passed as an option to [Local Accounts](/docs/accounts/l :::code-group ```ts twoslash [example.ts] -import { nonceManager } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' // [!code focus] +import { privateKeyToAccount, nonceManager } from 'viem/accounts' // [!code focus] import { client } from './config' const account = privateKeyToAccount('0x...', { nonceManager }) // [!code focus] diff --git a/src/accounts/index.test.ts b/src/accounts/index.test.ts index c8be204a58..01fb990611 100644 --- a/src/accounts/index.test.ts +++ b/src/accounts/index.test.ts @@ -30,6 +30,8 @@ test('exports utils', () => { "parseAccount", "publicKeyToAddress", "privateKeyToAddress", + "createNonceManager", + "nonceManager", ] `) }) diff --git a/src/accounts/index.ts b/src/accounts/index.ts index 40d23ab1f9..19e7495097 100644 --- a/src/accounts/index.ts +++ b/src/accounts/index.ts @@ -90,3 +90,10 @@ export { type PrivateKeyToAddressErrorType, privateKeyToAddress, } from './utils/privateKeyToAddress.js' +export { + type CreateNonceManagerParameters, + type NonceManager, + type NonceManagerSource, + createNonceManager, + nonceManager, +} from '../utils/nonceManager.js' diff --git a/src/index.test.ts b/src/index.test.ts index 6367580cd3..bd9e856fdf 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -399,7 +399,6 @@ test('exports', () => { "domainSeparator", "getTypesForEIP712Domain", "createNonceManager", - "jsonRpc", "nonceManager", ] `) diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 5ce521903f..c9124f05fb 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -148,7 +148,6 @@ test('exports utils', () => { "parseEther", "parseGwei", "createNonceManager", - "jsonRpc", "nonceManager", ] `) From e87dfb1de889473b6d3a241edf35811d939ec78e Mon Sep 17 00:00:00 2001 From: jxom Date: Tue, 18 Jun 2024 09:37:25 +1000 Subject: [PATCH 21/21] size --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 367aa658cf..79d5d2fd3f 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ { "name": "viem (esm)", "path": "./src/_esm/index.js", - "limit": "60 kB", + "limit": "60.2 kB", "import": "*" }, {