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

fix(store-sync): reduce latency in waitForTransaction #2665

Merged
merged 28 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bc4be63
feat(common): send fetch requests in parallel
alvrs Apr 11, 2024
527eecc
feat(common): do not dynamically fetch fixed tx params
alvrs Apr 11, 2024
3e9ef7f
fix: bring back separate gas estimation
alvrs Apr 11, 2024
814ae1e
feat(common): refresh fees in intervals instead of right before every…
alvrs Apr 11, 2024
58a0464
feat(common): don't care about max fee during gas estimation
alvrs Apr 11, 2024
7b5d7ef
remove stubbed values from result
alvrs Apr 11, 2024
858f163
fix: gas estimation
alvrs Apr 12, 2024
2ecc4d9
update viem
alvrs Apr 12, 2024
260d791
self-review
alvrs Apr 12, 2024
702eccb
Merge branch 'main' into latency
alvrs Apr 12, 2024
f3daaf9
run all-install
alvrs Apr 12, 2024
a592fa5
don't throw
alvrs Apr 12, 2024
b31383f
bump viem to 2.9.16
holic Apr 12, 2024
cec07e8
fix nonce handling
alvrs Apr 12, 2024
d62171a
use getAction
alvrs Apr 12, 2024
1e8acce
bring back block tag
alvrs Apr 12, 2024
b366ebf
cast return type for now
holic Apr 12, 2024
24610a1
Merge branch 'holic/viem-2.9' into latency
alvrs Apr 12, 2024
7e6635e
update log
alvrs Apr 12, 2024
fc7824c
Merge branch 'main' into latency
alvrs Apr 12, 2024
bbc1cee
Create fresh-nails-draw.md
alvrs Apr 12, 2024
c61f8dd
review feedback
alvrs Apr 15, 2024
263c679
Merge branch 'main' into latency
alvrs Apr 15, 2024
99d7a29
pull out store-sync changes
alvrs Apr 15, 2024
af3eadc
gasfee chain dependent
alvrs Apr 16, 2024
8c2bfd0
reduce latency in waitForTransaction
alvrs Apr 15, 2024
9c1d52f
Create twelve-hairs-fry.md
alvrs Apr 15, 2024
615664a
Merge branch 'main' into latency-2
alvrs Apr 16, 2024
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/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.
5 changes: 5 additions & 0 deletions .changeset/twelve-hairs-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/store-sync": patch
---

Small optimizations in `waitForTransaction` to parallelize network requests.
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
11 changes: 9 additions & 2 deletions packages/store-sync/src/createStoreSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
combineLatest,
scan,
identity,
mergeMap,
} from "rxjs";
import { debug as parentDebug } from "./debug";
import { SyncStep } from "./SyncStep";
Expand Down Expand Up @@ -197,7 +198,9 @@ export async function createStoreSync<config extends StoreConfig = StoreConfig>(
startBlock = range.startBlock;
endBlock = range.endBlock;
}),
concatMap((range) => {
// We use `map` instead of `concatMap` here to send the fetch request immediately when a new block range appears,
// instead of sending the next request only when the previous one completed.
map((range) => {
const storedBlocks = fetchAndStoreLogs({
publicClient,
address,
Expand All @@ -213,6 +216,8 @@ export async function createStoreSync<config extends StoreConfig = StoreConfig>(

return from(storedBlocks);
}),
// `concatMap` turns the stream of promises into their awaited values
concatMap(identity),
tap(({ blockNumber, logs }) => {
debug("stored", logs.length, "logs for block", blockNumber);
lastBlockNumberProcessed = blockNumber;
Expand Down Expand Up @@ -263,7 +268,9 @@ export async function createStoreSync<config extends StoreConfig = StoreConfig>(
// This currently blocks for async call on each block processed
// We could potentially speed this up a tiny bit by racing to see if 1) tx exists in processed block or 2) fetch tx receipt for latest block processed
const hasTransaction$ = recentBlocks$.pipe(
concatMap(async (blocks) => {
// We use `mergeMap` instead of `concatMap` here to send the fetch request immediately when a new block range appears,
// instead of sending the next request only when the previous one completed.
mergeMap(async (blocks) => {
const txs = blocks.flatMap((block) => block.logs.map((op) => op.transactionHash).filter(isDefined));
if (txs.includes(tx)) return true;

Expand Down
Loading