diff --git a/Dockerfile b/Dockerfile index 0e6ab30..adbb107 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,6 @@ FROM node:18-slim as ts-compiler WORKDIR /usr/app COPY package*.json ./ COPY tsconfig*.json ./ -# RUN apt update \ -# && apt install build-essential python3 libsqlite3-dev -y RUN npm install COPY . ./ RUN npm run build @@ -12,15 +10,10 @@ FROM node:18-slim as ts-remover WORKDIR /usr/app COPY --from=ts-compiler /usr/app/package*.json ./ COPY --from=ts-compiler /usr/app/dist ./ -# RUN apt update \ -# && apt install build-essential python3 libsqlite3-dev sqlite3 -y -# RUN npm install --build-from-source --sqlite=/usr/bin --omit=dev RUN npm install --omit=dev FROM node:18-slim WORKDIR /usr/app COPY --from=ts-remover /usr/app ./ COPY .env ./ -# RUN apt update \ -# && apt install libsqlite3-dev sqlite3 -y CMD node index.js \ No newline at end of file diff --git a/src/blockchain.ts b/src/blockchain.ts index 2dcbb4e..1268148 100644 --- a/src/blockchain.ts +++ b/src/blockchain.ts @@ -1,7 +1,9 @@ import 'dotenv/config.js'; -import { GasPrices } from '../types/gasPrices' import Web3 from 'web3'; +import { GasPrices } from '../types/gasPrices' +import redisClient from './redis'; + const rpcUrl = process.env.RPC_URL || 'ws://localhost:8545'; // Create a new web3 instance @@ -14,34 +16,46 @@ const web3 = new Web3(new Web3.providers.WebsocketProvider(rpcUrl, { } })); -// Get the current gas price in gwei -async function getGasPrice() { - const gasPrice = await web3.eth.getGasPrice(); +export const subToBlockHeaders = (setDiscordStatus: () => Promise) => { + web3.eth.subscribe('newBlockHeaders', (error, blockHeader) => { + if (error) console.error(error); + + // Set status every 10 blocks + const shouldSetStatus = blockHeader.number % 10 === 0; + + // Get the gas price for this block + web3.eth.getGasPrice((error, gasPrice) => { + if (error) console.error(error); + + console.log('Gas price in wei:', gasPrice); + + redisClient.set('gas-price', Math.round(Number(gasPrice))) + }); + + if (shouldSetStatus) setDiscordStatus() + }); +} + +const gweiFromWei = (priceInWei: number): number => + Math.round(Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei'))); + +const getGasPrice = async (): Promise => { + const gasPrice = await redisClient.get('gas-price'); return Number(gasPrice); } const getGasPricesInGwei = async (): Promise => { - const gweiFromWei = (priceInWei: number): number => - Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei')); + const gasPrice = await getGasPrice() + const fastPrice = gasPrice * 1.1; + const slowPrice = gasPrice * 0.9; + + const gasPrices = { + fast: gweiFromWei(fastPrice), + average: gweiFromWei(gasPrice), + slow: gweiFromWei(slowPrice), + }; - try { - const gasPrice = await getGasPrice() - const fastPrice = gasPrice * 1.2; - const slowPrice = gasPrice * 0.8; - - const gasPrices = { - fast: gweiFromWei(fastPrice), - average: gweiFromWei(gasPrice), - slow: gweiFromWei(slowPrice), - }; - - return Promise.resolve(gasPrices); - // await redisClient.set('gas-prices', JSON.stringify(gasPrices)); - } catch (error) { - console.log(error); - return Promise.reject(error); - } + return gasPrices; }; -// Export the getCurrentGasPrice function export { getGasPricesInGwei }; \ No newline at end of file diff --git a/src/commands/deleteAlert.ts b/src/commands/deleteAlert.ts new file mode 100644 index 0000000..9f49bf4 --- /dev/null +++ b/src/commands/deleteAlert.ts @@ -0,0 +1,12 @@ +import { SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { handleAlertDeleteCommand } from '../handlers'; + +module.exports = { + data: new SlashCommandBuilder() + .setName('alert-delete') + .setDescription('Remove your gwei alert threshold'), + + async execute(interaction: ChatInputCommandInteraction) { + return await handleAlertDeleteCommand(interaction); + } +} \ No newline at end of file diff --git a/src/deploy.ts b/src/deploy.ts index 884644f..7bb1e04 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -23,8 +23,6 @@ const rest = new REST({ version: '10' }).setToken(token); // and deploy your commands! export const deployCommands = async () => { try { - console.log(`Started refreshing ${commands.length} global application (/) commands.`); - // The put method is used to fully refresh all commands in the guild with the current set const data = await rest.put( Routes.applicationCommands(clientId), diff --git a/src/gasPriceChecker.ts b/src/gasPriceChecker.ts index a25fb7f..bab7b73 100644 --- a/src/gasPriceChecker.ts +++ b/src/gasPriceChecker.ts @@ -1,7 +1,8 @@ import { EmbedBuilder, TextChannel } from 'discord.js'; + import { getGasPricesInGwei } from './blockchain'; -import redisClient from './redis'; import { DiscordClient } from './discordClient'; +import redisClient from './redis'; import { GasAlert } from '../types/gasAlert'; @@ -11,8 +12,8 @@ const createGasPriceChecker = (client: DiscordClient) => { const gasPrices = await getGasPricesInGwei(); const gasAlerts: GasAlert[] = await redisClient - .get('gas-alerts') - .then((value) => (value ? JSON.parse(value) : [])); + .hVals('gas-alerts') + .then((values) => values.map(val => JSON.parse(val))); gasAlerts.forEach(async (gasAlert) => { if (gasPrices.average <= gasAlert.threshold) { @@ -23,15 +24,20 @@ const createGasPriceChecker = (client: DiscordClient) => { embeds: [ new EmbedBuilder() .setTitle('Gas price alert!') - .setDescription(`<@${gasAlert.userId}>!\n\nThe current gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei. The current gas prices are: \n\n Fast: ${gasPrices.fast} Gwei \n\n Average: ${gasPrices.average} Gwei \n\n Slow: ${gasPrices.slow} Gwei`) + .setDescription(`<@${gasAlert.userId}>! +Gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei: +⚡${gasPrices.fast} ⦚⦚ 🚶${gasPrices.average} ⦚⦚ 🐢${gasPrices.slow}`) .setColor('#FF8C00') ], target: user }) + + console.log(`Alerted ${user} under threshold ${gasAlert.threshold}: Removing alert...`) + await redisClient.hDel('gas-alerts', `${gasAlert.userId}`) } }); } catch (error) { - console.error(`Error checking gas prices: ${error}`); + console.log(`Error checking gas prices:\n`, error); } }, 15000); }; diff --git a/src/handlers.ts b/src/handlers.ts index 8590e1a..98a1ca7 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -8,7 +8,7 @@ import { GasAlert } from '../types/gasAlert'; const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise => { const gasPrices = await getGasPricesInGwei(); - console.log(`Replying to command "/gas": \n${gasPrices}`); + console.log(`Replying to command "/gas": \n`, gasPrices); const embed = new EmbedBuilder() .setColor('#0099ff') @@ -28,9 +28,7 @@ const handleGasAlertCommand = async (interaction: ChatInputCommandInteraction): console.log(`Replying to command "/gas gwei ${threshold}"`); - await redisClient.connect(); await redisClient.hSet('gas-alerts', `${userId}`, JSON.stringify(gasAlert)); - await redisClient.disconnect(); await interaction.reply(`Your gas alert threshold of ${threshold} Gwei has been set.`); }; @@ -39,17 +37,28 @@ const handleGasAlertCommand = async (interaction: ChatInputCommandInteraction): const handleGasPendingCommand = async (interaction: ChatInputCommandInteraction): Promise => { const userId = interaction.user.id; - await redisClient.connect(); - const alertThreshold = await redisClient.get(`${userId}`); - await redisClient.disconnect(); + const alertStr = await redisClient.hGet('gas-alerts', `${userId}`); + const { threshold = null } = alertStr ? JSON.parse(alertStr) : {}; console.log(`Replying to command "/gas pending"`); - if (alertThreshold === null) { + if (threshold === null) { await interaction.reply('No gas price alerts set'); } else { - await interaction.reply(`Current gas price alert threshold: ${alertThreshold} Gwei`); + await interaction.reply(`Current gas price alert threshold: ${threshold} Gwei`); } }; -export { handleGasCommand, handleGasAlertCommand, handleGasPendingCommand }; +// Respond to the "/alert-delete" command +const handleAlertDeleteCommand = async (interaction: ChatInputCommandInteraction): Promise => { + const userId = interaction.user.id; + const user = interaction.user.username; + + console.log(`Replying to command "/alert-delete"`); + + await redisClient.hDel('gas-alerts', `${userId}`); + + await interaction.reply(`Removed your gas price alert, ${user}`); +}; + +export { handleGasCommand, handleGasAlertCommand, handleGasPendingCommand, handleAlertDeleteCommand }; diff --git a/src/index.ts b/src/index.ts index 6f5f4c9..192392d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,15 @@ import 'dotenv/config.js'; import fs from 'node:fs'; import path from 'node:path'; -import { GatewayIntentBits } from 'discord.js'; +import { ActivityType, GatewayIntentBits } from 'discord.js'; + +import { getGasPricesInGwei, subToBlockHeaders } from './blockchain'; import { deployCommands } from './deploy'; import { DiscordClient } from './discordClient'; import { createGasPriceChecker } from './gasPriceChecker'; +import redisClient from './redis'; + +import { GasAlert } from '../types/gasAlert'; const token = process.env.DISCORD_BOT_TOKEN || ""; @@ -38,11 +43,29 @@ for (const file of eventFiles) { } } +deployCommands(); + console.log('Booting up discord bot') // Log in to Discord client.login(token) - .then(deployCommands) + .then(async () => { + // Connect to redis + await redisClient.connect() + .catch((reason) => console.log("Error connecting to redis!\n", reason)) + }) + .then(async () => { + const setDiscordStatus = async () => { + if (client.user) { + const { average, fast, slow } = await getGasPricesInGwei(); + client.user.setActivity( + `⚡${fast} ⦚ 🚶${average} ⦚ 🐢${slow}` + , { type: ActivityType.Watching }); + } + } + // Start listening to blockchain + subToBlockHeaders(setDiscordStatus); + }) .then(() => { // Start the gas price checker createGasPriceChecker(client);