"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Router = void 0; exports.getHealthStatus = getHealthStatus; exports.getGasPrices = getGasPrices; exports.formatStatus = formatStatus; exports.handleIndex = handleIndex; exports.handleStatus = handleStatus; exports.handleTornadoWithdraw = handleTornadoWithdraw; exports.handleGetJob = handleGetJob; exports.handleEvents = handleEvents; exports.handleTrees = handleTrees; exports.listenRouter = listenRouter; const path_1 = __importDefault(require("path")); const fs_1 = require("fs"); const fastify_1 = require("fastify"); const cors_1 = require("@fastify/cors"); const core_1 = require("@tornado/core"); const ethers_1 = require("ethers"); const config_1 = require("../config"); const logger_1 = require("./logger"); const routerMsg_1 = require("./routerMsg"); const data_1 = require("./data"); const schema_1 = require("./schema"); function getHealthStatus(netId, syncManagerStatus) { const { events, tokenPrice, gasPrice } = syncManagerStatus.syncStatus[netId]; return String(Boolean(events && tokenPrice && gasPrice)); } function getGasPrices(netId, syncManagerStatus) { const { gasPrice, l1Fee } = syncManagerStatus.cachedGasPrices[netId]; return { fast: Number(gasPrice), additionalProperties: l1Fee ? Number(l1Fee) : undefined, }; } function formatStatus({ url, netId, relayerConfig, syncManagerStatus, pendingWorks, }) { const config = (0, core_1.getConfig)(netId); return { url, rewardAccount: relayerConfig.rewardAccount, instances: (0, core_1.getActiveTokenInstances)(config), events: syncManagerStatus.cachedEvents[netId], gasPrices: getGasPrices(netId, syncManagerStatus), netId, ethPrices: syncManagerStatus.cachedPrices[netId], tornadoServiceFee: relayerConfig.serviceFee, latestBlock: syncManagerStatus.latestBlocks[netId], latestBalance: syncManagerStatus.latestBalances[netId], version: config_1.version, health: { status: getHealthStatus(netId, syncManagerStatus), error: '', errorsLog: [...syncManagerStatus.errors.filter((e) => e.netId === netId)], }, syncStatus: syncManagerStatus.syncStatus[netId], onSyncEvents: syncManagerStatus.onSyncEvents, currentQueue: pendingWorks, }; } function handleIndex(enabledNetworks) { return ('This is Tornado Cash Relayer service. Check the ' + enabledNetworks.map((netId) => `/${netId}/v1/status `).join(', ') + 'for settings'); } async function handleStatus(url, router, netId, reply) { const { relayerConfig } = router; const { syncManagerStatus, pendingWorks } = await (0, routerMsg_1.sendMessage)(router, { type: 'status' }); if (Array.isArray(netId)) { reply.send(netId.map((n) => formatStatus({ url, netId: n, relayerConfig, syncManagerStatus, pendingWorks, }))); return; } reply.send(formatStatus({ url, netId, relayerConfig, syncManagerStatus, pendingWorks, })); } /** * Since we check gasLimit and fees, should extend timeout at any proxy more than 60s */ async function handleTornadoWithdraw(router, netId, req, reply) { const { contract, proof, args } = req.body; const { id, error } = await (0, routerMsg_1.sendMessage)(router, { type: 'tornadoWithdraw', netId, contract, proof, args, }); if (error) { reply.code(502).send({ error }); return; } reply.send({ id }); } async function handleGetJob(router, req, reply) { const { id } = req.params; const job = await (0, routerMsg_1.sendMessage)(router, { type: 'job', id }); if (job.error) { reply.code(502).send(job); return; } reply.send(job); } async function handleEvents(router, netId, req, reply) { const { relayerConfig: { userEventsDir: userDirectory }, } = router; const { type, currency, amount, fromBlock, recent } = req.body; const name = ['deposit', 'withdrawal'].includes(type) ? `${type}s_${netId}_${currency}_${amount}` : `${type}_${netId}`; // Return 0 length events if not exist (likely 0 events, can be checked by lastSyncBlock === fromBlock) if (!(await (0, data_1.existsAsync)(path_1.default.join(userDirectory, `${name}.json`)))) { reply.send({ events: [], lastSyncBlock: fromBlock, }); return; } const { syncManagerStatus } = await (0, routerMsg_1.sendMessage)(router, { type: 'status' }); const lastSyncBlock = Number(['deposit', 'withdrawal'].includes(type) ? syncManagerStatus.cachedEvents[netId]?.tornado?.lastBlock : syncManagerStatus.cachedEvents[netId]?.[String(type)]?.lastBlock); if (type === 'deposit' && recent) { const { events } = await (0, data_1.loadSavedEvents)({ name: 'recent_' + name, userDirectory, }); reply.send({ events, lastSyncBlock, }); return; } const { events } = await (0, data_1.loadSavedEvents)({ name, userDirectory, }); if (recent) { reply.send({ events: events.slice(events.length - 10).reverse(), lastSyncBlock, }); return; } reply.send({ events: events.filter((e) => e.blockNumber >= (fromBlock || 0)).slice(0, core_1.MAX_TOVARISH_EVENTS), lastSyncBlock, }); } async function handleTrees(router, req, reply) { const treeRegex = /deposits_(?\d+)_(?\w+)_(?[\d.]+)_(?\w+).json.zip/g; const { netId, currency, amount, part } = treeRegex.exec(req.params.treeName)?.groups || {}; const treeName = `deposits_${netId}_${currency}_${amount}_${part}.json.zip`; const treePath = path_1.default.join(router.relayerConfig.userTreeDir, treeName); if (!(await (0, data_1.existsAsync)(treePath))) { reply.status(404).send(`Tree ${treeName} not found!`); return; } reply.send((0, fs_1.createReadStream)(treePath)); } function listenRouter(router) { const { relayerConfig, logger, app, admin, forkId } = router; // eslint-disable-next-line @typescript-eslint/no-explicit-any app.register(cors_1.fastifyCors, () => (req, callback) => { callback(null, { origin: req.headers.origin || '*', credentials: true, methods: ['GET, POST, OPTIONS'], headers: [ 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type', ], maxAge: 1728000, }); }); app.get('/', (_, reply) => { reply.type('text/html').send(handleIndex(relayerConfig.enabledNetworks)); }); app.get('/relayer', (_, reply) => { reply.type('text/html').send(handleIndex(relayerConfig.enabledNetworks)); }); app.get('/status', (req, reply) => { handleStatus(`${req.protocol}://${req.hostname}`, router, relayerConfig.enabledNetworks, reply); }); app.get('/enabledNetworks', (_, reply) => { reply.send(relayerConfig.enabledNetworks); }); if (forkId === 0) { logger.info('Router listening on /, /status, /enabledNetworks'); } for (const netId of relayerConfig.enabledNetworks) { app.get(`/${netId}`, (_, reply) => { reply.type('text/html').send(handleIndex([netId])); }); app.get(`/${netId}/status`, (req, reply) => { handleStatus(`${req.protocol}://${req.hostname}/${netId}`, router, netId, reply); }); const withdrawSchema = (0, schema_1.getWithdrawSchema)(netId); app.post(`/${netId}/relay`, { schema: withdrawSchema }, (req, reply) => { handleTornadoWithdraw(router, netId, req, reply); }); app.get(`/${netId}/v1/status`, (req, reply) => { handleStatus(`${req.protocol}://${req.hostname}/${netId}`, router, netId, reply); }); app.post(`/${netId}/v1/tornadoWithdraw`, { schema: withdrawSchema }, (req, reply) => { handleTornadoWithdraw(router, netId, req, reply); }); app.get(`/${netId}/v1/jobs/:id`, { schema: schema_1.idParamsSchema }, (req, reply) => { handleGetJob(router, req, reply); }); const eventSchema = (0, schema_1.getEventsSchema)(netId); app.post(`/${netId}/events`, { schema: eventSchema }, (req, reply) => { handleEvents(router, netId, req, reply); }); app.get(`/${netId}/trees/:treeName`, { schema: schema_1.treeNameSchema }, (req, reply) => { handleTrees(router, req, reply); }); if (forkId === 0) { logger.info(`Router listening on /${netId}, /${netId}/status, /${netId}/relay, /${netId}/v1/status, /${netId}/v1/tornadoWithdraw, /${netId}/v1/jobs/:id, /${netId}/events, /${netId}/trees/:treeName`); } } const { port, host } = relayerConfig; app.listen({ port, host }, (err, address) => { if (err) { logger.error('Router Error'); console.log(err); throw err; } else { logger.debug(`Router listening on ${address}`); } }); admin.get('/errors', (_, reply) => { (async () => { const { errors } = await (0, routerMsg_1.sendMessage)(router, { type: 'errors' }); reply.header('Content-Type', 'application/json').send(JSON.stringify(errors, null, 2)); })(); }); admin.listen({ port: port + 100, host }, (err, address) => { if (err) { logger.error('Admin Router Error'); console.log(err); throw err; } else { if (forkId === 0) { logger.debug(`Admin Router listening on ${address}`); } } }); (0, routerMsg_1.resolveMessages)(router); } class Router { relayerConfig; logger; forkId; app; // For viewing error logs admin; messages; constructor(relayerConfig, forkId = 0) { this.relayerConfig = relayerConfig; this.logger = (0, logger_1.getLogger)(`[Router ${forkId}]`, relayerConfig.logLevel); this.forkId = forkId; const app = (0, fastify_1.fastify)({ ajv: { customOptions: { keywords: [ { keyword: 'isAddress', // eslint-disable-next-line @typescript-eslint/no-explicit-any validate: (schema, data) => { try { return (0, ethers_1.isAddress)(data); } catch { return false; } }, errors: true, }, { keyword: 'BN', // eslint-disable-next-line @typescript-eslint/no-explicit-any validate: (schema, data) => { try { BigInt(data); return true; } catch { return false; } }, errors: true, }, (0, schema_1.getTreeNameKeyword)(), ...(0, schema_1.getAllWithdrawKeyword)(relayerConfig.rewardAccount), ...(0, schema_1.getAllEventsKeyword)(), ], }, }, trustProxy: relayerConfig.reverseProxy ? 1 : false, ignoreTrailingSlash: true, }); const admin = (0, fastify_1.fastify)(); this.app = app; this.admin = admin; this.messages = []; listenRouter(this); } } exports.Router = Router;