Skip to content

Commit 2b560d5

Browse files
committed
unused nonces worker
1 parent 7e16370 commit 2b560d5

35 files changed

+318
-200
lines changed

.eslintrc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
"@typescript-eslint/no-unused-vars": [
1414
"error",
1515
{ "argsIgnorePattern": "^_" }
16-
]
16+
],
17+
"require-await": "off",
18+
"@typescript-eslint/require-await": "error"
1719
}
1820
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@types/base-64": "^1.0.2",
4848
"base-64": "^1.0.0",
4949
"body-parser": "^1.20.2",
50-
"bullmq": "^5.7.8",
50+
"bullmq": "^5.10.3",
5151
"cookie": "^0.5.0",
5252
"cookie-parser": "^1.4.6",
5353
"copyfiles": "^2.4.1",

src/db/transactions/db.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class TransactionDB {
182182
* @param to Date?
183183
* @returns string[] List of queueIds
184184
*/
185-
static purgeTransactions = async (args: { from?: Date; to?: Date }) => {
185+
static pruneTransactions = async (args: { from?: Date; to?: Date }) => {
186186
const { from, to } = args;
187187
const min = from ? toSeconds(from) : 0;
188188
const max = to ? toSeconds(to) : "+inf";

src/db/transactions/queueTx.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DeployTransaction, Transaction } from "@thirdweb-dev/sdk";
22
import { ERC4337EthersSigner } from "@thirdweb-dev/wallets/dist/declarations/src/evm/connectors/smart-wallet/lib/erc4337-signer";
33
import { Address } from "thirdweb";
44
import type { ContractExtension } from "../../schema/extension";
5+
import { maybeBigInt } from "../../utils/primitiveTypes";
56
import { insertTransaction } from "../../utils/transaction/insertTransaction";
67

78
interface QueueTxParams {
@@ -65,13 +66,9 @@ export const queueTx = async ({
6566
signerAddress,
6667
accountAddress,
6768
target,
68-
gas: txOverrides?.gas ? BigInt(txOverrides.gas) : undefined,
69-
maxFeePerGas: txOverrides?.maxFeePerGas
70-
? BigInt(txOverrides?.maxFeePerGas)
71-
: undefined,
72-
maxPriorityFeePerGas: txOverrides?.maxPriorityFeePerGas
73-
? BigInt(txOverrides?.maxPriorityFeePerGas)
74-
: undefined,
69+
gas: maybeBigInt(txOverrides?.gas),
70+
maxFeePerGas: maybeBigInt(txOverrides?.maxFeePerGas),
71+
maxPriorityFeePerGas: maybeBigInt(txOverrides?.maxPriorityFeePerGas),
7572
from: signerAddress,
7673
},
7774
idempotencyKey,
@@ -96,13 +93,9 @@ export const queueTx = async ({
9693
deployedContractType,
9794
from: fromAddress.toLowerCase() as Address,
9895
to: toAddress?.toLowerCase() as Address,
99-
gas: txOverrides?.gas ? BigInt(txOverrides.gas) : undefined,
100-
maxFeePerGas: txOverrides?.maxFeePerGas
101-
? BigInt(txOverrides?.maxFeePerGas)
102-
: undefined,
103-
maxPriorityFeePerGas: txOverrides?.maxPriorityFeePerGas
104-
? BigInt(txOverrides?.maxPriorityFeePerGas)
105-
: undefined,
96+
gas: maybeBigInt(txOverrides?.gas),
97+
maxFeePerGas: maybeBigInt(txOverrides?.maxFeePerGas),
98+
maxPriorityFeePerGas: maybeBigInt(txOverrides?.maxPriorityFeePerGas),
10699
},
107100
idempotencyKey,
108101
shouldSimulate: simulateTx,

src/db/wallets/walletNonce.ts

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,83 @@ import { getChain } from "../../utils/chain";
33
import { redis } from "../../utils/redis/redis";
44
import { thirdwebClient } from "../../utils/sdk";
55

6-
const nonceKey = (chainId: number, walletAddress: Address) =>
6+
/**
7+
* !IMPORTANT
8+
*
9+
* The nonce stored is the LAST USED NONCE.
10+
* To get the next available nonce, use `incrWalletNonce()`.
11+
*/
12+
13+
const lastUsedNonceKey = (chainId: number, walletAddress: Address) =>
714
`nonce:${chainId}:${walletAddress}`;
815

16+
const unusedNoncesKey = (chainId: number, walletAddress: Address) =>
17+
`nonce-unused:${chainId}:${walletAddress}`;
18+
919
/**
10-
* Increment to get the next unused nonce.
11-
* If a nonce is acquired this way, it must be used for an onchain transaction
12-
* (or cancelled onchain).
20+
* Acquire an unused nonce.
21+
* This should be used to send an EOA transaction with this nonce.
1322
* @param chainId
1423
* @param walletAddress
1524
* @returns number - The next unused nonce value for this wallet.
1625
*/
17-
export const incrWalletNonce = async (
26+
export const getAndUpdateNonce = async (
1827
chainId: number,
1928
walletAddress: Address,
2029
) => {
21-
const key = nonceKey(chainId, walletAddress);
22-
let nonce = await redis.incr(key);
30+
// Acquire an unused nonce, if any.
31+
let nonce = await getAndUpdateLowestUnusedNonce(chainId, walletAddress);
32+
if (nonce) {
33+
return nonce;
34+
}
35+
36+
// Else increment the last used nonce.
37+
const key = lastUsedNonceKey(chainId, walletAddress);
38+
nonce = await redis.incr(key);
2339
if (nonce === 1) {
2440
// If INCR returned 1, the nonce was not set.
2541
// This may be a newly imported wallet. Sync and return the onchain nonce.
26-
nonce = await _syncWalletNonce(chainId, walletAddress);
42+
return await _syncNonce(chainId, walletAddress);
2743
}
2844
return nonce;
2945
};
3046

31-
const _syncWalletNonce = async (
47+
/**
48+
* Adds an unused nonce that can be reused by a future transaction.
49+
* This should be used if the current transaction that acquired this nonce is not valid.
50+
* @param chainId
51+
* @param walletAddress
52+
* @param nonce
53+
*/
54+
export const addUnusedNonce = async (
55+
chainId: number,
56+
walletAddress: Address,
57+
nonce: number,
58+
) => {
59+
const key = unusedNoncesKey(chainId, walletAddress);
60+
await redis.zadd(key, nonce, nonce);
61+
};
62+
63+
/**
64+
* Acquires the lowest unused nonce.
65+
* @param chainId
66+
* @param walletAddress
67+
* @returns
68+
*/
69+
export const getAndUpdateLowestUnusedNonce = async (
70+
chainId: number,
71+
walletAddress: Address,
72+
) => {
73+
const key = unusedNoncesKey(chainId, walletAddress);
74+
const res = await redis.zpopmin(key);
75+
if (res.length > 0) {
76+
// res will be [value, score] where the score is the nonce.
77+
return parseInt(res[1]);
78+
}
79+
return null;
80+
};
81+
82+
const _syncNonce = async (
3283
chainId: number,
3384
walletAddress: Address,
3485
): Promise<number> => {
@@ -42,7 +93,7 @@ const _syncWalletNonce = async (
4293
address: walletAddress,
4394
});
4495

45-
const key = nonceKey(chainId, walletAddress);
96+
const key = lastUsedNonceKey(chainId, walletAddress);
4697
await redis.set(key, transactionCount);
4798
return transactionCount;
4899
};
@@ -55,19 +106,16 @@ const _syncWalletNonce = async (
55106
* @param walletAddress
56107
* @returns number - The last used nonce value for this wallet.
57108
*/
58-
export const getWalletNonce = async (
59-
chainId: number,
60-
walletAddress: Address,
61-
) => {
62-
const key = nonceKey(chainId, walletAddress);
109+
export const getNonce = async (chainId: number, walletAddress: Address) => {
110+
const key = lastUsedNonceKey(chainId, walletAddress);
63111
const nonce = await redis.get(key);
64112
return nonce ? parseInt(nonce) : 0;
65113
};
66114

67115
/**
68116
* Delete all wallet nonces. Useful when the get out of sync.
69117
*/
70-
export const deleteWalletNonces = async () => {
118+
export const deleteAllNonces = async () => {
71119
const keys = await redis.keys("nonce:*");
72120
if (keys.length > 0) {
73121
await redis.del(keys);

src/server/routes/backend-wallet/getNonce.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Static, Type } from "@sinclair/typebox";
22
import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { Address } from "thirdweb";
5-
import { getWalletNonce } from "../../../db/wallets/walletNonce";
5+
import { getNonce } from "../../../db/wallets/walletNonce";
66
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
77
import { walletParamSchema } from "../../schemas/wallet";
88
import { getChainIdFromChain } from "../../utils/chain";
@@ -43,7 +43,7 @@ export const getBackendWalletNonce = async (fastify: FastifyInstance) => {
4343
handler: async (req, reply) => {
4444
const { chain, walletAddress } = req.params;
4545
const chainId = await getChainIdFromChain(chain);
46-
const nonce = await getWalletNonce(chainId, walletAddress as Address);
46+
const nonce = await getNonce(chainId, walletAddress as Address);
4747

4848
reply.status(StatusCodes.OK).send({
4949
result: {

src/server/routes/backend-wallet/resetNonces.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Static, Type } from "@sinclair/typebox";
22
import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { deleteWalletNonces } from "../../../db/wallets/walletNonce";
4+
import { deleteAllNonces } from "../../../db/wallets/walletNonce";
55
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
66

77
const responseSchema = Type.Object({
@@ -34,7 +34,7 @@ export const resetBackendWalletNonces = async (fastify: FastifyInstance) => {
3434
},
3535
},
3636
handler: async (req, reply) => {
37-
await deleteWalletNonces();
37+
await deleteAllNonces();
3838

3939
reply.status(StatusCodes.OK).send({
4040
result: {

src/server/routes/backend-wallet/sendTransaction.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Static, Type } from "@sinclair/typebox";
22
import { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { Address, Hex } from "thirdweb";
5+
import { maybeBigInt } from "../../../utils/primitiveTypes";
56
import { insertTransaction } from "../../../utils/transaction/insertTransaction";
67
import {
78
requestQuerystringSchema,
@@ -92,13 +93,11 @@ export async function sendTransaction(fastify: FastifyInstance) {
9293
data: data as Hex,
9394
value: BigInt(value),
9495

95-
gas: txOverrides?.gas ? BigInt(txOverrides.gas) : undefined,
96-
maxFeePerGas: txOverrides?.maxFeePerGas
97-
? BigInt(txOverrides.maxFeePerGas)
98-
: undefined,
99-
maxPriorityFeePerGas: txOverrides?.maxPriorityFeePerGas
100-
? BigInt(txOverrides.maxPriorityFeePerGas)
101-
: undefined,
96+
gas: maybeBigInt(txOverrides?.gas),
97+
maxFeePerGas: maybeBigInt(txOverrides?.maxFeePerGas),
98+
maxPriorityFeePerGas: maybeBigInt(
99+
txOverrides?.maxPriorityFeePerGas,
100+
),
102101
},
103102
shouldSimulate: simulateTx,
104103
idempotencyKey,

src/server/routes/backend-wallet/simulateTransaction.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { randomUUID } from "crypto";
33
import { FastifyInstance } from "fastify";
44
import { StatusCodes } from "http-status-codes";
55
import { Address, Hex } from "thirdweb";
6-
import { simulateQueuedTransaction } from "../../../utils/transaction/simulateTransaction";
6+
import { simulateQueuedTransaction } from "../../../utils/transaction/simulateQueuedTransaction";
77
import { QueuedTransaction } from "../../../utils/transaction/types";
88
import { createCustomError } from "../../middleware/error";
99
import {
@@ -15,7 +15,10 @@ import { getChainIdFromChain } from "../../utils/chain";
1515

1616
// INPUT
1717
const ParamsSchema = Type.Object({
18-
chain: Type.String(),
18+
chain: Type.String({
19+
examples: ["80002"],
20+
description: "Chain ID",
21+
}),
1922
});
2023

2124
const simulateRequestBodySchema = Type.Object({
@@ -25,7 +28,7 @@ const simulateRequestBodySchema = Type.Object({
2528
value: Type.Optional(
2629
Type.String({
2730
examples: ["0"],
28-
description: "The amount of native currency",
31+
description: "The amount of native currency in wei",
2932
}),
3033
),
3134
// Decoded transaction args

src/server/routes/backend-wallet/transfer.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
toWei,
1111
} from "thirdweb";
1212
import { transfer as transferERC20 } from "thirdweb/extensions/erc20";
13-
import { isContractDeployed } from "thirdweb/utils";
13+
import { isContractDeployed, resolvePromisedValue } from "thirdweb/utils";
1414
import { getChain } from "../../../utils/chain";
15-
import { resolvePromisedValue } from "../../../utils/resolvePromisedValue";
15+
import { maybeBigInt } from "../../../utils/primitiveTypes";
1616
import { thirdwebClient } from "../../../utils/sdk";
1717
import { insertTransaction } from "../../../utils/transaction/insertTransaction";
1818
import { InsertedTransaction } from "../../../utils/transaction/types";
@@ -84,13 +84,9 @@ export async function transfer(fastify: FastifyInstance) {
8484
const currencyAddress = _currencyAddress.toLowerCase();
8585
const chainId = await getChainIdFromChain(chain);
8686
const gasOverrides = {
87-
gas: txOverrides?.gas ? BigInt(txOverrides.gas) : undefined,
88-
maxFeePerGas: txOverrides?.maxFeePerGas
89-
? BigInt(txOverrides.maxFeePerGas)
90-
: undefined,
91-
maxPriorityFeePerGas: txOverrides?.maxPriorityFeePerGas
92-
? BigInt(txOverrides.maxPriorityFeePerGas)
93-
: undefined,
87+
gas: maybeBigInt(txOverrides?.gas),
88+
maxFeePerGas: maybeBigInt(txOverrides?.maxFeePerGas),
89+
maxPriorityFeePerGas: maybeBigInt(txOverrides?.maxPriorityFeePerGas),
9490
};
9591

9692
let insertedTransaction: InsertedTransaction;

0 commit comments

Comments
 (0)