mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
WIP health service, error handling
This commit is contained in:
parent
ec2f20bfaf
commit
978c70de1e
@ -23,8 +23,7 @@ export function mainHandler(server: FastifyInstance, options, next) {
|
|||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const ethPrices = await priceService.getPrices();
|
const ethPrices = await priceService.getPrices();
|
||||||
const currentQueue = await jobService.getQueueCount();
|
const currentQueue = await jobService.getQueueCount();
|
||||||
const errorsLog = await healthService.getErrors();
|
const health = await healthService.getStatus();
|
||||||
console.log(currentQueue);
|
|
||||||
res.send({
|
res.send({
|
||||||
rewardAccount,
|
rewardAccount,
|
||||||
instances: configService.instances,
|
instances: configService.instances,
|
||||||
@ -33,11 +32,7 @@ export function mainHandler(server: FastifyInstance, options, next) {
|
|||||||
tornadoServiceFee,
|
tornadoServiceFee,
|
||||||
miningServiceFee: 0,
|
miningServiceFee: 0,
|
||||||
version: '4.5.0',
|
version: '4.5.0',
|
||||||
health: {
|
health,
|
||||||
status: 'true',
|
|
||||||
error: '',
|
|
||||||
errorsLog
|
|
||||||
},
|
|
||||||
currentQueue,
|
currentQueue,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
10
src/modules/utils.ts
Normal file
10
src/modules/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const parseJSON = (str: string) => {
|
||||||
|
let parsed = null;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(str);
|
||||||
|
if (typeof parsed === 'string') parsed = parseJSON(parsed);
|
||||||
|
return parsed;
|
||||||
|
} catch (e) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
};
|
@ -2,7 +2,14 @@ import { getHealthService } from '../services';
|
|||||||
import { Processor } from 'bullmq';
|
import { Processor } from 'bullmq';
|
||||||
|
|
||||||
export const healthProcessor: Processor = async () => {
|
export const healthProcessor: Processor = async () => {
|
||||||
const healthService = getHealthService();
|
const healthService = getHealthService();
|
||||||
|
|
||||||
|
try {
|
||||||
await healthService.check();
|
await healthService.check();
|
||||||
|
await healthService.setStatus({ status: true, error: '' });
|
||||||
|
} catch (e) {
|
||||||
|
await healthService.saveError(e);
|
||||||
|
await healthService.setStatus({ status: false, error: e.message });
|
||||||
}
|
}
|
||||||
;
|
};
|
||||||
|
|
||||||
|
@ -14,8 +14,7 @@ export const relayerProcessor: RelayerProcessor = async (job) => {
|
|||||||
const txData = await txService.prepareTxData(withdrawalData);
|
const txData = await txService.prepareTxData(withdrawalData);
|
||||||
return await txService.sendTx(txData);
|
return await txService.sendTx(txData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
|
||||||
await job.update({ ...job.data, status: JobStatus.FAILED });
|
await job.update({ ...job.data, status: JobStatus.FAILED });
|
||||||
throw new Error(e.message);
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,8 @@ import { configService, getHealthService } from '../services';
|
|||||||
|
|
||||||
export const priceWorker = async () => {
|
export const priceWorker = async () => {
|
||||||
await configService.init();
|
await configService.init();
|
||||||
|
const healthService = getHealthService();
|
||||||
|
|
||||||
const price = new PriceQueueHelper();
|
const price = new PriceQueueHelper();
|
||||||
price.scheduler.on('stalled', (jobId, prev) => console.log({ jobId, prev }));
|
price.scheduler.on('stalled', (jobId, prev) => console.log({ jobId, prev }));
|
||||||
console.log('price worker', price.queue.name);
|
console.log('price worker', price.queue.name);
|
||||||
@ -12,7 +14,7 @@ export const priceWorker = async () => {
|
|||||||
price.worker.on('completed', async (job, result) => {
|
price.worker.on('completed', async (job, result) => {
|
||||||
console.log(`Job ${job.id} completed with result: ${result}`);
|
console.log(`Job ${job.id} completed with result: ${result}`);
|
||||||
});
|
});
|
||||||
price.worker.on('failed', (job, error) => console.log(error));
|
price.worker.on('failed', (job, error) => healthService.saveError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const relayerWorker = async () => {
|
export const relayerWorker = async () => {
|
||||||
@ -25,7 +27,7 @@ export const relayerWorker = async () => {
|
|||||||
});
|
});
|
||||||
relayer.worker.on('failed', (job, error) => {
|
relayer.worker.on('failed', (job, error) => {
|
||||||
healthService.saveError(error);
|
healthService.saveError(error);
|
||||||
console.log(error);
|
// console.log(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,5 +39,8 @@ export const healthWorker = async () => {
|
|||||||
health.worker.on('completed', (job, result) => {
|
health.worker.on('completed', (job, result) => {
|
||||||
console.log(`Job ${job.id} completed with result: `, result);
|
console.log(`Job ${job.id} completed with result: `, result);
|
||||||
});
|
});
|
||||||
health.worker.on('failed', (job, error) => console.log(error));
|
health.worker.on('failed', (job, error) => {
|
||||||
|
|
||||||
|
// console.log(error);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
instances,
|
instances,
|
||||||
|
mainnetRpcUrl,
|
||||||
minimumBalance,
|
minimumBalance,
|
||||||
minimumTornBalance,
|
minimumTornBalance,
|
||||||
netId,
|
netId,
|
||||||
@ -39,6 +40,7 @@ export class ConfigService {
|
|||||||
isLightMode: boolean;
|
isLightMode: boolean;
|
||||||
instances: NetInstances;
|
instances: NetInstances;
|
||||||
provider: providers.JsonRpcProvider;
|
provider: providers.JsonRpcProvider;
|
||||||
|
mainnentProvider: providers.JsonRpcProvider;
|
||||||
wallet: Wallet;
|
wallet: Wallet;
|
||||||
public readonly netId: availableIds = netId;
|
public readonly netId: availableIds = netId;
|
||||||
public readonly privateKey = privateKey;
|
public readonly privateKey = privateKey;
|
||||||
@ -50,13 +52,13 @@ export class ConfigService {
|
|||||||
private _tokenContract: ERC20Abi;
|
private _tokenContract: ERC20Abi;
|
||||||
balances: { MAIN: { warn: string; critical: string; }; TORN: { warn: string; critical: string; }; };
|
balances: { MAIN: { warn: string; critical: string; }; TORN: { warn: string; critical: string; }; };
|
||||||
|
|
||||||
|
|
||||||
constructor(private store: RedisStore) {
|
constructor(private store: RedisStore) {
|
||||||
this.netIdKey = `netId${this.netId}`;
|
this.netIdKey = `netId${this.netId}`;
|
||||||
this.queueName = `relayer_${this.netId}`;
|
this.queueName = `relayer_${this.netId}`;
|
||||||
this.isLightMode = ![1, 5].includes(netId);
|
this.isLightMode = ![1, 5].includes(netId);
|
||||||
this.instances = instances[this.netIdKey];
|
this.instances = instances[this.netIdKey];
|
||||||
this.provider = getProvider(false);
|
this.provider = getProvider(false);
|
||||||
|
this.mainnentProvider = getProvider(false, mainnetRpcUrl, 1);
|
||||||
this.wallet = new Wallet(this.privateKey, this.provider);
|
this.wallet = new Wallet(this.privateKey, this.provider);
|
||||||
this.balances = {
|
this.balances = {
|
||||||
MAIN: { warn: BigNumber.from(minimumBalance).mul(150).div(100).toString(), critical: minimumBalance },
|
MAIN: { warn: BigNumber.from(minimumBalance).mul(150).div(100).toString(), critical: minimumBalance },
|
||||||
@ -85,18 +87,18 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkNetwork() {
|
async checkNetwork() {
|
||||||
try {
|
await this.provider.getNetwork();
|
||||||
await this.provider.getNetwork();
|
if (this.isLightMode) {
|
||||||
} catch (e) {
|
await this.mainnentProvider.getNetwork();
|
||||||
throw new Error(`Could not detect network, check your rpc url: ${this.rpcUrl}. ` + e.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
if (this.isInit) return;
|
if (this.isInit) return;
|
||||||
await this._checkNetwork();
|
await this.checkNetwork();
|
||||||
this._tokenAddress = await resolve(torn.torn.address);
|
this._tokenAddress = await resolve(torn.torn.address);
|
||||||
this._tokenContract = await getTornTokenContract(this._tokenAddress);
|
this._tokenContract = await getTornTokenContract(this._tokenAddress);
|
||||||
if (this.isLightMode) {
|
if (this.isLightMode) {
|
||||||
@ -130,12 +132,15 @@ export class ConfigService {
|
|||||||
this.isInit = true;
|
this.isInit = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`${this.constructor.name} Error:`, e.message);
|
console.error(`${this.constructor.name} Error:`, e.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearRedisState() {
|
async clearRedisState() {
|
||||||
const queueKeys = (await this.store.client.keys('bull:*')).filter(s => s.indexOf('relayer') === -1);
|
const queueKeys = (await this.store.client.keys('bull:*')).filter(s => s.indexOf('relayer') === -1);
|
||||||
await this.store.client.del(queueKeys);
|
const errorKeys = await this.store.client.keys('errors:*');
|
||||||
|
// const alertKeys = await this.store.client.keys('alerts:*');
|
||||||
|
await this.store.client.del([...queueKeys, ...errorKeys]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstance(address: string) {
|
getInstance(address: string) {
|
||||||
|
@ -3,27 +3,73 @@ import { ConfigService } from './config.service';
|
|||||||
import { RedisStore } from '../modules/redis';
|
import { RedisStore } from '../modules/redis';
|
||||||
import { formatEther } from 'ethers/lib/utils';
|
import { formatEther } from 'ethers/lib/utils';
|
||||||
|
|
||||||
|
class RelayerError extends Error {
|
||||||
|
constructor(message: string, code: string) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
@autoInjectable()
|
@autoInjectable()
|
||||||
export class HealthService {
|
export class HealthService {
|
||||||
|
|
||||||
constructor(private config: ConfigService, private store: RedisStore) {
|
constructor(private config: ConfigService, private store: RedisStore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearErrors() {
|
async clearErrors() {
|
||||||
await this.store.client.del('errors');
|
await this.store.client.del('errors:log', 'errors:code');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getErrors(): Promise<{ message: string, score: number }[]> {
|
private async _getErrors(): Promise<{ errorsLog: { message: string, score: number }[], errorsCode: Record<string, number> }> {
|
||||||
const set = await this.store.client.zrevrange('errors', 0, -1, 'WITHSCORES');
|
const logSet = await this.store.client.zrevrange('errors:log', 0, -1, 'WITHSCORES');
|
||||||
const errors = [];
|
const codeSet = await this.store.client.zrevrange('errors:code', 0, -1, 'WITHSCORES');
|
||||||
while (set.length) {
|
|
||||||
const [message, score] = set.splice(0, 2);
|
return { errorsLog: HealthService._parseSet(logSet), errorsCode: HealthService._parseSet(codeSet, 'object') };
|
||||||
errors.push({ message, score });
|
}
|
||||||
|
|
||||||
|
private async _getStatus() {
|
||||||
|
return this.store.client.hgetall('health:status');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static _parseSet(log, to = 'array', keys = ['message', 'score']) {
|
||||||
|
let out;
|
||||||
|
if (to === 'array') {
|
||||||
|
out = [];
|
||||||
|
while (log.length) {
|
||||||
|
const [a, b] = log.splice(0, 2);
|
||||||
|
out.push({ [keys[0]]: a, [keys[1]]: b });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out = {};
|
||||||
|
while (log.length) {
|
||||||
|
const [a, b] = log.splice(0, 2);
|
||||||
|
out[a] = Number(b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return errors;
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setStatus(status: { status: boolean; error: string; }) {
|
||||||
|
await this.store.client.hset('health:status', status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus() {
|
||||||
|
const heathStatus = await this._getStatus();
|
||||||
|
const { errorsLog, errorsCode } = await this._getErrors();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...heathStatus,
|
||||||
|
errorsLog,
|
||||||
|
errorsCode,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveError(e) {
|
async saveError(e) {
|
||||||
await this.store.client.zadd('errors', 'INCR', 1, e.message);
|
await this.store.client.zadd('errors:code', 'INCR', 1, e?.code || 'RUNTIME_ERROR');
|
||||||
|
await this.store.client.zadd('errors:log', 'INCR', 1, e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkBalance(value, currency: 'MAIN' | 'TORN') {
|
private async _checkBalance(value, currency: 'MAIN' | 'TORN') {
|
||||||
@ -37,26 +83,31 @@ export class HealthService {
|
|||||||
level = 'WARN';
|
level = 'WARN';
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSent = await this.store.client.sismember(`${key}:sent`, `${type}_${currency}_${level}`);
|
const alert = {
|
||||||
if (!isSent) {
|
type: `${type}_${currency}_${level}`,
|
||||||
const alert = {
|
message: `Insufficient balance ${formatEther(value)} ${currency === 'MAIN' ? this.config.nativeCurrency : 'torn'}`,
|
||||||
type: `${type}_${currency}_${level}`,
|
level,
|
||||||
message: `Insufficient balance ${formatEther(value)} ${currency === 'MAIN' ? this.config.nativeCurrency : 'torn'}`,
|
time,
|
||||||
level,
|
};
|
||||||
time,
|
await this.store.client.rpush(key, JSON.stringify(alert));
|
||||||
};
|
|
||||||
await this.store.client.rpush(key, JSON.stringify(alert));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return alert;
|
||||||
}
|
}
|
||||||
|
|
||||||
async check() {
|
async check() {
|
||||||
|
await this.config.checkNetwork();
|
||||||
const mainBalance = await this.config.wallet.getBalance();
|
const mainBalance = await this.config.wallet.getBalance();
|
||||||
const tornBalance = await this.config.tokenContract.balanceOf(this.config.wallet.address);
|
const tornBalance = await this.config.tokenContract.balanceOf(this.config.wallet.address);
|
||||||
// const mainBalance = BigNumber.from(`${2e18}`).add(1);
|
// const mainBalance = BigNumber.from(`${1e18}`).add(1);
|
||||||
// const tornBalance = BigNumber.from(`${50e18}`);
|
// const tornBalance = BigNumber.from(`${45e18}`);
|
||||||
await this._checkBalance(mainBalance, 'MAIN');
|
const mainStatus = await this._checkBalance(mainBalance, 'MAIN');
|
||||||
await this._checkBalance(tornBalance, 'TORN');
|
const tornStatus = await this._checkBalance(tornBalance, 'TORN');
|
||||||
|
if (mainStatus.level === 'CRITICAL') {
|
||||||
|
throw new RelayerError(mainStatus.message, 'INSUFFICIENT_MAIN_BALANCE');
|
||||||
|
}
|
||||||
|
if (tornStatus.level === 'CRITICAL') {
|
||||||
|
throw new RelayerError(tornStatus.message, 'INSUFFICIENT_TORN_BALANCE');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ export class PriceService {
|
|||||||
|
|
||||||
async fetchPrices(tokens: Token[]) {
|
async fetchPrices(tokens: Token[]) {
|
||||||
try {
|
try {
|
||||||
|
if (!tokens?.length) return;
|
||||||
const names = tokens.reduce((p, c) => {
|
const names = tokens.reduce((p, c) => {
|
||||||
p[c.address] = c.symbol.toLowerCase();
|
p[c.address] = c.symbol.toLowerCase();
|
||||||
return p;
|
return p;
|
||||||
|
@ -11,6 +11,7 @@ import { Job } from 'bullmq';
|
|||||||
import { RelayerJobData } from '../queue';
|
import { RelayerJobData } from '../queue';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
import { container, injectable } from 'tsyringe';
|
import { container, injectable } from 'tsyringe';
|
||||||
|
import { parseJSON } from '../modules/utils';
|
||||||
|
|
||||||
export type WithdrawalData = {
|
export type WithdrawalData = {
|
||||||
contract: string,
|
contract: string,
|
||||||
@ -25,6 +26,15 @@ export type WithdrawalData = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExecutionError extends Error {
|
||||||
|
constructor(message: string, code?: string) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class TxService {
|
export class TxService {
|
||||||
set currentJob(value: Job) {
|
set currentJob(value: Job) {
|
||||||
@ -39,9 +49,9 @@ export class TxService {
|
|||||||
|
|
||||||
constructor(private config: ConfigService, private priceService: PriceService) {
|
constructor(private config: ConfigService, private priceService: PriceService) {
|
||||||
const { privateKey, rpcUrl, netId } = this.config;
|
const { privateKey, rpcUrl, netId } = this.config;
|
||||||
this.txManager = new TxManager({ privateKey, rpcUrl, config: { THROW_ON_REVERT: true } });
|
|
||||||
this.tornadoProxy = this.config.proxyContract;
|
this.tornadoProxy = this.config.proxyContract;
|
||||||
this.provider = this.tornadoProxy.provider;
|
this.provider = this.tornadoProxy.provider;
|
||||||
|
this.txManager = new TxManager({ privateKey, rpcUrl, config: { THROW_ON_REVERT: true }, provider: this.provider });
|
||||||
this.oracle = new GasPriceOracle({
|
this.oracle = new GasPriceOracle({
|
||||||
defaultRpc: rpcUrl,
|
defaultRpc: rpcUrl,
|
||||||
chainId: netId,
|
chainId: netId,
|
||||||
@ -77,7 +87,11 @@ export class TxService {
|
|||||||
} else throw new Error('Submitted transaction failed');
|
} else throw new Error('Submitted transaction failed');
|
||||||
return receipt;
|
return receipt;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e.message);
|
const regex = /body=("\{.*}}")/;
|
||||||
|
if (regex.test(e.message)) {
|
||||||
|
const { error } = parseJSON(regex.exec(e.message)[1]);
|
||||||
|
throw new ExecutionError(error.message, 'REVERTED');
|
||||||
|
} else throw e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user