diff --git a/server/api/configuration/webhooks/get.ts b/server/api/configuration/webhooks/get.ts new file mode 100644 index 000000000..e579fd125 --- /dev/null +++ b/server/api/configuration/webhooks/get.ts @@ -0,0 +1,38 @@ +import { Static, Type } from "@sinclair/typebox"; +import { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { getConfiguration } from "../../../../src/db/configuration/getConfiguration"; + +export const ReplySchema = Type.Object({ + result: Type.Object({ + webhookUrl: Type.String(), + webhookAuthBearerToken: Type.String(), + }), +}); + +export async function getWebhookConfiguration(fastify: FastifyInstance) { + fastify.route<{ + Reply: Static; + }>({ + method: "GET", + url: "/configuration/webhook", + schema: { + summary: "Get webhook configuration", + description: "Get the engine configuration for webhook", + tags: ["Configuration"], + operationId: "getWebhookConfiguration", + response: { + [StatusCodes.OK]: ReplySchema, + }, + }, + handler: async (req, res) => { + const config = await getConfiguration(); + res.status(200).send({ + result: { + webhookAuthBearerToken: config.webhookAuthBearerToken || "", + webhookUrl: config.webhookUrl || "", + }, + }); + }, + }); +} diff --git a/server/api/configuration/webhooks/update.ts b/server/api/configuration/webhooks/update.ts new file mode 100644 index 000000000..ff2bfc3ae --- /dev/null +++ b/server/api/configuration/webhooks/update.ts @@ -0,0 +1,40 @@ +import { Static, Type } from "@sinclair/typebox"; +import { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { updateConfiguration } from "../../../../src/db/configuration/updateConfiguration"; +import { ReplySchema } from "./get"; + +const BodySchema = Type.Partial( + Type.Object({ + webhookUrl: Type.String(), + webhookAuthBearerToken: Type.String(), + }), +); + +export async function updateWebhookConfiguration(fastify: FastifyInstance) { + fastify.route<{ + Body: Static; + }>({ + method: "POST", + url: "/configuration/webhook", + schema: { + summary: "Update webhook configuration", + description: "Update the engine configuration for webhook", + tags: ["Configuration"], + operationId: "updateWebhookConfiguration", + body: BodySchema, + response: { + [StatusCodes.OK]: ReplySchema, + }, + }, + handler: async (req, res) => { + const config = await updateConfiguration({ ...req.body }); + res.status(200).send({ + result: { + webhookUrl: config.webhookUrl, + webhookAuthBearerToken: config.webhookAuthBearerToken, + }, + }); + }, + }); +} diff --git a/server/api/index.ts b/server/api/index.ts index 400e2fdb9..5f52432f9 100644 --- a/server/api/index.ts +++ b/server/api/index.ts @@ -49,12 +49,18 @@ import { getBalance } from "./backend-wallet/getBalance"; import { importWallet } from "./backend-wallet/import"; import { sendTransaction } from "./backend-wallet/send"; import { transfer } from "./backend-wallet/transfer"; + +// Configuration import { getChainsConfiguration } from "./configuration/chains/get"; import { updateChainsConfiguration } from "./configuration/chains/update"; import { getTransactionConfiguration } from "./configuration/transactions/get"; import { updateTransactionConfiguration } from "./configuration/transactions/update"; import { getWalletsConfiguration } from "./configuration/wallets/get"; import { updateWalletsConfiguration } from "./configuration/wallets/update"; +import { getWebhookConfiguration } from "./configuration/webhooks/get"; +import { updateWebhookConfiguration } from "./configuration/webhooks/update"; + +// Accounts import { accountRoutes } from "./contract/extensions/account"; import { accountFactoryRoutes } from "./contract/extensions/accountFactory"; @@ -74,6 +80,8 @@ export const apiRoutes = async (fastify: FastifyInstance) => { await fastify.register(updateChainsConfiguration); await fastify.register(getTransactionConfiguration); await fastify.register(updateTransactionConfiguration); + await fastify.register(getWebhookConfiguration); + await fastify.register(updateWebhookConfiguration); // Chains await fastify.register(getChainData); diff --git a/server/controller/tx-update-listener.ts b/server/controller/tx-update-listener.ts index 942776e00..2ecdd0b39 100644 --- a/server/controller/tx-update-listener.ts +++ b/server/controller/tx-update-listener.ts @@ -1,6 +1,5 @@ import { knex } from "../../src/db/client"; import { getTxById } from "../../src/db/transactions/getTxById"; -import { env } from "../../src/utils/env"; import { logger } from "../../src/utils/logger"; import { formatSocketMessage, @@ -23,13 +22,7 @@ export const startTxUpdatesNotificationListener = async (): Promise => { const parsedPayload = JSON.parse(msg.payload); // Send webhook - if (env.WEBHOOK_URL.length > 0) { - await sendWebhook(parsedPayload); - } else { - logger.server.debug( - `Webhooks are disabled or no URL is provided. Skipping webhook update`, - ); - } + await sendWebhook(parsedPayload); // Send websocket message const index = subscriptionsData.findIndex( diff --git a/server/utilities/webhook.ts b/server/utilities/webhook.ts index 32a17bc8e..5ddb3ac39 100644 --- a/server/utilities/webhook.ts +++ b/server/utilities/webhook.ts @@ -1,9 +1,16 @@ import { getTxById } from "../../src/db/transactions/getTxById"; -import { env } from "../../src/utils/env"; import { logger } from "../../src/utils/logger"; +import { getWebhookConfig } from "../utils/cache/getWebhookConfig"; export const sendWebhook = async (data: any): Promise => { try { + const webhookConfig = await getWebhookConfig(); + + if (!webhookConfig.webhookUrl) { + logger.server.debug("No WebhookURL set, skipping webhook send"); + return; + } + const txData = await getTxById({ queueId: data.id }); const headers: { Accept: string; @@ -14,13 +21,13 @@ export const sendWebhook = async (data: any): Promise => { "Content-Type": "application/json", }; - if (process.env.WEBHOOK_AUTH_BEARER_TOKEN) { + if (webhookConfig.webhookAuthBearerToken) { headers[ "Authorization" - ] = `Bearer ${process.env.WEBHOOK_AUTH_BEARER_TOKEN}`; + ] = `Bearer ${webhookConfig.webhookAuthBearerToken}`; } - const response = await fetch(env.WEBHOOK_URL, { + const response = await fetch(webhookConfig.webhookUrl, { method: "POST", headers, body: JSON.stringify(txData), diff --git a/server/utils/cache/getWebhookConfig.ts b/server/utils/cache/getWebhookConfig.ts new file mode 100644 index 000000000..b90e7bb02 --- /dev/null +++ b/server/utils/cache/getWebhookConfig.ts @@ -0,0 +1,29 @@ +import { getConfiguration } from "../../../src/db/configuration/getConfiguration"; + +interface WebhookConfig { + webhookUrl: string; + webhookAuthBearerToken: string | null; +} + +export const webhookCache = new Map(); + +export const getWebhookConfig = async (): Promise => { + const cacheKey = `webhookConfig`; + if (webhookCache.has(cacheKey)) { + return webhookCache.get(cacheKey)! as WebhookConfig; + } + + const config = await getConfiguration(); + + if (config.webhookAuthBearerToken || config.webhookUrl) { + webhookCache.set(cacheKey, { + webhookUrl: config.webhookUrl!, + webhookAuthBearerToken: config.webhookAuthBearerToken, + }); + } + + return { + webhookUrl: config.webhookUrl!, + webhookAuthBearerToken: config.webhookAuthBearerToken, + }; +}; diff --git a/src/db/configuration/updateConfiguration.ts b/src/db/configuration/updateConfiguration.ts index bdcbaca85..d5712db0e 100644 --- a/src/db/configuration/updateConfiguration.ts +++ b/src/db/configuration/updateConfiguration.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { webhookCache } from "../../../server/utils/cache/getWebhookConfig"; import { encrypt } from "../../utils/cypto"; import { prisma } from "../client"; import { getConfiguration } from "./getConfiguration"; @@ -7,6 +8,10 @@ export const updateConfiguration = async ( data: Prisma.ConfigurationUpdateArgs["data"], ) => { const config = await getConfiguration(); + + // Clearing webhook cache on update + webhookCache.clear(); + return prisma.configuration.update({ where: { id: config.id, diff --git a/src/prisma/migrations/20231017214123_webhook_config/migration.sql b/src/prisma/migrations/20231017214123_webhook_config/migration.sql new file mode 100644 index 000000000..5da28fe81 --- /dev/null +++ b/src/prisma/migrations/20231017214123_webhook_config/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "configuration" ADD COLUMN "webhookAuthBearerToken" TEXT, +ADD COLUMN "webhookUrl" TEXT; diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma index 4449db882..7fa5fa597 100644 --- a/src/prisma/schema.prisma +++ b/src/prisma/schema.prisma @@ -33,6 +33,9 @@ model Configuration { gcpKmsKeyRingId String? @map("gcpKmsKeyRingId") gcpApplicationCredentialEmail String? @map("gcpApplicationCredentialEmail") gcpApplicationCredentialPrivateKey String? @map("gcpApplicationCredentialPrivateKey") + // Webhook + webhookUrl String? @map("webhookUrl") + webhookAuthBearerToken String? @map("webhookAuthBearerToken") @@map("configuration") } diff --git a/src/utils/env.ts b/src/utils/env.ts index 7839196a9..2b2d0b915 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -55,16 +55,6 @@ export const env = createEnv({ PORT: z.coerce.number().default(3005), HOST: z.string().default("0.0.0.0"), ACCESS_CONTROL_ALLOW_ORIGIN: z.string().default("*"), - WEBHOOK_URL: z - .string() - .default("") - .transform((url) => { - if (url.length > 0) { - return url; - } - return ""; - }), - WEBHOOK_AUTH_BEARER_TOKEN: z.string().default(""), }, clientPrefix: "NEVER_USED", client: {}, @@ -77,8 +67,6 @@ export const env = createEnv({ HOST: process.env.HOST, OPENAPI_BASE_ORIGIN: process.env.OPENAPI_BASE_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN: process.env.ACCESS_CONTROL_ALLOW_ORIGIN, - WEBHOOK_URL: process.env.WEBHOOK_URL, - WEBHOOK_AUTH_BEARER_TOKEN: process.env.WEBHOOK_AUTH_BEARER_TOKEN, }, onValidationError: (error: ZodError) => { console.error( diff --git a/src/worker/listeners/queuedTxListener.ts b/src/worker/listeners/queuedTxListener.ts index 58c26fbba..05629d443 100644 --- a/src/worker/listeners/queuedTxListener.ts +++ b/src/worker/listeners/queuedTxListener.ts @@ -1,7 +1,6 @@ import PQueue from "p-queue"; import { sendWebhook } from "../../../server/utilities/webhook"; import { knex } from "../../db/client"; -import { env } from "../../utils/env"; import { logger } from "../../utils/logger"; import { processTx } from "../tasks/processTx"; @@ -31,14 +30,8 @@ export const queuedTxListener = async (): Promise => { connection.on( "notification", async (msg: { channel: string; payload: string }) => { - if (env.WEBHOOK_URL.length > 0) { - const parsedPayload = JSON.parse(msg.payload); - await sendWebhook(parsedPayload); - } else { - logger.server.debug( - `Webhooks are disabled or no URL is provided. Skipping webhook update`, - ); - } + const parsedPayload = JSON.parse(msg.payload); + await sendWebhook(parsedPayload); queue.add(processTx); }, );