2024-03-13 22:43:08 -04:00
require ( 'dotenv' ) . config ( ) ;
const { Client , GatewayIntentBits , EmbedBuilder } = require ( 'discord.js' ) ;
2024-11-14 14:53:15 -05:00
const sqlite3 = require ( 'sqlite3' ) . verbose ( ) ;
2024-03-13 22:43:08 -04:00
const path = require ( 'path' ) ;
const client = new Client ( {
intents : [ GatewayIntentBits . Guilds , GatewayIntentBits . GuildMessages ] ,
} ) ;
2024-11-14 14:53:15 -05:00
const PRICE _CHECK _INTERVAL = 60000 ;
const PRICE _INCREASE _THRESHOLD = 2.5 ;
const DB _PATH = path . join ( _ _dirname , 'data' , 'solana.db' ) ;
2024-03-13 22:43:08 -04:00
2024-11-14 14:53:15 -05:00
const db = new sqlite3 . Database ( DB _PATH , ( err ) => {
if ( err ) console . error ( 'Database opening error: ' , err ) ;
initializeDatabase ( ) ;
2024-03-13 22:43:08 -04:00
} ) ;
2024-11-14 14:53:15 -05:00
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
) ` );
2024-11-25 07:40:52 -05:00
db . run ( 'CREATE INDEX IF NOT EXISTS idx_timestamp ON price_history(timestamp)' , [ ] , ( err ) => {
if ( err ) {
console . error ( 'Error creating index:' , err ) ;
} else {
initializeBotState ( ) ;
}
} ) ;
2024-11-14 14:53:15 -05:00
} ) ;
}
2024-11-25 07:40:52 -05:00
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 ) ;
}
} ) ;
}
2024-11-15 13:14:15 -05:00
2024-03-13 22:43:08 -04:00
async function fetchSolanaPrice ( ) {
try {
const fetch = ( await import ( 'node-fetch' ) ) . default ;
2024-11-14 14:53:15 -05:00
const response = await fetch ( 'https://min-api.cryptocompare.com/data/price?fsym=SOL&tsyms=USD' ) ;
2024-03-26 11:17:04 -04:00
if ( ! response . ok ) throw new Error ( ` HTTP error! status: ${ response . status } ` ) ;
2024-03-13 22:43:08 -04:00
const data = await response . json ( ) ;
return parseFloat ( data . USD ) . toFixed ( 2 ) ;
} catch ( error ) {
console . error ( 'Error fetching Solana price:' , error ) ;
return null ;
}
}
2024-11-14 14:53:15 -05:00
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 ( ) ) ;
} ) ;
2024-03-24 18:03:37 -04:00
}
2024-11-14 14:53:15 -05:00
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 ) ;
}
} ) ;
} ) ;
2024-03-13 22:43:08 -04:00
}
2024-11-14 14:53:15 -05:00
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 ) ;
}
} ) ;
} ) ;
2024-03-14 12:21:25 -04:00
}
2024-03-14 00:25:18 -04:00
2024-11-14 14:53:15 -05:00
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 ) ;
}
}
) ;
} ) ;
2024-03-14 00:25:18 -04:00
}
2024-11-14 14:53:15 -05:00
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 }
} ;
}
2024-03-14 00:25:18 -04:00
}
2024-11-14 14:53:15 -05:00
async function createPriceEmbed ( currentPrice , changes ) {
2024-03-14 02:22:26 -04:00
const randomColor = Math . floor ( Math . random ( ) * 16777215 ) . toString ( 16 ) . padStart ( 6 , '0' ) ;
2024-03-14 00:52:55 -04:00
2024-11-14 14:53:15 -05:00
return new EmbedBuilder ( )
2024-03-14 00:52:55 -04:00
. setColor ( ` # ${ randomColor } ` )
2024-03-14 00:25:18 -04:00
. setThumbnail ( 'https://solana.com/src/img/branding/solanaLogoMark.png' )
. setTitle ( 'Solana (SOL) Price Update' )
2024-03-24 02:58:17 -04:00
. setURL ( 'https://coinmarketcap.com/currencies/solana/' )
2024-11-14 14:53:15 -05:00
. setDescription ( ` **Current Price: \` $ ${ currentPrice } \` ** ` )
2024-03-14 00:25:18 -04:00
. addFields ( [
2024-11-14 14:53:15 -05:00
{ 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 }
2024-03-14 00:25:18 -04:00
] )
. setTimestamp ( )
. setImage ( process . env . IMAGE _URL ) ;
2024-11-14 14:53:15 -05:00
}
2024-03-14 00:25:18 -04:00
2024-11-14 14:53:15 -05:00
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 ) ;
}
} ) ;
} ) ;
}
2024-03-13 22:43:08 -04:00
2024-11-14 14:53:15 -05:00
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 ) ;
}
2024-03-13 22:43:08 -04:00
}
2024-11-14 14:53:15 -05:00
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 ) ;
2024-03-13 22:43:08 -04:00
}
}
2024-11-14 14:53:15 -05:00
async function checkPriceAndAnnounce ( ) {
try {
const currentPrice = await fetchSolanaPrice ( ) ;
if ( ! currentPrice ) {
setTimeout ( checkPriceAndAnnounce , PRICE _CHECK _INTERVAL ) ;
return ;
}
2024-03-14 00:25:18 -04:00
2024-11-14 14:53:15 -05:00
await savePriceData ( currentPrice ) ;
const lastAnnouncedPrice = await getLastAnnouncedPrice ( ) ;
2024-03-14 00:47:50 -04:00
2024-11-14 14:53:15 -05:00
const priceChannel = await client . channels . fetch ( process . env . SOLANA _PRICE _CHANNEL _ID ) ;
await updatePriceMessage ( priceChannel , currentPrice ) ;
2024-03-13 22:43:08 -04:00
2024-11-14 14:53:15 -05:00
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 ) ) ;
2024-03-13 22:43:08 -04:00
}
} catch ( error ) {
2024-11-14 14:53:15 -05:00
console . error ( 'Error in price check and announce cycle:' , error ) ;
2024-03-13 22:43:08 -04:00
}
2024-11-14 14:53:15 -05:00
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 ) ;
} ) ;
2024-03-13 22:43:08 -04:00
}
2024-11-14 14:53:15 -05:00
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 ) ;