Skip to content

Commit ce81074

Browse files
committed
reuse rpc request
1 parent de05929 commit ce81074

File tree

11 files changed

+103
-119
lines changed

11 files changed

+103
-119
lines changed

src/db/wallets/walletNonce.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Address, eth_getTransactionCount, getRpcClient } from "thirdweb";
1+
import { Address, eth_getTransactionCount } from "thirdweb";
22
import { getChain } from "../../utils/chain";
33
import { redis } from "../../utils/redis/redis";
4-
import { thirdwebClient } from "../../utils/sdk";
4+
import { getRpcRequest } from "../../utils/sdk";
55

66
// Data type: String
77
const lastUsedNonceKey = (chainId: number, walletAddress: Address) =>
@@ -68,10 +68,7 @@ const _syncNonce = async (
6868
chainId: number,
6969
walletAddress: Address,
7070
): Promise<number> => {
71-
const rpcRequest = getRpcClient({
72-
client: thirdwebClient,
73-
chain: await getChain(chainId),
74-
});
71+
const rpcRequest = getRpcRequest(await getChain(chainId));
7572

7673
// The next unused nonce = transactionCount.
7774
const transactionCount = await eth_getTransactionCount(rpcRequest, {

src/server/middleware/error.ts

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FastifyInstance } from "fastify";
22
import { ReasonPhrases, StatusCodes } from "http-status-codes";
33
import { ZodError } from "zod";
44
import { env } from "../../utils/env";
5+
import { parseEthersError } from "../../utils/ethers";
56
import { logger } from "../../utils/logger";
67

78
export type CustomError = {
@@ -21,41 +22,6 @@ export const createCustomError = (
2122
code,
2223
});
2324

24-
// https://github.com/ethers-io/ethers.js/blob/main/src.ts/utils/errors.ts
25-
const ETHERS_ERROR_CODES = new Set([
26-
// Generic Errors
27-
"UNKNOWN_ERROR",
28-
"NOT_IMPLEMENTED",
29-
"UNSUPPORTED_OPERATION",
30-
"NETWORK_ERROR",
31-
"SERVER_ERROR",
32-
"TIMEOUT",
33-
"BAD_DATA",
34-
"CANCELLED",
35-
36-
// Operational Errors
37-
"BUFFER_OVERRUN",
38-
"NUMERIC_FAULT",
39-
40-
// Argument Errors
41-
"INVALID_ARGUMENT",
42-
"MISSING_ARGUMENT",
43-
"UNEXPECTED_ARGUMENT",
44-
"VALUE_MISMATCH",
45-
46-
// Blockchain Errors
47-
"CALL_EXCEPTION",
48-
"INSUFFICIENT_FUNDS",
49-
"NONCE_EXPIRED",
50-
"REPLACEMENT_UNDERPRICED",
51-
"TRANSACTION_REPLACED",
52-
"UNCONFIGURED_NAME",
53-
"OFFCHAIN_FAULT",
54-
55-
// User Interaction
56-
"ACTION_REJECTED",
57-
]);
58-
5925
export const createCustomDateTimestampError = (key: string): CustomError => {
6026
return createCustomError(
6127
`Invalid ${key} Value. Needs to new Date() / new Date().toISOstring() / new Date().getTime() / Unix Epoch`,
@@ -73,15 +39,6 @@ const isZodError = (err: unknown): boolean => {
7339
);
7440
};
7541

76-
const isEthersError = (error: any): boolean => {
77-
return (
78-
error &&
79-
typeof error === "object" &&
80-
"code" in error &&
81-
ETHERS_ERROR_CODES.has(error.code)
82-
);
83-
};
84-
8542
export const withErrorHandler = async (server: FastifyInstance) => {
8643
server.setErrorHandler(
8744
(error: Error | CustomError | ZodError, request, reply) => {
@@ -93,7 +50,7 @@ export const withErrorHandler = async (server: FastifyInstance) => {
9350
});
9451

9552
// Ethers Error Codes
96-
if (isEthersError(error)) {
53+
if (parseEthersError(error)) {
9754
return reply.status(StatusCodes.BAD_REQUEST).send({
9855
error: {
9956
code: "BAD_REQUEST",

src/server/routes/transaction/blockchain/sendSignedTx.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Static, Type } from "@sinclair/typebox";
22
import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { eth_sendRawTransaction, getRpcClient, isHex } from "thirdweb";
4+
import { eth_sendRawTransaction, isHex } from "thirdweb";
55
import { getChain } from "../../../../utils/chain";
6-
import { thirdwebClient } from "../../../../utils/sdk";
6+
import { getRpcRequest } from "../../../../utils/sdk";
77
import { createCustomError } from "../../../middleware/error";
88
import { standardResponseSchema } from "../../../schemas/sharedApiSchemas";
99
import { walletChainParamSchema } from "../../../schemas/wallet";
@@ -52,12 +52,9 @@ export async function sendSignedTransaction(fastify: FastifyInstance) {
5252
);
5353
}
5454

55-
const rpc = getRpcClient({
56-
chain: await getChain(chainId),
57-
client: thirdwebClient,
58-
});
55+
const rpcRequest = getRpcRequest(await getChain(chainId));
5956
const transactionHash = await eth_sendRawTransaction(
60-
rpc,
57+
rpcRequest,
6158
signedTransaction,
6259
);
6360

src/utils/block.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { eth_blockNumber, getRpcClient } from "thirdweb";
1+
import { eth_blockNumber } from "thirdweb";
22
import { getChain } from "./chain";
33
import { redis } from "./redis/redis";
4-
import { thirdwebClient } from "./sdk";
4+
import { getRpcRequest } from "./sdk";
55

66
/**
77
* Returns the latest block number. Falls back to the last known block number.
@@ -11,16 +11,12 @@ import { thirdwebClient } from "./sdk";
1111
* @returns bigint - The latest block number.
1212
*/
1313
export const getBlockNumberish = async (chainId: number): Promise<bigint> => {
14-
const chain = await getChain(chainId);
15-
const rpcRequest = getRpcClient({
16-
client: thirdwebClient,
17-
chain,
18-
});
14+
const rpcRequest = getRpcRequest(await getChain(chainId));
1915

2016
const key = `latestBlock:${chainId}`;
2117
try {
2218
const blockNumber = await eth_blockNumber(rpcRequest);
23-
// Non-blocking cache.
19+
// Non-blocking update to cache.
2420
redis.set(key, blockNumber.toString()).catch((e) => {});
2521
return blockNumber;
2622
} catch (e) {

src/utils/error.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ethers } from "ethers";
22
import { getChainMetadata } from "thirdweb/chains";
33
import { getChain } from "./chain";
4-
import { EthersError } from "./ethers";
4+
import { isEthersErrorCode } from "./ethers";
55
import { simulateQueuedTransaction } from "./transaction/simulateQueuedTransaction";
66
import { AnyTransaction } from "./transaction/types";
77

@@ -10,19 +10,13 @@ export const prettifyError = async (
1010
error: Error,
1111
): Promise<string> => {
1212
if (!transaction.isUserOp) {
13-
if (
14-
(error as unknown as EthersError)?.code ===
15-
ethers.errors.INSUFFICIENT_FUNDS
16-
) {
13+
if (isEthersErrorCode(error, ethers.errors.INSUFFICIENT_FUNDS)) {
1714
const chain = await getChain(transaction.chainId);
1815
const metadata = await getChainMetadata(chain);
1916
return `Insufficient ${metadata.nativeCurrency?.symbol} on ${metadata.name} in ${transaction.from}.`;
2017
}
2118

22-
if (
23-
(error as unknown as EthersError)?.code ===
24-
ethers.errors.UNPREDICTABLE_GAS_LIMIT
25-
) {
19+
if (isEthersErrorCode(error, ethers.errors.UNPREDICTABLE_GAS_LIMIT)) {
2620
const simulateError = await simulateQueuedTransaction(transaction);
2721
if (simulateError) {
2822
return simulateError;

src/utils/ethers.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,51 @@
1-
export interface EthersError {
2-
reason: string;
3-
code: string;
4-
error: any;
5-
method: string;
6-
transaction: any;
1+
// import { EthersError } from "@ethersproject/logger";
2+
import { ethers } from "ethers";
3+
4+
// Copied from: https://github.com/ethers-io/ethers.js/blob/main/src.ts/utils/errors.ts#L156
5+
// EthersError in ethers isn't exported.
6+
export interface EthersError extends Error {
7+
/**
8+
* The string error code.
9+
*/
10+
code: ethers.errors;
11+
12+
/**
13+
* A short message describing the error, with minimal additional
14+
* details.
15+
*/
16+
shortMessage: string;
17+
18+
/**
19+
* Additional info regarding the error that may be useful.
20+
*
21+
* This is generally helpful mostly for human-based debugging.
22+
*/
23+
info?: Record<string, any>;
24+
25+
/**
26+
* Any related error.
27+
*/
28+
error?: Error;
729
}
30+
31+
export const ETHERS_ERROR_CODES = new Set(Object.values(ethers.errors));
32+
33+
/**
34+
* Returns an EthersError, or null if the error is not an ethers error.
35+
* @param error
36+
* @returns EthersError | null
37+
*/
38+
export const parseEthersError = (error: any): EthersError | null => {
39+
if (
40+
error &&
41+
typeof error === "object" &&
42+
"code" in error &&
43+
ETHERS_ERROR_CODES.has(error.code)
44+
) {
45+
return error as EthersError;
46+
}
47+
return null;
48+
};
49+
50+
export const isEthersErrorCode = (error: any, code: ethers.errors) =>
51+
parseEthersError(error)?.code === code;

src/utils/sdk.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { sha256HexSync } from "@thirdweb-dev/crypto";
2-
import { createThirdwebClient } from "thirdweb";
2+
import { Chain, createThirdwebClient, getRpcClient } from "thirdweb";
33
import { env } from "./env";
44

55
export const thirdwebClientId = sha256HexSync(
@@ -10,6 +10,21 @@ export const thirdwebClient = createThirdwebClient({
1010
secretKey: env.THIRDWEB_API_SECRET_KEY,
1111
});
1212

13+
/**
14+
* Share rpcRequest objects to reuse cached data.
15+
*/
16+
const _rpcClientByChain: Record<number, ReturnType<typeof getRpcClient>> = {};
17+
18+
export const getRpcRequest = (chain: Chain) => {
19+
if (!(chain.id in _rpcClientByChain)) {
20+
_rpcClientByChain[chain.id] = getRpcClient({
21+
client: thirdwebClient,
22+
chain,
23+
});
24+
}
25+
return _rpcClientByChain[chain.id];
26+
};
27+
1328
/**
1429
* Helper functions to handle v4 -> v5 SDK migration.
1530
*/

src/worker/tasks/cancelUnusedNoncesWorker.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Job, Processor, Worker } from "bullmq";
2+
import { ethers } from "ethers";
23
import { Address } from "thirdweb";
34
import { releaseNonce } from "../../db/wallets/walletNonce";
5+
import { isEthersErrorCode } from "../../utils/ethers";
46
import { logger } from "../../utils/logger";
57
import { redis } from "../../utils/redis/redis";
68
import { sendCancellationTransaction } from "../../utils/transaction/cancelTransaction";
@@ -13,15 +15,19 @@ import { logWorkerExceptions } from "../queues/queues";
1315
* - Cancel the transaction.
1416
* - If failed, add it back to the unused nonces list.
1517
*/
16-
const handler: Processor<any, void, string> = async (_: Job<string>) => {
18+
const handler: Processor<any, void, string> = async (job: Job<string>) => {
1719
const keys = await redis.keys("nonce-unused:*");
20+
1821
for (const key of keys) {
1922
const { chainId, walletAddress } = fromUnusedNoncesKey(key);
2023

2124
const unusedNonces = await getAndDeleteUnusedNonces(key);
25+
job.log(`Found unused nonces: key=${key} nonces=${unusedNonces}`);
26+
2227
if (unusedNonces.length > 0) {
2328
const success: number[] = [];
2429
const fail: number[] = [];
30+
const ignore: number[] = [];
2531
for (const nonce of unusedNonces) {
2632
try {
2733
await sendCancellationTransaction({
@@ -33,7 +39,7 @@ const handler: Processor<any, void, string> = async (_: Job<string>) => {
3339
} catch (e: unknown) {
3440
// If the error suggests the nonce is already used, do not release the nonce.
3541
if (isNonceUsedOnchain(e)) {
36-
success.push(nonce);
42+
ignore.push(nonce);
3743
continue;
3844
}
3945

@@ -43,11 +49,9 @@ const handler: Processor<any, void, string> = async (_: Job<string>) => {
4349
}
4450
}
4551

46-
logger({
47-
level: "info",
48-
message: `Cancelling nonces for ${key}. success=${success} fail=${fail}`,
49-
service: "worker",
50-
});
52+
const message = `Cancelling nonces for ${key}. success=${success} fail=${fail} ignored=${ignore}`;
53+
job.log(message);
54+
logger({ level: "info", message, service: "worker" });
5155
}
5256
}
5357
};
@@ -75,13 +79,11 @@ const getAndDeleteUnusedNonces = async (key: string) => {
7579
/**
7680
* Returns true if the error suggests the nonce is used onchain.
7781
* @param error
78-
* @returns
82+
* @returns true if the nonce is already used.
7983
*/
8084
const isNonceUsedOnchain = (error: unknown) => {
81-
if (error instanceof Error) {
82-
if (error.message.toLowerCase().includes("nonce too low")) {
83-
return true;
84-
}
85+
if (isEthersErrorCode(error, ethers.errors.NONCE_EXPIRED)) {
86+
return true;
8587
}
8688
return false;
8789
};

src/worker/tasks/mineTransactionWorker.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
Address,
55
eth_getTransactionByHash,
66
eth_getTransactionReceipt,
7-
getRpcClient,
87
} from "thirdweb";
98
import { stringify } from "thirdweb/utils";
109
import { getUserOpReceiptRaw } from "thirdweb/wallets/smart";
@@ -17,7 +16,7 @@ import { msSince } from "../../utils/date";
1716
import { env } from "../../utils/env";
1817
import { logger } from "../../utils/logger";
1918
import { redis } from "../../utils/redis/redis";
20-
import { thirdwebClient } from "../../utils/sdk";
19+
import { getRpcRequest, thirdwebClient } from "../../utils/sdk";
2120
import {
2221
ErroredTransaction,
2322
MinedTransaction,
@@ -103,13 +102,8 @@ const _handleTransaction = async (
103102
): Promise<MinedTransaction | null> => {
104103
const { queueId, chainId, sentTransactionHashes } = sentTransaction;
105104

106-
const chain = await getChain(chainId);
107-
const rpcRequest = getRpcClient({
108-
client: thirdwebClient,
109-
chain,
110-
});
111-
112105
// Check all sent transaction hashes since any retry could succeed.
106+
const rpcRequest = getRpcRequest(await getChain(chainId));
113107
const receiptResults = await Promise.allSettled(
114108
sentTransactionHashes.map((hash) =>
115109
eth_getTransactionReceipt(rpcRequest, { hash }),
@@ -172,17 +166,14 @@ const _handleUserOp = async (
172166
): Promise<MinedTransaction> => {
173167
const { chainId, userOpHash } = sentTransaction;
174168
const chain = await getChain(chainId);
175-
const rpcRequest = getRpcClient({
176-
client: thirdwebClient,
177-
chain,
178-
});
179169

180170
const userOpReceiptRaw = await getUserOpReceiptRaw({
181171
client: thirdwebClient,
182172
chain,
183173
userOpHash,
184174
});
185175
if (userOpReceiptRaw) {
176+
const rpcRequest = getRpcRequest(chain);
186177
const transaction = await eth_getTransactionByHash(rpcRequest, {
187178
hash: userOpReceiptRaw.receipt.transactionHash,
188179
});

0 commit comments

Comments
 (0)