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..cb4f6cf73 --- /dev/null +++ b/src/server/routes/backend-wallet/cancel-nonces.ts @@ -0,0 +1,107 @@ +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 { + requestQuerystringSchema, + standardResponseSchema, +} from "../../schemas/sharedApiSchemas"; +import { + walletChainParamSchema, + walletHeaderSchema, +} from "../../schemas/wallet"; +import { getChainIdFromChain } from "../../utils/chain"; + +const requestSchema = walletChainParamSchema; + +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 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, + 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", + }); + + 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 2f56406d9..1d6694a98 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,8 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(getTransactionsForBackendWallet); await fastify.register(getTransactionsForBackendWalletByNonce); await fastify.register(resetBackendWalletNoncesRoute); + await fastify.register(cancelBackendWalletNoncesRoute); + await fastify.register(resetBackendWalletNoncesRoute); 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;