Skip to content

Commit

Permalink
feat(common): allow specifying concurrency in transactionQueue (#2589)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Apr 2, 2024
1 parent da0f8d9 commit 620e4ec
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-maps-itch.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 18 additions & 5 deletions packages/common/src/actions/transactionQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@ import { writeContract as mud_writeContract } from "../writeContract";
import { sendTransaction as mud_sendTransaction } from "../sendTransaction";

export type TransactionQueueOptions<chain extends Chain> = {
/**
* `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<Transport, chain>;
/**
* 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<chain extends Chain, account extends Account>({
publicClient,
}: TransactionQueueOptions<chain> = {}): (
export function transactionQueue<chain extends Chain, account extends Account>(
opts: TransactionQueueOptions<chain> = {},
): (
client: Client<Transport, chain, account>,
) => Pick<WalletActions<chain, account>, "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),
});
}
4 changes: 3 additions & 1 deletion packages/common/src/createNonceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type CreateNonceManagerOptions = {
address: Hex;
blockTag?: BlockTag;
broadcastChannelName?: string;
queueConcurrency?: number;
};

export type CreateNonceManagerResult = {
Expand All @@ -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;
Expand Down Expand Up @@ -70,7 +72,7 @@ export function createNonceManager({
);
}

const mempoolQueue = new PQueue({ concurrency: 1 });
const mempoolQueue = new PQueue({ concurrency: queueConcurrency });

return {
hasNonce,
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/getContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?.({
Expand Down
24 changes: 20 additions & 4 deletions packages/common/src/sendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<chain extends Chain | undefined> = {
/**
* `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<Transport, chain>;
/**
* 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<
Expand All @@ -26,7 +41,7 @@ export async function sendTransaction<
>(
client: Client<Transport, chain, account>,
request: SendTransactionParameters<chain, account, chainOverride>,
publicClient?: PublicClient<Transport, chain>,
opts: SendTransactionExtraOptions<chain> = {},
): Promise<SendTransactionReturnType> {
const rawAccount = request.account ?? client.account;
if (!rawAccount) {
Expand All @@ -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<SendTransactionParameters<chain, account, chainOverride>> {
Expand All @@ -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,
Expand Down
24 changes: 20 additions & 4 deletions packages/common/src/writeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<chain extends Chain | undefined> = {
/**
* `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<Transport, chain>;
/**
* 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<
Expand All @@ -32,7 +47,7 @@ export async function writeContract<
>(
client: Client<Transport, chain, account>,
request: WriteContractParameters<abi, functionName, args, chain, account, chainOverride>,
publicClient?: PublicClient<Transport, chain>,
opts: WriteContractExtraOptions<chain> = {},
): Promise<WriteContractReturnType> {
const rawAccount = request.account ?? client.account;
if (!rawAccount) {
Expand All @@ -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<
Expand All @@ -57,7 +73,7 @@ export async function writeContract<

debug("simulating", request.functionName, "at", request.address);
const result = await simulateContract<chain, account | undefined, abi, functionName, args, chainOverride>(
publicClient ?? client,
opts.publicClient ?? client,
{
...request,
blockTag: "pending",
Expand Down

0 comments on commit 620e4ec

Please sign in to comment.