Skip to content

Commit

Permalink
fix(typescript): Make DiscordMonitor hang-up safe and refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-nikolov96 committed Sep 14, 2023
1 parent 34766a9 commit 33529eb
Show file tree
Hide file tree
Showing 4 changed files with 421 additions and 129 deletions.
2 changes: 2 additions & 0 deletions relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@effect/schema": "^0.33.0",
"discord.js": "^14.12.1",
"ts-node": "^10.9.1",
"web3": "^1.10.0",
"yargs": "^17.7.1"
Expand Down
3 changes: 2 additions & 1 deletion relay/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"./hardhat.config.ts"
],
"compilerOptions": {
"moduleResolution": "nodenext"
"moduleResolution": "nodenext",
"strictPropertyInitialization": false,
}
}
320 changes: 193 additions & 127 deletions relay/utils/discord_monitor.ts
Original file line number Diff line number Diff line change
@@ -1,152 +1,218 @@
import { SolidityContract } from '../implementations/solidity-contract';
import { BeaconApi } from '../implementations/beacon-api';
import { ethers } from "ethers";
import {sleep} from '../../libs/typescript/ts-utils/common-utils';
import { ethers } from 'ethers';
import { sleep } from '../../libs/typescript/ts-utils/common-utils';
import { GatewayIntentBits, Events, Partials } from 'discord.js';
import * as Discord from 'discord.js';
import lc_abi_json from '../../beacon-light-client/solidity/artifacts/contracts/bridge/src/truth/eth/BeaconLightClient.sol/BeaconLightClient.json';

const env = require('dotenv').config({path: '../../.env'}).parsed;

const { GatewayIntentBits, SlashCommandBuilder, Events,Partials } = require('discord.js');
const Discord = require('discord.js');
const env = process.env;

interface ContractData {
RPC: string;
Address: string;
SolidityContract?: SolidityContract;
RPC: string;
Address: string;
SolidityContract?: SolidityContract;
}

type SolidityDictionary = {
[name: string]: ContractData;
[name: string]: ContractData;
};

class DiscordMonitor {
client: any;
beaconApi: BeaconApi;
contracts: SolidityDictionary = {};

alert_threshold: number;

constructor(alert_threshold: number) {

this.alert_threshold = alert_threshold;

this.beaconApi = new BeaconApi([
'http://unstable.prater.beacon-api.nimbus.team/',
]);

this.client = new Discord.Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.DirectMessages
],
partials: [
Partials.Channel,
Partials.Message,
Partials.DirectMessages,
Partials.Reaction
]
});

this.client.login(env.token);
this.client.on(Events.ClientReady, async interaction => {
console.log('Client Ready!')
console.log(`Logged in as ${this.client.user.tag}!`)
});

// Track contracts if initialized in .env
if (env.GOERLI_RPC && env.LC_GOERLI){
this.contracts['Goerli'] = {RPC: env.GOERLI_RPC, Address: env.LC_GOERLI};
}
if (env.OPTIMISTIC_GOERLI_RPC && env.LC_OPTIMISTIC_GOERLI) {
this.contracts['OptimisticGoerli'] = {RPC: env.OPTIMISTIC_GOERLI_RPC, Address: env.LC_OPTIMISTIC_GOERLI};
}
if (env.BASE_GOERLI_RPC && env.LC_BASE_GOERLI) {
this.contracts['BaseGoerli'] = {RPC: env.BASE_GOERLI_RPC, Address: env.LC_BASE_GOERLI};
}
if (env.ARBITRUM_GOERLI_RPC && env.LC_ARBITRUM_GOERLI) {
this.contracts['ArbitrumGoerli'] = {RPC: env.ARBITRUM_GOERLI_RPC, Address: env.LC_ARBITRUM_GOERLI};
}
if (env.SEPOLIA_RPC && env.LC_SEPOLIA) {
this.contracts['Sepolia'] = {RPC: env.SEPOLIA_RPC, Address: env.LC_SEPOLIA};
}
if (env.MUMBAI_RPC && env.LC_MUMBAI) {
this.contracts['Mumbai'] = {RPC: env.MUMBAI_RPC, Address: env.LC_MUMBAI};
}
if (env.FANTOM_RPC && env.LC_FANTOM) {
this.contracts['Fantom'] = {RPC: env.FANTOM_RPC, Address: env.LC_FANTOM};
}
if (env.CHIADO_RPC && env.LC_CHIADO) {
this.contracts['Chiado'] = {RPC: env.CHIADO_RPC, Address: env.LC_CHIADO};
}
if (env.GNOSIS_RPC && env.LC_GNOSIS) {
this.contracts['Gnosis'] = {RPC: env.GNOSIS_RPC, Address: env.LC_GNOSIS};
}
if (env.BSC_RPC && env.LC_BSC) {
this.contracts['BSC'] = {RPC: env.BSC_RPC, Address: env.LC_BSC};
}
if (env.AURORA_RPC && env.LC_AURORA) {
this.contracts['Aurora'] = {RPC: env.AURORA_RPC, Address: env.LC_AURORA};
}

// Instantiate SolidityContracts from .env
for (var endpoint in this.contracts) {

var curLightClient = new ethers.Contract(
this.contracts[endpoint].Address,
lc_abi_json.abi,
new ethers.providers.JsonRpcProvider(this.contracts[endpoint].RPC) // Provider
);

var curSolidityContract = new SolidityContract(
curLightClient,
this.contracts[endpoint].RPC,
);
this.contracts[endpoint].SolidityContract= curSolidityContract;
}
client: Discord.Client;
beaconApi: BeaconApi;
contracts: SolidityDictionary = {};
channel: Discord.Channel;

alert_threshold: number;

public static async initializeDiscordMonitor(
alert_threshold: number,
): Promise<DiscordMonitor> {
let discordMonitor = new DiscordMonitor(alert_threshold);

discordMonitor.alert_threshold = alert_threshold;

discordMonitor.beaconApi = new BeaconApi([
'http://unstable.prater.beacon-api.nimbus.team/',
]);

discordMonitor.client = new Discord.Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.DirectMessages,
],
partials: [Partials.Channel, Partials.Message, Partials.Reaction],
});

const result = await discordMonitor.client.login(env.token);

await discordMonitor.client.on(Events.ClientReady, async interaction => {
console.log('Client Ready!');
console.log(`Logged in as ${discordMonitor.client.user?.tag}!`);
});

return discordMonitor;
}

private constructor(alert_threshold: number) {
// Track contracts if initialized in .env
if (env.GOERLI_RPC && env.LC_GOERLI) {
this.contracts['Goerli'] = {
RPC: env.GOERLI_RPC,
Address: env.LC_GOERLI,
};
}

private async getSlotDelay(contract: SolidityContract) {
return await this.beaconApi.getCurrentHeadSlot() - await contract.optimisticHeaderSlot();
if (env.OPTIMISTIC_GOERLI_RPC && env.LC_OPTIMISTIC_GOERLI) {
this.contracts['OptimisticGoerli'] = {
RPC: env.OPTIMISTIC_GOERLI_RPC,
Address: env.LC_OPTIMISTIC_GOERLI,
};
}

private async respondToMessage() { //TODO: Implement responsive commands
this.client.on(Events.MessageCreate, (message) => {
if (message.author.bot) return false;

// Nice to have, responsive bot
console.log(`Message from ${message.author.username}: ${message.content}`);
if (message.content === '') console.log('Empty message') //TODO: Bot can't read user messages
});
if (env.BASE_GOERLI_RPC && env.LC_BASE_GOERLI) {
this.contracts['BaseGoerli'] = {
RPC: env.BASE_GOERLI_RPC,
Address: env.LC_BASE_GOERLI,
};
}

private async dispatchMessage(messageToSend) {
this.client.channels.cache.get(env.channel_id).send(messageToSend)
if (env.ARBITRUM_GOERLI_RPC && env.LC_ARBITRUM_GOERLI) {
this.contracts['ArbitrumGoerli'] = {
RPC: env.ARBITRUM_GOERLI_RPC,
Address: env.LC_ARBITRUM_GOERLI,
};
}
if (env.SEPOLIA_RPC && env.LC_SEPOLIA) {
this.contracts['Sepolia'] = {
RPC: env.SEPOLIA_RPC,
Address: env.LC_SEPOLIA,
};
}
if (env.MUMBAI_RPC && env.LC_MUMBAI) {
this.contracts['Mumbai'] = {
RPC: env.MUMBAI_RPC,
Address: env.LC_MUMBAI,
};
}
if (env.FANTOM_RPC && env.LC_FANTOM) {
this.contracts['Fantom'] = {
RPC: env.FANTOM_RPC,
Address: env.LC_FANTOM,
};
}
if (env.CHIADO_RPC && env.LC_CHIADO) {
this.contracts['Chiado'] = {
RPC: env.CHIADO_RPC,
Address: env.LC_CHIADO,
};
}
if (env.GNOSIS_RPC && env.LC_GNOSIS) {
this.contracts['Gnosis'] = {
RPC: env.GNOSIS_RPC,
Address: env.LC_GNOSIS,
};
}
if (env.BSC_RPC && env.LC_BSC) {
this.contracts['BSC'] = { RPC: env.BSC_RPC, Address: env.LC_BSC };
}
if (env.AURORA_RPC && env.LC_AURORA) {
this.contracts['Aurora'] = {
RPC: env.AURORA_RPC,
Address: env.LC_AURORA,
};
}

public async monitor_delay() {

for (var contract of Object.keys(this.contracts)) {
var name = contract;
var delay = await this.getSlotDelay(this.contracts[contract].SolidityContract!)

// Dispatch
var minutes_delay = delay * 1 / 5;
// Instantiate SolidityContracts from .env
for (let endpoint in this.contracts) {
let curLightClient = new ethers.Contract(
this.contracts[endpoint].Address,
lc_abi_json.abi,
new ethers.providers.JsonRpcProvider(this.contracts[endpoint].RPC), // Provider
);

let curSolidityContract = new SolidityContract(
curLightClient,
this.contracts[endpoint].RPC,
);
this.contracts[endpoint].SolidityContract = curSolidityContract;
}
}

private async getSlotDelay(contract: SolidityContract) {
return (
(await this.beaconApi.getCurrentHeadSlot()) -
(await contract.optimisticHeaderSlot())
);
}

private async respondToMessage() {
//TODO: Implement responsive commands
this.client.on(Events.MessageCreate, message => {
if (message.author.bot) return;

// Nice to have, responsive bot
console.log(
`Message from ${message.author.username}: ${message.content}`,
);
if (message.content === '') console.log('Empty message'); //TODO: Bot can't read user messages
});
}

public async dispatchMessage(messageToSend) {
let channel = this.client.channels.cache.get(
env.channel_id!,
) as Discord.TextChannel;
if (!channel) {
channel = (await this.client.channels.fetch(
env.channel_id!,
)) as Discord.TextChannel;
}

if (minutes_delay >= this.alert_threshold || delay < 0) {
var message = `Contract: ${ name } is behind Beacon Head with ${ minutes_delay } minutes`
this.dispatchMessage(message);
}
}
await channel.send(messageToSend);
}

public async monitor_delay() {
for (let contract of Object.keys(this.contracts)) {
let name = contract;
let delay = await this.getSlotDelay(
this.contracts[contract].SolidityContract!,
);

// Dispatch
const minutes_delay = (delay * 1) / 5;
if (minutes_delay >= this.alert_threshold || delay < 0) {
let message = `Contract: ${name} is behind Beacon Head with ${minutes_delay} minutes`;
this.dispatchMessage(message);
}
}
}
}

(async () => {
var monitor = new DiscordMonitor(env.ping_threshold);
while(true) {
await monitor.monitor_delay();
await sleep(env.ping_timeout);
let monitor = await DiscordMonitor.initializeDiscordMonitor(
Number(env.ping_threshold),
);

monitor.dispatchMessage('Relayer bot starting!');

let retry_counter = 0;
while (true) {
if (retry_counter >= 10) {
throw new Error(
`Failed connection to Discord after ${retry_counter} retries`,
);
}
})();

const msTimeout = 10_000;
let waitPromise = new Promise<'timeout'>(resolve =>
setTimeout(() => resolve('timeout'), msTimeout),
);
let response = await Promise.race([monitor.monitor_delay(), waitPromise]);

retry_counter += response == 'timeout' ? 1 : 0;

await sleep(env.ping_timeout);
}
})();
Loading

0 comments on commit 33529eb

Please sign in to comment.