Compare commits
3 Commits
cea18e6a5f
...
bfaa6ebd25
Author | SHA1 | Date | |
---|---|---|---|
|
bfaa6ebd25 | ||
|
6036aea919 | ||
|
5b2975a0cc |
@ -2,8 +2,6 @@ FROM node:18-slim as ts-compiler
|
|||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY tsconfig*.json ./
|
COPY tsconfig*.json ./
|
||||||
# RUN apt update \
|
|
||||||
# && apt install build-essential python3 libsqlite3-dev -y
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@ -12,15 +10,10 @@ FROM node:18-slim as ts-remover
|
|||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
COPY --from=ts-compiler /usr/app/package*.json ./
|
COPY --from=ts-compiler /usr/app/package*.json ./
|
||||||
COPY --from=ts-compiler /usr/app/dist ./
|
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
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
FROM node:18-slim
|
FROM node:18-slim
|
||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
COPY --from=ts-remover /usr/app ./
|
COPY --from=ts-remover /usr/app ./
|
||||||
COPY .env ./
|
COPY .env ./
|
||||||
# RUN apt update \
|
|
||||||
# && apt install libsqlite3-dev sqlite3 -y
|
|
||||||
CMD node index.js
|
CMD node index.js
|
65
README.md
65
README.md
@ -1,2 +1,65 @@
|
|||||||
# gwei-alert-bot
|
# Gwei Alert Bot
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This repo contains Docker build files for a Discord bot built with Node.js and utilizes a composed Redis container for storage.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker + Docker Compose
|
||||||
|
- A Discord bot token
|
||||||
|
- A EVM RPC endpoint URL (websocket enabled) OR a local node running at `localhost:8545` (using docker host network mode)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Clone the repository to your local machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.ssh.surf/MrTuxedo/gwei-alert-bot.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a .env file in the root of the project and add the following environment variables:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
RPC_URL=<your rpc url>
|
||||||
|
DISCORD_BOT_TOKEN=<your discord bot token>
|
||||||
|
DISCORD_CLIENT=<your discord client ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the Docker container
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rebuilding with new changes
|
||||||
|
|
||||||
|
1. Stop the running bot and remove container by same name
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Pull the new work
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The following environment variables must be set in either `.env` or `docker-compose.yml` in order for the bot to function properly:
|
||||||
|
|
||||||
|
- RPC_URL: Your RPC url
|
||||||
|
- DISCORD_BOT_TOKEN: the Oauth2 token for your Discord bot.
|
||||||
|
- DISCORD_CLIENT: the client ID for your Discord bot. (app ID)
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
[WTFPL](./LICENSE)
|
@ -1,7 +1,9 @@
|
|||||||
import 'dotenv/config.js';
|
import 'dotenv/config.js';
|
||||||
import { GasPrices } from '../types/gasPrices'
|
|
||||||
import Web3 from 'web3';
|
import Web3 from 'web3';
|
||||||
|
|
||||||
|
import { GasPrices } from '../types/gasPrices'
|
||||||
|
import redisClient from './redis';
|
||||||
|
|
||||||
const rpcUrl = process.env.RPC_URL || 'ws://localhost:8545';
|
const rpcUrl = process.env.RPC_URL || 'ws://localhost:8545';
|
||||||
|
|
||||||
// Create a new web3 instance
|
// Create a new web3 instance
|
||||||
@ -14,20 +16,38 @@ const web3 = new Web3(new Web3.providers.WebsocketProvider(rpcUrl, {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Get the current gas price in gwei
|
export const subToBlockHeaders = (setDiscordStatus: () => Promise<void>) => {
|
||||||
async function getGasPrice() {
|
web3.eth.subscribe('newBlockHeaders', (error, blockHeader) => {
|
||||||
const gasPrice = await web3.eth.getGasPrice();
|
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);
|
||||||
|
|
||||||
|
if (shouldSetStatus) 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<number> => {
|
||||||
|
const gasPrice = await redisClient.get('gas-price');
|
||||||
return Number(gasPrice);
|
return Number(gasPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGasPricesInGwei = async (): Promise<GasPrices> => {
|
const getGasPricesInGwei = async (): Promise<GasPrices> => {
|
||||||
const gweiFromWei = (priceInWei: number): number =>
|
|
||||||
Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const gasPrice = await getGasPrice()
|
const gasPrice = await getGasPrice()
|
||||||
const fastPrice = gasPrice * 1.2;
|
const fastPrice = gasPrice * 1.1;
|
||||||
const slowPrice = gasPrice * 0.8;
|
const slowPrice = gasPrice * 0.9;
|
||||||
|
|
||||||
const gasPrices = {
|
const gasPrices = {
|
||||||
fast: gweiFromWei(fastPrice),
|
fast: gweiFromWei(fastPrice),
|
||||||
@ -35,13 +55,7 @@ const getGasPricesInGwei = async (): Promise<GasPrices> => {
|
|||||||
slow: gweiFromWei(slowPrice),
|
slow: gweiFromWei(slowPrice),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(gasPrices);
|
return gasPrices;
|
||||||
// await redisClient.set('gas-prices', JSON.stringify(gasPrices));
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export the getCurrentGasPrice function
|
|
||||||
export { getGasPricesInGwei };
|
export { getGasPricesInGwei };
|
12
src/commands/deleteAlert.ts
Normal file
12
src/commands/deleteAlert.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -23,8 +23,6 @@ const rest = new REST({ version: '10' }).setToken(token);
|
|||||||
// and deploy your commands!
|
// and deploy your commands!
|
||||||
export const deployCommands = async () => {
|
export const deployCommands = async () => {
|
||||||
try {
|
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
|
// The put method is used to fully refresh all commands in the guild with the current set
|
||||||
const data = await rest.put(
|
const data = await rest.put(
|
||||||
Routes.applicationCommands(clientId),
|
Routes.applicationCommands(clientId),
|
||||||
|
@ -20,5 +20,5 @@ module.exports = {
|
|||||||
console.error(`Error executing ${interaction.commandName}`);
|
console.error(`Error executing ${interaction.commandName}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
};
|
};
|
24
src/events/message.ts
Normal file
24
src/events/message.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import 'dotenv/config.js';
|
||||||
|
import { Events, Message, EmbedBuilder, User } from "discord.js";
|
||||||
|
import { DiscordClient } from '../discordClient';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: Events.MessageCreate,
|
||||||
|
async execute(client: DiscordClient, message: Message) {
|
||||||
|
if (message.author.bot) return;
|
||||||
|
if (message.mentions.has(client.user as User)) {
|
||||||
|
const embeds = [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setColor('#008000')
|
||||||
|
.setTitle('Available slash commands')
|
||||||
|
.setDescription(
|
||||||
|
`/gas
|
||||||
|
/alert [GWEI]
|
||||||
|
/alert-delete
|
||||||
|
/pending-alert`
|
||||||
|
)
|
||||||
|
]
|
||||||
|
message.reply({embeds});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -5,6 +5,6 @@ module.exports = {
|
|||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
once: true,
|
once: true,
|
||||||
execute(client: DiscordClient) {
|
execute(client: DiscordClient) {
|
||||||
if (client.user) return console.log(`Ready! Logged in as ${client.user.tag}`);
|
if (client.user) return console.log(`Ready! Logged in as ${client.user.id}: ${client.user.tag}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,7 +1,8 @@
|
|||||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||||
|
|
||||||
import { getGasPricesInGwei } from './blockchain';
|
import { getGasPricesInGwei } from './blockchain';
|
||||||
import redisClient from './redis';
|
|
||||||
import { DiscordClient } from './discordClient';
|
import { DiscordClient } from './discordClient';
|
||||||
|
import redisClient from './redis';
|
||||||
|
|
||||||
import { GasAlert } from '../types/gasAlert';
|
import { GasAlert } from '../types/gasAlert';
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ const createGasPriceChecker = (client: DiscordClient) => {
|
|||||||
const gasPrices = await getGasPricesInGwei();
|
const gasPrices = await getGasPricesInGwei();
|
||||||
|
|
||||||
const gasAlerts: GasAlert[] = await redisClient
|
const gasAlerts: GasAlert[] = await redisClient
|
||||||
.get('gas-alerts')
|
.hVals('gas-alerts')
|
||||||
.then((value) => (value ? JSON.parse(value) : []));
|
.then((values) => values.map(val => JSON.parse(val)));
|
||||||
|
|
||||||
gasAlerts.forEach(async (gasAlert) => {
|
gasAlerts.forEach(async (gasAlert) => {
|
||||||
if (gasPrices.average <= gasAlert.threshold) {
|
if (gasPrices.average <= gasAlert.threshold) {
|
||||||
@ -23,15 +24,20 @@ const createGasPriceChecker = (client: DiscordClient) => {
|
|||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setTitle('Gas price alert!')
|
.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')
|
.setColor('#FF8C00')
|
||||||
],
|
],
|
||||||
target: user
|
target: user
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(`Alerted ${user} under threshold ${gasAlert.threshold}: Removing alert...`)
|
||||||
|
await redisClient.hDel('gas-alerts', `${gasAlert.userId}`)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error checking gas prices: ${error}`);
|
console.log(`Error checking gas prices:\n`, error);
|
||||||
}
|
}
|
||||||
}, 15000);
|
}, 15000);
|
||||||
};
|
};
|
||||||
|
@ -8,12 +8,12 @@ import { GasAlert } from '../types/gasAlert';
|
|||||||
const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||||
const gasPrices = await getGasPricesInGwei();
|
const gasPrices = await getGasPricesInGwei();
|
||||||
|
|
||||||
console.log(`Replying to command "/gas": \n${gasPrices}`);
|
console.log(`Replying to command "/gas": \n`, gasPrices);
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor('#0099ff')
|
.setColor('#0099ff')
|
||||||
.setTitle('Current Gas Prices')
|
.setTitle('Current Gas Prices')
|
||||||
.setDescription(`The current gas prices are: \n\n Fast: ${gasPrices.fast} Gwei \n\n Average: ${gasPrices.average} Gwei \n\n Slow: ${gasPrices.slow} Gwei`)
|
.setDescription(`⚡${gasPrices.fast} ⦚⦚ 🚶${gasPrices.average} ⦚⦚ 🐢${gasPrices.slow}`)
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
await interaction.reply({ embeds: [embed] });
|
||||||
};
|
};
|
||||||
@ -28,9 +28,7 @@ const handleGasAlertCommand = async (interaction: ChatInputCommandInteraction):
|
|||||||
|
|
||||||
console.log(`Replying to command "/gas gwei ${threshold}"`);
|
console.log(`Replying to command "/gas gwei ${threshold}"`);
|
||||||
|
|
||||||
await redisClient.connect();
|
|
||||||
await redisClient.hSet('gas-alerts', `${userId}`, JSON.stringify(gasAlert));
|
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.`);
|
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<void> => {
|
const handleGasPendingCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
|
|
||||||
await redisClient.connect();
|
const alertStr = await redisClient.hGet('gas-alerts', `${userId}`);
|
||||||
const alertThreshold = await redisClient.get(`${userId}`);
|
const { threshold = null } = alertStr ? JSON.parse(alertStr) : {};
|
||||||
await redisClient.disconnect();
|
|
||||||
|
|
||||||
console.log(`Replying to command "/gas pending"`);
|
console.log(`Replying to command "/gas pending"`);
|
||||||
|
|
||||||
if (alertThreshold === null) {
|
if (threshold === null) {
|
||||||
await interaction.reply('No gas price alerts set');
|
await interaction.reply('No gas price alerts set');
|
||||||
} else {
|
} 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<void> => {
|
||||||
|
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 };
|
||||||
|
35
src/index.ts
35
src/index.ts
@ -1,10 +1,21 @@
|
|||||||
|
/* This program is free software. It comes without any warranty, to
|
||||||
|
* the extent permitted by applicable law. You can redistribute it
|
||||||
|
* and/or modify it under the terms of the Do What The Fuck You Want
|
||||||
|
* To Public License, Version 2, as published by Sam Hocevar. See
|
||||||
|
* http://www.wtfpl.net/ for more details. */
|
||||||
|
|
||||||
import 'dotenv/config.js';
|
import 'dotenv/config.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
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 { deployCommands } from './deploy';
|
||||||
import { DiscordClient } from './discordClient';
|
import { DiscordClient } from './discordClient';
|
||||||
import { createGasPriceChecker } from './gasPriceChecker';
|
import { createGasPriceChecker } from './gasPriceChecker';
|
||||||
|
import redisClient from './redis';
|
||||||
|
|
||||||
|
import { GasAlert } from '../types/gasAlert';
|
||||||
|
|
||||||
const token = process.env.DISCORD_BOT_TOKEN || "";
|
const token = process.env.DISCORD_BOT_TOKEN || "";
|
||||||
|
|
||||||
@ -33,16 +44,36 @@ for (const file of eventFiles) {
|
|||||||
const event = require(filePath);
|
const event = require(filePath);
|
||||||
if (event.once) {
|
if (event.once) {
|
||||||
client.once(event.name, (...args) => event.execute(...args));
|
client.once(event.name, (...args) => event.execute(...args));
|
||||||
|
} else if (event.name == 'messageCreate') {
|
||||||
|
client.on(event.name, (...args) => event.execute(client, ...args));
|
||||||
} else {
|
} else {
|
||||||
client.on(event.name, (...args) => event.execute(...args));
|
client.on(event.name, (...args) => event.execute(...args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deployCommands();
|
||||||
|
|
||||||
console.log('Booting up discord bot')
|
console.log('Booting up discord bot')
|
||||||
|
|
||||||
// Log in to Discord
|
// Log in to Discord
|
||||||
client.login(token)
|
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(() => {
|
.then(() => {
|
||||||
// Start the gas price checker
|
// Start the gas price checker
|
||||||
createGasPriceChecker(client);
|
createGasPriceChecker(client);
|
||||||
|
Loading…
Reference in New Issue
Block a user