send alert for tx errors, update docker

This commit is contained in:
smart_ex 2022-06-14 20:06:42 +10:00
parent 978c70de1e
commit 2c20febcab
12 changed files with 174 additions and 688 deletions

@ -2,8 +2,8 @@ name: build
on: on:
push: push:
branches: ['*'] branches: [ '*' ]
tags: ['v[0-9]+.[0-9]+.[0-9]+'] tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
pull_request: pull_request:
jobs: jobs:
@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: 12 node-version: 16
- run: yarn install - run: yarn install
- run: yarn test - run: yarn test
- run: yarn lint - run: yarn lint
@ -51,7 +51,7 @@ jobs:
dockerfile: Dockerfile dockerfile: Dockerfile
repository: tornadocash/relayer repository: tornadocash/relayer
tag_with_ref: true tag_with_ref: true
tags: mining,candidate tags: candidate
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}

@ -1,9 +1,30 @@
FROM node:12 FROM node:16-alpine as dev
WORKDIR /app
ENV NODE_ENV=development
WORKDIR /usr/app
COPY yarn.lock .
COPY package.json .
RUN yarn install && yarn cache clean
COPY package.json yarn.lock ./
RUN yarn && yarn cache clean --force
COPY . . COPY . .
EXPOSE 8000 RUN yarn build
ENTRYPOINT ["yarn"]
FROM node:16-alpine as production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=dev /usr/app/build /app
COPY --from=dev /usr/app/package.json /app/
COPY --from=dev /usr/app/yarn.lock /app/
RUN chown -R node: .
USER node
RUN yarn install --non-interactive --frozen-lockfile && yarn cache clean
CMD ["node", "index.js"]

@ -8,30 +8,15 @@ services:
restart: always restart: always
command: server command: server
env_file: .env env_file: .env
build:
context: .
dockerfile: Dockerfile
ports: ports:
- 8000:8000 - 8000:8000
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
restart: always
command: treeWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
priceWatcher:
image: tornadocash/relayer
restart: always
command: priceWatcher
env_file: .env
environment:
REDIS_URL: redis://redis/0
depends_on: [redis]
worker1: worker1:
image: tornadocash/relayer image: tornadocash/relayer
@ -40,7 +25,7 @@ services:
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 # image: tornadocash/relayer
@ -54,7 +39,7 @@ services:
redis: redis:
image: redis image: redis
restart: always restart: always
command: [redis-server, --appendonly, 'yes'] command: [ redis-server, --appendonly, 'yes' ]
volumes: volumes:
- redis:/data - redis:/data

@ -5,6 +5,7 @@
"scripts": { "scripts": {
"dev:app": "nodemon --watch './src/**/*.ts' --exec ts-node src/app/index.ts", "dev:app": "nodemon --watch './src/**/*.ts' --exec ts-node src/app/index.ts",
"dev:worker": "nodemon --watch './src/**/*.ts' --exec ts-node src/worker.ts", "dev:worker": "nodemon --watch './src/**/*.ts' --exec ts-node src/worker.ts",
"build": "tsc",
"eslint": "eslint --ext .js --ignore-path .gitignore .", "eslint": "eslint --ext .js --ignore-path .gitignore .",
"prettier:check": "npx prettier --check . --config .prettierrc", "prettier:check": "npx prettier --check . --config .prettierrc",
"prettier:fix": "npx prettier --write . --config .prettierrc", "prettier:fix": "npx prettier --write . --config .prettierrc",
@ -31,14 +32,12 @@
"torn-token": "link:../torn-token", "torn-token": "link:../torn-token",
"tsyringe": "^4.6.0", "tsyringe": "^4.6.0",
"tx-manager": "link:../tx-manager", "tx-manager": "link:../tx-manager",
"uuid": "^8.3.0", "uuid": "^8.3.0"
"web3": "^1.3.0",
"web3-core-promievent": "^1.3.0",
"web3-utils": "^1.2.2"
}, },
"devDependencies": { "devDependencies": {
"@typechain/ethers-v5": "^10.0.0", "@typechain/ethers-v5": "^10.0.0",
"@types/ioredis": "^4.28.10", "@types/ioredis": "^4.28.10",
"@types/node": "^17.0.42",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.20.0", "@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0", "@typescript-eslint/parser": "^5.20.0",

@ -14,6 +14,7 @@ export const privateKey = process.env.PRIVATE_KEY;
export const instances = tornConfig.instances; export const instances = tornConfig.instances;
export const torn = tornConfig; export const torn = tornConfig;
export const port = process.env.APP_PORT || 8000; export const port = process.env.APP_PORT || 8000;
export const host = process.env.VIRTUAL_HOST || `localhost:${port}`;
export const tornadoServiceFee = Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE); export const tornadoServiceFee = Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE);
export const rewardAccount = process.env.REWARD_ACCOUNT; export const rewardAccount = process.env.REWARD_ACCOUNT;
export const governanceAddress = '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce'; export const governanceAddress = '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce';

@ -6,6 +6,7 @@ export const healthProcessor: Processor = async () => {
try { try {
await healthService.check(); await healthService.check();
await healthService.clearErrorCodes();
await healthService.setStatus({ status: true, error: '' }); await healthService.setStatus({ status: true, error: '' });
} catch (e) { } catch (e) {
await healthService.saveError(e); await healthService.saveError(e);

@ -26,8 +26,8 @@ export const relayerWorker = async () => {
console.log(`Job ${job.id} completed with result: `, result); console.log(`Job ${job.id} completed with result: `, result);
}); });
relayer.worker.on('failed', (job, error) => { relayer.worker.on('failed', (job, error) => {
healthService.saveError(error); healthService.saveError(error, job.id);
// console.log(error); console.log(error);
}); });
}; };

@ -1,4 +1,5 @@
import { import {
host,
instances, instances,
mainnetRpcUrl, mainnetRpcUrl,
minimumBalance, minimumBalance,
@ -51,11 +52,13 @@ export class ConfigService {
private _tokenAddress: string; private _tokenAddress: string;
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; }; };
host: 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.host = host;
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.mainnentProvider = getProvider(false, mainnetRpcUrl, 1);
@ -140,7 +143,8 @@ export class ConfigService {
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);
const errorKeys = await this.store.client.keys('errors:*'); const errorKeys = await this.store.client.keys('errors:*');
// const alertKeys = await this.store.client.keys('alerts:*'); // const alertKeys = await this.store.client.keys('alerts:*');
await this.store.client.del([...queueKeys, ...errorKeys]); const keys = [...queueKeys, ...errorKeys];
if (keys.length) await this.store.client.del([...queueKeys, ...errorKeys]);
} }
getInstance(address: string) { getInstance(address: string) {

@ -2,6 +2,7 @@ import { autoInjectable, container } from 'tsyringe';
import { ConfigService } from './config.service'; 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';
import { Levels } from './notifier.service';
class RelayerError extends Error { class RelayerError extends Error {
constructor(message: string, code: string) { constructor(message: string, code: string) {
@ -18,8 +19,8 @@ export class HealthService {
constructor(private config: ConfigService, private store: RedisStore) { constructor(private config: ConfigService, private store: RedisStore) {
} }
async clearErrors() { async clearErrorCodes() {
await this.store.client.del('errors:log', 'errors:code'); await this.store.client.del('errors:code');
} }
private async _getErrors(): Promise<{ errorsLog: { message: string, score: number }[], errorsCode: Record<string, number> }> { private async _getErrors(): Promise<{ errorsLog: { message: string, score: number }[], errorsCode: Record<string, number> }> {
@ -57,8 +58,11 @@ export class HealthService {
} }
async getStatus() { async getStatus() {
const heathStatus = await this._getStatus();
const { errorsLog, errorsCode } = await this._getErrors(); const { errorsLog, errorsCode } = await this._getErrors();
if (errorsCode['NETWORK_ERROR'] > 5) {
await this.setStatus({ status: false, error: 'Network error' });
}
const heathStatus = await this._getStatus();
return { return {
...heathStatus, ...heathStatus,
@ -67,15 +71,28 @@ export class HealthService {
}; };
} }
async saveError(e) { async saveError(e, jobId?: string) {
await this.store.client.zadd('errors:code', 'INCR', 1, e?.code || 'RUNTIME_ERROR'); await this.store.client.zadd('errors:code', 'INCR', 1, e?.code || 'RUNTIME_ERROR');
await this.store.client.zadd('errors:log', 'INCR', 1, e.message); await this.store.client.zadd('errors:log', 'INCR', 1, e.message);
if (e?.code === 'REVERTED') {
const jobUrl = `https://${this.config.host}/v1/jobs/${jobId}`;
await this.pushAlert({
message: `${e.message} \n ${jobUrl}`,
type: 'REVERTED',
level: 'WARN',
time: new Date().getTime(),
});
}
}
async pushAlert(alert: Alert) {
await this.store.client.rpush('alerts:list', JSON.stringify(alert));
} }
private async _checkBalance(value, currency: 'MAIN' | 'TORN') { private async _checkBalance(value, currency: 'MAIN' | 'TORN') {
let level = 'OK'; let level: Levels = 'OK';
const type = 'BALANCE'; const type = 'BALANCE';
const key = 'alerts:list';
const time = new Date().getTime(); const time = new Date().getTime();
if (value.lt(this.config.balances[currency].critical)) { if (value.lt(this.config.balances[currency].critical)) {
level = 'CRITICAL'; level = 'CRITICAL';
@ -89,7 +106,7 @@ export class HealthService {
level, level,
time, time,
}; };
await this.store.client.rpush(key, JSON.stringify(alert)); await this.pushAlert(alert);
return alert; return alert;
} }
@ -99,7 +116,7 @@ export class HealthService {
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(`${1e18}`).add(1); // const mainBalance = BigNumber.from(`${1e18}`).add(1);
// const tornBalance = BigNumber.from(`${45e18}`); // const tornBalance = BigNumber.from(`${60e18}`);
const mainStatus = await this._checkBalance(mainBalance, 'MAIN'); const mainStatus = await this._checkBalance(mainBalance, 'MAIN');
const tornStatus = await this._checkBalance(tornBalance, 'TORN'); const tornStatus = await this._checkBalance(tornBalance, 'TORN');
if (mainStatus.level === 'CRITICAL') { if (mainStatus.level === 'CRITICAL') {
@ -117,5 +134,10 @@ type HealthData = {
error: string, error: string,
errorsLog: { message: string, score: number }[] errorsLog: { message: string, score: number }[]
} }
type Alert = {
type: string,
message: string,
level: Levels,
time?: number,
}
export default () => container.resolve(HealthService); export default () => container.resolve(HealthService);

@ -9,7 +9,7 @@ export enum AlertLevel {
'WARN' = '⚠️', 'WARN' = '⚠️',
'CRITICAL' = '‼️', 'CRITICAL' = '‼️',
'ERROR' = '💩', 'ERROR' = '💩',
'RECOVERED' = '✅' 'OK' = '✅'
} }
export enum AlertType { export enum AlertType {

@ -84,14 +84,14 @@ export class TxService {
}); });
if (receipt.status === 1) { if (receipt.status === 1) {
await this.updateJobData({ status: JobStatus.CONFIRMED }); await this.updateJobData({ status: JobStatus.CONFIRMED });
} else throw new Error('Submitted transaction failed'); } else throw new ExecutionError('Submitted transaction failed', 'REVERTED');
return receipt; return receipt;
} catch (e) { } catch (e) {
const regex = /body=("\{.*}}")/; const regex = /body=("\{.*}}")/;
if (regex.test(e.message)) { if (regex.test(e.message)) {
const { error } = parseJSON(regex.exec(e.message)[1]); const { error } = parseJSON(regex.exec(e.message)[1]);
throw new ExecutionError(error.message, 'REVERTED'); throw new ExecutionError(error.message, 'REVERTED');
} else throw e.message; } else throw new ExecutionError(e.message, 'SEND_ERROR');
} }
} }

727
yarn.lock

File diff suppressed because it is too large Load Diff