solana-price/main.js

311 lines
9.8 KiB
JavaScript

require('dotenv').config();
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages],
});
const PRICE_CHECK_INTERVAL = 60000;
const PRICE_INCREASE_THRESHOLD = 2.5;
const DB_PATH = path.join(__dirname, 'data', 'solana.db');
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) console.error('Database opening error: ', err);
initializeDatabase();
});
function initializeDatabase() {
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp INTEGER NOT NULL,
price REAL NOT NULL
)`);
db.run(`CREATE TABLE IF NOT EXISTS bot_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`);
db.run('CREATE INDEX IF NOT EXISTS idx_timestamp ON price_history(timestamp)', [], (err) => {
if (err) {
console.error('Error creating index:', err);
} else {
initializeBotState();
}
});
});
}
function initializeBotState() {
db.get('SELECT value FROM bot_state WHERE key = ?', ['lastAnnouncedPrice'], (err, row) => {
if (err) {
console.error('Error checking lastAnnouncedPrice:', err);
} else if (!row) {
db.run('INSERT INTO bot_state (key, value) VALUES (?, ?)',
['lastAnnouncedPrice', '195'],
(err) => {
if (err) console.error('Error setting initial lastAnnouncedPrice:', err);
else console.log('Initial lastAnnouncedPrice set to 195');
}
);
} else {
console.log('lastAnnouncedPrice already initialized to', row.value);
}
});
}
async function fetchSolanaPrice() {
try {
const fetch = (await import('node-fetch')).default;
const response = await fetch('https://min-api.cryptocompare.com/data/price?fsym=SOL&tsyms=USD');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return parseFloat(data.USD).toFixed(2);
} catch (error) {
console.error('Error fetching Solana price:', error);
return null;
}
}
function savePriceData(price) {
return new Promise((resolve, reject) => {
const timestamp = Date.now();
db.run('INSERT INTO price_history (timestamp, price) VALUES (?, ?)',
[timestamp, price],
(err) => err ? reject(err) : resolve());
});
}
function getLastAnnouncedPrice() {
return new Promise((resolve, reject) => {
db.get('SELECT value FROM bot_state WHERE key = "lastAnnouncedPrice"',
(err, row) => {
if (err) {
console.error('Error getting last announced price:', err);
resolve(null);
} else {
resolve(row ? parseFloat(row.value) : null);
}
});
});
}
function updateLastAnnouncedPrice(price) {
return new Promise((resolve, reject) => {
db.run('INSERT OR REPLACE INTO bot_state (key, value) VALUES (?, ?)',
['lastAnnouncedPrice', price.toString()],
(err) => {
if (err) {
console.error('Error updating last announced price:', err);
resolve(false);
} else {
resolve(true);
}
});
});
}
function getTimeBasedPrice(minutesAgo) {
return new Promise((resolve) => {
const timestamp = Date.now() - (minutesAgo * 60 * 1000);
db.get(
`SELECT price FROM price_history
WHERE timestamp <= ?
ORDER BY timestamp DESC LIMIT 1`,
[timestamp],
(err, row) => {
if (err) {
console.error(`Error getting price from ${minutesAgo} minutes ago:`, err);
resolve(null);
} else {
resolve(row?.price || null);
}
}
);
});
}
async function calculateChanges(currentPrice) {
try {
const [oneMinAgo, fiveMinAgo, oneHourAgo, sixHourAgo, oneDayAgo] = await Promise.all([
getTimeBasedPrice(1),
getTimeBasedPrice(5),
getTimeBasedPrice(60),
getTimeBasedPrice(360),
getTimeBasedPrice(1440)
]);
const calculateChange = (oldPrice) => {
if (!oldPrice) return { percent: 0, dollar: 0 };
const dollarChange = currentPrice - oldPrice;
const percentChange = (dollarChange / oldPrice) * 100;
return { percent: percentChange, dollar: dollarChange };
};
return {
oneMin: calculateChange(oneMinAgo),
fiveMin: calculateChange(fiveMinAgo),
oneHour: calculateChange(oneHourAgo),
sixHour: calculateChange(sixHourAgo),
oneDay: calculateChange(oneDayAgo)
};
} catch (error) {
console.error('Error calculating changes:', error);
return {
oneMin: { percent: 0, dollar: 0 },
fiveMin: { percent: 0, dollar: 0 },
oneHour: { percent: 0, dollar: 0 },
sixHour: { percent: 0, dollar: 0 },
oneDay: { percent: 0, dollar: 0 }
};
}
}
async function createPriceEmbed(currentPrice, changes) {
const randomColor = Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
return new EmbedBuilder()
.setColor(`#${randomColor}`)
.setThumbnail('https://solana.com/src/img/branding/solanaLogoMark.png')
.setTitle('Solana (SOL) Price Update')
.setURL('https://coinmarketcap.com/currencies/solana/')
.setDescription(`**Current Price: \`$${currentPrice}\`**`)
.addFields([
{ name: '💰 Current Price', value: `**\`$${currentPrice}\`**`, inline: false },
{ name: '1 Minute Change', value: `${changes.oneMin.percent.toFixed(2)}% (${changes.oneMin.dollar.toFixed(2)} USD)`, inline: true },
{ name: '5 Minute Change', value: `${changes.fiveMin.percent.toFixed(2)}% (${changes.fiveMin.dollar.toFixed(2)} USD)`, inline: true },
{ name: '1 Hour Change', value: `${changes.oneHour.percent.toFixed(2)}% (${changes.oneHour.dollar.toFixed(2)} USD)`, inline: true },
{ name: '6 Hour Change', value: `${changes.sixHour.percent.toFixed(2)}% (${changes.sixHour.dollar.toFixed(2)} USD)`, inline: true },
{ name: '1 Day Change', value: `${changes.oneDay.percent.toFixed(2)}% (${changes.oneDay.dollar.toFixed(2)} USD)`, inline: true }
])
.setTimestamp()
.setImage(process.env.IMAGE_URL);
}
async function getMessageId() {
return new Promise((resolve) => {
db.get('SELECT value FROM bot_state WHERE key = "priceMessageId"',
(err, row) => {
if (err) {
console.error('Error getting message ID:', err);
resolve(null);
} else {
resolve(row?.value || null);
}
});
});
}
async function updateMessageId(messageId) {
return new Promise((resolve) => {
db.run('INSERT OR REPLACE INTO bot_state (key, value) VALUES (?, ?)',
['priceMessageId', messageId],
err => {
if (err) {
console.error('Error updating message ID:', err);
resolve(false);
} else {
resolve(true);
}
});
});
}
async function updatePriceMessage(channel, currentPrice) {
try {
const changes = await calculateChanges(currentPrice);
const embed = await createPriceEmbed(currentPrice, changes);
let messageId = await getMessageId();
let success = false;
if (messageId) {
try {
const message = await channel.messages.fetch(messageId);
await message.edit({ embeds: [embed] });
success = true;
} catch (error) {
console.error('Failed to edit existing message:', error);
}
}
if (!success) {
try {
const message = await channel.send({ embeds: [embed] });
await updateMessageId(message.id);
} catch (error) {
console.error('Failed to send new message:', error);
await new Promise(resolve => setTimeout(resolve, 5000));
try {
const message = await channel.send({ embeds: [embed] });
await updateMessageId(message.id);
} catch (retryError) {
console.error('Failed to send message after retry:', retryError);
}
}
}
} catch (error) {
console.error('Error in updatePriceMessage:', error);
}
}
async function checkPriceAndAnnounce() {
try {
const currentPrice = await fetchSolanaPrice();
if (!currentPrice) {
setTimeout(checkPriceAndAnnounce, PRICE_CHECK_INTERVAL);
return;
}
await savePriceData(currentPrice);
const lastAnnouncedPrice = await getLastAnnouncedPrice();
const priceChannel = await client.channels.fetch(process.env.SOLANA_PRICE_CHANNEL_ID);
await updatePriceMessage(priceChannel, currentPrice);
if (lastAnnouncedPrice && (parseFloat(currentPrice) - lastAnnouncedPrice >= PRICE_INCREASE_THRESHOLD)) {
const announcementsChannel = await client.channels.fetch(process.env.ANNOUNCEMENTS_CHANNEL_ID);
await announcementsChannel.send(
`@everyone Solana price has increased significantly! Current price: $${currentPrice} (Up $${(parseFloat(currentPrice) - lastAnnouncedPrice).toFixed(2)} from last announcement)`
);
await updateLastAnnouncedPrice(parseFloat(currentPrice));
}
} catch (error) {
console.error('Error in price check and announce cycle:', error);
}
setTimeout(checkPriceAndAnnounce, PRICE_CHECK_INTERVAL);
}
function cleanupOldData() {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000);
db.run('DELETE FROM price_history WHERE timestamp < ?', [oneDayAgo], (err) => {
if (err) console.error('Error cleaning up old data:', err);
});
}
db.on('error', (err) => {
console.error('Database error:', err);
});
client.once('ready', () => {
console.log('Bot is online!');
checkPriceAndAnnounce();
setInterval(cleanupOldData, 6 * 60 * 60 * 1000);
});
client.on('error', error => {
console.error('Discord client error:', error);
});
client.on('shardError', error => {
console.error('Discord websocket error:', error);
});
process.on('unhandledRejection', error => {
console.error('Unhandled promise rejection:', error);
});
client.login(process.env.DISCORD_BOT_TOKEN);