diff --git a/package.json b/package.json index bb4e1e0..b540c42 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "kysely": "^0.27.4", "lodash": "^4.17.21", "lru-cache": "^11.0.0", + "node-cron": "^3.0.3", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", "rollup": "^4.12.0", @@ -93,6 +94,7 @@ "@swc/cli": "^0.3.12", "@swc/core": "^1.4.15", "@types/body-parser": "^1.19.5", + "@types/node-cron": "^3.0.11", "@types/pg": "^8.11.6", "@types/sinon": "^17.0.2", "@types/swagger-ui-express": "^4.1.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec93c8b..a385d0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: lru-cache: specifier: ^11.0.0 version: 11.0.0 + node-cron: + specifier: ^3.0.3 + version: 3.0.3 pg: specifier: ^8.12.0 version: 8.12.0 @@ -201,6 +204,9 @@ importers: '@types/body-parser': specifier: ^1.19.5 version: 1.19.5 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 '@types/pg': specifier: ^8.11.6 version: 8.11.6 @@ -2717,6 +2723,9 @@ packages: '@types/mysql@2.15.22': resolution: {integrity: sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==} + '@types/node-cron@3.0.11': + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + '@types/node@18.15.13': resolution: {integrity: sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==} @@ -5703,6 +5712,10 @@ packages: node-addon-api@3.2.1: resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + node-cron@3.0.3: + resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==} + engines: {node: '>=6.0.0'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -7203,6 +7216,7 @@ packages: uuid@3.4.0: resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. hasBin: true uuid@8.3.2: @@ -11009,6 +11023,8 @@ snapshots: dependencies: '@types/node': 20.10.6 + '@types/node-cron@3.0.11': {} + '@types/node@18.15.13': {} '@types/node@20.10.6': @@ -14608,6 +14624,10 @@ snapshots: node-addon-api@3.2.1: optional: true + node-cron@3.0.3: + dependencies: + uuid: 8.3.2 + node-domexception@1.0.0: {} node-fetch-native@1.6.4: {} diff --git a/src/controllers/SignatureRequestController.ts b/src/controllers/SignatureRequestController.ts index 609ab58..42d70eb 100644 --- a/src/controllers/SignatureRequestController.ts +++ b/src/controllers/SignatureRequestController.ts @@ -11,7 +11,7 @@ import { import { SupabaseDataService } from "../services/SupabaseDataService.js"; import { verifyAuthSignedData } from "../utils/verifyAuthSignedData.js"; -import { SignatureRequestProcessor } from "../services/SignatureRequestProcessor.js"; +import SignatureRequestProcessor from "../services/SignatureRequestProcessor.js"; interface CancelSignatureRequest { signature: string; diff --git a/src/cron/SignatureRequestProcessing.ts b/src/cron/SignatureRequestProcessing.ts new file mode 100644 index 0000000..de8b293 --- /dev/null +++ b/src/cron/SignatureRequestProcessing.ts @@ -0,0 +1,31 @@ +import cron from "node-cron"; + +import SignatureRequestProcessor from "../services/SignatureRequestProcessor.js"; + +export default class SignatureRequestProcessorCron { + private static instance: SignatureRequestProcessorCron; + private processor: SignatureRequestProcessor; + + private constructor() { + this.processor = SignatureRequestProcessor.getInstance(); + this.setupCronJob(); + } + + private setupCronJob() { + // Run every 30 seconds + cron.schedule("*/30 * * * * *", async () => { + try { + await this.processor.processPendingRequests(); + } catch (error) { + console.error("Error in signature request processor cron job:", error); + } + }); + } + + public static start(): void { + if (!SignatureRequestProcessorCron.instance) { + SignatureRequestProcessorCron.instance = + new SignatureRequestProcessorCron(); + } + } +} diff --git a/src/index.ts b/src/index.ts index ff035a9..786c721 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,63 +1,61 @@ -import './instrument.js'; -import express, {type Express} from "express"; +import "./instrument.js"; +import express, { type Express } from "express"; import "reflect-metadata"; import cors from "cors"; -import {assertExists} from "./utils/assertExists.js"; -import {yoga} from "./client/graphql.js"; +import { assertExists } from "./utils/assertExists.js"; +import { yoga } from "./client/graphql.js"; import swaggerUi from "swagger-ui-express"; -import swaggerJson from "./__generated__/swagger.json" assert {type: "json"} -import {RegisterRoutes} from "./__generated__/routes/routes.js"; -import * as Sentry from '@sentry/node'; +import swaggerJson from "./__generated__/swagger.json" assert { type: "json" }; +import { RegisterRoutes } from "./__generated__/routes/routes.js"; +import * as Sentry from "@sentry/node"; +import SignatureRequestProcessorCron from "./cron/SignatureRequestProcessing.js"; // @ts-expect-error BigInt is not supported by JSON BigInt.prototype.toJSON = function () { - const int = Number.parseInt(this.toString()); - return int ?? this.toString(); + const int = Number.parseInt(this.toString()); + return int ?? this.toString(); }; // @ts-expect-error BigInt is not supported by JSON BigInt.prototype.fromJSON = function () { - return BigInt(this.toString()); + return BigInt(this.toString()); }; const PORT = assertExists(process.env.PORT, "PORT"); const app: Express = express(); -app.use(express.urlencoded({extended: true, limit: '1mb'})); -app.use(express.json({limit: '1mb'})); +app.use(express.urlencoded({ extended: true, limit: "1mb" })); +app.use(express.json({ limit: "1mb" })); app.use(cors()); -app.get('/health', (req, res) => { - const data = { - uptime: process.uptime(), - message: 'OK', - date: new Date() - } +app.get("/health", (req, res) => { + const data = { + uptime: process.uptime(), + message: "OK", + date: new Date(), + }; - res.status(200).send(data); + res.status(200).send(data); }); // Bind GraphQL Yoga to the graphql endpoint to avoid rendering the playground on any path app.use(yoga.graphqlEndpoint, yoga); -app.use( - "/spec", - swaggerUi.serve, - swaggerUi.setup(swaggerJson) -); +app.use("/spec", swaggerUi.serve, swaggerUi.setup(swaggerJson)); RegisterRoutes(app); // The error handler must be registered before any other error middleware and after all controllers Sentry.setupExpressErrorHandler(app); -app.listen(PORT, () => { - console.log( - `πŸ•ΈοΈ Running a GraphQL API server at http://localhost:${PORT}/v1/graphql` - ); +// Start Safe signature request processing cron job +SignatureRequestProcessorCron.start(); - console.log(`πŸš€ Running Swagger docs at http://localhost:${PORT}/spec`); +app.listen(PORT, () => { + console.log( + `πŸ•ΈοΈ Running a GraphQL API server at http://localhost:${PORT}/v1/graphql`, + ); + console.log(`πŸš€ Running Swagger docs at http://localhost:${PORT}/spec`); }); - diff --git a/src/services/SignatureRequestProcessor.ts b/src/services/SignatureRequestProcessor.ts index 6456a0c..0b9087e 100644 --- a/src/services/SignatureRequestProcessor.ts +++ b/src/services/SignatureRequestProcessor.ts @@ -8,7 +8,7 @@ import { SafeApiQueue } from "./SafeApiQueue.js"; type SignatureRequest = Database["public"]["Tables"]["signature_requests"]["Row"]; -export class SignatureRequestProcessor { +export default class SignatureRequestProcessor { private static instance: SignatureRequestProcessor; private readonly dataService: SupabaseDataService; @@ -22,7 +22,7 @@ export class SignatureRequestProcessor { async processPendingRequests(): Promise { const pendingRequests = await this.getPendingRequests(); - console.log(`Found ${pendingRequests.length} pending requests`); + console.log(`Found ${pendingRequests.length} pending signature requests`); for (const request of pendingRequests) { const command = getCommand(request);