Skip to content

Commit 620e4ec

Browse files
authored
feat(common): allow specifying concurrency in transactionQueue (#2589)
1 parent da0f8d9 commit 620e4ec

File tree

6 files changed

+67
-15
lines changed

6 files changed

+67
-15
lines changed

Diff for: .changeset/poor-maps-itch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/common": patch
3+
---
4+
5+
`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 for: packages/common/src/actions/transactionQueue.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,31 @@ import { writeContract as mud_writeContract } from "../writeContract";
33
import { sendTransaction as mud_sendTransaction } from "../sendTransaction";
44

55
export type TransactionQueueOptions<chain extends Chain> = {
6+
/**
7+
* `publicClient` can be provided to be used in place of the extended viem client for making public action calls
8+
* (`getChainId`, `getTransactionCount`, `simulateContract`, `call`). This helps in cases where the extended
9+
* viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js),
10+
* where the transport is the bundler, not an RPC.
11+
*/
612
publicClient?: PublicClient<Transport, chain>;
13+
/**
14+
* Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered
15+
* and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions
16+
* arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry.
17+
* @default 1
18+
*/
19+
queueConcurrency?: number;
720
};
821

9-
export function transactionQueue<chain extends Chain, account extends Account>({
10-
publicClient,
11-
}: TransactionQueueOptions<chain> = {}): (
22+
export function transactionQueue<chain extends Chain, account extends Account>(
23+
opts: TransactionQueueOptions<chain> = {},
24+
): (
1225
client: Client<Transport, chain, account>,
1326
) => Pick<WalletActions<chain, account>, "writeContract" | "sendTransaction"> {
1427
return (client) => ({
1528
// Applies to: `client.writeContract`, `getContract(client, ...).write`
16-
writeContract: (args) => mud_writeContract(client, args, publicClient),
29+
writeContract: (args) => mud_writeContract(client, args, opts),
1730
// Applies to: `client.sendTransaction`
18-
sendTransaction: (args) => mud_sendTransaction(client, args, publicClient),
31+
sendTransaction: (args) => mud_sendTransaction(client, args, opts),
1932
});
2033
}

Diff for: packages/common/src/createNonceManager.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type CreateNonceManagerOptions = {
1111
address: Hex;
1212
blockTag?: BlockTag;
1313
broadcastChannelName?: string;
14+
queueConcurrency?: number;
1415
};
1516

1617
export type CreateNonceManagerResult = {
@@ -26,6 +27,7 @@ export function createNonceManager({
2627
address, // TODO: rename to account?
2728
blockTag = "pending",
2829
broadcastChannelName,
30+
queueConcurrency = 1,
2931
}: CreateNonceManagerOptions): CreateNonceManagerResult {
3032
const nonceRef = { current: -1 };
3133
let channel: BroadcastChannel | null = null;
@@ -70,7 +72,7 @@ export function createNonceManager({
7072
);
7173
}
7274

73-
const mempoolQueue = new PQueue({ concurrency: 1 });
75+
const mempoolQueue = new PQueue({ concurrency: queueConcurrency });
7476

7577
return {
7678
hasNonce,

Diff for: packages/common/src/getContract.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function getContract<
118118
TChain,
119119
TAccount
120120
>;
121-
const result = writeContract(walletClient, request, publicClient);
121+
const result = writeContract(walletClient, request, { publicClient });
122122

123123
const id = `${walletClient.chain.id}:${walletClient.account.address}:${nextWriteId++}`;
124124
onWrite?.({

Diff for: packages/common/src/sendTransaction.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,22 @@ import { parseAccount } from "viem/accounts";
1616

1717
const debug = parentDebug.extend("sendTransaction");
1818

19-
// TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230
19+
export type SendTransactionExtraOptions<chain extends Chain | undefined> = {
20+
/**
21+
* `publicClient` can be provided to be used in place of the extended viem client for making public action calls
22+
* (`getChainId`, `getTransactionCount`, `call`). This helps in cases where the extended
23+
* viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js),
24+
* where the transport is the bundler, not an RPC.
25+
*/
26+
publicClient?: PublicClient<Transport, chain>;
27+
/**
28+
* Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered
29+
* and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions
30+
* arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry.
31+
* @default 1
32+
*/
33+
queueConcurrency?: number;
34+
};
2035

2136
/** @deprecated Use `walletClient.extend(transactionQueue())` instead. */
2237
export async function sendTransaction<
@@ -26,7 +41,7 @@ export async function sendTransaction<
2641
>(
2742
client: Client<Transport, chain, account>,
2843
request: SendTransactionParameters<chain, account, chainOverride>,
29-
publicClient?: PublicClient<Transport, chain>,
44+
opts: SendTransactionExtraOptions<chain> = {},
3045
): Promise<SendTransactionReturnType> {
3146
const rawAccount = request.account ?? client.account;
3247
if (!rawAccount) {
@@ -36,9 +51,10 @@ export async function sendTransaction<
3651
const account = parseAccount(rawAccount);
3752

3853
const nonceManager = await getNonceManager({
39-
client: publicClient ?? client,
54+
client: opts.publicClient ?? client,
4055
address: account.address,
4156
blockTag: "pending",
57+
queueConcurrency: opts.queueConcurrency,
4258
});
4359

4460
async function prepare(): Promise<SendTransactionParameters<chain, account, chainOverride>> {
@@ -48,7 +64,7 @@ export async function sendTransaction<
4864
}
4965

5066
debug("simulating tx to", request.to);
51-
await call(publicClient ?? client, {
67+
await call(opts.publicClient ?? client, {
5268
...request,
5369
blockTag: "pending",
5470
account,

Diff for: packages/common/src/writeContract.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,22 @@ import { parseAccount } from "viem/accounts";
1919

2020
const debug = parentDebug.extend("writeContract");
2121

22-
// TODO: migrate away from this approach once we can hook into viem's nonce management: https://github.com/wagmi-dev/viem/discussions/1230
22+
export type WriteContractExtraOptions<chain extends Chain | undefined> = {
23+
/**
24+
* `publicClient` can be provided to be used in place of the extended viem client for making public action calls
25+
* (`getChainId`, `getTransactionCount`, `simulateContract`). This helps in cases where the extended
26+
* viem client is a smart account client, like in [permissionless.js](https://github.com/pimlicolabs/permissionless.js),
27+
* where the transport is the bundler, not an RPC.
28+
*/
29+
publicClient?: PublicClient<Transport, chain>;
30+
/**
31+
* Adjust the number of concurrent calls to the mempool. This defaults to `1` to ensure transactions are ordered
32+
* and nonces are handled properly. Any number greater than that is likely to see nonce errors and/or transactions
33+
* arriving out of order, but this may be an acceptable trade-off for some applications that can safely retry.
34+
* @default 1
35+
*/
36+
queueConcurrency?: number;
37+
};
2338

2439
/** @deprecated Use `walletClient.extend(transactionQueue())` instead. */
2540
export async function writeContract<
@@ -32,7 +47,7 @@ export async function writeContract<
3247
>(
3348
client: Client<Transport, chain, account>,
3449
request: WriteContractParameters<abi, functionName, args, chain, account, chainOverride>,
35-
publicClient?: PublicClient<Transport, chain>,
50+
opts: WriteContractExtraOptions<chain> = {},
3651
): Promise<WriteContractReturnType> {
3752
const rawAccount = request.account ?? client.account;
3853
if (!rawAccount) {
@@ -42,9 +57,10 @@ export async function writeContract<
4257
const account = parseAccount(rawAccount);
4358

4459
const nonceManager = await getNonceManager({
45-
client: publicClient ?? client,
60+
client: opts.publicClient ?? client,
4661
address: account.address,
4762
blockTag: "pending",
63+
queueConcurrency: opts.queueConcurrency,
4864
});
4965

5066
async function prepareWrite(): Promise<
@@ -57,7 +73,7 @@ export async function writeContract<
5773

5874
debug("simulating", request.functionName, "at", request.address);
5975
const result = await simulateContract<chain, account | undefined, abi, functionName, args, chainOverride>(
60-
publicClient ?? client,
76+
opts.publicClient ?? client,
6177
{
6278
...request,
6379
blockTag: "pending",

0 commit comments

Comments
 (0)