Compare commits
21 Commits
bfaa6ebd25
...
main
Author | SHA1 | Date | |
---|---|---|---|
76101943de | |||
852b9cfc76 | |||
1ae3ef7a03 | |||
3a9c68f21c | |||
31d5a250b5 | |||
20185635fb | |||
09e409c62d | |||
d9de65911b | |||
f78674578b | |||
ef228254b3 | |||
34f7f23ae8 | |||
b16ef7c368 | |||
8b06d7dbdf | |||
8b2ae82cfd | |||
f64fbfb71d | |||
d28cc9b3f7 | |||
deee0f205a | |||
4f5812c2b8 | |||
5c3e1fdd6a | |||
08c71e3a60 | |||
9fcddcaa28 |
@ -2,6 +2,8 @@ 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 -y
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
@ -10,10 +12,11 @@ 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 -y
|
||||||
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 ./
|
|
||||||
CMD node index.js
|
CMD node index.js
|
@ -7,11 +7,11 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- redisdb:/data
|
- redisdb:/data
|
||||||
command: redis-server --save 900 1 --appendonly yes
|
command: redis-server --save 300 1 --appendonly yes
|
||||||
environment:
|
environment:
|
||||||
- REDIS_REPLICATION_MODE=master
|
- REDIS_REPLICATION_MODE=master
|
||||||
- REDIS_APPENDONLY=yes
|
- REDIS_APPENDONLY=yes
|
||||||
- REDIS_SAVE=900 1
|
- REDIS_SAVE=300 1
|
||||||
gwei-alert-bot:
|
gwei-alert-bot:
|
||||||
depends_on:
|
depends_on:
|
||||||
- gwei-bot-redis
|
- gwei-bot-redis
|
||||||
@ -22,6 +22,10 @@ services:
|
|||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
- REDIS_URL=redis://gwei-bot-redis:6379
|
||||||
|
- DISCORD_BOT_TOKEN
|
||||||
|
- DISCORD_CLIENT
|
||||||
|
- RPC_URL
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
3497
package-lock.json
generated
3497
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,12 +13,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
"ethers": "^6.3.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"redis": "^4.6.5",
|
"redis": "^4.6.5",
|
||||||
"web3": "^1.9.0"
|
"rxjs": "^7.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dotenv": "^8.2.0",
|
"@types/lodash": "^4.14.194",
|
||||||
"@types/redis": "^4.0.11",
|
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,59 @@
|
|||||||
import 'dotenv/config.js';
|
import 'dotenv/config.js';
|
||||||
import Web3 from 'web3';
|
import { WebSocketProvider, formatUnits } from 'ethers';
|
||||||
|
import { Observable, throwError } from 'rxjs';
|
||||||
|
import { catchError, map, scan, retry } from 'rxjs/operators';
|
||||||
|
|
||||||
import { GasPrices } from '../types/gasPrices'
|
import { GasPrices } from '../types/gasPrices';
|
||||||
import redisClient from './redis';
|
|
||||||
|
|
||||||
const rpcUrl = process.env.RPC_URL || 'ws://localhost:8545';
|
const rpcUrl = process.env.RPC_URL || "wss://ropsten.infura.io/ws/v3/YOUR_INFURA_PROJECT_ID";
|
||||||
|
|
||||||
// Create a new web3 instance
|
const provider = new WebSocketProvider(rpcUrl);
|
||||||
const web3 = new Web3(new Web3.providers.WebsocketProvider(rpcUrl, {
|
|
||||||
reconnect: {
|
const blockBaseFeePerGasObservable$ = new Observable<number>((observer) => {
|
||||||
auto: true,
|
provider.on('block', async (blockNumber) => {
|
||||||
delay: 5000,
|
try {
|
||||||
maxAttempts: 5,
|
const { baseFeePerGas } = await provider.getBlock(blockNumber) || {};
|
||||||
onTimeout: false
|
|
||||||
|
if (!baseFeePerGas) throw new Error(`Error fetching block! ${blockNumber}`);
|
||||||
|
|
||||||
|
// Log averages every 10 blocks
|
||||||
|
if (blockNumber % 10 == 0) console.log(
|
||||||
|
`Found new block data for ${blockNumber}! Gas price: 🐢 ${Number(formatUnits(baseFeePerGas, "gwei")).toFixed(2)} Gwei`
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.next(Number(formatUnits(baseFeePerGas, "gwei")));
|
||||||
|
} catch (error) {
|
||||||
|
observer.error(`Error fetching block! ${error}`);
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
export const subToBlockHeaders = (setDiscordStatus: () => Promise<void>) => {
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (shouldSetStatus) console.log('Gas price in wei:', gasPrice);
|
|
||||||
|
|
||||||
redisClient.set('gas-price', Math.round(Number(gasPrice)))
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (shouldSetStatus) setDiscordStatus()
|
const baseGasPricesObservable$ = blockBaseFeePerGasObservable$.pipe(
|
||||||
});
|
scan<number, GasPrices>(
|
||||||
}
|
(acc: GasPrices, curr: number): GasPrices => {
|
||||||
|
// Keep only the 20 latest values
|
||||||
|
const values: number[] = acc.values ? [...acc.values.slice(-19), curr] : [curr];
|
||||||
|
|
||||||
const gweiFromWei = (priceInWei: number): number =>
|
const fast: number = Math.max(...values);
|
||||||
Math.round(Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei')));
|
const slow: number = Math.min(...values);
|
||||||
|
const sum: number = values.reduce((a, b) => a + b, 0);
|
||||||
|
const average: number = sum / values.length;
|
||||||
|
|
||||||
const getGasPrice = async (): Promise<number> => {
|
return { values, fast, slow, average };
|
||||||
const gasPrice = await redisClient.get('gas-price');
|
},
|
||||||
return Number(gasPrice);
|
{ values: [], fast: -Infinity, slow: Infinity, average: NaN } // Initial value
|
||||||
}
|
),
|
||||||
|
// Only emit the computed prices
|
||||||
const getGasPricesInGwei = async (): Promise<GasPrices> => {
|
map<GasPrices, GasPrices>((computedGasPrices: GasPrices): GasPrices => {
|
||||||
const gasPrice = await getGasPrice()
|
const { fast, average, slow } = computedGasPrices;
|
||||||
const fastPrice = gasPrice * 1.1;
|
return {
|
||||||
const slowPrice = gasPrice * 0.9;
|
fast: Math.round(fast),
|
||||||
|
average: Math.round(average),
|
||||||
const gasPrices = {
|
slow: Math.round(slow)
|
||||||
fast: gweiFromWei(fastPrice),
|
|
||||||
average: gweiFromWei(gasPrice),
|
|
||||||
slow: gweiFromWei(slowPrice),
|
|
||||||
};
|
};
|
||||||
|
}),
|
||||||
|
catchError(err => throwError(() => new Error(err))),
|
||||||
|
retry(-1) // retry forever
|
||||||
|
);
|
||||||
|
|
||||||
return gasPrices;
|
export { baseGasPricesObservable$ };
|
||||||
};
|
|
||||||
|
|
||||||
export { getGasPricesInGwei };
|
|
@ -1,12 +1,22 @@
|
|||||||
import 'dotenv/config.js';
|
import 'dotenv/config.js';
|
||||||
import { Events, Message, EmbedBuilder, User } from "discord.js";
|
import {
|
||||||
|
Events,
|
||||||
|
Message,
|
||||||
|
EmbedBuilder,
|
||||||
|
User
|
||||||
|
} from "discord.js";
|
||||||
import { DiscordClient } from '../discordClient';
|
import { DiscordClient } from '../discordClient';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: Events.MessageCreate,
|
name: Events.MessageCreate,
|
||||||
async execute(client: DiscordClient, message: Message) {
|
async execute(client: DiscordClient, message: Message) {
|
||||||
|
// Don't respond to bots or replies
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
if (message.mentions.has(client.user as User)) {
|
if (message.mentions.has(client.user as User, {
|
||||||
|
ignoreRoles: true,
|
||||||
|
ignoreRepliedUser: true,
|
||||||
|
ignoreEveryone: true
|
||||||
|
})) {
|
||||||
const embeds = [
|
const embeds = [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setColor('#008000')
|
.setColor('#008000')
|
||||||
|
@ -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.id}: ${client.user.tag}`);
|
if (client.user) return console.log(`Ready! Logged in as ${client.user.tag}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -1,22 +1,20 @@
|
|||||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||||
|
|
||||||
import { getGasPricesInGwei } from './blockchain';
|
|
||||||
import { DiscordClient } from './discordClient';
|
import { DiscordClient } from './discordClient';
|
||||||
import redisClient from './redis';
|
import redisClient from './redis';
|
||||||
|
|
||||||
import { GasAlert } from '../types/gasAlert';
|
import { GasAlert } from '../types/gasAlert';
|
||||||
|
import { GasPrices } from '../types/gasPrices';
|
||||||
|
|
||||||
const createGasPriceChecker = (client: DiscordClient) => {
|
const createGasAlertChecker = (client: DiscordClient) =>
|
||||||
setInterval(async () => {
|
async (gasPrices: GasPrices) => {
|
||||||
try {
|
try {
|
||||||
const gasPrices = await getGasPricesInGwei();
|
|
||||||
|
|
||||||
const gasAlerts: GasAlert[] = await redisClient
|
const gasAlerts: GasAlert[] = await redisClient
|
||||||
.hVals('gas-alerts')
|
.hVals('gas-alerts')
|
||||||
.then((values) => values.map(val => JSON.parse(val)));
|
.then((values) => values.map(val => JSON.parse(val)));
|
||||||
|
|
||||||
gasAlerts.forEach(async (gasAlert) => {
|
gasAlerts.forEach(async (gasAlert) => {
|
||||||
if (gasPrices.average <= gasAlert.threshold) {
|
if (gasPrices.fast <= gasAlert.threshold) {
|
||||||
const channel = await client.channels.fetch(gasAlert.channelId) as TextChannel;
|
const channel = await client.channels.fetch(gasAlert.channelId) as TextChannel;
|
||||||
const user = await client.users.fetch(gasAlert.userId);
|
const user = await client.users.fetch(gasAlert.userId);
|
||||||
|
|
||||||
@ -26,7 +24,7 @@ const createGasPriceChecker = (client: DiscordClient) => {
|
|||||||
.setTitle('Gas price alert!')
|
.setTitle('Gas price alert!')
|
||||||
.setDescription(`<@${gasAlert.userId}>!
|
.setDescription(`<@${gasAlert.userId}>!
|
||||||
Gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei:
|
Gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei:
|
||||||
⚡${gasPrices.fast} ⦚⦚ 🚶${gasPrices.average} ⦚⦚ 🐢${gasPrices.slow}`)
|
⚡ ${gasPrices.fast} ⦚⦚🚶 ${gasPrices.average} ⦚⦚ 🐢 ${gasPrices.slow}`)
|
||||||
.setColor('#FF8C00')
|
.setColor('#FF8C00')
|
||||||
],
|
],
|
||||||
target: user
|
target: user
|
||||||
@ -39,7 +37,6 @@ Gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei:
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Error checking gas prices:\n`, error);
|
console.log(`Error checking gas prices:\n`, error);
|
||||||
}
|
}
|
||||||
}, 15000);
|
};
|
||||||
};
|
|
||||||
|
|
||||||
export { createGasPriceChecker };
|
export { createGasAlertChecker };
|
@ -1,21 +1,27 @@
|
|||||||
import { ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
|
import { ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
|
||||||
import redisClient from './redis';
|
import redisClient from './redis';
|
||||||
import { getGasPricesInGwei } from './blockchain';
|
|
||||||
|
|
||||||
import { GasAlert } from '../types/gasAlert';
|
import { GasAlert } from '../types/gasAlert';
|
||||||
|
import { GasPrices } from '../types/gasPrices';
|
||||||
|
|
||||||
// Respond to the "/gas" command
|
// Respond to the "/gas" command
|
||||||
const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||||
const gasPrices = await getGasPricesInGwei();
|
const gasPricesStr = await redisClient.get('current-prices');
|
||||||
|
|
||||||
|
if (gasPricesStr) {
|
||||||
|
const gasPrices = JSON.parse(gasPricesStr) as GasPrices;
|
||||||
|
|
||||||
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(`⚡${gasPrices.fast} ⦚⦚ 🚶${gasPrices.average} ⦚⦚ 🐢${gasPrices.slow}`)
|
.setDescription(`⚡ ${gasPrices.fast} ⦚⦚🚶 ${gasPrices.average} ⦚⦚ 🐢 ${gasPrices.slow}`);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
await interaction.reply({ embeds: [embed] });
|
||||||
|
} else {
|
||||||
|
console.log(`Error fetching gas prices!`);
|
||||||
|
await interaction.reply('ERROR FETCHING GAS!!!1!')
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Respond to the "/gas alert ${gwei}" command
|
// Respond to the "/gas alert ${gwei}" command
|
||||||
|
48
src/index.ts
48
src/index.ts
@ -9,19 +9,30 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { ActivityType, GatewayIntentBits } from 'discord.js';
|
import { ActivityType, GatewayIntentBits } from 'discord.js';
|
||||||
|
|
||||||
import { getGasPricesInGwei, subToBlockHeaders } from './blockchain';
|
import { baseGasPricesObservable$ } from './blockchain';
|
||||||
import { deployCommands } from './deploy';
|
import { deployCommands } from './deploy';
|
||||||
import { DiscordClient } from './discordClient';
|
import { DiscordClient } from './discordClient';
|
||||||
import { createGasPriceChecker } from './gasPriceChecker';
|
import { createGasAlertChecker } from './gasAlertChecker';
|
||||||
import redisClient from './redis';
|
import redisClient from './redis';
|
||||||
|
|
||||||
import { GasAlert } from '../types/gasAlert';
|
import { GasPrices } from '../types/gasPrices';
|
||||||
|
|
||||||
const token = process.env.DISCORD_BOT_TOKEN || "";
|
const token = process.env.DISCORD_BOT_TOKEN || "";
|
||||||
|
|
||||||
// Create a new Discord client
|
// Create a new Discord client
|
||||||
const client = new DiscordClient({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
|
const client = new DiscordClient({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] });
|
||||||
|
|
||||||
|
const doAlerts = createGasAlertChecker(client);
|
||||||
|
|
||||||
|
const setDiscordStatus = ({ average, fast, slow }: GasPrices) => {
|
||||||
|
if (client.user) {
|
||||||
|
client.user.setActivity(
|
||||||
|
`⚡ ${fast} ⦚🚶 ${average} ⦚ 🐢 ${slow}`
|
||||||
|
, { type: ActivityType.Watching });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bot commands
|
||||||
const commandsPath = path.join(__dirname, 'commands');
|
const commandsPath = path.join(__dirname, 'commands');
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
@ -36,6 +47,7 @@ for (const file of commandFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load bot events
|
||||||
const eventsPath = path.join(__dirname, 'events');
|
const eventsPath = path.join(__dirname, 'events');
|
||||||
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
@ -62,19 +74,19 @@ client.login(token)
|
|||||||
await redisClient.connect()
|
await redisClient.connect()
|
||||||
.catch((reason) => console.log("Error connecting to redis!\n", reason))
|
.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 listening to blockchain
|
||||||
createGasPriceChecker(client);
|
baseGasPricesObservable$.subscribe({
|
||||||
});
|
next: async (averageGasPrices) => {
|
||||||
|
setDiscordStatus(averageGasPrices);
|
||||||
|
await redisClient.set('current-prices', JSON.stringify(averageGasPrices))
|
||||||
|
await doAlerts(averageGasPrices);
|
||||||
|
},
|
||||||
|
error: (err) => console.error(err),
|
||||||
|
complete: () => console.log("Blockchain data stream closed")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(
|
||||||
|
(err) => console.log(`Bot runtime error!
|
||||||
|
${err}`)
|
||||||
|
);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { createClient } from 'redis';
|
import { createClient } from 'redis';
|
||||||
|
|
||||||
|
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
||||||
|
|
||||||
// Create a new Redis client
|
// Create a new Redis client
|
||||||
const client = createClient({
|
const client = createClient({ url: redisUrl });
|
||||||
url: 'redis://gwei-bot-redis:6379'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log any Redis errors to the console
|
// Log any Redis errors to the console
|
||||||
client.on('error', (error) => {
|
client.on('error', (error) => {
|
||||||
|
1
types/gasPrices.d.ts
vendored
1
types/gasPrices.d.ts
vendored
@ -2,4 +2,5 @@ export interface GasPrices {
|
|||||||
fast: number;
|
fast: number;
|
||||||
average: number;
|
average: number;
|
||||||
slow: number;
|
slow: number;
|
||||||
|
values?: number[];
|
||||||
}
|
}
|
Reference in New Issue
Block a user