Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): allow specifying concurrency in transactionQueue #2589

Merged
merged 5 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading