diff --git a/src/db/wallets/walletNonce.ts b/src/db/wallets/walletNonce.ts index 041b7d58b..1d7124bd8 100644 --- a/src/db/wallets/walletNonce.ts +++ b/src/db/wallets/walletNonce.ts @@ -250,18 +250,19 @@ export const inspectNonce = async (chainId: number, walletAddress: Address) => { }; /** - * Delete all wallet nonces. Useful when they get out of sync. + * Delete nonce state for the provided wallets. + * @param backendWallets */ -export const deleteAllNonces = async () => { - const keys = [ - ...(await redis.keys("nonce:*")), - ...(await redis.keys("nonce-recycled:*")), - ...(await redis.keys("sent-nonce:*")), - ]; - if (keys.length > 0) { - await redis.del(keys); - } -}; +export async function deleteNoncesForBackendWallets( + backendWallets: { chainId: number; walletAddress: Address }[], +) { + const keys = backendWallets.flatMap(({ chainId, walletAddress }) => [ + lastUsedNonceKey(chainId, walletAddress), + recycledNoncesKey(chainId, walletAddress), + sentNoncesKey(chainId, walletAddress), + ]); + await redis.del(keys); +} /** * Resync the nonce to the higher of (db nonce, onchain nonce). diff --git a/src/server/routes/backend-wallet/reset-nonces.ts b/src/server/routes/backend-wallet/reset-nonces.ts new file mode 100644 index 000000000..0ec4c30e8 --- /dev/null +++ b/src/server/routes/backend-wallet/reset-nonces.ts @@ -0,0 +1,96 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { getAddress } from "thirdweb"; +import { + deleteNoncesForBackendWallets, + getUsedBackendWallets, + syncLatestNonceFromOnchain, +} from "../../../db/wallets/walletNonce"; +import { AddressSchema } from "../../schemas/address"; +import { standardResponseSchema } from "../../schemas/sharedApiSchemas"; + +const requestBodySchema = Type.Object({ + chainId: Type.Optional( + Type.Number({ + description: "The chain ID to reset nonces for.", + }), + ), + walletAddress: Type.Optional({ + ...AddressSchema, + description: + "The backend wallet address to reset nonces for. Omit to reset all backend wallets.", + }), +}); + +const responseSchema = Type.Object({ + result: Type.Object({ + status: Type.String(), + count: Type.Number({ + description: "The number of backend wallets processed.", + }), + }), +}); + +responseSchema.example = { + result: { + status: "success", + count: 1, + }, +}; + +export const resetBackendWalletNoncesRoute = async ( + fastify: FastifyInstance, +) => { + fastify.route<{ + Reply: Static; + Body: Static; + }>({ + method: "POST", + url: "/backend-wallet/reset-nonces", + schema: { + summary: "Reset nonces", + description: + "Reset nonces for all backend wallets. This is for debugging purposes and does not impact held tokens.", + tags: ["Backend Wallet"], + operationId: "resetNonces", + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, reply) => { + const { chainId, walletAddress: _walletAddress } = req.body; + + // If chain+wallet are provided, only process that wallet. + // Otherwise process all used wallets that has nonce state. + const backendWallets = + chainId && _walletAddress + ? [{ chainId, walletAddress: getAddress(_walletAddress) }] + : await getUsedBackendWallets(); + + const RESYNC_BATCH_SIZE = 50; + for (let i = 0; i < backendWallets.length; i += RESYNC_BATCH_SIZE) { + const batch = backendWallets.slice(i, i + RESYNC_BATCH_SIZE); + + // Delete nonce state for these backend wallets. + await deleteNoncesForBackendWallets(backendWallets); + + // Resync nonces for these backend wallets. + await Promise.allSettled( + batch.map(({ chainId, walletAddress }) => + syncLatestNonceFromOnchain(chainId, walletAddress), + ), + ); + } + + reply.status(StatusCodes.OK).send({ + result: { + status: "success", + count: backendWallets.length, + }, + }); + }, + }); +}; diff --git a/src/server/routes/backend-wallet/resetNonces.ts b/src/server/routes/backend-wallet/resetNonces.ts deleted file mode 100644 index 8a7690637..000000000 --- a/src/server/routes/backend-wallet/resetNonces.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Static, Type } from "@sinclair/typebox"; -import { FastifyInstance } from "fastify"; -import { StatusCodes } from "http-status-codes"; -import { Address, getAddress } from "thirdweb"; -import { - deleteAllNonces, - syncLatestNonceFromOnchain, -} from "../../../db/wallets/walletNonce"; -import { redis } from "../../../utils/redis/redis"; -import { standardResponseSchema } from "../../schemas/sharedApiSchemas"; - -const responseSchema = Type.Object({ - result: Type.Object({ - status: Type.String(), - }), -}); - -responseSchema.example = { - result: { - status: "success", - }, -}; - -export const resetBackendWalletNonces = async (fastify: FastifyInstance) => { - fastify.route<{ - Reply: Static; - }>({ - method: "POST", - url: "/backend-wallet/reset-nonces", - schema: { - summary: "Reset nonces", - description: - "Reset nonces for all backend wallets. This is for debugging purposes and does not impact held tokens.", - tags: ["Backend Wallet"], - operationId: "resetNonces", - response: { - ...standardResponseSchema, - [StatusCodes.OK]: responseSchema, - }, - }, - handler: async (req, reply) => { - const backendWallets = await getUsedBackendWallets(); - - // Delete all nonce state for used backend wallets. - await deleteAllNonces(); - - // Attempt to re-sync nonces for used backend wallets. - await Promise.allSettled( - backendWallets.map(({ chainId, walletAddress }) => - syncLatestNonceFromOnchain(chainId, walletAddress), - ), - ); - - reply.status(StatusCodes.OK).send({ - result: { - status: "success", - }, - }); - }, - }); -}; - -// TODO: replace with getUsedBackendWallets() helper. -const getUsedBackendWallets = async (): Promise< - { - chainId: number; - walletAddress: Address; - }[] -> => { - const keys = await redis.keys("nonce:*:*"); - return keys.map((key) => { - const tokens = key.split(":"); - return { - chainId: parseInt(tokens[1]), - walletAddress: getAddress(tokens[2]), - }; - }); -}; diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 4345859ca..2f56406d9 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -19,7 +19,7 @@ import { getTransactionsForBackendWallet } from "./backend-wallet/getTransaction import { getTransactionsForBackendWalletByNonce } from "./backend-wallet/getTransactionsByNonce"; import { importBackendWallet } from "./backend-wallet/import"; import { removeBackendWallet } from "./backend-wallet/remove"; -import { resetBackendWalletNonces } from "./backend-wallet/resetNonces"; +import { resetBackendWalletNoncesRoute } from "./backend-wallet/reset-nonces"; import { sendTransaction } from "./backend-wallet/sendTransaction"; import { sendTransactionBatch } from "./backend-wallet/sendTransactionBatch"; import { signMessageRoute } from "./backend-wallet/signMessage"; @@ -128,7 +128,7 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(signTypedData); await fastify.register(getTransactionsForBackendWallet); await fastify.register(getTransactionsForBackendWalletByNonce); - await fastify.register(resetBackendWalletNonces); + await fastify.register(resetBackendWalletNoncesRoute); await fastify.register(getBackendWalletNonce); await fastify.register(simulateTransaction);