tovarish-relayer/lib/services/router.js

312 lines
12 KiB
JavaScript
Raw Normal View History

2024-11-13 04:42:13 +03:00
"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 <a href=https://tornado.ws>Tornado Cash</a> Relayer service. Check the ' +
enabledNetworks.map((netId) => `<a href=/${netId}/v1/status>/${netId}/v1/status</a> `).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 = [core_1.DEPOSIT, core_1.WITHDRAWAL].includes(type) ? `${type}s_${netId}_${currency}_${amount}` : `${type}_${netId}`;
// Can return 0 events but we just return error codes here
if (!(await (0, data_1.existsAsync)(path_1.default.join(userDirectory, `${name}.json`)))) {
reply.code(404).send(`Events ${name} not found!`);
return;
}
const { syncManagerStatus } = await (0, routerMsg_1.sendMessage)(router, { type: 'status' });
const lastSyncBlock = Number([core_1.DEPOSIT, core_1.WITHDRAWAL].includes(type)
? syncManagerStatus.cachedEvents[netId]?.instances?.[String(currency)]?.[String(amount)]?.[`${type}s`]?.lastBlock
: syncManagerStatus.cachedEvents[netId]?.[String(type)]?.lastBlock);
const { events } = await (0, data_1.loadSavedEvents)({
name,
userDirectory,
});
if (recent) {
reply.send({
events: events.slice(-10).sort((a, b) => {
if (a.blockNumber === b.blockNumber) {
return b.logIndex - a.logIndex;
}
return b.blockNumber - a.blockNumber;
}),
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_(?<netId>\d+)_(?<currency>\w+)_(?<amount>[\d.]+)_(?<part>\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;