Use rxjs observables for blockchain data

This commit is contained in:
David Keathley 2023-04-21 15:51:09 -07:00
parent 4f5812c2b8
commit deee0f205a
7 changed files with 158 additions and 3510 deletions

3491
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,61 +1,45 @@
import 'dotenv/config.js'; import 'dotenv/config.js';
import Web3 from 'web3'; import { WebSocketProvider, formatUnits } from 'ethers';
import { Observable } from 'rxjs';
import { map, scan } 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: {
auto: true,
delay: 5000,
maxAttempts: 5,
onTimeout: false
}
}));
export const subToBlockHeaders = (setDiscordStatus: () => Promise<void>) => { const blockGasPricesObservable = new Observable<GasPrices>((observer) => {
web3.eth.subscribe('newBlockHeaders', (error, blockHeader) => { provider.on('block', async (blockNumber) => {
if (error) console.error(error); try {
const block = await provider.getBlock(blockNumber, true);
const shouldLogWei = blockHeader.number % 10 === 0;
if (!block) throw new Error(`Error fetching block! ${blockNumber}`);
// Get the gas price for this block
web3.eth.getGasPrice((error, gasPrice) => { const gasPrices = block.prefetchedTransactions.map((tx) => tx.gasPrice);
if (error) console.error(error); const fast = Number(formatUnits(gasPrices[Math.floor(gasPrices.length * 0.9)], "gwei"));
const average = Number(formatUnits(gasPrices[Math.floor(gasPrices.length / 2)], "gwei"));
if (shouldLogWei) console.log('Gas price in wei:', gasPrice); const slow = Number(formatUnits(gasPrices[Math.floor(gasPrices.length * 0.05)], "gwei"));
redisClient.set('gas-price', Math.round(Number(gasPrice))) observer.next({ fast, average, slow } as GasPrices);
}); } catch (error) {
observer.error(`Error fetching block! ${error}`);
// Set status every block }
setDiscordStatus()
}); });
} });
const gweiFromWei = (priceInWei: number): number => const averageGasPricesObservable = blockGasPricesObservable.pipe(
Math.round(Number(web3.utils.fromWei(`${Math.round(priceInWei)}`, 'gwei'))); scan((acc, curr) => [...acc.slice(-19), curr], [] as GasPrices[]),
map((blocks) => {
const fastSum = blocks.reduce((sum, block) => sum + block.fast, 0);
const averageSum = blocks.reduce((sum, block) => sum + block.average, 0);
const slowSum = blocks.reduce((sum, block) => sum + block.slow, 0);
return {
fast: fastSum / blocks.length,
average: averageSum / blocks.length,
slow: slowSum / blocks.length,
};
})
);
const getGasPrice = async (): Promise<number> => { export { averageGasPricesObservable };
const gasPrice = await redisClient.get('gas-price');
return Number(gasPrice);
}
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 gasPrices;
};
export { getGasPricesInGwei };

View File

@ -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}`);
} }
}; };

View File

@ -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 };

View File

@ -1,21 +1,28 @@
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');
console.log(`Replying to command "/gas": \n`, gasPrices);
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') .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

View File

@ -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 { averageGasPricesObservable } 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 = async ({ 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'));
@ -63,18 +75,14 @@ client.login(token)
.catch((reason) => console.log("Error connecting to redis!\n", reason)) .catch((reason) => console.log("Error connecting to redis!\n", reason))
}) })
.then(async () => { .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 // Start listening to blockchain
subToBlockHeaders(setDiscordStatus); averageGasPricesObservable.subscribe({
}) next: async (averageGasPrices) => {
.then(() => { await redisClient.set('current-prices', JSON.stringify(averageGasPrices))
// Start the gas price checker await setDiscordStatus(averageGasPrices);
createGasPriceChecker(client); await doAlerts(averageGasPrices);
}); },
error: console.error,
complete: () => console.log("Blockchain data stream closed")
})
});