WIP monitoring and alerts
This commit is contained in:
parent
3a74ebf90e
commit
03f9bfac45
@ -22,3 +22,6 @@ CONFIRMATIONS=4
|
|||||||
MAX_GAS_PRICE=1000
|
MAX_GAS_PRICE=1000
|
||||||
BASE_FEE_RESERVE_PERCENTAGE=25
|
BASE_FEE_RESERVE_PERCENTAGE=25
|
||||||
AGGREGATOR=0x8cb1436F64a3c33aD17bb42F94e255c4c0E871b2
|
AGGREGATOR=0x8cb1436F64a3c33aD17bb42F94e255c4c0E871b2
|
||||||
|
# Telegram bot alerts
|
||||||
|
TELEGRAM_NOTIFIER_BOT_TOKEN=
|
||||||
|
TELEGRAM_NOTIFIER_CHAT_ID=
|
||||||
|
@ -2,50 +2,23 @@ version: '2'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
server:
|
||||||
image: tornadocash/relayer:mining
|
image: tornadocash/relayer
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
REDIS_URL: redis://redis/0
|
REDIS_URL: redis://redis/0
|
||||||
nginx_proxy_read_timeout: 600
|
nginx_proxy_read_timeout: 600
|
||||||
depends_on: [redis]
|
depends_on: [ redis ]
|
||||||
|
|
||||||
treeWatcher:
|
|
||||||
image: tornadocash/relayer:mining
|
|
||||||
restart: always
|
|
||||||
command: treeWatcher
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
REDIS_URL: redis://redis/0
|
|
||||||
depends_on: [redis]
|
|
||||||
|
|
||||||
priceWatcher:
|
|
||||||
image: tornadocash/relayer:mining
|
|
||||||
restart: always
|
|
||||||
command: priceWatcher
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
REDIS_URL: redis://redis/0
|
|
||||||
depends_on: [redis]
|
|
||||||
|
|
||||||
healthWatcher:
|
|
||||||
image: tornadocash/relayer:mining
|
|
||||||
restart: always
|
|
||||||
command: healthWatcher
|
|
||||||
env_file: .env
|
|
||||||
environment:
|
|
||||||
REDIS_URL: redis://redis/0
|
|
||||||
depends_on: [redis]
|
|
||||||
|
|
||||||
worker1:
|
worker1:
|
||||||
image: tornadocash/relayer:mining
|
image: tornadocash/relayer
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env
|
env_file: .env
|
||||||
environment:
|
environment:
|
||||||
REDIS_URL: redis://redis/0
|
REDIS_URL: redis://redis/0
|
||||||
depends_on: [redis]
|
depends_on: [ redis ]
|
||||||
|
|
||||||
# worker2:
|
# worker2:
|
||||||
# image: tornadocash/relayer:mining
|
# image: tornadocash/relayer:mining
|
||||||
@ -92,28 +65,21 @@ services:
|
|||||||
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
|
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
|
||||||
# TELEGRAM_NOTIFIER_CHAT_ID: ...
|
# TELEGRAM_NOTIFIER_CHAT_ID: ...
|
||||||
|
|
||||||
# # this container will send Telegram notifications if specified address doesn't have enough funds
|
|
||||||
# monitor_mainnet:
|
|
||||||
# image: peppersec/monitor_eth
|
|
||||||
# restart: always
|
|
||||||
# environment:
|
|
||||||
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
|
|
||||||
# TELEGRAM_NOTIFIER_CHAT_ID: ...
|
|
||||||
# ADDRESS: '0x0000000000000000000000000000000000000000'
|
|
||||||
# THRESHOLD: 0.5 # ETH
|
|
||||||
# RPC_URL: https://mainnet.infura.io
|
|
||||||
# BLOCK_EXPLORER: etherscan.io
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
restart: always
|
restart: always
|
||||||
command: [redis-server, --appendonly, 'yes']
|
command: [ redis-server, '/usr/local/etc/redis/redis.conf', --appendonly, 'yes', ]
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./redis.conf:/usr/local/etc/redis/redis.conf
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: nginx
|
# container_name: nginx
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
|
5
redis.conf
Normal file
5
redis.conf
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
timeout 0
|
||||||
|
tcp-keepalive 0
|
||||||
|
databases 1
|
||||||
|
save 60 100000
|
||||||
|
notify-keyspace-events KAE
|
@ -3,7 +3,7 @@ import createServer from './server';
|
|||||||
import { utils } from 'ethers';
|
import { utils } from 'ethers';
|
||||||
import { port, rewardAccount } from '../config';
|
import { port, rewardAccount } from '../config';
|
||||||
import { version } from '../../package.json';
|
import { version } from '../../package.json';
|
||||||
import { configService, getJobService } from '../services';
|
import { configService, getJobService, getNotifierService } from '../services';
|
||||||
|
|
||||||
|
|
||||||
if (!utils.isAddress(rewardAccount)) {
|
if (!utils.isAddress(rewardAccount)) {
|
||||||
@ -14,6 +14,8 @@ server.listen(port, '0.0.0.0', async (err, address) => {
|
|||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
await configService.init();
|
await configService.init();
|
||||||
await getJobService().setupRepeatableJobs();
|
await getJobService().setupRepeatableJobs();
|
||||||
|
await getNotifierService().subscribe();
|
||||||
|
|
||||||
console.log(`Relayer ${version} started on port ${address}`);
|
console.log(`Relayer ${version} started on port ${address}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@ import { FastifyInstance } from 'fastify';
|
|||||||
import { jobsSchema, statusSchema, withdrawBodySchema, withdrawSchema } from './schema';
|
import { jobsSchema, statusSchema, withdrawBodySchema, withdrawSchema } from './schema';
|
||||||
import { FromSchema } from 'json-schema-to-ts';
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
import { rewardAccount, tornadoServiceFee } from '../config';
|
import { rewardAccount, tornadoServiceFee } from '../config';
|
||||||
import { version } from '../../package.json';
|
import { configService, getHealthService, getJobService, getPriceService } from '../services';
|
||||||
import { configService, getJobService, getPriceService } from '../services';
|
|
||||||
import { RelayerJobType } from '../types';
|
import { RelayerJobType } from '../types';
|
||||||
|
|
||||||
|
|
||||||
export function mainHandler(server: FastifyInstance, options, next) {
|
export function mainHandler(server: FastifyInstance, options, next) {
|
||||||
const jobService = getJobService();
|
const jobService = getJobService();
|
||||||
const priceService = getPriceService();
|
const priceService = getPriceService();
|
||||||
|
const healthService = getHealthService();
|
||||||
|
|
||||||
server.get('/',
|
server.get('/',
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
@ -23,6 +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();
|
||||||
console.log(currentQueue);
|
console.log(currentQueue);
|
||||||
res.send({
|
res.send({
|
||||||
rewardAccount,
|
rewardAccount,
|
||||||
@ -31,10 +32,11 @@ export function mainHandler(server: FastifyInstance, options, next) {
|
|||||||
ethPrices,
|
ethPrices,
|
||||||
tornadoServiceFee,
|
tornadoServiceFee,
|
||||||
miningServiceFee: 0,
|
miningServiceFee: 0,
|
||||||
version,
|
version: '4.5.0',
|
||||||
health: {
|
health: {
|
||||||
status: true,
|
status: 'true',
|
||||||
error: '',
|
error: '',
|
||||||
|
errorsLog
|
||||||
},
|
},
|
||||||
currentQueue,
|
currentQueue,
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ export const gasLimits = {
|
|||||||
[RelayerJobType.MINING_WITHDRAW]: 400000,
|
[RelayerJobType.MINING_WITHDRAW]: 400000,
|
||||||
};
|
};
|
||||||
export const minimumBalance = '1000000000000000000';
|
export const minimumBalance = '1000000000000000000';
|
||||||
export const minimumTornBalance = '50000000000000000000';
|
export const minimumTornBalance = '30000000000000000000';
|
||||||
export const baseFeeReserve = Number(process.env.BASE_FEE_RESERVE_PERCENTAGE);
|
export const baseFeeReserve = Number(process.env.BASE_FEE_RESERVE_PERCENTAGE);
|
||||||
export const tornToken = {
|
export const tornToken = {
|
||||||
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
tokenAddress: '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||||
|
@ -6,6 +6,13 @@ const getNewInstance: () => Redis = () => new IORedis(redisUrl, { maxRetriesPerR
|
|||||||
|
|
||||||
@singleton()
|
@singleton()
|
||||||
export class RedisStore {
|
export class RedisStore {
|
||||||
|
get publisher(): Redis {
|
||||||
|
if (!this._publisher) {
|
||||||
|
this._publisher = getNewInstance();
|
||||||
|
}
|
||||||
|
return this._publisher;
|
||||||
|
}
|
||||||
|
|
||||||
get client() {
|
get client() {
|
||||||
if (!this._client) {
|
if (!this._client) {
|
||||||
this._client = getNewInstance();
|
this._client = getNewInstance();
|
||||||
@ -20,8 +27,9 @@ export class RedisStore {
|
|||||||
return this._subscriber;
|
return this._subscriber;
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscriber: Redis;
|
private _subscriber: Redis;
|
||||||
_client: Redis;
|
private _publisher: Redis;
|
||||||
|
private _client: Redis;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
src/queue/health.processor.ts
Normal file
8
src/queue/health.processor.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { getHealthService } from '../services';
|
||||||
|
import { Processor } from 'bullmq';
|
||||||
|
|
||||||
|
export const healthProcessor: Processor = async () => {
|
||||||
|
const healthService = getHealthService();
|
||||||
|
await healthService.check();
|
||||||
|
}
|
||||||
|
;
|
@ -1,16 +1,18 @@
|
|||||||
import { Processor, Queue, QueueScheduler, Worker } from 'bullmq';
|
import { Processor, Queue, QueueScheduler, Worker } from 'bullmq';
|
||||||
import { JobStatus, RelayerJobType, Token } from '../types';
|
import { JobStatus, RelayerJobType, Token } from '../types';
|
||||||
import { WithdrawalData } from '../services/tx.service';
|
import { WithdrawalData } from '../services/tx.service';
|
||||||
import { BigNumber } from 'ethers';
|
|
||||||
import { priceProcessor } from './price.processor';
|
import { priceProcessor } from './price.processor';
|
||||||
import { autoInjectable } from 'tsyringe';
|
import { autoInjectable } from 'tsyringe';
|
||||||
import { RedisStore } from '../modules/redis';
|
import { RedisStore } from '../modules/redis';
|
||||||
import { ConfigService } from '../services/config.service';
|
import { ConfigService } from '../services/config.service';
|
||||||
import { relayerProcessor } from './relayer.processor';
|
import { relayerProcessor } from './relayer.processor';
|
||||||
|
import { healthProcessor } from './health.processor';
|
||||||
|
|
||||||
type PriceJobData = Token[]
|
type PriceJobData = Token[]
|
||||||
type PriceJobReturn = number
|
type PriceJobReturn = number
|
||||||
type HealthJobReturn = { balance: BigNumber, isEnought: boolean }
|
|
||||||
|
type HealthJobReturn = void
|
||||||
|
type HealthJobData = null
|
||||||
|
|
||||||
export type RelayerJobData =
|
export type RelayerJobData =
|
||||||
WithdrawalData
|
WithdrawalData
|
||||||
@ -109,4 +111,51 @@ export class RelayerQueueHelper {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autoInjectable()
|
||||||
|
export class HealthQueueHelper {
|
||||||
|
|
||||||
|
private _queue: Queue<HealthJobData, HealthJobReturn, 'checkHealth'>;
|
||||||
|
private _worker: Worker<HealthJobData, HealthJobReturn, 'checkHealth'>;
|
||||||
|
private _scheduler: QueueScheduler;
|
||||||
|
|
||||||
|
constructor(private store?: RedisStore, private config?: ConfigService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
get scheduler(): QueueScheduler {
|
||||||
|
if (!this._scheduler) {
|
||||||
|
this._scheduler = new QueueScheduler('health', { connection: this.store.client });
|
||||||
|
}
|
||||||
|
return this._scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
get worker() {
|
||||||
|
if (!this._worker) {
|
||||||
|
this._worker = new Worker<HealthJobData, HealthJobReturn, 'checkHealth'>('health', healthProcessor, {
|
||||||
|
connection: this.store.client,
|
||||||
|
concurrency: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
get queue() {
|
||||||
|
if (!this._queue) {
|
||||||
|
this._queue = new Queue<HealthJobData, HealthJobReturn, 'checkHealth'>('health', {
|
||||||
|
connection: this.store.client,
|
||||||
|
defaultJobOptions: { stackTraceLimit: 100 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addRepeatable() {
|
||||||
|
await this.queue.add('checkHealth', null, {
|
||||||
|
repeat: {
|
||||||
|
every: 30000,
|
||||||
|
immediately: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -4,5 +4,7 @@ import { PriceProcessor } from './index';
|
|||||||
export const priceProcessor: PriceProcessor = async (job) => {
|
export const priceProcessor: PriceProcessor = async (job) => {
|
||||||
const priceService = getPriceService();
|
const priceService = getPriceService();
|
||||||
const result = await priceService.fetchPrices(job.data);
|
const result = await priceService.fetchPrices(job.data);
|
||||||
return await priceService.savePrices(result);
|
if (result) return await priceService.savePrices(result);
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { configService } from '../services';
|
|
||||||
import { Processor } from 'bullmq';
|
|
||||||
|
|
||||||
export const checkBalance: Processor = async (job) => {
|
|
||||||
return await configService.getBalance();
|
|
||||||
};
|
|
@ -1,11 +1,12 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { PriceQueueHelper, RelayerQueueHelper } from './';
|
import { HealthQueueHelper, PriceQueueHelper, RelayerQueueHelper } from './';
|
||||||
import { configService } from '../services';
|
import { configService, getHealthService } from '../services';
|
||||||
|
|
||||||
|
|
||||||
export const schedulerWorker = async () => {
|
export const priceWorker = async () => {
|
||||||
await configService.init();
|
await configService.init();
|
||||||
const price = new PriceQueueHelper();
|
const price = new PriceQueueHelper();
|
||||||
|
price.scheduler.on('stalled', (jobId, prev) => console.log({ jobId, prev }));
|
||||||
console.log('price worker', price.queue.name);
|
console.log('price worker', price.queue.name);
|
||||||
price.worker.on('active', () => console.log('worker active'));
|
price.worker.on('active', () => console.log('worker active'));
|
||||||
price.worker.on('completed', async (job, result) => {
|
price.worker.on('completed', async (job, result) => {
|
||||||
@ -17,9 +18,24 @@ export const schedulerWorker = async () => {
|
|||||||
export const relayerWorker = async () => {
|
export const relayerWorker = async () => {
|
||||||
await configService.init();
|
await configService.init();
|
||||||
const relayer = new RelayerQueueHelper();
|
const relayer = new RelayerQueueHelper();
|
||||||
|
const healthService = getHealthService();
|
||||||
console.log(relayer.queue.name, 'worker started');
|
console.log(relayer.queue.name, 'worker started');
|
||||||
relayer.worker.on('completed', (job, result) => {
|
relayer.worker.on('completed', (job, result) => {
|
||||||
console.log(`Job ${job.id} completed with result: `, result);
|
console.log(`Job ${job.id} completed with result: `, result);
|
||||||
});
|
});
|
||||||
relayer.worker.on('failed', (job, error) => console.log(error));
|
relayer.worker.on('failed', (job, error) => {
|
||||||
|
healthService.saveError(error);
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const healthWorker = async () => {
|
||||||
|
await configService.init();
|
||||||
|
const health = new HealthQueueHelper();
|
||||||
|
health.scheduler.on('stalled', (jobId, prev) => console.log({ jobId, prev }));
|
||||||
|
console.log(health.queue.name, 'worker started');
|
||||||
|
health.worker.on('completed', (job, result) => {
|
||||||
|
console.log(`Job ${job.id} completed with result: `, result);
|
||||||
|
});
|
||||||
|
health.worker.on('failed', (job, error) => console.log(error));
|
||||||
};
|
};
|
||||||
|
15
src/sandbox.ts
Normal file
15
src/sandbox.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { configService, getHealthService } from './services';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await configService.init();
|
||||||
|
const healthService = getHealthService();
|
||||||
|
console.log(healthService);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Top level catch', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -20,11 +20,10 @@ import {
|
|||||||
import { resolve } from '../modules';
|
import { resolve } from '../modules';
|
||||||
import { ERC20Abi, ProxyLightABI, TornadoProxyABI } from '../../contracts';
|
import { ERC20Abi, ProxyLightABI, TornadoProxyABI } from '../../contracts';
|
||||||
import { availableIds, netIds, NetInstances } from '../../../torn-token';
|
import { availableIds, netIds, NetInstances } from '../../../torn-token';
|
||||||
import { formatEther, getAddress } from 'ethers/lib/utils';
|
import { getAddress } from 'ethers/lib/utils';
|
||||||
import { providers, Wallet } from 'ethers';
|
import { BigNumber, providers, Wallet } from 'ethers';
|
||||||
import { container, singleton } from 'tsyringe';
|
import { container, singleton } from 'tsyringe';
|
||||||
import { GasPrice } from 'gas-price-oracle/lib/types';
|
import { GasPrice } from 'gas-price-oracle/lib/types';
|
||||||
import { configService } from './index';
|
|
||||||
|
|
||||||
type relayerQueueName = `relayer_${availableIds}`
|
type relayerQueueName = `relayer_${availableIds}`
|
||||||
|
|
||||||
@ -48,6 +47,7 @@ export class ConfigService {
|
|||||||
fallbackGasPrices: GasPrice;
|
fallbackGasPrices: GasPrice;
|
||||||
private _tokenAddress: string;
|
private _tokenAddress: string;
|
||||||
private _tokenContract: ERC20Abi;
|
private _tokenContract: ERC20Abi;
|
||||||
|
balances: { MAIN: { warn: string; critical: string; }; TORN: { warn: string; critical: string; }; };
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -57,6 +57,10 @@ export class ConfigService {
|
|||||||
this.instances = instances[this.netIdKey];
|
this.instances = instances[this.netIdKey];
|
||||||
this.provider = getProvider(false);
|
this.provider = getProvider(false);
|
||||||
this.wallet = new Wallet(this.privateKey, this.provider);
|
this.wallet = new Wallet(this.privateKey, this.provider);
|
||||||
|
this.balances = {
|
||||||
|
MAIN: { warn: BigNumber.from(minimumBalance).mul(150).div(100).toString(), critical: minimumBalance },
|
||||||
|
TORN: { warn: BigNumber.from(minimumTornBalance).mul(2).toString(), critical: minimumTornBalance },
|
||||||
|
};
|
||||||
this._fillInstanceMap();
|
this._fillInstanceMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,21 +68,19 @@ export class ConfigService {
|
|||||||
return this._proxyContract;
|
return this._proxyContract;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get tokenContract(): ERC20Abi {
|
||||||
|
return this._tokenContract;
|
||||||
|
}
|
||||||
|
|
||||||
private _fillInstanceMap() {
|
private _fillInstanceMap() {
|
||||||
if (!this.instances) throw new Error('config mismatch, check your environment variables');
|
if (!this.instances) throw new Error('config mismatch, check your environment variables');
|
||||||
// TODO
|
// TODO
|
||||||
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(this.instances)) {
|
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(this.instances)) {
|
||||||
Object.entries(instanceAddress).forEach(([amount, address]) => {
|
for (const [amount, address] of Object.entries(instanceAddress)) {
|
||||||
if (address) {
|
if (address) this.addressMap.set(getAddress(address), {
|
||||||
this.addressMap.set(getAddress(address), {
|
currency, amount, symbol, decimals,
|
||||||
currency,
|
|
||||||
amount,
|
|
||||||
symbol,
|
|
||||||
decimals,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +106,7 @@ export class ConfigService {
|
|||||||
this.fallbackGasPrices = gasPrices;
|
this.fallbackGasPrices = gasPrices;
|
||||||
} else {
|
} else {
|
||||||
this._proxyAddress = tornadoGoerliProxy;
|
this._proxyAddress = tornadoGoerliProxy;
|
||||||
|
this.nativeCurrency = 'eth';
|
||||||
if (this.netId === 1) {
|
if (this.netId === 1) {
|
||||||
this._proxyAddress = await resolve(torn.tornadoRouter.address);
|
this._proxyAddress = await resolve(torn.tornadoRouter.address);
|
||||||
}
|
}
|
||||||
@ -117,18 +120,12 @@ export class ConfigService {
|
|||||||
decimals: el.decimals,
|
decimals: el.decimals,
|
||||||
symbol: el.symbol,
|
symbol: el.symbol,
|
||||||
})).filter(Boolean);
|
})).filter(Boolean);
|
||||||
const { balance } = await configService.getBalance();
|
|
||||||
const { balance: tornBalance } = await configService.getTornBalance();
|
|
||||||
console.log(
|
console.log(
|
||||||
'Configuration completed\n',
|
'Configuration completed\n',
|
||||||
`-- netId: ${this.netId}\n`,
|
`-- netId: ${this.netId}\n`,
|
||||||
`-- rpcUrl: ${this.rpcUrl}\n`,
|
`-- rpcUrl: ${this.rpcUrl}\n`,
|
||||||
`-- relayer Address: ${this.wallet.address}\n`,
|
`-- relayer Address: ${this.wallet.address}\n`,
|
||||||
`-- relayer Balance: ${formatEther(balance)}\n`,
|
|
||||||
`-- relayer Torn balance: ${formatEther(tornBalance)}\n`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
@ -139,18 +136,6 @@ export class ConfigService {
|
|||||||
return this.addressMap.get(getAddress(address));
|
return this.addressMap.get(getAddress(address));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBalance() {
|
|
||||||
const balance = await this.wallet.getBalance();
|
|
||||||
const isEnougth = balance.gt(minimumBalance);
|
|
||||||
return { balance, isEnougth };
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTornBalance() {
|
|
||||||
const balance = await this._tokenContract.balanceOf(this.wallet.address);
|
|
||||||
const isEnougth = balance.gt(minimumTornBalance);
|
|
||||||
return { balance, isEnougth };
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InstanceProps = {
|
type InstanceProps = {
|
||||||
|
70
src/services/health.service.ts
Normal file
70
src/services/health.service.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { autoInjectable, container } from 'tsyringe';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { RedisStore } from '../modules/redis';
|
||||||
|
import { formatEther } from 'ethers/lib/utils';
|
||||||
|
|
||||||
|
@autoInjectable()
|
||||||
|
export class HealthService {
|
||||||
|
constructor(private config: ConfigService, private store: RedisStore) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearErrors() {
|
||||||
|
await this.store.client.del('errors');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getErrors(): Promise<{ message: string, score: number }[]> {
|
||||||
|
const set = await this.store.client.zrevrange('errors', 0, -1, 'WITHSCORES');
|
||||||
|
const errors = [];
|
||||||
|
while (set.length) {
|
||||||
|
const [message, score] = set.splice(0, 2);
|
||||||
|
errors.push({ message, score });
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveError(e) {
|
||||||
|
await this.store.client.zadd('errors', 'INCR', 1, e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _checkBalance(value, currency: 'MAIN' | 'TORN') {
|
||||||
|
let level = 'OK';
|
||||||
|
const type = 'BALANCE';
|
||||||
|
const key = 'alerts';
|
||||||
|
const time = new Date().getTime();
|
||||||
|
if (value.lt(this.config.balances[currency].critical)) {
|
||||||
|
level = 'CRITICAL';
|
||||||
|
} else if (value.lt(this.config.balances[currency].warn)) {
|
||||||
|
level = 'WARN';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSent = await this.store.client.sismember(`${key}:sent`, `${type}_${currency}_${level}`);
|
||||||
|
if (!isSent) {
|
||||||
|
const alert = {
|
||||||
|
type: `${type}_${currency}_${level}`,
|
||||||
|
message: `Insufficient balance ${formatEther(value)} ${currency === 'MAIN' ? this.config.nativeCurrency : 'torn'}`,
|
||||||
|
level,
|
||||||
|
time,
|
||||||
|
};
|
||||||
|
await this.store.client.rpush(key, JSON.stringify(alert));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async check() {
|
||||||
|
const mainBalance = await this.config.wallet.getBalance();
|
||||||
|
const tornBalance = await this.config.tokenContract.balanceOf(this.config.wallet.address);
|
||||||
|
// const mainBalance = BigNumber.from(`${2e18}`).add(1);
|
||||||
|
// const tornBalance = BigNumber.from(`${50e18}`);
|
||||||
|
await this._checkBalance(mainBalance, 'MAIN');
|
||||||
|
await this._checkBalance(tornBalance, 'TORN');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthData = {
|
||||||
|
status: boolean,
|
||||||
|
error: string,
|
||||||
|
errorsLog: { message: string, score: number }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default () => container.resolve(HealthService);
|
@ -2,3 +2,6 @@ export { default as configService } from './config.service';
|
|||||||
export { default as getPriceService } from './price.service';
|
export { default as getPriceService } from './price.service';
|
||||||
export { default as getJobService } from './job.service';
|
export { default as getJobService } from './job.service';
|
||||||
export { default as getTxService } from './tx.service';
|
export { default as getTxService } from './tx.service';
|
||||||
|
export { default as getNotifierService } from './notifier.service';
|
||||||
|
export { default as getHealthService } from './health.service';
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { JobStatus, RelayerJobType } from '../types';
|
import { JobStatus, RelayerJobType } from '../types';
|
||||||
import { PriceQueueHelper, RelayerQueueHelper } from '../queue';
|
import { HealthQueueHelper, PriceQueueHelper, RelayerQueueHelper } from '../queue';
|
||||||
import { WithdrawalData } from './tx.service';
|
import { WithdrawalData } from './tx.service';
|
||||||
import { container, injectable } from 'tsyringe';
|
import { container, injectable } from 'tsyringe';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class JobService {
|
export class JobService {
|
||||||
constructor(private price?: PriceQueueHelper, private relayer?: RelayerQueueHelper, public config?: ConfigService) {
|
constructor(private price?: PriceQueueHelper,
|
||||||
|
private relayer?: RelayerQueueHelper,
|
||||||
|
private health?: HealthQueueHelper,
|
||||||
|
public config?: ConfigService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async postJob(type: RelayerJobType, data: WithdrawalData) {
|
async postJob(type: RelayerJobType, data: WithdrawalData) {
|
||||||
@ -35,13 +38,20 @@ export class JobService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _clearSchedulerJobs() {
|
private async _clearSchedulerJobs() {
|
||||||
const jobs = await this.price.queue.getJobs();
|
try {
|
||||||
await Promise.all(jobs.map(job => job.remove()));
|
|
||||||
|
|
||||||
|
const jobs = await Promise.all([this.price.queue.getJobs(), this.health.queue.getJobs()]);
|
||||||
|
await Promise.all(jobs.flat().map(job => job?.remove()));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupRepeatableJobs() {
|
async setupRepeatableJobs() {
|
||||||
await this._clearSchedulerJobs();
|
await this._clearSchedulerJobs();
|
||||||
await this.price.addRepeatable(this.config.tokens);
|
await this.price.addRepeatable(this.config.tokens);
|
||||||
|
await this.health.addRepeatable();
|
||||||
// await this.schedulerQ.add('checkBalance', null, {
|
// await this.schedulerQ.add('checkBalance', null, {
|
||||||
// repeat: {
|
// repeat: {
|
||||||
// every: 30000,
|
// every: 30000,
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
import { Telegram } from 'telegraf';
|
import { Telegram } from 'telegraf';
|
||||||
import { autoInjectable } from 'tsyringe';
|
import { autoInjectable, container } from 'tsyringe';
|
||||||
|
import { RedisStore } from '../modules/redis';
|
||||||
|
|
||||||
|
export type Levels = keyof typeof AlertLevel
|
||||||
|
|
||||||
|
export enum AlertLevel {
|
||||||
|
'INFO' = 'ℹ️️',
|
||||||
|
'WARN' = '⚠️',
|
||||||
|
'CRITICAL' = '‼️',
|
||||||
|
'ERROR' = '💩',
|
||||||
|
'RECOVERED' = '✅'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AlertType {
|
||||||
|
'INSUFFICIENT_BALANCE',
|
||||||
|
'INSUFFICIENT_TORN_BALANCE',
|
||||||
|
'RPC'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@autoInjectable()
|
@autoInjectable()
|
||||||
export class NotifierService {
|
export class NotifierService {
|
||||||
@ -7,16 +25,44 @@ export class NotifierService {
|
|||||||
private readonly token: string;
|
private readonly token: string;
|
||||||
private readonly chatId: string;
|
private readonly chatId: string;
|
||||||
|
|
||||||
constructor() {
|
constructor(private store: RedisStore) {
|
||||||
this.token = process.env.TELEGRAM_NOTIFIER_BOT_TOKEN || '';
|
this.token = process.env.TELEGRAM_NOTIFIER_BOT_TOKEN || '';
|
||||||
this.chatId = process.env.TELEGRAM_NOTIFIER_CHAT_ID || '';
|
this.chatId = process.env.TELEGRAM_NOTIFIER_CHAT_ID || '';
|
||||||
this.telegram = new Telegram(this.token);
|
this.telegram = new Telegram(this.token);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
send(message: string) {
|
async processAlert(message: string) {
|
||||||
|
const alert = JSON.parse(message);
|
||||||
|
const [a, b] = alert.type.split('_');
|
||||||
|
if (alert.level === 'OK') {
|
||||||
|
this.store.client.srem('alerts:sent', ...['WARN', 'CRITICAL'].map(l => `${a}_${b}_${l}`));
|
||||||
|
} else {
|
||||||
|
await this.send(alert.message, alert.level);
|
||||||
|
this.store.client.sadd('alerts:sent', alert.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async subscribe() {
|
||||||
|
const sub = await this.store.subscriber;
|
||||||
|
sub.subscribe('__keyspace@0__:alerts', 'rpush');
|
||||||
|
sub.on('message', async (channel, event) => {
|
||||||
|
if (event === 'rpush') {
|
||||||
|
const messages = await this.store.client.brpop('alerts', 10);
|
||||||
|
while (messages.length) {
|
||||||
|
const [, message] = messages.splice(0, 2);
|
||||||
|
await this.processAlert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message: string, level: Levels) {
|
||||||
|
const text = `${AlertLevel[level]} ${message}`;
|
||||||
|
console.log('sending message: ', text);
|
||||||
return this.telegram.sendMessage(
|
return this.telegram.sendMessage(
|
||||||
this.chatId,
|
this.chatId,
|
||||||
message,
|
text,
|
||||||
{ parse_mode: 'HTML' },
|
{ parse_mode: 'HTML' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -32,3 +78,5 @@ export class NotifierService {
|
|||||||
return this.telegram.getMe();
|
return this.telegram.getMe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default () => container.resolve(NotifierService);
|
||||||
|
@ -27,6 +27,7 @@ export class PriceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchPrices(tokens: Token[]) {
|
async fetchPrices(tokens: Token[]) {
|
||||||
|
try {
|
||||||
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;
|
||||||
@ -45,6 +46,9 @@ export class PriceService {
|
|||||||
prices[names[tokens[i].address]] = price.toString();
|
prices[names[tokens[i].address]] = price.toString();
|
||||||
}
|
}
|
||||||
return prices;
|
return prices;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrice(currency: string) {
|
async getPrice(currency: string) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { schedulerWorker, relayerWorker } from './queue/worker';
|
import { priceWorker, relayerWorker, healthWorker } from './queue/worker';
|
||||||
|
|
||||||
schedulerWorker();
|
priceWorker();
|
||||||
relayerWorker();
|
relayerWorker();
|
||||||
|
healthWorker();
|
||||||
|
Loading…
Reference in New Issue
Block a user