446 lines
14 KiB
TypeScript
446 lines
14 KiB
TypeScript
import path from 'path';
|
|
import { createReadStream } from 'fs';
|
|
import type { Logger } from 'winston';
|
|
import { fastify, FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
import { fastifyCors } from '@fastify/cors';
|
|
|
|
import {
|
|
NetIdType,
|
|
getConfig,
|
|
DepositsEvents,
|
|
WithdrawalsEvents,
|
|
EchoEvents,
|
|
EncryptedNotesEvents,
|
|
AllGovernanceEvents,
|
|
TornadoWithdrawParams,
|
|
RelayerTornadoWithdraw,
|
|
getActiveTokenInstances,
|
|
TovarishEventsStatus,
|
|
MAX_TOVARISH_EVENTS,
|
|
TovarishStatus,
|
|
TovarishEventsQuery,
|
|
BaseTovarishEvents,
|
|
AllRelayerRegistryEvents,
|
|
StakeBurnedEvents,
|
|
} from 'tornado-scripts';
|
|
|
|
import { isAddress, BigNumberish } from 'ethers';
|
|
|
|
import { RelayerConfig, version } from '../config';
|
|
import { getLogger } from './logger';
|
|
import { resolveMessages, sendMessage, SentMsg } from './routerMsg';
|
|
import { SyncManagerStatus } from './sync';
|
|
import { existsAsync, loadSavedEvents } from './data';
|
|
import { RelayerTornadoQueue } from './worker';
|
|
import {
|
|
getTreeNameKeyword,
|
|
getAllEventsKeyword,
|
|
getAllWithdrawKeyword,
|
|
getEventsSchema,
|
|
getWithdrawSchema,
|
|
idParamsSchema,
|
|
treeNameSchema,
|
|
} from './schema';
|
|
import { ErrorMessages } from './error';
|
|
|
|
export function getHealthStatus(netId: NetIdType, syncManagerStatus: SyncManagerStatus) {
|
|
const { events, tokenPrice, gasPrice } = syncManagerStatus.syncStatus[netId];
|
|
|
|
return String(Boolean(events && tokenPrice && gasPrice));
|
|
}
|
|
|
|
export function getGasPrices(netId: NetIdType, syncManagerStatus: SyncManagerStatus) {
|
|
const { gasPrice, l1Fee } = syncManagerStatus.cachedGasPrices[netId];
|
|
|
|
return {
|
|
fast: Number(gasPrice),
|
|
additionalProperties: l1Fee ? Number(l1Fee) : undefined,
|
|
};
|
|
}
|
|
|
|
export function formatStatus({
|
|
netId,
|
|
relayerConfig,
|
|
syncManagerStatus,
|
|
pendingWorks,
|
|
}: {
|
|
netId: NetIdType;
|
|
relayerConfig: RelayerConfig;
|
|
syncManagerStatus: SyncManagerStatus;
|
|
pendingWorks: number;
|
|
}): Omit<TovarishStatus, 'url'> {
|
|
const config = getConfig(netId);
|
|
|
|
return {
|
|
rewardAccount: relayerConfig.rewardAccount,
|
|
instances: 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,
|
|
health: {
|
|
status: getHealthStatus(netId, syncManagerStatus),
|
|
error: '',
|
|
errorsLog: [...syncManagerStatus.errors.filter((e) => e.netId === netId)],
|
|
},
|
|
syncStatus: syncManagerStatus.syncStatus[netId],
|
|
onSyncEvents: syncManagerStatus.onSyncEvents,
|
|
currentQueue: pendingWorks,
|
|
};
|
|
}
|
|
|
|
export function handleIndex(enabledNetworks: NetIdType[]) {
|
|
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'
|
|
);
|
|
}
|
|
|
|
export async function handleStatus(router: Router, netId: NetIdType | NetIdType[], reply: FastifyReply) {
|
|
const { relayerConfig } = router;
|
|
|
|
const { syncManagerStatus, pendingWorks } = await sendMessage<{
|
|
syncManagerStatus: SyncManagerStatus;
|
|
pendingWorks: number;
|
|
}>(router, { type: 'status' });
|
|
|
|
if (Array.isArray(netId)) {
|
|
reply.send(
|
|
netId.map((n) =>
|
|
formatStatus({
|
|
netId: n,
|
|
relayerConfig,
|
|
syncManagerStatus,
|
|
pendingWorks,
|
|
}),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
reply.send(
|
|
formatStatus({
|
|
netId,
|
|
relayerConfig,
|
|
syncManagerStatus,
|
|
pendingWorks,
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Since we check gasLimit and fees, should extend timeout at any proxy more than 60s
|
|
*/
|
|
export async function handleTornadoWithdraw(
|
|
router: Router,
|
|
netId: NetIdType,
|
|
req: FastifyRequest,
|
|
reply: FastifyReply,
|
|
) {
|
|
const { contract, proof, args } = req.body as unknown as TornadoWithdrawParams;
|
|
|
|
const { id, error } = await sendMessage<RelayerTornadoWithdraw>(router, {
|
|
type: 'tornadoWithdraw',
|
|
netId,
|
|
contract,
|
|
proof,
|
|
args,
|
|
});
|
|
|
|
if (error) {
|
|
reply.send({ error });
|
|
return;
|
|
}
|
|
|
|
reply.send({ id });
|
|
}
|
|
|
|
export async function handleGetJob(router: Router, req: FastifyRequest, reply: FastifyReply) {
|
|
const { id } = req.params as unknown as { id: string };
|
|
|
|
const job = await sendMessage<{ error: string } | RelayerTornadoQueue>(router, { type: 'job', id });
|
|
|
|
if (job.error) {
|
|
reply.send(job);
|
|
return;
|
|
}
|
|
|
|
reply.send(job as RelayerTornadoQueue);
|
|
}
|
|
|
|
export type AllTovarishEvents =
|
|
| DepositsEvents
|
|
| WithdrawalsEvents
|
|
| EchoEvents
|
|
| EncryptedNotesEvents
|
|
| AllGovernanceEvents
|
|
| AllRelayerRegistryEvents
|
|
| StakeBurnedEvents;
|
|
|
|
export async function handleEvents(router: Router, netId: NetIdType, req: FastifyRequest, reply: FastifyReply) {
|
|
const {
|
|
relayerConfig: { userEventsDir: userDirectory },
|
|
} = router;
|
|
const { type, currency, amount, fromBlock, recent } = req.body as unknown as TovarishEventsQuery;
|
|
|
|
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 existsAsync(path.join(userDirectory, `${name}.json`)))) {
|
|
reply.send({
|
|
events: [],
|
|
lastSyncBlock: fromBlock,
|
|
} as BaseTovarishEvents<AllTovarishEvents>);
|
|
return;
|
|
}
|
|
|
|
const { syncManagerStatus } = await sendMessage<{
|
|
syncManagerStatus: SyncManagerStatus;
|
|
}>(router, { type: 'status' });
|
|
|
|
const lastSyncBlock = Number(
|
|
['deposit', 'withdrawal'].includes(type)
|
|
? syncManagerStatus.cachedEvents[netId]?.tornado?.lastBlock
|
|
: syncManagerStatus.cachedEvents[netId]?.[String(type) as keyof TovarishEventsStatus]?.lastBlock,
|
|
);
|
|
|
|
if (type === 'deposit' && recent) {
|
|
const { events } = await loadSavedEvents<AllTovarishEvents>({
|
|
name: 'recent_' + name,
|
|
userDirectory,
|
|
});
|
|
|
|
reply.send({
|
|
events,
|
|
lastSyncBlock,
|
|
} as BaseTovarishEvents<AllTovarishEvents>);
|
|
return;
|
|
}
|
|
|
|
const { events } = await loadSavedEvents<AllTovarishEvents>({
|
|
name,
|
|
userDirectory,
|
|
});
|
|
|
|
if (recent) {
|
|
reply.send({
|
|
events: events.slice(-10).reverse(),
|
|
lastSyncBlock,
|
|
} as BaseTovarishEvents<AllTovarishEvents>);
|
|
return;
|
|
}
|
|
|
|
reply.send({
|
|
events: events.filter((e) => e.blockNumber >= (fromBlock || 0)).slice(0, MAX_TOVARISH_EVENTS),
|
|
lastSyncBlock,
|
|
} as BaseTovarishEvents<AllTovarishEvents>);
|
|
}
|
|
|
|
export async function handleTrees(router: Router, req: FastifyRequest, reply: FastifyReply) {
|
|
const treeRegex = /deposits_(?<netId>\d+)_(?<currency>\w+)_(?<amount>[\d.]+)_(?<part>\w+).json.zip/g;
|
|
const { netId, currency, amount, part } =
|
|
treeRegex.exec((req.params as unknown as { treeName: string }).treeName)?.groups || {};
|
|
|
|
const treeName = `deposits_${netId}_${currency}_${amount}_${part}.json.zip`;
|
|
const treePath = path.join(router.relayerConfig.userTreeDir, treeName);
|
|
|
|
if (!(await existsAsync(treePath))) {
|
|
reply.status(404).send(`Tree ${treeName} not found!`);
|
|
return;
|
|
}
|
|
|
|
reply.send(createReadStream(treePath));
|
|
}
|
|
|
|
export function listenRouter(router: Router) {
|
|
const { relayerConfig, logger, app, admin, forkId } = router;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
app.register(fastifyCors, () => (req: FastifyRequest, callback: any) => {
|
|
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(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(router, netId, reply);
|
|
});
|
|
|
|
const withdrawSchema = getWithdrawSchema(netId);
|
|
|
|
app.post(`/${netId}/relay`, { schema: withdrawSchema }, (req, reply) => {
|
|
handleTornadoWithdraw(router, netId, req, reply);
|
|
});
|
|
|
|
app.get(`/${netId}/v1/status`, (req, reply) => {
|
|
handleStatus(router, netId, reply);
|
|
});
|
|
|
|
app.post(`/${netId}/v1/tornadoWithdraw`, { schema: withdrawSchema }, (req, reply) => {
|
|
handleTornadoWithdraw(router, netId, req, reply);
|
|
});
|
|
|
|
app.get(`/${netId}/v1/jobs/:id`, { schema: idParamsSchema }, (req, reply) => {
|
|
handleGetJob(router, req, reply);
|
|
});
|
|
|
|
const eventSchema = getEventsSchema(netId);
|
|
|
|
app.post(`/${netId}/events`, { schema: eventSchema }, (req, reply) => {
|
|
handleEvents(router, netId, req, reply);
|
|
});
|
|
|
|
if (relayerConfig.enableTrees) {
|
|
app.get(`/${netId}/trees/:treeName`, { schema: 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 sendMessage<{
|
|
errors: ErrorMessages[];
|
|
}>(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}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
resolveMessages(router);
|
|
}
|
|
|
|
export class Router {
|
|
relayerConfig: RelayerConfig;
|
|
logger: Logger;
|
|
forkId: number;
|
|
|
|
app: FastifyInstance;
|
|
|
|
// For viewing error logs
|
|
admin: FastifyInstance;
|
|
|
|
messages: SentMsg[];
|
|
|
|
constructor(relayerConfig: RelayerConfig, forkId = 0) {
|
|
this.relayerConfig = relayerConfig;
|
|
this.logger = getLogger(`[Router ${forkId}]`, relayerConfig.logLevel);
|
|
this.forkId = forkId;
|
|
|
|
const app = fastify({
|
|
ajv: {
|
|
customOptions: {
|
|
keywords: [
|
|
{
|
|
keyword: 'isAddress',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
validate: (schema: any, data: string) => {
|
|
try {
|
|
return isAddress(data);
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
errors: true,
|
|
},
|
|
{
|
|
keyword: 'BN',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
validate: (schema: any, data: BigNumberish) => {
|
|
try {
|
|
BigInt(data);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
errors: true,
|
|
},
|
|
getTreeNameKeyword(),
|
|
...getAllWithdrawKeyword(relayerConfig.rewardAccount),
|
|
...getAllEventsKeyword(),
|
|
],
|
|
},
|
|
},
|
|
trustProxy: relayerConfig.reverseProxy ? 1 : false,
|
|
ignoreTrailingSlash: true,
|
|
});
|
|
|
|
const admin = fastify();
|
|
|
|
this.app = app;
|
|
this.admin = admin;
|
|
this.messages = [];
|
|
|
|
listenRouter(this);
|
|
}
|
|
}
|