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);