Skip to content

Commit

Permalink
cleanup the repo
Browse files Browse the repository at this point in the history
  • Loading branch information
Alwin24 committed Aug 3, 2024
1 parent b2aef99 commit 7d3dadc
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 48 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# Whale Notification Bot
A telegram bot which watches and sends message to the group when a swap above threshold amount takes place for the solana SPL-TOKEN.
A telegram bot which watches and sends message to the group when a swap above threshold amount takes place for the solana SPL-TOKEN on meteora pools. Currently, the bot is limited to 4 tokens per group (this can be modified in the by changing _maxTokensPerGroup_).

## Environment variables
## Executing on local

### Environment variables
Create an .env file and add the following variables.
```.env
BOT_TOKEN=
DB_URI=
HELIUS_API_KEY=
BACKEND_RPC=
BIRDSEYE_API_KEY=
```
### Running the bot
```bash
yarn install
yarn start
```

## Commands
- list - Get the list of tokens and the min. amount registered. Usage : /list
- register - Register the token with min. amount. Usage : /register <token_mint> <min_value>
- unregister - Unregister a token. Usage : /unregister <token_mint>
## Telegram Bot Commands
- config - Configure the bot; add/remove tokens to watch and modify their settings.
- setup - Setup the portal, add the bot to the group.
85 changes: 44 additions & 41 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ configDotenv();
const bot = new Telegraf(process.env.BOT_TOKEN!);

const apiKey = process.env.HELIUS_API_KEY;
const maxTokensPerGroup = 4;

// Initialize WebSocket connection to Helius
function initializeWebSocket() {
let lastMessageDate = new Date();
let ws: WebSocket;
Expand All @@ -33,6 +35,7 @@ function initializeWebSocket() {
method: "transactionSubscribe",
params: [
{
//Meteora Pools
accountInclude: [
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo",
"Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB",
Expand All @@ -51,6 +54,7 @@ function initializeWebSocket() {
ws.send(JSON.stringify(request));
}

// Send a ping every 30 seconds to keep the connection alive
function startPing() {
pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
Expand All @@ -65,6 +69,7 @@ function initializeWebSocket() {
}, 30000);
}

// Check if we have received any messages in the last 20 seconds
function statusCheck() {
statusCheckInterval = setInterval(() => {
if (lastMessageDate.getTime() < Date.now() - 20000) {
Expand All @@ -84,7 +89,6 @@ function initializeWebSocket() {
});

ws.on("message", async function incoming(data) {
// console.log("Received message");
const messageStr = data.toString("utf8");
try {
const messageObj = JSON.parse(messageStr);
Expand Down Expand Up @@ -112,6 +116,7 @@ function initializeWebSocket() {
console.log("WebSocket error:", err);
});

// Cleanup and restart the WebSocket connection if it's closed
ws.on("close", function close() {
console.log("WebSocket is closed, attempting to restart...");
clearInterval(statusCheckInterval);
Expand Down Expand Up @@ -139,7 +144,7 @@ const sendQueuedMessages = async (groupId: number) => {
// Check if we can send more messages
if (messageTimestamps[groupId].length >= 20) {
console.log(`Rate limit reached for group ${groupId}. Skipping...`);
setTimeout(() => sendQueuedMessages(groupId), 40000); // Retry after 1 minute
setTimeout(() => sendQueuedMessages(groupId), 40000); // Retry after 40 seconds
return;
}

Expand All @@ -157,7 +162,7 @@ const sendQueuedMessages = async (groupId: number) => {
messageTimestamps[groupId].push(now);
} catch (error) {
console.log(`Failed to send message to group ${groupId}:`, error);
// Retry after 1 minute to avoid spamming retries on persistent errors
// Retry after 40 seconds to avoid spamming retries on persistent errors
setTimeout(() => sendQueuedMessages(groupId), 40000);
return;
}
Expand All @@ -170,8 +175,8 @@ const sendQueuedMessages = async (groupId: number) => {
}
};

// Continuously check for queued messages and send them
const handleQueuedMessages = () => {
// console.log("Checking for queued messages...", Object.keys(messageQueues));
Object.keys(messageQueues).forEach((groupId) => {
const parsedGroupId = Number(groupId);

Expand All @@ -192,6 +197,7 @@ const handleQueuedMessages = () => {

handleQueuedMessages();

// Start the telegram bot
bot.start(async (ctx) => {
const [command, groupId] = ctx.payload.split("_");

Expand All @@ -210,13 +216,13 @@ bot.start(async (ctx) => {
}),
];

if (tokens.length < 4)
if (tokens.length < maxTokensPerGroup)
inline_keyboard.push([
{ text: "➕ Add New Token", callback_data: `add_${groupId}` },
]);

await ctx.reply(
"*Active Tokens*\n\nTrack upto 4 tokens at once with @MeteoraWhaleBot",
`*Active Tokens*\n\nTrack upto ${maxTokensPerGroup} tokens at once with @MeteoraWhaleBot`,
{
reply_markup: {
inline_keyboard,
Expand Down Expand Up @@ -345,9 +351,6 @@ bot.hears(/^(?!\/).*/, async (ctx) => {
}

let message = ctx.message.text;
if (message.startsWith("/")) {
return;
}

await connectToDatabase();
const userState = await UserState.findOne({ userId: ctx.from.id });
Expand Down Expand Up @@ -407,7 +410,7 @@ bot.hears(/^(?!\/).*/, async (ctx) => {
});
return;
}
//get pool with highest liquidity
//Currently the pool address is used only to display the dexTools link
const pool = tokenPools.reduce((prev: any, current: any) => {
return prev.liquidity > current.liquidity ? prev : current;
});
Expand All @@ -420,7 +423,7 @@ bot.hears(/^(?!\/).*/, async (ctx) => {
}

const minValue = 20;
const minValueEmojis = "🟢🟢";
const minValueEmojis = "🚀🚀";

try {
await Token.create({
Expand Down Expand Up @@ -511,36 +514,6 @@ bot.hears(/^(?!\/).*/, async (ctx) => {
}
});

bot.command("config", async (ctx) => {
if (ctx.chat.type === "private") {
await ctx.reply("*This command can only be used in groups*", {
parse_mode: "Markdown",
});
return;
}
const groupId = ctx.chat.id;

await ctx.reply("*Click the button below to configure the bot*", {
reply_markup: {
inline_keyboard: [
[
{
text: "Configure Bot 🤖",
url: `https://t.me/${ctx.botInfo.username}?start=config_${groupId}`,
},
],
],
},
parse_mode: "Markdown",
});
});

bot.catch((err) => {
console.log("Error occurred", err);
});

bot.launch().then(() => console.log("Bot started!"));

bot.command("setup", async (ctx) => {
//if not a private message return
if (ctx.chat.type !== "private") {
Expand Down Expand Up @@ -590,3 +563,33 @@ bot.command("setup", async (ctx) => {
}
);
});

bot.command("config", async (ctx) => {
if (ctx.chat.type === "private") {
await ctx.reply("*This command can only be used in groups*", {
parse_mode: "Markdown",
});
return;
}
const groupId = ctx.chat.id;

await ctx.reply("*Click the button below to configure the bot*", {
reply_markup: {
inline_keyboard: [
[
{
text: "Configure Bot 🤖",
url: `https://t.me/${ctx.botInfo.username}?start=config_${groupId}`,
},
],
],
},
parse_mode: "Markdown",
});
});

bot.catch((err) => {
console.log("Error occurred", err);
});

bot.launch().then(() => console.log("Bot started!"));
6 changes: 5 additions & 1 deletion src/utils/listenerCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const buyerUrl = "https://solscan.io/account/";
const dexTUrl = "https://www.dextools.io/app/en/solana/pair-explorer/";
const solTrendingUrl = "https://t.me/SOLTRENDING";

//Fetch token price from jupipter or birdseye
const getTokenPrice = async (tokenMint: string) => {
try {
async function fetchTokenPrice(tokenMint: string) {
Expand Down Expand Up @@ -42,7 +43,6 @@ const getTokenPrice = async (tokenMint: string) => {
}
}

// Usage
let tokenPrice: number,
solPrice: number = 0;
try {
Expand Down Expand Up @@ -82,6 +82,7 @@ const getTokenPrice = async (tokenMint: string) => {
}
};

//Fetch total supply of token
const getTotalSupply = async (tokenMint: string) => {
try {
const connection = new Connection(process.env.BACKEND_RPC!);
Expand Down Expand Up @@ -122,6 +123,7 @@ const callback = async (data: any) => {
}

const logMessages: string[] = data.transaction.meta.logMessages;
// Consider only swap transactions from meteora
if (!logMessages.some((log) => log.includes("Instruction: Swap"))) return;

if (!data.transaction.transaction) {
Expand Down Expand Up @@ -194,6 +196,7 @@ const callback = async (data: any) => {
poolAddress,
} = listeningGroup;

// Stock image if no image is provided
image =
image ||
"https://static.vecteezy.com/system/resources/previews/006/153/238/original/solana-sol-logo-crypto-currency-purple-theme-background-neon-design-vector.jpg";
Expand Down Expand Up @@ -231,6 +234,7 @@ const callback = async (data: any) => {

caption = caption.replace("__emojis__", emojis);

// Add to message queue of the respective group
if (!messageQueues[groupId]) {
messageQueues[groupId] = [];
}
Expand Down

0 comments on commit 7d3dadc

Please sign in to comment.