Compare commits

...

21 Commits

Author SHA1 Message Date
76101943de Move redis connection url to env var 2023-05-08 18:23:41 -07:00
852b9cfc76 rm pull_policy: build 2023-05-03 14:10:38 -07:00
1ae3ef7a03 pass env vars through docker-compose.yml 2023-04-23 11:32:44 -07:00
3a9c68f21c always build new bot image 2023-04-23 11:24:56 -07:00
31d5a250b5 Don't import .env explicitly 2023-04-23 11:14:02 -07:00
20185635fb retry forever? 2023-04-22 20:48:28 -07:00
09e409c62d retry more times 2023-04-22 20:38:29 -07:00
d9de65911b don't prefetch txns 2023-04-22 19:37:10 -07:00
f78674578b Refactor to compute GasPrices differently 2023-04-22 15:58:38 -07:00
ef228254b3 handle observable errors 2023-04-22 11:46:24 -07:00
34f7f23ae8 remove unnecessary await 2023-04-21 22:26:48 -07:00
b16ef7c368 auto reply ignore roles and @everyone/@here 2023-04-21 20:37:52 -07:00
8b06d7dbdf fix some formatting 2023-04-21 17:24:10 -07:00
8b2ae82cfd Need to sort gas price list to find correct percentiles 2023-04-21 17:09:00 -07:00
f64fbfb71d reduce once instead of thrice 2023-04-21 16:44:51 -07:00
d28cc9b3f7 fix the formulas for fast/slow 2023-04-21 16:19:16 -07:00
deee0f205a Use rxjs observables for blockchain data 2023-04-21 15:51:09 -07:00
4f5812c2b8 set status every block, discord doesn't rate limit until 50 req/sec 2023-04-18 21:30:23 -07:00
5c3e1fdd6a save to disk every 5 minutes 2023-04-18 17:02:29 -07:00
08c71e3a60 Fix responding to bot replies 2023-04-18 17:00:28 -07:00
9fcddcaa28 fix build for ARM 2023-04-18 15:52:09 -07:00
12 changed files with 210 additions and 3517 deletions

View File

@ -2,6 +2,8 @@ FROM node:18-slim as ts-compiler
WORKDIR /usr/app
COPY package*.json ./
COPY tsconfig*.json ./
RUN apt update \
&& apt install build-essential python3 -y
RUN npm install
COPY . ./
RUN npm run build
@ -10,10 +12,11 @@ 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 -y
RUN npm install --omit=dev
FROM node:18-slim
WORKDIR /usr/app
COPY --from=ts-remover /usr/app ./
COPY .env ./
CMD node index.js

View File

@ -7,11 +7,11 @@ services:
restart: always
volumes:
- redisdb:/data
command: redis-server --save 900 1 --appendonly yes
command: redis-server --save 300 1 --appendonly yes
environment:
- REDIS_REPLICATION_MODE=master
- REDIS_APPENDONLY=yes
- REDIS_SAVE=900 1
- REDIS_SAVE=300 1
gwei-alert-bot:
depends_on:
- gwei-bot-redis
@ -22,6 +22,10 @@ services:
- 1.1.1.1
environment:
- NODE_ENV=production
- REDIS_URL=redis://gwei-bot-redis:6379
- DISCORD_BOT_TOKEN
- DISCORD_CLIENT
- RPC_URL
restart: unless-stopped
volumes:

3497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,12 +13,13 @@
"dependencies": {
"discord.js": "^14.9.0",
"dotenv": "^16.0.3",
"ethers": "^6.3.0",
"lodash": "^4.17.21",
"redis": "^4.6.5",
"web3": "^1.9.0"
"rxjs": "^7.8.0"
},
"devDependencies": {
"@types/dotenv": "^8.2.0",
"@types/redis": "^4.0.11",
"@types/lodash": "^4.14.194",
"typescript": "^4.9.5"
}
}

View File

@ -1,61 +1,59 @@
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 redisClient from './redis';
import { GasPrices } from '../types/gasPrices';
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 web3 = new Web3(new Web3.providers.WebsocketProvider(rpcUrl, {
reconnect: {
auto: true,
delay: 5000,
maxAttempts: 5,
onTimeout: false
}
}));
const provider = new WebSocketProvider(rpcUrl);
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)))
});
const blockBaseFeePerGasObservable$ = new Observable<number>((observer) => {
provider.on('block', async (blockNumber) => {
try {
const { baseFeePerGas } = await provider.getBlock(blockNumber) || {};
if (shouldSetStatus) setDiscordStatus()
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}`);
}
});
}
});
const gweiFromWei = (priceInWei: number): number =>
Math.round(Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei')));
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 getGasPrice = async (): Promise<number> => {
const gasPrice = await redisClient.get('gas-price');
return Number(gasPrice);
}
const fast: number = Math.max(...values);
const slow: number = Math.min(...values);
const sum: number = values.reduce((a, b) => a + b, 0);
const average: number = sum / values.length;
const getGasPricesInGwei = async (): Promise<GasPrices> => {
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),
};
return { values, fast, slow, average };
},
{ values: [], fast: -Infinity, slow: Infinity, average: NaN } // Initial value
),
// Only emit the computed prices
map<GasPrices, GasPrices>((computedGasPrices: GasPrices): GasPrices => {
const { fast, average, slow } = computedGasPrices;
return {
fast: Math.round(fast),
average: Math.round(average),
slow: Math.round(slow)
};
}),
catchError(err => throwError(() => new Error(err))),
retry(-1) // retry forever
);
return gasPrices;
};
export { getGasPricesInGwei };
export { baseGasPricesObservable$ };

View File

@ -1,12 +1,22 @@
import 'dotenv/config.js';
import { Events, Message, EmbedBuilder, User } from "discord.js";
import {
Events,
Message,
EmbedBuilder,
User
} from "discord.js";
import { DiscordClient } from '../discordClient';
module.exports = {
name: Events.MessageCreate,
async execute(client: DiscordClient, message: Message) {
// Don't respond to bots or replies
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 = [
new EmbedBuilder()
.setColor('#008000')

View File

@ -5,6 +5,6 @@ module.exports = {
name: Events.ClientReady,
once: true,
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}`);
}
};

View File

@ -1,22 +1,20 @@
import { EmbedBuilder, TextChannel } from 'discord.js';
import { getGasPricesInGwei } from './blockchain';
import { DiscordClient } from './discordClient';
import redisClient from './redis';
import { GasAlert } from '../types/gasAlert';
import { GasPrices } from '../types/gasPrices';
const createGasPriceChecker = (client: DiscordClient) => {
setInterval(async () => {
const createGasAlertChecker = (client: DiscordClient) =>
async (gasPrices: GasPrices) => {
try {
const gasPrices = await getGasPricesInGwei();
const gasAlerts: GasAlert[] = await redisClient
.hVals('gas-alerts')
.then((values) => values.map(val => JSON.parse(val)));
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 user = await client.users.fetch(gasAlert.userId);
@ -26,7 +24,7 @@ const createGasPriceChecker = (client: DiscordClient) => {
.setTitle('Gas price alert!')
.setDescription(`<@${gasAlert.userId}>!
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')
],
target: user
@ -39,7 +37,6 @@ Gas prices have fallen below your alert threshold of ${gasAlert.threshold} Gwei:
} catch (error) {
console.log(`Error checking gas prices:\n`, error);
}
}, 15000);
};
};
export { createGasPriceChecker };
export { createGasAlertChecker };

View File

@ -1,21 +1,27 @@
import { ChatInputCommandInteraction, EmbedBuilder } from 'discord.js';
import redisClient from './redis';
import { getGasPricesInGwei } from './blockchain';
import { GasAlert } from '../types/gasAlert';
import { GasPrices } from '../types/gasPrices';
// Respond to the "/gas" command
const handleGasCommand = async (interaction: ChatInputCommandInteraction): Promise<void> => {
const gasPrices = await getGasPricesInGwei();
console.log(`Replying to command "/gas": \n`, gasPrices);
const gasPricesStr = await redisClient.get('current-prices');
const embed = new EmbedBuilder()
if (gasPricesStr) {
const gasPrices = JSON.parse(gasPricesStr) as GasPrices;
console.log(`Replying to command "/gas": \n`, gasPrices);
const embed = new EmbedBuilder()
.setColor('#0099ff')
.setTitle('Current Gas Prices')
.setDescription(`${gasPrices.fast} ⦚⦚ 🚶${gasPrices.average} ⦚⦚ 🐢${gasPrices.slow}`)
await interaction.reply({ embeds: [embed] });
.setDescription(` ${gasPrices.fast} ⦚⦚🚶 ${gasPrices.average} ⦚⦚ 🐢 ${gasPrices.slow}`);
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

View File

@ -9,19 +9,30 @@ import fs from 'node:fs';
import path from 'node:path';
import { ActivityType, GatewayIntentBits } from 'discord.js';
import { getGasPricesInGwei, subToBlockHeaders } from './blockchain';
import { baseGasPricesObservable$ } from './blockchain';
import { deployCommands } from './deploy';
import { DiscordClient } from './discordClient';
import { createGasPriceChecker } from './gasPriceChecker';
import { createGasAlertChecker } from './gasAlertChecker';
import redisClient from './redis';
import { GasAlert } from '../types/gasAlert';
import { GasPrices } from '../types/gasPrices';
const token = process.env.DISCORD_BOT_TOKEN || "";
// Create a new Discord client
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 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 eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
@ -62,19 +74,19 @@ client.login(token)
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);
});
// Start listening to blockchain
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}`)
);

View File

@ -1,9 +1,9 @@
import { createClient } from 'redis';
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
// Create a new Redis client
const client = createClient({
url: 'redis://gwei-bot-redis:6379'
});
const client = createClient({ url: redisUrl });
// Log any Redis errors to the console
client.on('error', (error) => {

View File

@ -2,4 +2,5 @@ export interface GasPrices {
fast: number;
average: number;
slow: number;
}
values?: number[];
}