Skip to content

Commit 7e16370

Browse files
committed
fix build, add nonce helpers
1 parent 09a7377 commit 7e16370

File tree

8 files changed

+158
-18
lines changed

8 files changed

+158
-18
lines changed

src/db/wallets/walletNonce.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import { thirdwebClient } from "../../utils/sdk";
66
const nonceKey = (chainId: number, walletAddress: Address) =>
77
`nonce:${chainId}:${walletAddress}`;
88

9+
/**
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).
13+
* @param chainId
14+
* @param walletAddress
15+
* @returns number - The next unused nonce value for this wallet.
16+
*/
917
export const incrWalletNonce = async (
1018
chainId: number,
1119
walletAddress: Address,
@@ -15,12 +23,12 @@ export const incrWalletNonce = async (
1523
if (nonce === 1) {
1624
// If INCR returned 1, the nonce was not set.
1725
// This may be a newly imported wallet. Sync and return the onchain nonce.
18-
nonce = await syncWalletNonce(chainId, walletAddress);
26+
nonce = await _syncWalletNonce(chainId, walletAddress);
1927
}
2028
return nonce;
2129
};
2230

23-
export const syncWalletNonce = async (
31+
const _syncWalletNonce = async (
2432
chainId: number,
2533
walletAddress: Address,
2634
): Promise<number> => {
@@ -38,3 +46,30 @@ export const syncWalletNonce = async (
3846
await redis.set(key, transactionCount);
3947
return transactionCount;
4048
};
49+
50+
/**
51+
* Returns the last used nonce.
52+
* This function should be used to inspect nonce values only.
53+
* Use `incrWalletNonce` if using this nonce to send a transaction.
54+
* @param chainId
55+
* @param walletAddress
56+
* @returns number - The last used nonce value for this wallet.
57+
*/
58+
export const getWalletNonce = async (
59+
chainId: number,
60+
walletAddress: Address,
61+
) => {
62+
const key = nonceKey(chainId, walletAddress);
63+
const nonce = await redis.get(key);
64+
return nonce ? parseInt(nonce) : 0;
65+
};
66+
67+
/**
68+
* Delete all wallet nonces. Useful when the get out of sync.
69+
*/
70+
export const deleteWalletNonces = async () => {
71+
const keys = await redis.keys("nonce:*");
72+
if (keys.length > 0) {
73+
await redis.del(keys);
74+
}
75+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { Address } from "thirdweb";
5+
import { getWalletNonce } from "../../../db/wallets/walletNonce";
6+
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
7+
import { walletParamSchema } from "../../schemas/wallet";
8+
import { getChainIdFromChain } from "../../utils/chain";
9+
10+
const requestSchema = walletParamSchema;
11+
12+
const responseSchema = Type.Object({
13+
result: Type.Object({
14+
nonce: Type.Number(),
15+
}),
16+
});
17+
18+
responseSchema.example = {
19+
result: {
20+
nonce: 100,
21+
},
22+
};
23+
24+
export const getBackendWalletNonce = async (fastify: FastifyInstance) => {
25+
fastify.route<{
26+
Params: Static<typeof requestSchema>;
27+
Reply: Static<typeof responseSchema>;
28+
}>({
29+
method: "GET",
30+
url: "/backend-wallet/:chain/:walletAddress/get-nonce",
31+
schema: {
32+
summary: "Get nonce",
33+
description:
34+
"Get the last used nonce for this backend wallet. This value managed by Engine may differ from the onchain value. Use `/backend-wallet/reset-nonces` if this value looks incorrect while idle.",
35+
tags: ["Backend Wallet"],
36+
operationId: "getNonce",
37+
params: requestSchema,
38+
response: {
39+
...standardResponseSchema,
40+
[StatusCodes.OK]: responseSchema,
41+
},
42+
},
43+
handler: async (req, reply) => {
44+
const { chain, walletAddress } = req.params;
45+
const chainId = await getChainIdFromChain(chain);
46+
const nonce = await getWalletNonce(chainId, walletAddress as Address);
47+
48+
reply.status(StatusCodes.OK).send({
49+
result: {
50+
nonce,
51+
},
52+
});
53+
},
54+
});
55+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { deleteWalletNonces } from "../../../db/wallets/walletNonce";
5+
import { standardResponseSchema } from "../../schemas/sharedApiSchemas";
6+
7+
const responseSchema = Type.Object({
8+
result: Type.Object({
9+
status: Type.String(),
10+
}),
11+
});
12+
13+
responseSchema.example = {
14+
result: {
15+
status: "success",
16+
},
17+
};
18+
19+
export const resetBackendWalletNonces = async (fastify: FastifyInstance) => {
20+
fastify.route<{
21+
Reply: Static<typeof responseSchema>;
22+
}>({
23+
method: "POST",
24+
url: "/backend-wallet/reset-nonces",
25+
schema: {
26+
summary: "Reset nonces",
27+
description:
28+
"Reset nonces for all backend wallets. This is for debugging purposes and does not impact held tokens.",
29+
tags: ["Backend Wallet"],
30+
operationId: "resetNonces",
31+
response: {
32+
...standardResponseSchema,
33+
[StatusCodes.OK]: responseSchema,
34+
},
35+
},
36+
handler: async (req, reply) => {
37+
await deleteWalletNonces();
38+
39+
reply.status(StatusCodes.OK).send({
40+
result: {
41+
status: "success",
42+
},
43+
});
44+
},
45+
});
46+
};

src/server/routes/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { extractFunctions } from "./contract/metadata/functions";
4646
import { createBackendWallet } from "./backend-wallet/create";
4747
import { getAll } from "./backend-wallet/getAll";
4848
import { getBalance } from "./backend-wallet/getBalance";
49+
import { getBackendWalletNonce } from "./backend-wallet/getNonce";
4950
import { importBackendWallet } from "./backend-wallet/import";
5051
import { sendTransaction } from "./backend-wallet/sendTransaction";
5152
import { transfer } from "./backend-wallet/transfer";
@@ -107,6 +108,7 @@ import { revokeRelayer } from "./relayer/revoke";
107108

108109
// System
109110
import { getAllTransactions } from "./backend-wallet/getTransactions";
111+
import { resetBackendWalletNonces } from "./backend-wallet/resetNonces";
110112
import { sendTransactionBatch } from "./backend-wallet/sendTransactionBatch";
111113
import { simulateTransaction } from "./backend-wallet/simulateTransaction";
112114
import { withdraw } from "./backend-wallet/withdraw";
@@ -152,6 +154,8 @@ export const withRoutes = async (fastify: FastifyInstance) => {
152154
await fastify.register(signMessage);
153155
await fastify.register(signTypedData);
154156
await fastify.register(getAllTransactions);
157+
await fastify.register(resetBackendWalletNonces);
158+
await fastify.register(getBackendWalletNonce);
155159
await fastify.register(simulateTransaction);
156160

157161
// Configuration

src/server/schemas/wallet/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Type } from "@sinclair/typebox";
2+
import { env } from "../../../utils/env";
23

34
export const walletHeaderSchema = Type.Object({
45
"x-backend-wallet-address": Type.String({
56
description: "Backend wallet address",
67
}),
78
"x-idempotency-key": Type.Optional(
89
Type.String({
9-
description:
10-
"A string that uniquely identifies this transaction. Submitting the same idempotency key will not enqueue a new transaction for 24 hours.",
10+
description: `A string that uniquely identifies this transaction. Submitting the same idempotency key will not enqueue a new transaction for ${env.PRUNE_TRANSACTIONS} day(s).`,
1111
}),
1212
),
1313
});

src/utils/transaction/insertTransaction.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,11 @@ export const insertTransaction = async (
6363
return queueId;
6464
};
6565

66+
// This logic is more accurate in the worker, but withdraws are generally not used programmatically during high volume.
6667
const getWithdrawValue = async (
6768
queuedTransaction: QueuedTransaction,
6869
): Promise<bigint> => {
69-
const { chainId, from, to } = queuedTransaction;
70-
if (!to) {
71-
throw new Error('Missing "to".');
72-
}
70+
const { chainId, from } = queuedTransaction;
7371

7472
const chain = await getChain(chainId);
7573

@@ -82,15 +80,15 @@ const getWithdrawValue = async (
8280

8381
// Estimate gas for a transfer.
8482
const transaction = prepareTransaction({
85-
value: BigInt(1),
86-
to,
8783
chain,
8884
client: thirdwebClient,
85+
value: 1n, // dummy value
86+
to: from, // dummy value
8987
});
9088
const { wei: transferCostWei } = await estimateGasCost({ transaction });
9189

92-
// Add a 20% buffer for gas variance.
93-
const buffer = BigInt(Math.round(Number(transferCostWei) * 0.2));
90+
// Add a +50% buffer for gas variance.
91+
const buffer = transferCostWei / 2n;
9492

9593
return balanceWei - transferCostWei - buffer;
9694
};

src/worker/tasks/mineTransactionWorker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,9 @@ const _handleTransaction = async (
160160
}
161161

162162
// If the transaction is not mined yet, throw to retry later.
163-
job.log("Transaction is not mined yet. Check again later...");
163+
job.log(
164+
`Transaction is not mined yet (${sentTransactionHashes}). Check again later...`,
165+
);
164166
throw new Error("NOT_CONFIRMED_YET");
165167
};
166168

src/worker/tasks/purgeTransactionsWorker.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { Job, Processor, Worker } from "bullmq";
22
import { TransactionDB } from "../../db/transactions/db";
3+
import { env } from "../../utils/env";
34
import { redis } from "../../utils/redis/redis";
45
import { PURGE_TRANSACTIONS_QUEUE_NAME } from "../queues/purgeTransactionsQueue";
56
import { logWorkerEvents } from "../queues/queues";
67

78
const handler: Processor<any, void, string> = async (job: Job<string>) => {
8-
const purgeBeforeDate =
9-
Date.now() - TransactionDB.COMPLETED_TRANSACTIONS_MAX_AGE_SECONDS * 1000;
9+
// Purge transactions up to `PRUNE_TRANSACTIONS` days ago.
10+
const to = new Date();
11+
to.setDate(to.getDate() - env.PRUNE_TRANSACTIONS);
1012

11-
await TransactionDB.purgeTransactions({
12-
to: new Date(purgeBeforeDate),
13-
});
13+
await TransactionDB.purgeTransactions({ to });
1414
};
1515

1616
// Worker

0 commit comments

Comments
 (0)