312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
|
"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;
|