diff --git a/index.ts b/index.ts index 7e9b279..649f6c6 100644 --- a/index.ts +++ b/index.ts @@ -1,47 +1,56 @@ import axios from 'axios'; import TelegramBot, { Message } from 'node-telegram-bot-api'; import * as dotenv from 'dotenv'; +import { TVLResponse, VolumeResponse, DailyVolumeResponse } from './src/types'; +import { generateColorPalette } from './src/helper'; -dotenv.config(); -// Type for response data from DefiLlama API -interface TVLResponse { - currentChainTvls: { [chain: string]: number }; -} - -interface VolumeResponse { - total24h: number; - total48hto24h: number; - total7d: number; - totalAllTime: number; -} - -// Type for the breakdown of daily volume per chain -interface DailyVolumeBreakdown { - [chain: string]: { Raindex: number }; - } - -// Type for response data from DeFiLlama API for daily volume -interface DailyVolumeResponse { - totalDataChartBreakdown: [number, { [chain: string]: DailyVolumeBreakdown }][]; - name: string; -} +dotenv.config(); // Replace the value below with the Telegram token you receive from @BotFather const token: string = process.env.TELEGRAM_BOT_TOKEN as string; -// Create a bot that uses 'polling' to fetch new updates -const bot = new TelegramBot(token, { polling: true }); +const bot = new TelegramBot(token, { + polling: { + interval: 3000, // 3 seconds between polling requests + autoStart: true, + params: { + timeout: 10 // Long polling timeout (in seconds) + } + } +}); // Set bot commands so they appear in the menu bot.setMyCommands([ + { command: '/start', description: 'Start' }, { command: '/get_tvls', description: 'Get the current TVLs for Raindex' }, { command: '/get_volume', description: 'Get the total volume data for Raindex' }, { command: '/get_daily_volume', description: 'Get daily volume per chain for raindex' }, - { command: '/get_token_usd_distribution', description: 'Get token distrubition' } - + { command: '/get_daily_token_distribution', description: 'Get token distrubition' }, + { command: '/get_most_traded_tokens', description: 'Get most traded tokens on raindex' }, + { command: '/get_monthly_volume', description: 'Get monthly volumes for past 12 months.' }, ]); +bot.onText(/\/start/, (msg: Message) => { + const chatId: number = msg.chat.id; + + const startMessage = ` + Welcome to the Raindex Bot! Here are the available commands: + + 1. /get_tvls - Get the current TVLs (Total Value Locked) for Raindex. + 2. /get_volume - Get the total volume data for Raindex. + 3. /get_daily_volume - Get daily volume per chain for Raindex. + 4. /get_daily_token_distribution - Get token distribution across chains for Raindex. + 5. /get_most_traded_tokens - Get the most traded tokens on Raindex. + 6. /get_monthly_volume - Get monthly volumes for the past 12 months. + + Use any of these commands to retrieve the latest information about Raindex. Enjoy exploring the data! + `; + + // Send the start message with descriptions + bot.sendMessage(chatId, startMessage); +}); + bot.onText(/\/get_tvls/, async (msg: Message) => { const chatId: number = msg.chat.id; @@ -147,22 +156,7 @@ bot.onText(/\/get_tvls/, async (msg: Message) => { } }); -// Function to generate a dynamic darker and medium-dark color palette -function generateColorPalette(numColors: number): string[] { - const colors: string[] = []; - const darkColors = ['#2c3e50', '#34495e', '#1abc9c', '#16a085', '#f39c12', '#d35400', '#c0392b', '#7f8c8d']; - const mediumDarkColors = ['#FF9F40', '#FF6384', '#36A2EB', '#9966FF', '#C9CBCF', '#FFCE56', '#4BC0C0', '#FF4500']; - - for (let i = 0; i < numColors; i++) { - const colorSet = i % 2 === 0 ? darkColors : mediumDarkColors; - const color = colorSet[i % colorSet.length]; // Cycle through the colors - colors.push(color); - } - - return colors; -} - -bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { +bot.onText(/\/get_daily_token_distribution/, async (msg: Message) => { const chatId: number = msg.chat.id; try { @@ -180,25 +174,35 @@ bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { let tokenNames = Object.keys(lastTokensInUsd); let tokenValues = Object.values(lastTokensInUsd) as number[]; - // Sort tokens by value in ascending order + // Sort tokens by value in descending order (so the highest are at the top) const sortedTokens = tokenNames.map((token, index) => ({ tokenName: token, tokenValue: tokenValues[index] - })).sort((a, b) => a.tokenValue - b.tokenValue); + })).sort((a, b) => b.tokenValue - a.tokenValue); + + // Split tokens into top 10 and others + const topTokens = sortedTokens.slice(0, 10); // Get top 10 tokens + const otherTokens = sortedTokens.slice(10); // Get the rest of the tokens + + // Sum the values of the remaining tokens into "Others" + const othersValue = otherTokens.reduce((sum, token) => sum + token.tokenValue, 0); + + // Combine the top 10 tokens with "Others" as the 11th token + const finalTokens = [...topTokens, { tokenName: 'Others', tokenValue: othersValue }]; - // Extract sorted token names and values - tokenNames = sortedTokens.map(t => t.tokenName); - tokenValues = sortedTokens.map(t => t.tokenValue); + // Extract final token names and values + tokenNames = finalTokens.map(t => t.tokenName); + tokenValues = finalTokens.map(t => t.tokenValue); // Calculate total value and percentages for each token const totalValue = tokenValues.reduce((sum: number, value: number) => sum + value, 0); - const percentages = tokenValues.map((value: number) => ((value / Number(totalValue)) * 100).toFixed(2)); + const percentages = tokenValues.map((value: number) => ((value / totalValue) * 100).toFixed(2)); // Generate chart labels with token names and percentages const chartLabels = tokenNames.map((token, index) => `${token} ($${tokenValues[index].toLocaleString()} - ${percentages[index]}%)`); // Generate the token distribution message - let tokenMessage = 'Raindex - Token Distribution (USD):\n\n'; + let tokenMessage = 'Raindex - Top 10 Token Distribution (USD):\n\n'; tokenNames.forEach((token, index) => { tokenMessage += `${token}: $${Number(tokenValues[index]).toLocaleString(undefined, { minimumFractionDigits: 2, @@ -215,7 +219,7 @@ bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { data: { labels: chartLabels, datasets: [{ - label: 'Token Distribution (USD)', + label: 'Top 10 Token Distribution (USD)', data: tokenValues, backgroundColor: backgroundColor, // Dynamically generated background colors borderColor: '#ffffff', @@ -228,7 +232,7 @@ bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { plugins: { title: { display: true, - text: 'Raindex - Token Distribution (USD)', + text: 'Raindex - Top 10 Token Distribution (USD)', font: { size: 22, weight: 'bold', @@ -275,7 +279,143 @@ bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { }))}&w=600&h=600`; // Set the width and height of the pie chart // Send the pie chart to the user - bot.sendPhoto(chatId, chartUrl, { caption: 'Token Distribution (USD) for Raindex' }); + bot.sendPhoto(chatId, chartUrl, { caption: 'Top 10 Token Distribution (USD) for Raindex' }); + // Send the token distribution in text format + bot.sendMessage(chatId, tokenMessage); + + } catch (error) { + console.error('Error fetching token distribution data:', error); + bot.sendMessage(chatId, 'Sorry, there was an error fetching the token distribution data.'); + } +}); + +bot.onText(/\/get_most_traded_tokens/, async (msg: Message) => { + const chatId: number = msg.chat.id; + + try { + // Make the GET request to the Llama API + const response = await axios.get('https://api.llama.fi/protocol/raindex', { + headers: { + accept: '*/*' + } + }); + + const tokensInUsdArray = response.data.tokensInUsd; + + // Initialize an object to store aggregated token values + const tokenAggregates: { [token: string]: number } = {}; + + // Iterate over each entry in the tokensInUsdArray + tokensInUsdArray.forEach((entry: any) => { + if (entry && entry.tokens) { + Object.keys(entry.tokens).forEach(tokenName => { + const tokenValue = entry.tokens[tokenName]; + if (tokenValue && typeof tokenValue === 'number') { + // Add to the aggregate for the token + tokenAggregates[tokenName] = (tokenAggregates[tokenName] || 0) + tokenValue; + } + }); + } + }); + + // Convert the tokenAggregates object to an array for sorting + const aggregatedTokens = Object.keys(tokenAggregates).map(tokenName => ({ + tokenName, + tokenValue: tokenAggregates[tokenName] + })); + + // Sort tokens by value in descending order + const sortedTokens = aggregatedTokens.sort((a, b) => b.tokenValue - a.tokenValue); + + // Extract the top 10 tokens + const topTokens = sortedTokens.slice(0, 10); + + // Calculate total value for top 10 tokens and percentages + const totalValue = topTokens.reduce((sum, token) => sum + token.tokenValue, 0); + const percentages = topTokens.map(token => ((token.tokenValue / totalValue) * 100).toFixed(2)); + + // Generate chart labels for the top 10 tokens + const chartLabels = topTokens.map((token, index) => `${token.tokenName} ($${token.tokenValue.toLocaleString()} - ${percentages[index]}%)`); + + // Generate the token distribution message + let tokenMessage = 'Raindex - Top 10 Traded Tokens All-Time (USD):\n\n'; + topTokens.forEach((token, index) => { + tokenMessage += `${token.tokenName}: $${token.tokenValue.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + })} (${percentages[index]}%)\n`; + }); + + // Generate dynamic background colors + const backgroundColor = generateColorPalette(topTokens.length); + + // Create the pie chart URL using QuickChart.io + const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify({ + type: 'pie', + data: { + labels: chartLabels, + datasets: [{ + label: 'Top 10 Traded Tokens (USD)', + data: topTokens.map(token => token.tokenValue), + backgroundColor: backgroundColor, // Dynamically generated background colors + borderColor: '#ffffff', + borderWidth: 3, + hoverBorderWidth: 4, + hoverBorderColor: '#ccc' + }] + }, + options: { + plugins: { + title: { + display: true, + text: 'Raindex - Top 10 Traded Tokens All-Time (USD)', + font: { + size: 22, + weight: 'bold', + family: "'Helvetica', 'Arial', sans-serif" + }, + color: '#ffffff' + }, + legend: { + position: 'right', + labels: { + font: { + size: 14, + family: "'Helvetica', 'Arial', sans-serif", + weight: 'bold' + }, + boxWidth: 18, + padding: 25, + color: '#ffffff' // White text for contrast on dark background + } + }, + tooltip: { + callbacks: { + label: (tooltipItem: any) => { + const label = chartLabels[tooltipItem.dataIndex]; + const value = topTokens[tooltipItem.dataIndex].tokenValue.toLocaleString(undefined, { minimumFractionDigits: 2 }); + return `${label}: $${value}`; + } + } + }, + datalabels: { + display: false // Disable data labels on the pie chart itself + } + }, + layout: { + padding: { + left: 15, + right: 15, + top: 15, + bottom: 15 + } + }, + backgroundColor: '#2c2c2c' // Grey background color + } + }))}&w=600&h=600`; // Set the width and height of the pie chart + + // Send the pie chart to the user + bot.sendPhoto(chatId, chartUrl, { caption: 'Top 10 Traded Tokens All-Time (USD) for Raindex' }); // Send the token distribution in text format bot.sendMessage(chatId, tokenMessage); @@ -285,7 +425,6 @@ bot.onText(/\/get_token_usd_distribution/, async (msg: Message) => { } }); -// Handler function to query the Llama API and send volume data for Raindex bot.onText(/\/get_volume/, async (msg: Message) => { const chatId: number = msg.chat.id; @@ -426,5 +565,163 @@ bot.onText(/\/get_daily_volume/, async (msg: Message) => { } }); +bot.onText(/\/get_monthly_volume/, async (msg: Message) => { + const chatId: number = msg.chat.id; - + try { + // Make the GET request to the Llama API for Raindex + const response = await axios.get('https://api.llama.fi/summary/dexs/raindex', { + headers: { + accept: '*/*' + } + }); + + const totalDataChart = response.data.totalDataChart; + + // Get the current timestamp and calculate the timestamp for 12 months ago + const currentDate = new Date(); + const twelveMonthsAgo = new Date(currentDate.setMonth(currentDate.getMonth() - 12)).getTime() / 1000; + + // Filter data for the last 12 months and aggregate month by month + const monthlyData: { [key: string]: number } = {}; + + totalDataChart.forEach(([timestamp, volume]: [number, number]) => { + if (timestamp >= twelveMonthsAgo) { + const date = new Date(timestamp * 1000); + const monthYear = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; // Format as YYYY-MM + monthlyData[monthYear] = (monthlyData[monthYear] || 0) + volume; + } + }); + + // Sort months chronologically + const sortedMonths = Object.keys(monthlyData).sort(); + const sortedVolumes = sortedMonths.map(month => monthlyData[month]); + + // Generate a darker blue color for the filled area chart + const backgroundColor = 'rgba(26, 82, 118, 0.5)'; // Darker blue with opacity for fill + const borderColor = 'rgba(26, 82, 118, 1)'; // Solid darker blue for the border + + // Create the filled area chart using QuickChart.io + const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify({ + type: 'line', + data: { + labels: sortedMonths, // Months (x-axis) + datasets: [{ + label: 'Total Volume (USD)', + data: sortedVolumes, // Corresponding volumes + fill: true, // Fill the area below the line + backgroundColor: backgroundColor, // Semi-transparent dark blue + borderColor: borderColor, // Solid dark blue for line + borderWidth: 3, + pointRadius: 4, + pointBackgroundColor: borderColor, + pointBorderColor: '#ffffff', + tension: 0.4 // Curve the lines slightly + }] + }, + options: { + plugins: { + title: { + display: true, + text: 'Raindex - Monthly Total Volume (Last 12 Months)', + font: { + size: 22, + weight: 'bold', + family: "'Helvetica', 'Arial', sans-serif" + }, + color: '#ffffff' + }, + legend: { + display: true, // Show legend + position: 'top', // Position legend at the top + labels: { + color: '#ffffff', + font: { + size: 14, + family: "'Helvetica', 'Arial', sans-serif", + weight: 'bold' + } + } + }, + tooltip: { + callbacks: { + label: (tooltipItem: any) => { + const label = sortedMonths[tooltipItem.dataIndex]; + const value = sortedVolumes[tooltipItem.dataIndex].toLocaleString(undefined, { minimumFractionDigits: 2 }); + return `${label}: $${value}`; + } + } + } + }, + scales: { + x: { + title: { + display: true, + text: 'Month', // X-axis label + color: '#ffffff', + font: { + size: 16, + weight: 'bold' + } + }, + ticks: { + color: '#ffffff', + font: { + size: 12 + } + }, + grid: { + color: '#4c4c4c' // Subtle grid lines + } + }, + y: { + title: { + display: true, + text: 'Volume (USD)', // Y-axis label + color: '#ffffff', + font: { + size: 16, + weight: 'bold' + } + }, + ticks: { + color: '#ffffff', + // Explicitly format the y-axis labels with the dollar sign + callback: function(value: number) { + return '$' + value.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ','); // Format with commas and add $ + } + }, + grid: { + color: '#4c4c4c' // Subtle grid lines + } + } + }, + layout: { + padding: { + left: 20, + right: 20, + top: 20, + bottom: 20 + } + }, + backgroundColor: '#2c2c2c' // Grey background color + } + }))}&w=800&h=400`; // Set width and height of the chart + + // Send the filled area chart to the user (no text message) + bot.sendPhoto(chatId, chartUrl, { caption: 'Raindex - Monthly Total Volume (Last 12 Months)' }); + + } catch (error) { + console.error('Error fetching volume data:', error); + bot.sendMessage(chatId, 'Sorry, there was an error fetching the monthly volume data.'); + } +}); + +bot.on('polling_error', (error) => { + console.error('Polling error:', error); +}); + +bot.on('error', (err) => { + console.error('Bot error:', err); + bot.startPolling(); +}); diff --git a/src/helper.ts b/src/helper.ts new file mode 100644 index 0000000..90cbcfa --- /dev/null +++ b/src/helper.ts @@ -0,0 +1,35 @@ +// Function to generate a color palette with shades of blue ranging from dark to light, avoiding white +function generateColorPalette(numColors: number): string[] { + const colors: string[] = []; + + // Start with a base dark blue (can be adjusted if needed) + const baseHue = 210; // Hue value for blue (210° in HSL) + + // Loop through and generate varying shades of blue, avoid very light shades (e.g., white) + for (let i = 0; i < numColors; i++) { + // Calculate lightness, starting from very dark to light but avoiding too light (e.g., max 85%) + const lightness = (10 + (i * (75 / (numColors - 1)))); // Range from 10% to 85% lightness + + // Generate the HSL color and convert to a hex code + const hslColor = `hsl(${baseHue}, 70%, ${lightness}%)`; // Keeping saturation at 70% for vibrant blues + + // Push the generated color to the array + colors.push(hslToHex(baseHue, 70, lightness)); + } + + return colors; +} + +// Helper function to convert HSL to HEX +function hslToHex(h: number, s: number, l: number): string { + s /= 100; + l /= 100; + const k = (n: number) => (n + h / 30) % 12; + const a = s * Math.min(l, 1 - l); + const f = (n: number) => + l - a * Math.max(Math.min(k(n) - 3, 9 - k(n), 1), -1); + const toHex = (x: number) => Math.round(x * 255).toString(16).padStart(2, '0'); + return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`; + } + +export { generateColorPalette, hslToHex }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..bcfbb79 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,29 @@ +// Type for response data from DefiLlama API +interface TVLResponse { + currentChainTvls: { [chain: string]: number }; +} + +interface VolumeResponse { + total24h: number; + total48hto24h: number; + total7d: number; + totalAllTime: number; +} + +// Type for the breakdown of daily volume per chain +interface DailyVolumeBreakdown { + [chain: string]: { Raindex: number }; +} + +// Type for response data from DeFiLlama API for daily volume +interface DailyVolumeResponse { + totalDataChartBreakdown: [number, { [chain: string]: DailyVolumeBreakdown }][]; + name: string; +} + +export type { + TVLResponse, + VolumeResponse, + DailyVolumeBreakdown, + DailyVolumeResponse +}; \ No newline at end of file