Skip to content

Commit

Permalink
fix(common): latency improvements (#2641)
Browse files Browse the repository at this point in the history
Co-authored-by: Kevin Ingersoll <[email protected]>
  • Loading branch information
alvrs and holic authored Apr 16, 2024
1 parent 9720b56 commit 6c8ab47
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-nails-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/common": patch
---

Reduced the number of RPC requests before sending a transaction in the `transactionQueue` viem decorator.
29 changes: 29 additions & 0 deletions packages/common/src/createFeeRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { EstimateFeesPerGasParameters, Client, EstimateFeesPerGasReturnType } from "viem";
import { estimateFeesPerGas } from "viem/actions";

export type CreateFeeRefOptions = {
client: Client;
refreshInterval: number;
args?: EstimateFeesPerGasParameters;
};

export type FeeRef = {
fees: EstimateFeesPerGasReturnType | {};
lastUpdatedTimestamp: number;
};

/** Update fee values once every `refreshInterval` instead of right before every request */
export async function createFeeRef({ client, args, refreshInterval }: CreateFeeRefOptions): Promise<FeeRef> {
const feeRef: FeeRef = { fees: {}, lastUpdatedTimestamp: 0 };

async function updateFees(): Promise<void> {
const fees = await estimateFeesPerGas(client, args);
feeRef.fees = fees;
feeRef.lastUpdatedTimestamp = Date.now();
}

setInterval(updateFees, refreshInterval);
await updateFees();

return feeRef;
}
7 changes: 7 additions & 0 deletions packages/common/src/createNonceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type CreateNonceManagerOptions = {

export type CreateNonceManagerResult = {
hasNonce: () => boolean;
getNonce: () => number;
nextNonce: () => number;
resetNonce: () => Promise<void>;
shouldResetNonce: (error: unknown) => boolean;
Expand Down Expand Up @@ -51,6 +52,11 @@ export function createNonceManager({
return nonceRef.current >= 0;
}

function getNonce(): number {
if (!hasNonce()) throw new Error("call resetNonce before using getNonce");
return nonceRef.current;
}

function nextNonce(): number {
if (!hasNonce()) throw new Error("call resetNonce before using nextNonce");
const nonce = nonceRef.current++;
Expand All @@ -76,6 +82,7 @@ export function createNonceManager({

return {
hasNonce,
getNonce,
nextNonce,
resetNonce,
shouldResetNonce,
Expand Down
17 changes: 17 additions & 0 deletions packages/common/src/getFeeRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getChainId } from "viem/actions";
import { CreateFeeRefOptions, FeeRef, createFeeRef } from "./createFeeRef";

const feeRefs = new Map<number, FeeRef>();

export async function getFeeRef(opts: CreateFeeRefOptions): Promise<FeeRef> {
const chainId = opts.args?.chain?.id ?? opts.client.chain?.id ?? (await getChainId(opts.client));

const existingFeeRef = feeRefs.get(chainId);
if (existingFeeRef) {
return existingFeeRef;
}

const feeRef = await createFeeRef(opts);
feeRefs.set(chainId, feeRef);
return feeRef;
}
83 changes: 60 additions & 23 deletions packages/common/src/writeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ import {
Account,
Chain,
Client,
SimulateContractParameters,
Transport,
WriteContractParameters,
WriteContractReturnType,
ContractFunctionName,
ContractFunctionArgs,
PublicClient,
encodeFunctionData,
EncodeFunctionDataParameters,
} from "viem";
import { simulateContract, writeContract as viem_writeContract } from "viem/actions";
import {
prepareTransactionRequest as viem_prepareTransactionRequest,
writeContract as viem_writeContract,
} from "viem/actions";
import pRetry from "p-retry";
import { debug as parentDebug } from "./debug";
import { getNonceManager } from "./getNonceManager";
import { parseAccount } from "viem/accounts";
import { getFeeRef } from "./getFeeRef";
import { getAction } from "viem/utils";

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

Expand Down Expand Up @@ -55,6 +61,12 @@ export async function writeContract<
throw new Error("No account provided");
}
const account = parseAccount(rawAccount);
const chain = client.chain;

const defaultParameters = {
chain,
...(chain?.fees ? { type: "eip1559" } : {}),
} satisfies Omit<WriteContractParameters, "address" | "abi" | "account" | "functionName">;

const nonceManager = await getNonceManager({
client: opts.publicClient ?? client,
Expand All @@ -63,43 +75,68 @@ export async function writeContract<
queueConcurrency: opts.queueConcurrency,
});

async function prepareWrite(): Promise<
WriteContractParameters<abi, functionName, args, chain, account, chainOverride>
> {
const feeRef = await getFeeRef({
client: opts.publicClient ?? client,
refreshInterval: 10000,
args: { chain },
});

async function prepare(): Promise<WriteContractParameters<abi, functionName, args, chain, account, chainOverride>> {
if (request.gas) {
debug("gas provided, skipping simulate", request.functionName, request.address);
debug("gas provided, skipping preparation", request.functionName, request.address);
return request;
}

debug("simulating", request.functionName, "at", request.address);
const result = await simulateContract<chain, account | undefined, abi, functionName, args, chainOverride>(
opts.publicClient ?? client,
{
...request,
blockTag: "pending",
account,
} as unknown as SimulateContractParameters<abi, functionName, args, chain, chainOverride>,
);
const { abi, address, args, dataSuffix, functionName } = request;
const data = encodeFunctionData({
abi,
args,
functionName,
} as EncodeFunctionDataParameters);

return result.request as unknown as WriteContractParameters<abi, functionName, args, chain, account, chainOverride>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { nonce, maxFeePerGas, maxPriorityFeePerGas, ...preparedTransaction } = await getAction(
client,
viem_prepareTransactionRequest,
"prepareTransactionRequest",
)({
// The fee values don't need to be accurate for gas estimation
// and we can save a couple rpc calls by providing stubs here.
// These are later overridden with accurate values from `feeRef`.
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
// Send the current nonce without increasing the stored value
nonce: nonceManager.getNonce(),
...defaultParameters,
...request,
blockTag: "pending",
account,
// From `viem/writeContract`
data: `${data}${dataSuffix ? dataSuffix.replace("0x", "") : ""}`,
to: address,
} as never);

return preparedTransaction as never;
}

return nonceManager.mempoolQueue.add(
() =>
pRetry(
async () => {
const preparedWrite = await prepareWrite();

if (!nonceManager.hasNonce()) {
await nonceManager.resetNonce();
}

// We estimate gas before increasing the local nonce to prevent nonce gaps.
// Invalid transactions fail the gas estimation step are never submitted
// to the network, so they should not increase the nonce.
const preparedRequest = await prepare();

const nonce = nonceManager.nextNonce();
debug("calling", preparedWrite.functionName, "with nonce", nonce, "at", preparedWrite.address);
return await viem_writeContract(client, {
nonce,
...preparedWrite,
} as typeof preparedWrite);

const fullRequest = { ...preparedRequest, nonce, ...feeRef.fees };
debug("calling", fullRequest.functionName, "with nonce", nonce, "at", fullRequest.address);
return await viem_writeContract(client, fullRequest as never);
},
{
retries: 3,
Expand Down

0 comments on commit 6c8ab47

Please sign in to comment.