diff --git a/.eslintrc.json b/.eslintrc.json index fc48dfc..78bda97 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,10 +20,7 @@ "prettier" ], "rules": { - "indent": [ - "error", - 2 - ], + "indent": ["error", 2, { "SwitchCase": 1 }], "linebreak-style": [ "error", "unix" diff --git a/package-lock.json b/package-lock.json index 3974ee5..b0863fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "axios": "^1.6.2", "dotenv": "^16.3.1", "ethers": "^6.9.1" }, @@ -593,6 +594,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -712,6 +728,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -761,6 +788,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -1225,6 +1260,38 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1566,6 +1633,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1809,6 +1895,11 @@ "node": ">=6.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 99f8340..0ba84e8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "axios": "^1.6.2", "dotenv": "^16.3.1", "ethers": "^6.9.1" } diff --git a/src/EventProcessor.ts b/src/EventProcessor.ts index 35b8633..a98e86e 100644 --- a/src/EventProcessor.ts +++ b/src/EventProcessor.ts @@ -1,7 +1,13 @@ import { EventData, EventQueue } from './EventQueue'; +import { sleep } from './Utils'; +import { SendTelegramMessage } from './TelegramHelper'; + +const TG_BOT_ID: string | undefined = process.env.TG_BOT_ID; +const TG_CHAT_ID: string | undefined = process.env.TG_CHAT_ID; async function startEventProcessor() { console.log('Started the event processor'); + // eslint-disable-next-line no-constant-condition while (true) { if (EventQueue.length > 0) { @@ -16,18 +22,152 @@ async function startEventProcessor() { } async function ProcessAsync(event: EventData) { + if (!TG_BOT_ID) { + throw new Error('No TG_BOT_ID found in env'); + } + if (!TG_CHAT_ID) { + throw new Error('No TG_BOT_ID found in env'); + } + console.log(`NEW EVENT DETECTED AT BLOCK ${event.block}: ${event.eventName}`, { args: event.eventArgs }); + const msgToSend: string | undefined = buildMessageFromEvent(event); + if (!msgToSend) { + console.log('Nothing to send to TG'); + } else { + await SendTelegramMessage(TG_CHAT_ID, TG_BOT_ID, msgToSend, false); + } +} + +function buildMessageFromEvent(event: EventData): string | undefined { + switch (event.eventName.toLowerCase()) { + default: + return `${buildMsgHeader(event)}\n` + 'NO SPECIFIC IMPLEMENTATION'; + case 'updatelasttotalassets': + case 'accrueinterest': + case 'createmetamorpho': + case 'transfer': + case 'deposit': + case 'withdraw': + case 'approval': + // user facing events, no need for an alert + return undefined; + case 'submittimelock': + // event SubmitTimelock(uint256 newTimelock); + return `${buildMsgHeader(event)}\n` + `newTimelock: ${event.eventArgs[0]}\n`; + case 'settimelock': + // event SetTimelock(address indexed caller, uint256 newTimelock); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `newTimelock: ${event.eventArgs[1]}\n`; + case 'setskimrecipient': + // event SetSkimRecipient(address indexed newSkimRecipient); + return `${buildMsgHeader(event)}\n` + `newSkimRecipient: ${event.eventArgs[0]}\n`; + case 'setfee': + // event SetFee(address indexed caller, uint256 newFee); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `newFee: ${event.eventArgs[1]}\n`; + case 'setfeerecipient': + // event SetFeeRecipient(address indexed newFeeRecipient); + return `${buildMsgHeader(event)}\n` + `newFeeRecipient: ${event.eventArgs[0]}\n`; + case 'submitguardian': + // event SubmitGuardian(address indexed newGuardian); + return `${buildMsgHeader(event)}\n` + `newGuardian: ${event.eventArgs[0]}\n`; + case 'setguardian': + // event SetGuardian(address indexed caller, address indexed guardian); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `guardian: ${event.eventArgs[1]}\n`; + case 'submitcap': + // event SubmitCap(address indexed caller, Id indexed id, uint256 cap); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `id: ${event.eventArgs[1]}\n` + + `cap: ${event.eventArgs[2]}\n` + ); + + case 'setcap': + // event SetCap(address indexed caller, Id indexed id, uint256 cap); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `id: ${event.eventArgs[1]}\n` + + `cap: ${event.eventArgs[2]}\n` + ); + + case 'submitmarketremoval': + // event SubmitMarketRemoval(address indexed caller, Id indexed id); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `id: ${event.eventArgs[1]}\n`; + + case 'setcurator': + // event SetCurator(address indexed newCurator); + return `${buildMsgHeader(event)}\n` + `newCurator: ${event.eventArgs[0]}\n`; + + case 'setisallocator': + // event SetIsAllocator(address indexed allocator, bool isAllocator); + return ( + `${buildMsgHeader(event)}\n` + `allocator: ${event.eventArgs[0]}\n` + `isAllocator: ${event.eventArgs[1]}\n` + ); + + case 'revokependingtimelock': + // event RevokePendingTimelock(address indexed caller); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n`; + + case 'revokependingcap': + // event RevokePendingCap(address indexed caller, Id indexed id); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `id: ${event.eventArgs[1]}\n`; + + case 'revokependingguardian': + // event RevokePendingGuardian(address indexed caller); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n`; + + case 'revokependingmarketremoval': + // event RevokePendingMarketRemoval(address indexed caller, Id indexed id); + return `${buildMsgHeader(event)}\n` + `caller: ${event.eventArgs[0]}\n` + `id: ${event.eventArgs[1]}\n`; + + case 'setsupplyqueue': + // event SetSupplyQueue(address indexed caller, Id[] newSupplyQueue); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `newSupplyQueue: ${event.originArgs[1].map((_: any) => _.toString()).join(', ')}\n` + ); + + case 'setwithdrawqueue': + // event SetWithdrawQueue(address indexed caller, Id[] newWithdrawQueue); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `newWithdrawQueue:\n${event.originArgs[1].map((_: any) => '- ' + _.toString()).join('\n')}\n` + ); + case 'reallocatesupply': + // event ReallocateSupply(address indexed caller, Id indexed id, uint256 suppliedAssets, uint256 suppliedShares); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `id: ${event.eventArgs[1]}\n` + + `suppliedAssets: ${event.eventArgs[2]}\n` + + `suppliedShares: ${event.eventArgs[3]}\n` + ); + + case 'reallocatewithdraw': + // event ReallocateWithdraw(address indexed caller, Id indexed id, uint256 withdrawnAssets, uint256 withdrawnShares); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `id: ${event.eventArgs[1]}\n` + + `withdrawnAssets: ${event.eventArgs[2]}\n` + + `withdrawnShares: ${event.eventArgs[3]}\n` + ); + + case 'skim': + // event Skim(address indexed caller, address indexed token, uint256 amount); + return ( + `${buildMsgHeader(event)}\n` + + `caller: ${event.eventArgs[0]}\n` + + `token: ${event.eventArgs[1]}\n` + + `amount: ${event.eventArgs[2]}\n` + ); + } } -/** - * - * @param {number} ms milliseconds to sleep - * @returns async promise - */ -async function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); +function buildMsgHeader(event: EventData): string { + return `New ${event.eventName} detected on block ${event.block}:`; } startEventProcessor(); diff --git a/src/EventQueue.ts b/src/EventQueue.ts index a783784..4a7ca1d 100644 --- a/src/EventQueue.ts +++ b/src/EventQueue.ts @@ -1,7 +1,10 @@ +import { Result } from 'ethers'; + export const EventQueue: EventData[] = []; export interface EventData { eventName: string; eventArgs: string[]; block: number; + originArgs: Result; } diff --git a/src/EventWatcher.ts b/src/EventWatcher.ts index b57e182..e5dde0c 100644 --- a/src/EventWatcher.ts +++ b/src/EventWatcher.ts @@ -1,4 +1,4 @@ -import { ethers, Contract, Interface, Log, ContractEventPayload } from 'ethers'; +import { ethers, Contract, Interface } from 'ethers'; import WebSocket from 'ws'; import dotenv from 'dotenv'; import { metamorphoAbi } from './abis/MetaMorphoAbi'; @@ -54,7 +54,8 @@ function startListening() { EventQueue.push({ eventName: parsed.name, eventArgs: parsed.args.map((_) => _.toString()), - block: event.log.blockNumber + block: event.log.blockNumber, + originArgs: parsed.args }); }); } diff --git a/src/TelegramHelper.ts b/src/TelegramHelper.ts new file mode 100644 index 0000000..cf35904 --- /dev/null +++ b/src/TelegramHelper.ts @@ -0,0 +1,60 @@ +import axios from 'axios'; +import { sleep } from './Utils'; + +let lastTGCall = Date.now(); +type TGBody = { + chat_id: string; + text: string; + parse_mode?: string; +}; +async function CallTelegram(msg: string, botid: string, chatid: string, isMarkdown = false) { + const body: TGBody = { + chat_id: chatid, + text: msg + }; + + if (isMarkdown) { + body.parse_mode = 'MarkdownV2'; + } + + const url = `https://api.telegram.org/bot${botid}/sendMessage`; + const config = { + headers: { + 'Content-type': 'application/json', + Accept: 'text/plain' + } + }; + const timeToWait = 3000 - (Date.now() - lastTGCall); + if (timeToWait > 0) { + console.log(`SendTelegramMessage: waiting ${timeToWait} ms before calling telegram`); + await sleep(timeToWait); + } + let mustReCall = true; + while (mustReCall) { + mustReCall = false; + + try { + await axios.post(url, body, config); + console.log('Message sent to telegram with success'); + lastTGCall = Date.now(); + } catch (err) { + if (axios.isAxiosError(err) && err.response) { + // console.log(err.response?.data) + if (!err?.response) { + console.log('SendTelegramMessage: No Server Response', err); + throw err; + } else if (err.response?.status === 429) { + console.log('SendTelegramMessage: rate limited, sleeping 5 sec', err); + await sleep(5000); + mustReCall = true; + } else { + console.log('SendTelegramMessage: Unknown error', err); + } + } else throw err; + } + } +} + +export async function SendTelegramMessage(chatId: string, botId: string, msg: string, isMarkdown = false) { + await CallTelegram(msg, botId, chatId, isMarkdown); +} diff --git a/src/Utils.ts b/src/Utils.ts new file mode 100644 index 0000000..3b6a3be --- /dev/null +++ b/src/Utils.ts @@ -0,0 +1,10 @@ +/** + * sleep + * @param {number} ms milliseconds to sleep + * @returns async promise + */ +export async function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/src/index.ts b/src/index.ts index a31137d..483a2c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import './EventWatcher'; -import './EventProcessor'; +import './EventWatcher'; // this launch the event watcher +import './EventProcessor'; // this launch the event processor async function main() { console.log('Started MetaMorpho alerter');