Skip to content

Commit 652206c

Browse files
committed
feat: cancel nonces endpoint
1 parent b0fd43e commit 652206c

File tree

4 files changed

+121
-2
lines changed

4 files changed

+121
-2
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { eth_getTransactionCount, getRpcClient } from "thirdweb";
5+
import { checksumAddress } from "thirdweb/utils";
6+
import { getChain } from "../../../utils/chain";
7+
import { thirdwebClient } from "../../../utils/sdk";
8+
import { sendCancellationTransaction } from "../../../utils/transaction/cancelTransaction";
9+
import { createCustomError } from "../../middleware/error";
10+
import {
11+
requestQuerystringSchema,
12+
standardResponseSchema,
13+
} from "../../schemas/sharedApiSchemas";
14+
import {
15+
walletHeaderSchema,
16+
walletWithAddressParamSchema,
17+
} from "../../schemas/wallet";
18+
import { getChainIdFromChain } from "../../utils/chain";
19+
20+
const requestSchema = Type.Omit(walletWithAddressParamSchema, [
21+
"walletAddress",
22+
]);
23+
24+
const requestBodySchema = Type.Object({
25+
toNonce: Type.Number({
26+
description:
27+
"The nonce to cancel up to, inclusive. Example: If the onchain nonce is 10 and 'toNonce' is 15, this request will cancel nonces: 11, 12, 13, 14, 15",
28+
examples: ["42"],
29+
}),
30+
});
31+
32+
const responseBodySchema = Type.Object({
33+
result: Type.Object(
34+
{
35+
cancelledNonces: Type.Array(Type.Number()),
36+
},
37+
{
38+
examples: [
39+
{
40+
result: {
41+
cancelledNonces: [11, 12, 13, 14, 15],
42+
},
43+
},
44+
],
45+
},
46+
),
47+
});
48+
49+
export async function cancelBackendWalletNoncesRoute(fastify: FastifyInstance) {
50+
fastify.route<{
51+
Params: Static<typeof requestSchema>;
52+
Reply: Static<typeof responseBodySchema>;
53+
Body: Static<typeof requestBodySchema>;
54+
Querystring: Static<typeof requestQuerystringSchema>;
55+
}>({
56+
method: "POST",
57+
url: "/backend-wallet/:chain/cancel-nonces",
58+
schema: {
59+
summary: "Cancel nonces",
60+
description:
61+
"Cancel nonces from the next available onchain nonce to the provided nonce. This is useful to unblock a backend wallet that has transactions waiting for nonces to be mined.",
62+
tags: ["Backend Wallet"],
63+
operationId: "cancelNonces",
64+
params: requestSchema,
65+
body: requestBodySchema,
66+
headers: walletHeaderSchema,
67+
querystring: requestQuerystringSchema,
68+
response: {
69+
...standardResponseSchema,
70+
[StatusCodes.OK]: responseBodySchema,
71+
},
72+
},
73+
handler: async (request, reply) => {
74+
const { chain } = request.params;
75+
const { toNonce } = request.body;
76+
const { "x-backend-wallet-address": walletAddress } =
77+
request.headers as Static<typeof walletHeaderSchema>;
78+
79+
const chainId = await getChainIdFromChain(chain);
80+
const from = checksumAddress(walletAddress);
81+
82+
const rpcRequest = getRpcClient({
83+
client: thirdwebClient,
84+
chain: await getChain(chainId),
85+
});
86+
87+
// Cancel starting from the next unused onchain nonce.
88+
const transactionCount = await eth_getTransactionCount(rpcRequest, {
89+
address: walletAddress,
90+
blockTag: "latest",
91+
});
92+
if (transactionCount > toNonce) {
93+
throw createCustomError(
94+
`"toNonce" (${toNonce}) is lower than the next unused onchain nonce (${transactionCount}).`,
95+
StatusCodes.BAD_REQUEST,
96+
"BAD_REQUEST",
97+
);
98+
}
99+
100+
const cancelledNonces: number[] = [];
101+
for (let nonce = transactionCount; nonce <= toNonce; nonce++) {
102+
await sendCancellationTransaction({
103+
chainId,
104+
from,
105+
nonce,
106+
});
107+
cancelledNonces.push(nonce);
108+
}
109+
110+
reply.status(StatusCodes.OK).send({
111+
result: {
112+
cancelledNonces,
113+
},
114+
});
115+
},
116+
});
117+
}

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { removePublicKey } from "./auth/keypair/remove";
1111
import { getAllPermissions } from "./auth/permissions/getAll";
1212
import { grantPermissions } from "./auth/permissions/grant";
1313
import { revokePermissions } from "./auth/permissions/revoke";
14+
import { cancelBackendWalletNoncesRoute } from "./backend-wallet/cancel-nonces";
1415
import { createBackendWallet } from "./backend-wallet/create";
1516
import { getAll } from "./backend-wallet/getAll";
1617
import { getBalance } from "./backend-wallet/getBalance";
@@ -129,6 +130,7 @@ export async function withRoutes(fastify: FastifyInstance) {
129130
await fastify.register(getTransactionsForBackendWallet);
130131
await fastify.register(getTransactionsForBackendWalletByNonce);
131132
await fastify.register(resetBackendWalletNonces);
133+
await fastify.register(cancelBackendWalletNoncesRoute);
132134
await fastify.register(getBackendWalletNonce);
133135
await fastify.register(simulateTransaction);
134136

src/server/routes/transaction/getAll.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const requestQuerySchema = Type.Object({
2727
),
2828
});
2929

30-
export const responseBodySchema = Type.Object({
30+
const responseBodySchema = Type.Object({
3131
result: Type.Object({
3232
transactions: Type.Array(TransactionSchema),
3333
totalCount: Type.Integer(),

src/worker/tasks/nonceResyncWorker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const initNonceResyncWorker = async () => {
4040
* This is to unblock a wallet that has been stuck due to one or more skipped nonces.
4141
*/
4242
const handler: Processor<any, void, string> = async (job: Job<string>) => {
43-
const sentNoncesKeys = await redis.keys("nonce-sent*");
43+
const sentNoncesKeys = await redis.keys("nonce-sent:*");
4444
if (sentNoncesKeys.length === 0) {
4545
job.log("No active wallets.");
4646
return;

0 commit comments

Comments
 (0)