diff --git a/.changeset/poor-maps-itch.md b/.changeset/poor-maps-itch.md new file mode 100644 index 0000000000..8b89f82a93 --- /dev/null +++ b/.changeset/poor-maps-itch.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/common": patch +--- + +`transactionQueue` now accepts a `queueConcurrency` to allow adjusting the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered and nonces are handled properly. Any number greater than that is likely to see nonce errors and transactions arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry. diff --git a/packages/common/src/actions/transactionQueue.ts b/packages/common/src/actions/transactionQueue.ts index 76b0c58b69..d6fe03353b 100644 --- a/packages/common/src/actions/transactionQueue.ts +++ b/packages/common/src/actions/transactionQueue.ts @@ -3,18 +3,31 @@ import { writeContract as mud_writeContract } from "../writeContract"; import { sendTransaction as mud_sendTransaction } from "../sendTransaction"; export type TransactionQueueOptions = { + /** + * `publicClient` can be provided to be used in place of the extended viem client for making public action calls + * (`getChainId`, `getTransactionCount`, `simulateContract`, `call`). This helps in cases where the extended + * viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js), + * where the transport is the bundler, not an RPC. + */ publicClient?: PublicClient; + /** + * Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered + * and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions + * arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry. + * @default 1 + */ + queueConcurrency?: number; }; -export function transactionQueue({ - publicClient, -}: TransactionQueueOptions = {}): ( +export function transactionQueue( + opts: TransactionQueueOptions = {}, +): ( client: Client, ) => Pick, "writeContract" | "sendTransaction"> { return (client) => ({ // Applies to: `client.writeContract`, `getContract(client, ...).write` - writeContract: (args) => mud_writeContract(client, args, publicClient), + writeContract: (args) => mud_writeContract(client, args, opts), // Applies to: `client.sendTransaction` - sendTransaction: (args) => mud_sendTransaction(client, args, publicClient), + sendTransaction: (args) => mud_sendTransaction(client, args, opts), }); } diff --git a/packages/common/src/createNonceManager.ts b/packages/common/src/createNonceManager.ts index d3dbb1dba2..f3e06b7028 100644 --- a/packages/common/src/createNonceManager.ts +++ b/packages/common/src/createNonceManager.ts @@ -11,6 +11,7 @@ export type CreateNonceManagerOptions = { address: Hex; blockTag?: BlockTag; broadcastChannelName?: string; + queueConcurrency?: number; }; export type CreateNonceManagerResult = { @@ -26,6 +27,7 @@ export function createNonceManager({ address, // TODO: rename to account? blockTag = "pending", broadcastChannelName, + queueConcurrency = 1, }: CreateNonceManagerOptions): CreateNonceManagerResult { const nonceRef = { current: -1 }; let channel: BroadcastChannel | null = null; @@ -70,7 +72,7 @@ export function createNonceManager({ ); } - const mempoolQueue = new PQueue({ concurrency: 1 }); + const mempoolQueue = new PQueue({ concurrency: queueConcurrency }); return { hasNonce, diff --git a/packages/common/src/getContract.ts b/packages/common/src/getContract.ts index ab0972dcfa..c8e53057d3 100644 --- a/packages/common/src/getContract.ts +++ b/packages/common/src/getContract.ts @@ -118,7 +118,7 @@ export function getContract< TChain, TAccount >; - const result = writeContract(walletClient, request, publicClient); + const result = writeContract(walletClient, request, { publicClient }); const id = `${walletClient.chain.id}:${walletClient.account.address}:${nextWriteId++}`; onWrite?.({ diff --git a/packages/common/src/sendTransaction.ts b/packages/common/src/sendTransaction.ts index 316cbc052e..60d2cf9006 100644 --- a/packages/common/src/sendTransaction.ts +++ b/packages/common/src/sendTransaction.ts @@ -16,7 +16,22 @@ import { parseAccount } from "viem/accounts"; const debug = parentDebug.extend("sendTransaction"); -// TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230 +export type SendTransactionExtraOptions = { + /** + * `publicClient` can be provided to be used in place of the extended viem client for making public action calls + * (`getChainId`, `getTransactionCount`, `call`). This helps in cases where the extended + * viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js), + * where the transport is the bundler, not an RPC. + */ + publicClient?: PublicClient; + /** + * Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered + * and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions + * arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry. + * @default 1 + */ + queueConcurrency?: number; +}; /** @deprecated Use `walletClient.extend(transactionQueue())` instead. */ export async function sendTransaction< @@ -26,7 +41,7 @@ export async function sendTransaction< >( client: Client, request: SendTransactionParameters, - publicClient?: PublicClient, + opts: SendTransactionExtraOptions = {}, ): Promise { const rawAccount = request.account ?? client.account; if (!rawAccount) { @@ -36,9 +51,10 @@ export async function sendTransaction< const account = parseAccount(rawAccount); const nonceManager = await getNonceManager({ - client: publicClient ?? client, + client: opts.publicClient ?? client, address: account.address, blockTag: "pending", + queueConcurrency: opts.queueConcurrency, }); async function prepare(): Promise> { @@ -48,7 +64,7 @@ export async function sendTransaction< } debug("simulating tx to", request.to); - await call(publicClient ?? client, { + await call(opts.publicClient ?? client, { ...request, blockTag: "pending", account, diff --git a/packages/common/src/writeContract.ts b/packages/common/src/writeContract.ts index 874f1d30a6..53d6598d28 100644 --- a/packages/common/src/writeContract.ts +++ b/packages/common/src/writeContract.ts @@ -19,7 +19,22 @@ import { parseAccount } from "viem/accounts"; const debug = parentDebug.extend("writeContract"); -// TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230 +export type WriteContractExtraOptions = { + /** + * `publicClient` can be provided to be used in place of the extended viem client for making public action calls + * (`getChainId`, `getTransactionCount`, `simulateContract`). This helps in cases where the extended + * viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js), + * where the transport is the bundler, not an RPC. + */ + publicClient?: PublicClient; + /** + * Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered + * and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions + * arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry. + * @default 1 + */ + queueConcurrency?: number; +}; /** @deprecated Use `walletClient.extend(transactionQueue())` instead. */ export async function writeContract< @@ -32,7 +47,7 @@ export async function writeContract< >( client: Client, request: WriteContractParameters, - publicClient?: PublicClient, + opts: WriteContractExtraOptions = {}, ): Promise { const rawAccount = request.account ?? client.account; if (!rawAccount) { @@ -42,9 +57,10 @@ export async function writeContract< const account = parseAccount(rawAccount); const nonceManager = await getNonceManager({ - client: publicClient ?? client, + client: opts.publicClient ?? client, address: account.address, blockTag: "pending", + queueConcurrency: opts.queueConcurrency, }); async function prepareWrite(): Promise< @@ -57,7 +73,7 @@ export async function writeContract< debug("simulating", request.functionName, "at", request.address); const result = await simulateContract( - publicClient ?? client, + opts.publicClient ?? client, { ...request, blockTag: "pending",