From 652206c4d7604944d0ce1df117d4fa1045026278 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Wed, 4 Dec 2024 11:50:29 +0800 Subject: [PATCH 1/2] feat: cancel nonces endpoint --- .../routes/backend-wallet/cancel-nonces.ts | 117 ++++++++++++++++++ src/server/routes/index.ts | 2 + src/server/routes/transaction/getAll.ts | 2 +- src/worker/tasks/nonceResyncWorker.ts | 2 +- 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/server/routes/backend-wallet/cancel-nonces.ts diff --git a/src/server/routes/backend-wallet/cancel-nonces.ts b/src/server/routes/backend-wallet/cancel-nonces.ts new file mode 100644 index 000000000..dc13fac7d --- /dev/null +++ b/src/server/routes/backend-wallet/cancel-nonces.ts @@ -0,0 +1,117 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { eth_getTransactionCount, getRpcClient } from "thirdweb"; +import { checksumAddress } from "thirdweb/utils"; +import { getChain } from "../../../utils/chain"; +import { thirdwebClient } from "../../../utils/sdk"; +import { sendCancellationTransaction } from "../../../utils/transaction/cancelTransaction"; +import { createCustomError } from "../../middleware/error"; +import { + requestQuerystringSchema, + standardResponseSchema, +} from "../../schemas/sharedApiSchemas"; +import { + walletHeaderSchema, + walletWithAddressParamSchema, +} from "../../schemas/wallet"; +import { getChainIdFromChain } from "../../utils/chain"; + +const requestSchema = Type.Omit(walletWithAddressParamSchema, [ + "walletAddress", +]); + +const requestBodySchema = Type.Object({ + toNonce: Type.Number({ + description: + "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", + examples: ["42"], + }), +}); + +const responseBodySchema = Type.Object({ + result: Type.Object( + { + cancelledNonces: Type.Array(Type.Number()), + }, + { + examples: [ + { + result: { + cancelledNonces: [11, 12, 13, 14, 15], + }, + }, + ], + }, + ), +}); + +export async function cancelBackendWalletNoncesRoute(fastify: FastifyInstance) { + fastify.route<{ + Params: Static; + Reply: Static; + Body: Static; + Querystring: Static; + }>({ + method: "POST", + url: "/backend-wallet/:chain/cancel-nonces", + schema: { + summary: "Cancel nonces", + description: + "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.", + tags: ["Backend Wallet"], + operationId: "cancelNonces", + params: requestSchema, + body: requestBodySchema, + headers: walletHeaderSchema, + querystring: requestQuerystringSchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseBodySchema, + }, + }, + handler: async (request, reply) => { + const { chain } = request.params; + const { toNonce } = request.body; + const { "x-backend-wallet-address": walletAddress } = + request.headers as Static; + + const chainId = await getChainIdFromChain(chain); + const from = checksumAddress(walletAddress); + + const rpcRequest = getRpcClient({ + client: thirdwebClient, + chain: await getChain(chainId), + }); + + // Cancel starting from the next unused onchain nonce. + const transactionCount = await eth_getTransactionCount(rpcRequest, { + address: walletAddress, + blockTag: "latest", + }); + if (transactionCount > toNonce) { + throw createCustomError( + `"toNonce" (${toNonce}) is lower than the next unused onchain nonce (${transactionCount}).`, + StatusCodes.BAD_REQUEST, + "BAD_REQUEST", + ); + } + + const cancelledNonces: number[] = []; + for (let nonce = transactionCount; nonce <= toNonce; nonce++) { + await sendCancellationTransaction({ + chainId, + from, + nonce, + }); + cancelledNonces.push(nonce); + } + + reply.status(StatusCodes.OK).send({ + result: { + cancelledNonces, + }, + }); + }, + }); +} diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 4345859ca..93333103f 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -11,6 +11,7 @@ import { removePublicKey } from "./auth/keypair/remove"; import { getAllPermissions } from "./auth/permissions/getAll"; import { grantPermissions } from "./auth/permissions/grant"; import { revokePermissions } from "./auth/permissions/revoke"; +import { cancelBackendWalletNoncesRoute } from "./backend-wallet/cancel-nonces"; import { createBackendWallet } from "./backend-wallet/create"; import { getAll } from "./backend-wallet/getAll"; import { getBalance } from "./backend-wallet/getBalance"; @@ -129,6 +130,7 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(getTransactionsForBackendWallet); await fastify.register(getTransactionsForBackendWalletByNonce); await fastify.register(resetBackendWalletNonces); + await fastify.register(cancelBackendWalletNoncesRoute); await fastify.register(getBackendWalletNonce); await fastify.register(simulateTransaction); diff --git a/src/server/routes/transaction/getAll.ts b/src/server/routes/transaction/getAll.ts index d8c023167..5b58f5b3e 100644 --- a/src/server/routes/transaction/getAll.ts +++ b/src/server/routes/transaction/getAll.ts @@ -27,7 +27,7 @@ const requestQuerySchema = Type.Object({ ), }); -export const responseBodySchema = Type.Object({ +const responseBodySchema = Type.Object({ result: Type.Object({ transactions: Type.Array(TransactionSchema), totalCount: Type.Integer(), diff --git a/src/worker/tasks/nonceResyncWorker.ts b/src/worker/tasks/nonceResyncWorker.ts index 9a0642250..8f5cb0c65 100644 --- a/src/worker/tasks/nonceResyncWorker.ts +++ b/src/worker/tasks/nonceResyncWorker.ts @@ -40,7 +40,7 @@ export const initNonceResyncWorker = async () => { * This is to unblock a wallet that has been stuck due to one or more skipped nonces. */ const handler: Processor = async (job: Job) => { - const sentNoncesKeys = await redis.keys("nonce-sent*"); + const sentNoncesKeys = await redis.keys("nonce-sent:*"); if (sentNoncesKeys.length === 0) { job.log("No active wallets."); return; From f8aefbc421342e6ec7bea01e2c19cd8b1bb54d73 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Wed, 4 Dec 2024 13:10:48 +0800 Subject: [PATCH 2/2] remove exception --- .../routes/backend-wallet/cancel-nonces.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/server/routes/backend-wallet/cancel-nonces.ts b/src/server/routes/backend-wallet/cancel-nonces.ts index dc13fac7d..cb4f6cf73 100644 --- a/src/server/routes/backend-wallet/cancel-nonces.ts +++ b/src/server/routes/backend-wallet/cancel-nonces.ts @@ -6,20 +6,17 @@ import { checksumAddress } from "thirdweb/utils"; import { getChain } from "../../../utils/chain"; import { thirdwebClient } from "../../../utils/sdk"; import { sendCancellationTransaction } from "../../../utils/transaction/cancelTransaction"; -import { createCustomError } from "../../middleware/error"; import { requestQuerystringSchema, standardResponseSchema, } from "../../schemas/sharedApiSchemas"; import { + walletChainParamSchema, walletHeaderSchema, - walletWithAddressParamSchema, } from "../../schemas/wallet"; import { getChainIdFromChain } from "../../utils/chain"; -const requestSchema = Type.Omit(walletWithAddressParamSchema, [ - "walletAddress", -]); +const requestSchema = walletChainParamSchema; const requestBodySchema = Type.Object({ toNonce: Type.Number({ @@ -58,7 +55,7 @@ export async function cancelBackendWalletNoncesRoute(fastify: FastifyInstance) { schema: { summary: "Cancel nonces", description: - "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.", + "Cancel all nonces up to the provided nonce. This is useful to unblock a backend wallet that has transactions waiting for nonces to be mined.", tags: ["Backend Wallet"], operationId: "cancelNonces", params: requestSchema, @@ -89,13 +86,6 @@ export async function cancelBackendWalletNoncesRoute(fastify: FastifyInstance) { address: walletAddress, blockTag: "latest", }); - if (transactionCount > toNonce) { - throw createCustomError( - `"toNonce" (${toNonce}) is lower than the next unused onchain nonce (${transactionCount}).`, - StatusCodes.BAD_REQUEST, - "BAD_REQUEST", - ); - } const cancelledNonces: number[] = []; for (let nonce = transactionCount; nonce <= toNonce; nonce++) {