├── src ├── raydium │ ├── index.ts │ ├── market │ │ ├── index.ts │ │ └── market.ts │ ├── types │ │ ├── index.ts │ │ └── mint.ts │ ├── liquidity │ │ ├── index.ts │ │ └── liquidity.ts │ └── utils │ │ ├── formatClmmConfigs.ts │ │ ├── formatClmmKeysById.ts │ │ ├── formatAmmKeysById.ts │ │ ├── formatClmmKeys.ts │ │ └── formatAmmKeys.ts ├── pump │ ├── types.ts │ ├── constants.ts │ ├── api.ts │ ├── utils.ts │ └── swap.ts ├── utils │ ├── wait.ts │ ├── get.signature.ts │ ├── jupiter.transaction.sender.ts │ ├── v0.transaction.ts │ └── index.ts ├── controllers │ ├── common.controller.ts │ ├── referral.history.ts │ ├── referral.channel.ts │ ├── message.handler.ts │ └── callback.handler.ts ├── services │ ├── mongodb.ts │ ├── redis.service.ts │ ├── referrer.list.service.ts │ ├── redis.ts │ ├── referral.channel.service.ts │ ├── msglog.service.ts │ ├── openmarket.service.ts │ ├── raydium.token.service.ts │ ├── trade.service.ts │ ├── fee.service.ts │ ├── user.service.ts │ ├── position.service.ts │ ├── jito.bundle.ts │ ├── user.trade.setting.service.ts │ ├── pnl.service.ts │ ├── referral.service.ts │ ├── alert.bot.module.ts │ └── token.metadata.ts ├── models │ ├── index.ts │ ├── referral.history.ts │ ├── referral.channel.ts │ ├── referrer.list.model.ts │ ├── openmarket.model.ts │ ├── msglog.model.ts │ ├── position.model.ts │ ├── trade.model.ts │ ├── user.model.ts │ └── token.model.ts ├── cron │ ├── remove.openmarket.cron.ts │ ├── sol.price.cron.ts │ └── alert.bot.cron.ts ├── bot.opts.ts ├── screens │ ├── referral.link.handler.ts │ ├── common.screen.ts │ ├── welcome.referral.screen.ts │ ├── bot.dashboard.ts │ ├── payout.screen.ts │ ├── welcome.screen.ts │ └── position.screen.ts ├── config.ts └── main.ts ├── .gitignore ├── .env.example ├── serve.ts ├── package.json ├── README.md └── tsconfig.json /src/raydium/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./raydium"; 2 | -------------------------------------------------------------------------------- /src/raydium/market/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./market"; 2 | -------------------------------------------------------------------------------- /src/raydium/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./mint"; 2 | -------------------------------------------------------------------------------- /src/raydium/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./liquidity"; 2 | -------------------------------------------------------------------------------- /src/pump/types.ts: -------------------------------------------------------------------------------- 1 | export enum TransactionMode { 2 | Simulation, 3 | Execution, 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export const wait = (time: number) => 2 | new Promise((resolve) => setTimeout(resolve, time)); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | #Hardhat files 4 | cache 5 | artifacts 6 | .vscode 7 | .env 8 | .yarn 9 | .yarnrc.yml 10 | 11 | yarn.lock 12 | 13 | .DS_Store 14 | test.ts -------------------------------------------------------------------------------- /src/controllers/common.controller.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | 3 | export const CommonController = { 4 | dismissMessage: (bot: TelegramBot, msg: TelegramBot.Message) => {}, 5 | }; 6 | -------------------------------------------------------------------------------- /src/services/mongodb.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { MONGODB_URL } from '../config'; 3 | 4 | const connectOptions: mongoose.ConnectOptions = { 5 | autoCreate: true, 6 | retryReads: true, 7 | }; 8 | const connectMongodb = () => { 9 | return mongoose.connect(MONGODB_URL, connectOptions); 10 | } 11 | export default connectMongodb; 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGODB_URL= 2 | REDIS_URI= 3 | 4 | # Local 5 | GROWTRADE_BOT_ID= 6 | GROWSOL_ALERT_BOT_ID= 7 | BridgeBotID= 8 | ALERT_BOT_API_TOKEN= 9 | TELEGRAM_BOT_API_TOKEN= 10 | 11 | MAINNET_RPC= 12 | PRIVATE_RPC_ENDPOINT= 13 | RPC_WEBSOCKET_ENDPOINT= 14 | 15 | JITO_UUID= 16 | 17 | BIRD_EVEY_API= 18 | 19 | GROWSOL_API_ENDPOINT= 20 | 21 | PNL_IMG_GENERATOR_API= -------------------------------------------------------------------------------- /src/services/redis.service.ts: -------------------------------------------------------------------------------- 1 | import redisClient from "./redis"; 2 | 3 | export const setFlagForBundleVerify = async (username: string) => { 4 | const key = `${username}_wait_bundle`; 5 | await redisClient.set(key, "true"); 6 | await redisClient.expire(key, 30); 7 | } 8 | export const waitFlagForBundleVerify = async (username: string) => { 9 | const key = `${username}_wait_bundle`; 10 | const res = await redisClient.get(key); 11 | if (!res) return false; 12 | return true; 13 | } -------------------------------------------------------------------------------- /src/utils/get.signature.ts: -------------------------------------------------------------------------------- 1 | import bs58 from "bs58"; 2 | import { Transaction, VersionedTransaction } from "@solana/web3.js"; 3 | 4 | export function getSignature( 5 | transaction: Transaction | VersionedTransaction 6 | ): string { 7 | const signature = 8 | "signature" in transaction 9 | ? transaction.signature 10 | : transaction.signatures[0]; 11 | if (!signature) { 12 | throw new Error( 13 | "Missing transaction signature, the transaction was not signed by the fee payer" 14 | ); 15 | } 16 | return bs58.encode(signature); 17 | } -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import UserSchema from "./user.model"; 2 | import TradeSchema, { TradeTypeEnum } from "./trade.model"; 3 | import MsgLogSchema from "./msglog.model"; 4 | import TokenSchema from "./token.model"; 5 | import OpenMarketSchema from "./openmarket.model"; 6 | import PositionSchema from "./position.model"; 7 | import ReferralChannelSchema from "./referral.channel"; 8 | import ReferralHistorySchema from "./referral.history"; 9 | 10 | export { 11 | UserSchema, 12 | TradeSchema, 13 | MsgLogSchema, 14 | TokenSchema, 15 | PositionSchema, 16 | TradeTypeEnum, 17 | OpenMarketSchema, 18 | ReferralHistorySchema, 19 | ReferralChannelSchema, 20 | }; 21 | -------------------------------------------------------------------------------- /src/models/referral.history.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Referral Chat Schema 7 | const ReferralHistorySchema = new Schema( 8 | { 9 | username: { 10 | type: String, 11 | default: "", 12 | }, 13 | uniquecode: { 14 | type: String, 15 | default: "", 16 | }, 17 | referrer_address: { 18 | type: String, 19 | default: "", 20 | }, 21 | amount: { 22 | type: Number, 23 | default: 0, 24 | }, 25 | }, 26 | { 27 | timestamps: true, // This option adds createdAt and updatedAt fields 28 | } 29 | ); 30 | 31 | export default mongoose.model("referralhistory", ReferralHistorySchema); 32 | -------------------------------------------------------------------------------- /src/models/referral.channel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Referral Chat Schema 7 | const ReferralChannelSchema = new Schema( 8 | { 9 | // chat id 10 | chat_id: { 11 | type: String, 12 | default: "", 13 | required: true, 14 | }, 15 | channel_name: { 16 | type: String, 17 | default: "", 18 | }, 19 | creator: { 20 | type: String, 21 | default: "", 22 | required: true, 23 | }, 24 | referral_code: { 25 | type: String, 26 | default: "", 27 | required: true, 28 | }, 29 | }, 30 | { 31 | timestamps: true, // This option adds createdAt and updatedAt fields 32 | } 33 | ); 34 | 35 | export default mongoose.model("referralchannel", ReferralChannelSchema); 36 | -------------------------------------------------------------------------------- /src/models/referrer.list.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Referral Chat Schema 7 | const ReferrerListSchema = new Schema( 8 | { 9 | // chat id 10 | chatId: { 11 | type: String, 12 | default: "", 13 | required: true, 14 | }, 15 | messageId: { 16 | type: String, 17 | default: "", 18 | required: true, 19 | }, 20 | referrer: { 21 | type: String, 22 | default: "", 23 | required: true, 24 | }, 25 | channelName: { 26 | type: String, 27 | default: "", 28 | required: true, 29 | }, 30 | }, 31 | { 32 | timestamps: true, // This option adds createdAt and updatedAt fields 33 | } 34 | ); 35 | 36 | export default mongoose.model("referrerList", ReferrerListSchema); 37 | -------------------------------------------------------------------------------- /src/models/openmarket.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | export const MarketSchema = new Schema({ 7 | bids: { 8 | type: String, 9 | default: "", 10 | }, 11 | asks: { 12 | type: String, 13 | default: "", 14 | }, 15 | eventQueue: { 16 | type: String, 17 | default: "", 18 | }, 19 | }); 20 | // OpenMarket Schema 21 | const OpenMarket = new Schema( 22 | { 23 | mint: { 24 | type: String, 25 | default: "", 26 | required: true, 27 | unique: true, 28 | }, 29 | market: { 30 | type: MarketSchema, 31 | required: true, 32 | }, 33 | }, 34 | { 35 | timestamps: true, // This option adds createdAt and updatedAt fields 36 | } 37 | ); 38 | 39 | export default mongoose.model("openmarket", OpenMarket); 40 | -------------------------------------------------------------------------------- /src/pump/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, SystemProgram } from "@solana/web3.js"; 2 | 3 | export const GLOBAL = new PublicKey( 4 | "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" 5 | ); 6 | export const FEE_RECIPIENT = new PublicKey( 7 | "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" 8 | ); 9 | export const TOKEN_PROGRAM_ID = new PublicKey( 10 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 11 | ); 12 | export const ASSOC_TOKEN_ACC_PROG = new PublicKey( 13 | "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 14 | ); 15 | export const RENT = new PublicKey( 16 | "SysvarRent111111111111111111111111111111111" 17 | ); 18 | export const PUMP_FUN_PROGRAM = new PublicKey( 19 | "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 20 | ); 21 | export const PUMP_FUN_ACCOUNT = new PublicKey( 22 | "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" 23 | ); 24 | export const SYSTEM_PROGRAM_ID = SystemProgram.programId; 25 | -------------------------------------------------------------------------------- /src/services/referrer.list.service.ts: -------------------------------------------------------------------------------- 1 | import referrerListModelSchema from "../models/referrer.list.model"; 2 | 3 | export const ReferrerListService = { 4 | create: async (props: any) => { 5 | try { 6 | return await referrerListModelSchema.create(props); 7 | } catch (err: any) { 8 | console.log(err); 9 | throw new Error(err.message); 10 | } 11 | }, 12 | find: async (props: any) => { 13 | const filter = props; 14 | try { 15 | const result = await referrerListModelSchema.find(filter); 16 | 17 | return result; 18 | } catch (err: any) { 19 | throw new Error(err.message); 20 | } 21 | }, 22 | findLastOne: async (props: any) => { 23 | const filter = props; 24 | try { 25 | const result = await referrerListModelSchema.findOne(filter).sort({ updatedAt: -1 }); 26 | 27 | return result; 28 | } catch (err: any) { 29 | throw new Error(err.message); 30 | } 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/cron/remove.openmarket.cron.ts: -------------------------------------------------------------------------------- 1 | import { NATIVE_MINT } from "@solana/spl-token"; 2 | import cron from "node-cron"; 3 | import redisClient from "../services/redis"; 4 | import { OpenMarketSchema } from "../models"; 5 | const EVERY_1_MIN = "*/1 * * * *"; 6 | export const runOpenmarketCronSchedule = () => { 7 | try { 8 | cron 9 | .schedule(EVERY_1_MIN, () => { 10 | removeOldDatas(); 11 | }) 12 | .start(); 13 | } catch (error) { 14 | console.error( 15 | `Error running the Schedule Job for fetching the chat data: ${error}` 16 | ); 17 | } 18 | }; 19 | 20 | const removeOldDatas = async () => { 21 | try { 22 | const threeHoursAgo = new Date(Date.now() - 3 * 60 * 60 * 1000); 23 | try { 24 | const result = await OpenMarketSchema.deleteMany({ 25 | createdAt: { $lt: threeHoursAgo }, 26 | }); 27 | console.log(`Deleted ${result.deletedCount} old documents.`); 28 | } catch (error) { 29 | console.error("Error deleting old documents:", error); 30 | } 31 | } catch (e) { 32 | console.log("🚀 ~ SOL price cron job ~ Failed", e); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/pump/api.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export async function getCoinData(mintStr: string) { 4 | try { 5 | const url = `https://frontend-api.pump.fun/coins/${mintStr}`; 6 | const response = await axios.get(url, { 7 | headers: { 8 | "User-Agent": 9 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0", 10 | Accept: "*/*", 11 | "Accept-Language": "en-US,en;q=0.5", 12 | "Accept-Encoding": "gzip, deflate, br", 13 | Referer: "https://www.pump.fun/", 14 | Origin: "https://www.pump.fun", 15 | Connection: "keep-alive", 16 | "Sec-Fetch-Dest": "empty", 17 | "Sec-Fetch-Mode": "cors", 18 | "Sec-Fetch-Site": "cross-site", 19 | "If-None-Match": 'W/"43a-tWaCcS4XujSi30IFlxDCJYxkMKg"', 20 | }, 21 | }); 22 | if (response.status === 200) { 23 | return response.data; 24 | } else { 25 | console.error("Failed to retrieve coin data:", response.status); 26 | return null; 27 | } 28 | } catch (error) { 29 | console.error("Error fetching coin data:", error); 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/raydium/market/market.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, PublicKey } from "@solana/web3.js"; 2 | import { 3 | GetStructureSchema, 4 | MARKET_STATE_LAYOUT_V3, 5 | } from "@raydium-io/raydium-sdk"; 6 | import { MINIMAL_MARKET_STATE_LAYOUT_V3 } from "../liquidity"; 7 | 8 | export type MinimalMarketStateLayoutV3 = typeof MINIMAL_MARKET_STATE_LAYOUT_V3; 9 | export type MinimalMarketLayoutV3 = 10 | GetStructureSchema; 11 | 12 | export async function getMinimalMarketV3( 13 | connection: Connection, 14 | marketId: PublicKey, 15 | commitment?: Commitment 16 | ): Promise { 17 | const marketInfo = await connection.getAccountInfo(marketId, { 18 | commitment, 19 | dataSlice: { 20 | offset: MARKET_STATE_LAYOUT_V3.offsetOf("eventQueue"), 21 | length: 32 * 3, 22 | }, 23 | }); 24 | 25 | return MINIMAL_MARKET_STATE_LAYOUT_V3.decode(marketInfo!.data); 26 | } 27 | 28 | export const convertDBForMarketV3 = (market: any) => { 29 | return { 30 | eventQueue: new PublicKey(market.eventQueue), 31 | bids: new PublicKey(market.bids), 32 | asks: new PublicKey(market.asks), 33 | } as MinimalMarketLayoutV3; 34 | }; 35 | -------------------------------------------------------------------------------- /serve.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import startTradeBot from "./src/main"; 3 | import connectMongodb from "./src/services/mongodb"; 4 | import redisClient from "./src/services/redis"; 5 | 6 | 7 | // connect mongodb 8 | connectMongodb() 9 | .then(() => { 10 | console.log('MongoDB connected'); 11 | // redis 12 | connectRedis(); 13 | }) 14 | .catch(error => console.log("MongoDB connect failed", error)); 15 | 16 | // connect redis 17 | const connectRedis = () => { 18 | redisClient.on('connect', function () { 19 | console.log('Redis database connected' + '\n'); 20 | // start tradeBot 21 | startTradeBot(); 22 | }); 23 | 24 | redisClient.on('reconnecting', function () { 25 | console.log('Redis client reconnecting'); 26 | }); 27 | 28 | redisClient.on('ready', function () { 29 | console.log('Redis client is ready'); 30 | }); 31 | 32 | redisClient.on('error', function (err) { 33 | console.log('Something went wrong ' + err); 34 | }); 35 | 36 | redisClient.on('end', function () { 37 | console.log('\nRedis client disconnected'); 38 | console.log('Server is going down now...'); 39 | process.exit(); 40 | }); 41 | 42 | redisClient.connect(); 43 | } 44 | -------------------------------------------------------------------------------- /src/raydium/utils/formatClmmConfigs.ts: -------------------------------------------------------------------------------- 1 | import { AmmConfigLayout, ApiClmmConfigItem } from "@raydium-io/raydium-sdk"; 2 | import { AccountInfo, PublicKey } from "@solana/web3.js"; 3 | 4 | import { private_connection } from "../../config"; 5 | 6 | export function formatConfigInfo( 7 | id: PublicKey, 8 | account: AccountInfo 9 | ): ApiClmmConfigItem { 10 | const info = AmmConfigLayout.decode(account.data); 11 | 12 | return { 13 | id: id.toString(), 14 | index: info.index, 15 | protocolFeeRate: info.protocolFeeRate, 16 | tradeFeeRate: info.tradeFeeRate, 17 | tickSpacing: info.tickSpacing, 18 | fundFeeRate: info.fundFeeRate, 19 | fundOwner: info.fundOwner.toString(), 20 | description: "", 21 | }; 22 | } 23 | 24 | export async function formatClmmConfigs(programId: string) { 25 | const configAccountInfo = await private_connection.getProgramAccounts( 26 | new PublicKey(programId), 27 | { filters: [{ dataSize: AmmConfigLayout.span }] } 28 | ); 29 | return configAccountInfo 30 | .map((i) => formatConfigInfo(i.pubkey, i.account)) 31 | .reduce((a, b) => { 32 | a[b.id] = b; 33 | return a; 34 | }, {} as { [id: string]: ApiClmmConfigItem }); 35 | } 36 | -------------------------------------------------------------------------------- /src/services/redis.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'redis'; 2 | import { REDIS_URI } from '../config'; 3 | import { GasFeeEnum, JitoFeeEnum } from './user.trade.setting.service'; 4 | import { TokenOverviewDataType, TokenSecurityInfoDataType } from './birdeye.api.service'; 5 | 6 | const redisClient = createClient({ 7 | url: REDIS_URI 8 | }); 9 | 10 | export default redisClient; 11 | 12 | // [username_mint] => { chatId, slippage, slippagebps, gasSetting } 13 | // [mint_price] => price # every 10 seconds 14 | // [mint_overview] => overview # every 6 hour (MC) 15 | // [mint_secureinfo] => secureinfo # 1day 16 | // [wallet_tokenaccounts] => Array 17 | export interface ITradeSlippageSetting { 18 | chatId?: number; 19 | slippage: number; 20 | slippagebps: number; 21 | wallet?: string 22 | } 23 | 24 | export interface ITradeGasSetting { 25 | gas: GasFeeEnum; 26 | value?: number; 27 | } 28 | 29 | export interface ITradeJioFeeSetting { 30 | jitoOption: JitoFeeEnum; 31 | value?: number; 32 | } 33 | 34 | export interface IMintPrice { 35 | price: number 36 | } 37 | 38 | export interface IMintOverview { 39 | overview: TokenOverviewDataType; 40 | } 41 | 42 | export interface IMintSecureInfo { 43 | secureinfo: TokenSecurityInfoDataType; 44 | } 45 | -------------------------------------------------------------------------------- /src/models/msglog.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Trade Schema 7 | const MsgLog = new Schema( 8 | { 9 | username: { 10 | type: String, 11 | default: "", 12 | required: true, 13 | }, 14 | mint: { 15 | type: String, 16 | default: "", 17 | }, 18 | wallet_address: { 19 | type: String, 20 | default: "", 21 | }, 22 | chat_id: { 23 | type: Number, 24 | default: 0, 25 | required: true, 26 | }, 27 | msg_id: { 28 | type: Number, 29 | default: 0, 30 | required: true, 31 | }, 32 | parent_msgid: { 33 | type: Number, 34 | default: 0, 35 | }, 36 | sol_amount: { 37 | type: Number, 38 | default: 0, 39 | }, 40 | spl_amount: { 41 | type: Number, 42 | default: 0, 43 | }, 44 | extra_id: { 45 | type: Number, 46 | }, 47 | extra_key: { 48 | type: String, 49 | default: "", 50 | }, 51 | creation_time: { 52 | type: Number, 53 | default: 0, 54 | }, 55 | }, 56 | { 57 | timestamps: true, // This option adds createdAt and updatedAt fields 58 | } 59 | ); 60 | 61 | export default mongoose.model("msglog", MsgLog); 62 | -------------------------------------------------------------------------------- /src/cron/sol.price.cron.ts: -------------------------------------------------------------------------------- 1 | import { NATIVE_MINT } from "@solana/spl-token"; 2 | import cron from "node-cron"; 3 | import redisClient from "../services/redis"; 4 | const EVERY_1_MIN = "*/5 * * * * *"; 5 | export const runSOLPriceUpdateSchedule = () => { 6 | try { 7 | cron 8 | .schedule(EVERY_1_MIN, () => { 9 | updateSolPrice(); 10 | }) 11 | .start(); 12 | } catch (error) { 13 | console.error( 14 | `Error running the Schedule Job for fetching the chat data: ${error}` 15 | ); 16 | } 17 | }; 18 | 19 | const BIRDEYE_API_KEY = process.env.BIRD_EVEY_API || ""; 20 | const REQUEST_HEADER = { 21 | accept: "application/json", 22 | "x-chain": "solana", 23 | "X-API-KEY": BIRDEYE_API_KEY, 24 | }; 25 | 26 | const updateSolPrice = async () => { 27 | try { 28 | const solmint = NATIVE_MINT.toString(); 29 | const key = `${solmint}_price`; 30 | const options = { method: "GET", headers: REQUEST_HEADER }; 31 | const response = await fetch( 32 | `https://public-api.birdeye.so/defi/price?address=${solmint}`, 33 | options 34 | ); 35 | const res = await response.json(); 36 | const price = res.data.value; 37 | await redisClient.set(key, price); 38 | } catch (e) { 39 | console.log("🚀 ~ SOL price cron job ~ Failed", e); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/cron/alert.bot.cron.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { 3 | alertbotModule, 4 | sendAlertForOurChannel, 5 | } from "../services/alert.bot.module"; 6 | import cron from "node-cron"; 7 | import { ALERT_BOT_TOKEN_SECRET } from "../config"; 8 | 9 | const alertBotToken = ALERT_BOT_TOKEN_SECRET; 10 | if (!alertBotToken) { 11 | throw new Error( 12 | "ALERT_BOT_TOKEN_SECRET is not defined in the environment variables" 13 | ); 14 | } 15 | 16 | export const alertBot = new TelegramBot(alertBotToken, { polling: true }); 17 | 18 | const EVERY_1_MIN = "*/1 * * * *"; 19 | export const runAlertBotSchedule = () => { 20 | try { 21 | cron 22 | .schedule(EVERY_1_MIN, () => { 23 | alertbotModule(alertBot); 24 | }) 25 | .start(); 26 | } catch (error) { 27 | console.error( 28 | `Error running the Schedule Job for fetching the chat data: ${error}` 29 | ); 30 | } 31 | }; 32 | 33 | const EVERY_10_MIN = "0 * * * *"; 34 | export const runAlertBotForChannel = () => { 35 | try { 36 | cron 37 | .schedule(EVERY_10_MIN, () => { 38 | sendAlertForOurChannel(alertBot); 39 | }) 40 | .start(); 41 | } catch (error) { 42 | console.error( 43 | `Error running the Schedule Job for fetching the chat data: ${error}` 44 | ); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/models/position.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Position Schema 7 | const Position = new Schema( 8 | { 9 | // username: { 10 | // type: String, 11 | // default: "", 12 | // required: true 13 | // }, 14 | mint: { 15 | type: String, 16 | default: "", 17 | required: true, 18 | }, 19 | wallet_address: { 20 | type: String, 21 | default: "", 22 | required: true, 23 | }, 24 | // chat_id: { 25 | // type: Number, 26 | // default: 0, 27 | // required: true, 28 | // }, 29 | // Total Profit in SOL 30 | volume: { 31 | type: Number, 32 | default: 0.0, 33 | }, 34 | // Initial SOL buy amount 35 | sol_amount: { 36 | type: Number, 37 | default: 0.0, 38 | }, 39 | received_sol_amount: { 40 | type: Number, 41 | default: 0.0, 42 | }, 43 | // Total traded SPL amount 44 | amount: { 45 | type: Number, 46 | default: 0.0, 47 | }, 48 | creation_time: { 49 | type: Number, 50 | default: 0, 51 | }, 52 | }, 53 | { 54 | timestamps: true, // This option adds createdAt and updatedAt fields 55 | } 56 | ); 57 | 58 | export default mongoose.model("position", Position); 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solgradetradebot", 3 | "version": "1.0.0", 4 | "description": "growtrade bot on the top of growsol project", 5 | "main": "serve.ts", 6 | "scripts": { 7 | "serve": "nodemon ./serve.ts", 8 | "start": "npx ts-node serve.ts", 9 | "build": "npm run clean && tsc", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@jup-ag/api": "^6.0.20", 17 | "@jup-ag/referral-sdk": "^0.1.6", 18 | "@metaplex-foundation/js": "^0.20.1", 19 | "@raydium-io/raydium-sdk": "^1.3.1-beta.47", 20 | "@solana/spl-token": "^0.4.3", 21 | "@solana/spl-token-metadata": "^0.1.2", 22 | "@solana/web3.js": "^1.91.2", 23 | "axios": "^1.6.8", 24 | "bech32": "^2.0.0", 25 | "bn.js": "^5.2.1", 26 | "bs58": "^5.0.0", 27 | "cors": "^2.8.5", 28 | "dotenv": "^16.4.5", 29 | "express": "^4.19.2", 30 | "mongoose": "^8.2.3", 31 | "node-cron": "^3.0.3", 32 | "node-telegram-bot-api": "^0.65.1", 33 | "nodemon": "^3.1.0", 34 | "promise-retry": "^2.0.1", 35 | "redis": "^4.6.13", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.4.3" 38 | }, 39 | "devDependencies": { 40 | "@types/bn.js": "^5.1.5", 41 | "@types/cors": "^2.8.17", 42 | "@types/express": "^4.17.21", 43 | "@types/jsonwebtoken": "^9.0.6", 44 | "@types/node": "^20.11.30", 45 | "@types/node-cron": "^3.0.11", 46 | "@types/node-telegram-bot-api": "^0.64.6", 47 | "@types/promise-retry": "^1.1.6" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Solana Bot (Raydium, Jupiter, Pump.fun) 2 | ## Features 3 | 4 | - Track All tokens, All Pools on Raydium(AMM, CLMM), Jupiter, Pump.fun 5 | - Buy and sell all SPL tokens using JITO on Raydium, Jupiter, Pump.fun 6 | - Auto-but/sell according to the user setting 7 | - PNL Card generation 8 | - Provide a security by creating new GT wallet, not requires user wallet private key 9 | 10 | ## Tech stack 11 | - Typescript 12 | - Telegram API 13 | - Solana/web3 14 | - Raydium SDK 15 | - Jupiter API 16 | - Pump.fun 17 | - JITO 18 | - Birdeye API 19 | - MongoDB 20 | - Redis 21 | 22 | ## Prerequisites 23 | 24 | Before you begin, ensure you have met the following requirements: 25 | 26 | - Node.js installed (v18 or above recommended) 27 | - Telegram bot token from bot father 28 | - MongoDB Cluster URI 29 | - Redis URI 30 | 31 | ## Configurations 32 | 33 | Create a new `.env` file and add your Private key, Rpc URL 34 | 35 | `.env` file 36 | ``` 37 | 38 | MONGODB_URL= 39 | REDIS_URI= 40 | 41 | # Local 42 | GROWTRADE_BOT_ID= 43 | GROWSOL_ALERT_BOT_ID= 44 | BridgeBotID= 45 | ALERT_BOT_API_TOKEN= 46 | TELEGRAM_BOT_API_TOKEN= 47 | 48 | MAINNET_RPC= 49 | PRIVATE_RPC_ENDPOINT= 50 | RPC_WEBSOCKET_ENDPOINT= 51 | 52 | JITO_UUID= 53 | 54 | BIRD_EVEY_API= 55 | 56 | GROWSOL_API_ENDPOINT= 57 | 58 | PNL_IMG_GENERATOR_API= 59 | 60 | ``` 61 | 62 | Then run 63 | 64 | ```sh 65 | npm run serve 66 | ``` 67 | 68 | ![6](https://github.com/btcoin23/Growtradebot/assets/138183918/351d8203-6f4d-4560-8b70-cecf0468ad9a) 69 | ![z](https://github.com/btcoin23/Growtradebot/assets/138183918/20e824c4-82ab-4774-a4b3-5434d4cf925f) 70 | 71 | 72 | ## Version 1.0, 21/6/2024 73 | -------------------------------------------------------------------------------- /src/models/trade.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Define enum values for trade_type 7 | export const TradeTypeEnum = { 8 | NONE: 0, 9 | BUY: 1, 10 | SELL: 2, 11 | SNIPE: 3, 12 | }; 13 | 14 | // Trade Schema 15 | const Trade = new Schema( 16 | { 17 | username: { 18 | type: String, 19 | default: "", 20 | required: true, 21 | }, 22 | mint: { 23 | type: String, 24 | default: "", 25 | required: true, 26 | }, 27 | wallet_address: { 28 | type: String, 29 | default: "", 30 | required: true, 31 | }, 32 | trade_type: { 33 | type: Number, 34 | default: TradeTypeEnum.NONE, // Default value set to NONE 35 | enum: Object.values(TradeTypeEnum), // Allowed enum values 36 | }, 37 | sol_amount: { 38 | type: Number, 39 | default: 0, 40 | }, 41 | sol_price: { 42 | type: Number, 43 | default: 0, 44 | }, 45 | spl_amount: { 46 | type: Number, 47 | default: 0, 48 | }, 49 | spl_price: { 50 | type: Number, 51 | default: 0, 52 | }, 53 | nonce: { 54 | type: Number, 55 | default: 0, 56 | }, 57 | creation_time: { 58 | type: Number, 59 | default: 0, 60 | }, 61 | retired: { 62 | type: Boolean, 63 | default: false, 64 | }, 65 | }, 66 | { 67 | timestamps: true, // This option adds createdAt and updatedAt fields 68 | } 69 | ); 70 | 71 | // Create compound index for username, wallet_address, and nonce 72 | Trade.index({ mint: 1, wallet_address: 1, nonce: 1 }, { unique: true }); 73 | 74 | export default mongoose.model("trade", Trade); 75 | -------------------------------------------------------------------------------- /src/bot.opts.ts: -------------------------------------------------------------------------------- 1 | export const BotMenu = [ 2 | { command: 'start', description: 'Welcome' }, 3 | { command: 'position', description: 'Positions' }, 4 | { command: 'settings', description: 'Settings & Tools' }, 5 | ]; 6 | 7 | export const BUY_XSOL_TEXT = `🌳Buy X SOL\n\n💲 Enter SOL Value in format "0.05"`; 8 | export const PRESET_BUY_TEXT = `🌳Preset Buy SOL Button \n\n💲 Enter SOL Value in format "0.0X"`; 9 | export const AUTO_BUY_TEXT = `🌳Auto Buy SOL Button \n\n💲 Enter SOL Value in format "0.0X"`; 10 | export const SELL_XPRO_TEXT = `🌳Sell X %\n\n💲 Enter X Value in format "25.5"`; 11 | export const WITHDRAW_XTOKEN_TEXT = `🌳Withdraw X token\n\n💲 Enter X Value in format "25.5"`; 12 | export const SET_SLIPPAGE_TEXT = `🌳Slippage X %\n\n💲 Enter X Value in format "2.5"`; 13 | export const TradeBotID = process.env.GROWTRADE_BOT_ID; 14 | export const WELCOME_REFERRAL = 'https://imgtr.ee/images/2024/04/22/24635465dd390956e0fb39857a66bab5.png'; 15 | export const ALERT_GT_IMAGE = 'https://imgtr.ee/images/2024/04/22/a84bf0785b7eef4a64cde8c26b28686b.png'; 16 | export const ALERT_GB_IMAGE = 'https://imgtr.ee/images/2024/03/28/24ec15df80dad1223fcea15793278bbe.png'; 17 | export const AlertBotID = process.env.GROWSOL_ALERT_BOT_ID; 18 | export const BridgeBotID = process.env.BridgeBotID; 19 | 20 | export const INPUT_SOL_ADDRESS = 'Please send your SOL payout address in solana network.'; 21 | export const SET_GAS_FEE = `🌳 Custom GAS\n\n💲 Enter SOL Value in format "0.001"`; 22 | export const SET_JITO_FEE = `🌳 Custom Fee Amount\n\n💲 Enter SOL Value in format "0.001"`; 23 | 24 | export const WITHDRAW_TOKEN_AMT_TEXT = `🌳 Enter your receive wallet address`; 25 | export enum CommandEnum { 26 | CLOSE = "dismiss_message", 27 | Dismiss = "dismiss_message", 28 | REFRESH = "refresh" 29 | } 30 | -------------------------------------------------------------------------------- /src/raydium/types/mint.ts: -------------------------------------------------------------------------------- 1 | import { struct, u32, u8 } from "@solana/buffer-layout"; 2 | import { bool, publicKey, u64 } from "@solana/buffer-layout-utils"; 3 | import { PublicKey } from "@solana/web3.js"; 4 | 5 | /** Information about a mint */ 6 | export interface Mint { 7 | /** Address of the mint */ 8 | address: PublicKey; 9 | /** 10 | * Optional authority used to mint new tokens. The mint authority may only be provided during mint creation. 11 | * If no mint authority is present then the mint has a fixed supply and no further tokens may be minted. 12 | */ 13 | mintAuthority: PublicKey | null; 14 | /** Total supply of tokens */ 15 | supply: bigint; 16 | /** Number of base 10 digits to the right of the decimal place */ 17 | decimals: number; 18 | /** Is this mint initialized */ 19 | isInitialized: boolean; 20 | /** Optional authority to freeze token accounts */ 21 | freezeAuthority: PublicKey | null; 22 | } 23 | 24 | /** Mint as stored by the program */ 25 | export interface RawMint { 26 | mintAuthorityOption: 1 | 0; 27 | mintAuthority: PublicKey; 28 | supply: bigint; 29 | decimals: number; 30 | isInitialized: boolean; 31 | freezeAuthorityOption: 1 | 0; 32 | freezeAuthority: PublicKey; 33 | } 34 | 35 | /** Buffer layout for de/serializing a mint */ 36 | export const MintLayout = struct([ 37 | u32("mintAuthorityOption"), 38 | publicKey("mintAuthority"), 39 | u64("supply"), 40 | u8("decimals"), 41 | bool("isInitialized"), 42 | u32("freezeAuthorityOption"), 43 | publicKey("freezeAuthority"), 44 | ]); 45 | 46 | /** Mint as stored by the program */ 47 | export interface TokenAccount { 48 | uiAmount: bigint; 49 | } 50 | /** Buffer layout for de/serializing a mint */ 51 | export const TokenAccountLayout = struct([u64("uiAmount")]); 52 | -------------------------------------------------------------------------------- /src/screens/referral.link.handler.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { showWelcomeReferralProgramMessage } from "./welcome.referral.screen"; 3 | import { generateReferralCode } from "../utils"; 4 | import { UserService } from "../services/user.service"; 5 | import { ReferralChannelService } from "../services/referral.channel.service"; 6 | 7 | export const OpenReferralWindowHandler = async ( 8 | bot: TelegramBot, 9 | msg: TelegramBot.Message 10 | ) => { 11 | const chat = msg.chat; 12 | const username = chat.username; 13 | if (!username) { 14 | return; 15 | } 16 | // const data = await get_referral_info(username); 17 | // // if not created 18 | // if (!data) { 19 | // showWelcomeReferralProgramMessage(bot, chat); 20 | // return; 21 | // } 22 | // // if already created a link, we show link 23 | // const { uniquecode } = data; 24 | 25 | const referrerCode = await GenerateReferralCode(username); 26 | showWelcomeReferralProgramMessage(bot, chat, referrerCode); 27 | }; 28 | 29 | export const GenerateReferralCode = async (username: string) => { 30 | const userInfo = await UserService.findOne({ username: username }); 31 | if (!userInfo) return; 32 | const { referrer_code } = userInfo; 33 | 34 | let referrerCode = ""; 35 | if (referrer_code && referrer_code !== "") { 36 | referrerCode = referrer_code; 37 | } else { 38 | let uniquecode = generateReferralCode(10); 39 | referrerCode = uniquecode; 40 | const referralChannelService = new ReferralChannelService(); 41 | const res = await referralChannelService.createReferralChannel( 42 | username, 43 | uniquecode 44 | ); 45 | console.log(res); 46 | if (!res) return; 47 | await UserService.updateMany( 48 | { username: username }, 49 | { referrer_code: uniquecode } 50 | ); 51 | } 52 | 53 | return referrerCode; 54 | }; 55 | -------------------------------------------------------------------------------- /src/services/referral.channel.service.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { GROWSOL_API_ENDPOINT } from "../config"; 3 | 4 | export enum ReferralPlatform { 5 | TradeBot, 6 | BridgeBot 7 | } 8 | export class ReferralChannelService { 9 | endpoint: string; 10 | constructor() { 11 | this.endpoint = GROWSOL_API_ENDPOINT; 12 | } 13 | 14 | async createReferralChannel( 15 | creator: string, 16 | referral_code: string 17 | ) { 18 | try { 19 | const url = `${this.endpoint}/referral/upsert_channel`; 20 | const data = { 21 | creator, 22 | referral_code, 23 | platform: ReferralPlatform.TradeBot 24 | } 25 | const result = await axios.post(url, data); 26 | return result.data; 27 | } catch (e) { 28 | return null; 29 | } 30 | } 31 | 32 | async addReferralChannel(data: any) { 33 | try { 34 | console.log("StartDouble 2"); 35 | 36 | const url = `${this.endpoint}/referral/add_channel`; 37 | const result = await axios.post(url, data); 38 | return result.data; 39 | } catch (e) { 40 | return null; 41 | } 42 | } 43 | async updateReferralChannel(data: any) { 44 | try { 45 | const url = `${this.endpoint}/referral/upsert_channel`; 46 | const result = await axios.post(url, data); 47 | return result.data; 48 | } catch (e) { 49 | return null; 50 | } 51 | } 52 | async deleteReferralChannel(data: any) { 53 | try { 54 | const url = `${this.endpoint}/referral/delete_channel`; 55 | const result = await axios.post(url, data); 56 | return result.data; 57 | } catch (e) { 58 | console.log(e) 59 | return null; 60 | } 61 | } 62 | async getAllReferralChannels() { 63 | try { 64 | const url = `${this.endpoint}/referral/get_all_channels`; 65 | const result = await axios.get(url); 66 | return result.data; 67 | } catch (e) { 68 | return null; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/controllers/referral.history.ts: -------------------------------------------------------------------------------- 1 | import { ReferralHistorySchema } from "../models"; 2 | 3 | export const ReferralHistoryControler = { 4 | create: async (props: any) => { 5 | try { 6 | return await ReferralHistorySchema.create(props); 7 | } catch (err: any) { 8 | console.log(err); 9 | } 10 | }, 11 | findById: async (props: any) => { 12 | try { 13 | const { id } = props; 14 | const result = await ReferralHistorySchema.findById(id); 15 | 16 | return result; 17 | } catch (err: any) { 18 | throw new Error(err.message); 19 | } 20 | }, 21 | findOne: async (props: any) => { 22 | try { 23 | const filter = props; 24 | const result = await ReferralHistorySchema.findOne(filter); 25 | 26 | return result; 27 | } catch (err: any) { 28 | throw new Error(err.message); 29 | } 30 | }, 31 | findLastOne: async (props: any) => { 32 | try { 33 | const filter = props; 34 | const result = await ReferralHistorySchema.findOne(filter).sort({ 35 | updatedAt: -1, 36 | }); 37 | 38 | return result; 39 | } catch (err: any) { 40 | throw new Error(err.message); 41 | } 42 | }, 43 | find: async (props: any) => { 44 | const filter = props; 45 | try { 46 | const result = await ReferralHistorySchema.find(filter); 47 | 48 | return result; 49 | } catch (err: any) { 50 | throw new Error(err.message); 51 | } 52 | }, 53 | updateOne: async (props: any) => { 54 | const { id } = props; 55 | try { 56 | const result = await ReferralHistorySchema.findByIdAndUpdate(id, props); 57 | return result; 58 | } catch (err: any) { 59 | throw new Error(err.message); 60 | } 61 | }, 62 | deleteOne: async (props: any) => { 63 | try { 64 | const result = await ReferralHistorySchema.findOneAndDelete({ props }); 65 | return result; 66 | } catch (err: any) { 67 | throw new Error(err.message); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/pump/utils.ts: -------------------------------------------------------------------------------- 1 | import { ComputeBudgetProgram, Keypair } from "@solana/web3.js"; 2 | import { 3 | Connection, 4 | PublicKey, 5 | Transaction, 6 | TransactionInstruction, 7 | sendAndConfirmTransaction, 8 | } from "@solana/web3.js"; 9 | import bs58 from "bs58"; 10 | 11 | export async function getKeyPairFromPrivateKey(key: string) { 12 | return Keypair.fromSecretKey(new Uint8Array(bs58.decode(key))); 13 | } 14 | 15 | export async function createTransaction( 16 | connection: Connection, 17 | instructions: TransactionInstruction[], 18 | payer: PublicKey, 19 | priorityFeeInSol: number = 0 20 | ): Promise { 21 | const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({ 22 | units: 1400000, 23 | }); 24 | 25 | const transaction = new Transaction().add(modifyComputeUnits); 26 | 27 | if (priorityFeeInSol > 0) { 28 | const microLamports = priorityFeeInSol * 1_000_000_000; // convert SOL to microLamports 29 | const addPriorityFee = ComputeBudgetProgram.setComputeUnitPrice({ 30 | microLamports, 31 | }); 32 | transaction.add(addPriorityFee); 33 | } 34 | 35 | transaction.add(...instructions); 36 | 37 | transaction.feePayer = payer; 38 | transaction.recentBlockhash = ( 39 | await connection.getRecentBlockhash() 40 | ).blockhash; 41 | return transaction; 42 | } 43 | 44 | export async function sendAndConfirmTransactionWrapper( 45 | connection: Connection, 46 | transaction: Transaction, 47 | signers: any[] 48 | ) { 49 | try { 50 | const signature = await sendAndConfirmTransaction( 51 | connection, 52 | transaction, 53 | signers, 54 | { skipPreflight: true, preflightCommitment: "confirmed" } 55 | ); 56 | console.log("Transaction confirmed with signature:", signature); 57 | return signature; 58 | } catch (error) { 59 | console.error("Error sending transaction:", error); 60 | return null; 61 | } 62 | } 63 | 64 | export function bufferFromUInt64(value: number | string) { 65 | let buffer = Buffer.alloc(8); 66 | buffer.writeBigUInt64LE(BigInt(value)); 67 | return buffer; 68 | } 69 | -------------------------------------------------------------------------------- /src/raydium/utils/formatClmmKeysById.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClmmConfigItem, 3 | ApiClmmPoolsItem, 4 | PoolInfoLayout, 5 | } from "@raydium-io/raydium-sdk"; 6 | import { PublicKey } from "@solana/web3.js"; 7 | 8 | import { private_connection } from "../../config"; 9 | import { formatConfigInfo } from "./formatClmmConfigs"; 10 | import { getApiClmmPoolsItemStatisticsDefault } from "./formatClmmKeys"; 11 | 12 | async function getMintProgram(mint: PublicKey) { 13 | const account = await private_connection.getAccountInfo(mint); 14 | if (account === null) throw Error(" get id info error "); 15 | return account.owner; 16 | } 17 | async function getConfigInfo(configId: PublicKey): Promise { 18 | const account = await private_connection.getAccountInfo(configId); 19 | if (account === null) throw Error(" get id info error "); 20 | return formatConfigInfo(configId, account); 21 | } 22 | 23 | export async function formatClmmKeysById( 24 | id: string 25 | ): Promise { 26 | const account = await private_connection.getAccountInfo(new PublicKey(id)); 27 | if (account === null) throw Error(" get id info error "); 28 | const info = PoolInfoLayout.decode(account.data); 29 | 30 | return { 31 | id, 32 | mintProgramIdA: (await getMintProgram(info.mintA)).toString(), 33 | mintProgramIdB: (await getMintProgram(info.mintB)).toString(), 34 | mintA: info.mintA.toString(), 35 | mintB: info.mintB.toString(), 36 | vaultA: info.vaultA.toString(), 37 | vaultB: info.vaultB.toString(), 38 | mintDecimalsA: info.mintDecimalsA, 39 | mintDecimalsB: info.mintDecimalsB, 40 | ammConfig: await getConfigInfo(info.ammConfig), 41 | rewardInfos: await Promise.all( 42 | info.rewardInfos 43 | .filter((i) => !i.tokenMint.equals(PublicKey.default)) 44 | .map(async (i) => ({ 45 | mint: i.tokenMint.toString(), 46 | programId: (await getMintProgram(i.tokenMint)).toString(), 47 | })) 48 | ), 49 | tvl: 0, 50 | day: getApiClmmPoolsItemStatisticsDefault(), 51 | week: getApiClmmPoolsItemStatisticsDefault(), 52 | month: getApiClmmPoolsItemStatisticsDefault(), 53 | lookupTableAccount: PublicKey.default.toString(), 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Language and Environment */ 4 | "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 | "lib": [ 6 | "dom", 7 | "ES5", 8 | "ES6", 9 | "ESNext", 10 | "ES2015", 11 | "es2020" 12 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 13 | 14 | /* Modules */ 15 | "module": "CommonJS" /* Specify what module code is generated. */, 16 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 17 | "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, 18 | "resolveJsonModule": true /* Enable importing .json files. */, 19 | "esModuleInterop": true, 20 | 21 | /* JavaScript Support */ 22 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 23 | "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, 24 | 25 | /* Emit */ 26 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 27 | "declarationMap": true /* Create sourcemaps for d.ts files. */, 28 | "outDir": "./" /* Specify an output folder for all emitted files. */, 29 | "noEmit": true /* Disable emitting files from a compilation. */, 30 | 31 | /* Interop Constraints */ 32 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 33 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 34 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 35 | 36 | /* Type Checking */ 37 | "strict": true /* Enable all strict type-checking options. */, 38 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, 39 | "skipLibCheck": true 40 | }, 41 | "exclude": ["node_modules", "/"], 42 | "typeRoots": ["./node_modules/@types", "./src/@types"] 43 | } 44 | -------------------------------------------------------------------------------- /src/services/msglog.service.ts: -------------------------------------------------------------------------------- 1 | import { timeStamp } from "console"; 2 | import { MsgLogSchema } from "../models/index"; 3 | 4 | export const MsgLogService = { 5 | create: async (props: any) => { 6 | try { 7 | return await MsgLogSchema.create(props); 8 | } catch (err: any) { 9 | console.log(err); 10 | throw new Error(err.message); 11 | } 12 | }, 13 | findById: async (props: any) => { 14 | try { 15 | const { id } = props; 16 | const result = await MsgLogSchema.findById(id); 17 | 18 | return result; 19 | } catch (err: any) { 20 | throw new Error(err.message); 21 | } 22 | }, 23 | findOne: async (props: any) => { 24 | try { 25 | const filter = props; 26 | const result = await MsgLogSchema.findOne(filter).sort({ timeStamp: -1 }); 27 | 28 | return result; 29 | } catch (err: any) { 30 | throw new Error(err.message); 31 | } 32 | }, 33 | findLastOne: async (props: any) => { 34 | try { 35 | const filter = props; 36 | const result = await MsgLogSchema.findOne(filter).sort({ updatedAt: -1 }); 37 | 38 | return result; 39 | } catch (err: any) { 40 | throw new Error(err.message); 41 | } 42 | }, 43 | find: async (props: any) => { 44 | const filter = props; 45 | try { 46 | const result = await MsgLogSchema.find(filter); 47 | 48 | return result; 49 | } catch (err: any) { 50 | throw new Error(err.message); 51 | } 52 | }, 53 | updateOne: async (props: any) => { 54 | const { id } = props; 55 | try { 56 | const result = await MsgLogSchema.findByIdAndUpdate(id, props); 57 | return result; 58 | } catch (err: any) { 59 | throw new Error(err.message); 60 | } 61 | }, 62 | findOneAndUpdate: async (props: any) => { 63 | const { filter, data } = props; 64 | try { 65 | const result = await MsgLogSchema.findOneAndUpdate( 66 | filter, 67 | { $set: data }, 68 | { new: true } 69 | ); 70 | return result; 71 | } catch (err: any) { 72 | throw new Error(err.message); 73 | } 74 | }, 75 | deleteOne: async (props: any) => { 76 | try { 77 | const result = await MsgLogSchema.findOneAndDelete({ props }); 78 | return result; 79 | } catch (err: any) { 80 | throw new Error(err.message); 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/services/openmarket.service.ts: -------------------------------------------------------------------------------- 1 | import { timeStamp } from "console"; 2 | import { OpenMarketSchema } from "../models/index"; 3 | 4 | export const OpenMarketService = { 5 | create: async (props: any) => { 6 | try { 7 | return await OpenMarketSchema.create(props); 8 | } catch (err: any) { 9 | console.log(err); 10 | throw new Error(err.message); 11 | } 12 | }, 13 | findById: async (props: any) => { 14 | try { 15 | const { id } = props; 16 | const result = await OpenMarketSchema.findById(id); 17 | 18 | return result; 19 | } catch (err: any) { 20 | throw new Error(err.message); 21 | } 22 | }, 23 | findOne: async (props: any) => { 24 | try { 25 | const filter = props; 26 | const result = await OpenMarketSchema.findOne(filter).sort({ timeStamp: -1 }); 27 | 28 | return result; 29 | } catch (err: any) { 30 | throw new Error(err.message); 31 | } 32 | }, 33 | findLastOne: async (props: any) => { 34 | try { 35 | const filter = props; 36 | const result = await OpenMarketSchema.findOne(filter).sort({ updatedAt: -1 }); 37 | 38 | return result; 39 | } catch (err: any) { 40 | throw new Error(err.message); 41 | } 42 | }, 43 | find: async (props: any) => { 44 | const filter = props; 45 | try { 46 | const result = await OpenMarketSchema.find(filter); 47 | 48 | return result; 49 | } catch (err: any) { 50 | throw new Error(err.message); 51 | } 52 | }, 53 | updateOne: async (props: any) => { 54 | const { id } = props; 55 | try { 56 | const result = await OpenMarketSchema.findByIdAndUpdate(id, props); 57 | return result; 58 | } catch (err: any) { 59 | throw new Error(err.message); 60 | } 61 | }, 62 | findOneAndUpdate: async (props: any) => { 63 | const { filter, data } = props; 64 | try { 65 | const result = await OpenMarketSchema.findOneAndUpdate( 66 | filter, 67 | { $set: data }, 68 | { new: true } 69 | ); 70 | return result; 71 | } catch (err: any) { 72 | throw new Error(err.message); 73 | } 74 | }, 75 | deleteOne: async (props: any) => { 76 | try { 77 | const result = await OpenMarketSchema.findOneAndDelete({ props }); 78 | return result; 79 | } catch (err: any) { 80 | throw new Error(err.message); 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, PublicKey } from "@solana/web3.js"; 2 | import 'dotenv/config'; 3 | 4 | export const MONGODB_URL = process.env.MONGODB_URL || "mongodb://127.0.0.1:27017/growtrade"; 5 | export const TELEGRAM_BOT_API_TOKEN = process.env.TELEGRAM_BOT_API_TOKEN; 6 | export const ALERT_BOT_TOKEN_SECRET = process.env.ALERT_BOT_API_TOKEN; 7 | export const REDIS_URI = process.env.REDIS_URI || "redis://localhost:6379"; 8 | 9 | export const MAINNET_RPC = process.env.MAINNET_RPC || "https://api.mainnet-beta.solana.com"; 10 | export const RPC_WEBSOCKET_ENDPOINT = process.env.RPC_WEBSOCKET_ENDPOINT || "ws://api.mainnet-beta.solana.com"; 11 | export const PRIVATE_RPC_ENDPOINT = process.env.PRIVATE_RPC_ENDPOINT || "https://api.mainnet-beta.solana.com"; 12 | 13 | export const COMMITMENT_LEVEL = 'finalized' as Commitment; 14 | export const connection = new Connection(MAINNET_RPC, COMMITMENT_LEVEL); 15 | export const private_connection = new Connection(PRIVATE_RPC_ENDPOINT, COMMITMENT_LEVEL); 16 | 17 | export const RESERVE_WALLET = new PublicKey("B474hx9ktA2pq48ctLm9QXJpfitg59AWwMEQRn7YhyB7"); 18 | export const BIRDEYE_API_URL = "https://public-api.birdeye.so"; 19 | export const BIRDEYE_API_KEY = process.env.BIRD_EVEY_API || ""; 20 | export const JITO_UUID = process.env.JITO_UUID || ""; 21 | export const REQUEST_HEADER = { 22 | 'accept': 'application/json', 23 | 'x-chain': 'solana', 24 | 'X-API-KEY': BIRDEYE_API_KEY, 25 | }; 26 | 27 | export const REFERRAL_ACCOUNT = "DgzkEQqczAZCrUeq52cMbfKgx3mSHUon7wtiVuivs7Q7"; 28 | 29 | export const MIN = 60; 30 | export const HOUR = 60 * MIN; 31 | export const DAY = 24 * HOUR; 32 | export const WK = 7 * DAY; 33 | 34 | export const JUPITER_PROJECT = new PublicKey( 35 | "45ruCyfdRkWpRNGEqWzjCiXRHkZs8WXCLQ67Pnpye7Hp", 36 | ); 37 | 38 | export const MAX_CHECK_JITO = 20 39 | 40 | export const MAX_WALLET = 5; 41 | export const GrowTradeVersion = '| Beta Version'; 42 | 43 | export const GROWSOL_API_ENDPOINT = process.env.GROWSOL_API_ENDPOINT || "http://127.0.0.1:5001"; 44 | export const PNL_IMG_GENERATOR_API = process.env.PNL_IMG_GENERATOR_API || "http://127.0.0.1:3001"; 45 | 46 | export const PNL_SHOW_THRESHOLD_USD = 0.00000005; 47 | export const RAYDIUM_PASS_TIME = 5 * 60 * 60 * 1000; // 5 * 24 3days * 24h * 60mins * 60 seconds * 1000 millisecons 48 | export const RAYDIUM_AMM_URL = 'https://api.raydium.io/v2/main/pairs' 49 | export const RAYDIUM_CLMM_URL = 'https://api.raydium.io/v2/ammV3/ammPools' -------------------------------------------------------------------------------- /src/raydium/utils/formatAmmKeysById.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiPoolInfoV4, 3 | LIQUIDITY_STATE_LAYOUT_V4, 4 | Liquidity, 5 | MARKET_STATE_LAYOUT_V3, 6 | Market, 7 | SPL_MINT_LAYOUT, 8 | } from "@raydium-io/raydium-sdk"; 9 | import { PublicKey } from "@solana/web3.js"; 10 | 11 | import { private_connection } from "../../config"; 12 | 13 | export async function formatAmmKeysById(id: string): Promise { 14 | const account = await private_connection.getAccountInfo(new PublicKey(id)); 15 | if (account === null) throw Error(" get id info error "); 16 | const info = LIQUIDITY_STATE_LAYOUT_V4.decode(account.data); 17 | 18 | const marketId = info.marketId; 19 | const marketAccount = await private_connection.getAccountInfo(marketId); 20 | if (marketAccount === null) throw Error(" get market info error"); 21 | const marketInfo = MARKET_STATE_LAYOUT_V3.decode(marketAccount.data); 22 | 23 | const lpMint = info.lpMint; 24 | const lpMintAccount = await private_connection.getAccountInfo(lpMint); 25 | if (lpMintAccount === null) throw Error(" get lp mint info error"); 26 | const lpMintInfo = SPL_MINT_LAYOUT.decode(lpMintAccount.data); 27 | return { 28 | id, 29 | baseMint: info.baseMint.toString(), 30 | quoteMint: info.quoteMint.toString(), 31 | lpMint: info.lpMint.toString(), 32 | baseDecimals: info.baseDecimal.toNumber(), 33 | quoteDecimals: info.quoteDecimal.toNumber(), 34 | lpDecimals: lpMintInfo.decimals, 35 | version: 4, 36 | programId: account.owner.toString(), 37 | authority: Liquidity.getAssociatedAuthority({ 38 | programId: account.owner, 39 | }).publicKey.toString(), 40 | openOrders: info.openOrders.toString(), 41 | targetOrders: info.targetOrders.toString(), 42 | baseVault: info.baseVault.toString(), 43 | quoteVault: info.quoteVault.toString(), 44 | withdrawQueue: info.withdrawQueue.toString(), 45 | lpVault: info.lpVault.toString(), 46 | marketVersion: 3, 47 | marketProgramId: info.marketProgramId.toString(), 48 | marketId: info.marketId.toString(), 49 | marketAuthority: Market.getAssociatedAuthority({ 50 | programId: info.marketProgramId, 51 | marketId: info.marketId, 52 | }).publicKey.toString(), 53 | marketBaseVault: marketInfo.baseVault.toString(), 54 | marketQuoteVault: marketInfo.quoteVault.toString(), 55 | marketBids: marketInfo.bids.toString(), 56 | marketAsks: marketInfo.asks.toString(), 57 | marketEventQueue: marketInfo.eventQueue.toString(), 58 | lookupTableAccount: PublicKey.default.toString(), 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/services/raydium.token.service.ts: -------------------------------------------------------------------------------- 1 | import { timeStamp } from "console"; 2 | import { TokenSchema } from "../models/index"; 3 | 4 | export const RaydiumTokenService = { 5 | create: async (props: any) => { 6 | try { 7 | // return await TokenSchema.create(props); 8 | const existing = await TokenSchema.findOne({ poolId: props.poolId }); 9 | if (existing == null) { 10 | // console.log(props) 11 | return await TokenSchema.create(props); 12 | } else { 13 | return; 14 | } 15 | } catch (err: any) { 16 | console.log(err); 17 | // throw new Error(err.message); 18 | } 19 | }, 20 | findById: async (props: any) => { 21 | try { 22 | const { id } = props; 23 | const result = await TokenSchema.findById(id); 24 | 25 | return result; 26 | } catch (err: any) { 27 | throw new Error(err.message); 28 | } 29 | }, 30 | findOne: async (props: any) => { 31 | try { 32 | const filter = props; 33 | const result = await TokenSchema.findOne(filter).sort({ timeStamp: -1 }); 34 | 35 | return result; 36 | } catch (err: any) { 37 | throw new Error(err.message); 38 | } 39 | }, 40 | findLastOne: async (props: any) => { 41 | try { 42 | const filter = props; 43 | const result = await TokenSchema.findOne(filter).sort({ creation_ts: 1 }); 44 | 45 | return result; 46 | } catch (err: any) { 47 | throw new Error(err.message); 48 | } 49 | }, 50 | find: async (props: any) => { 51 | const filter = props; 52 | try { 53 | const result = await TokenSchema.find(filter); 54 | 55 | return result; 56 | } catch (err: any) { 57 | throw new Error(err.message); 58 | } 59 | }, 60 | updateOne: async (props: any) => { 61 | const { id } = props; 62 | try { 63 | const result = await TokenSchema.findByIdAndUpdate(id, props); 64 | return result; 65 | } catch (err: any) { 66 | throw new Error(err.message); 67 | } 68 | }, 69 | findOneAndUpdate: async (props: any) => { 70 | const { filter, data } = props; 71 | try { 72 | const result = await TokenSchema.findOneAndUpdate( 73 | filter, 74 | { $set: data }, 75 | { new: true } 76 | ); 77 | return result; 78 | } catch (err: any) { 79 | throw new Error(err.message); 80 | } 81 | }, 82 | deleteOne: async (props: any) => { 83 | try { 84 | const result = await TokenSchema.findOneAndDelete({ props }); 85 | return result; 86 | } catch (err: any) { 87 | throw new Error(err.message); 88 | } 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/screens/common.screen.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot, { SendMessageOptions } from "node-telegram-bot-api"; 2 | 3 | export const closeReplyMarkup = { 4 | parse_mode: "HTML", 5 | disable_web_page_preview: true, 6 | reply_markup: { 7 | inline_keyboard: [ 8 | [ 9 | { 10 | text: "❌ Close", 11 | callback_data: JSON.stringify({ 12 | command: "dismiss_message", 13 | }), 14 | }, 15 | ], 16 | ], 17 | }, 18 | } as SendMessageOptions; 19 | 20 | export const closeInlinekeyboardOpts = { 21 | text: "❌ Close", 22 | callback_data: JSON.stringify({ 23 | command: "dismiss_message", 24 | }), 25 | }; 26 | 27 | export const sendNoneUserNotification = async ( 28 | bot: TelegramBot, 29 | msg: TelegramBot.Message 30 | ) => { 31 | const { id: chat_id } = msg.chat; 32 | const sentMsg = await bot.sendMessage( 33 | chat_id, 34 | "⚠︎ Error\nThis account does not exist. Please contact support team.", 35 | closeReplyMarkup 36 | ); 37 | deleteDelayMessage(bot, chat_id, sentMsg.message_id, 5000); 38 | }; 39 | 40 | export const sendNoneExistTokenNotification = async ( 41 | bot: TelegramBot, 42 | msg: TelegramBot.Message 43 | ) => { 44 | const { id: chat_id } = msg.chat; 45 | const sentMsg = await bot.sendMessage( 46 | chat_id, 47 | "⚠︎ Error\nThis token does not exist. Please verify the mint address again or try later.", 48 | { 49 | parse_mode: "HTML", 50 | } 51 | ); 52 | deleteDelayMessage(bot, chat_id, sentMsg.message_id, 5000); 53 | }; 54 | 55 | export const sendInsufficientNotification = async ( 56 | bot: TelegramBot, 57 | msg: TelegramBot.Message 58 | ) => { 59 | const { id: chat_id } = msg.chat; 60 | const sentMsg = await bot.sendMessage( 61 | chat_id, 62 | "⚠︎ Error\nInsufficient amount.", 63 | { 64 | parse_mode: "HTML", 65 | } 66 | ); 67 | deleteDelayMessage(bot, chat_id, sentMsg.message_id, 5000); 68 | }; 69 | 70 | export const sendUsernameRequiredNotification = async ( 71 | bot: TelegramBot, 72 | msg: TelegramBot.Message 73 | ) => { 74 | const { id: chat_id } = msg.chat; 75 | const sentMsg = await bot.sendMessage( 76 | chat_id, 77 | "⚠︎ Error\nYou have no telegram username yourself. Please edit your profile and try it again.", 78 | closeReplyMarkup 79 | ); 80 | }; 81 | 82 | // delay: ms 83 | export const deleteDelayMessage = ( 84 | bot: TelegramBot, 85 | chat_id: number, 86 | message_id: number, 87 | delay: number 88 | ) => { 89 | try { 90 | setTimeout(() => { 91 | bot.deleteMessage(chat_id, message_id); 92 | }, delay); 93 | } catch (e) {} 94 | }; 95 | -------------------------------------------------------------------------------- /src/controllers/referral.channel.ts: -------------------------------------------------------------------------------- 1 | import { ReferralChannelSchema } from "../models"; 2 | 3 | export const ReferralChannelController = { 4 | create: async ( 5 | creator: string, 6 | channel_name: string, 7 | chat_id: string, 8 | referral_code: string 9 | ) => { 10 | try { 11 | const data = { 12 | channel_name, 13 | creator, 14 | chat_id: chat_id, 15 | referral_code, 16 | }; 17 | const filter = { 18 | chat_id, 19 | }; 20 | // Define options for findOneAndUpdate 21 | const options = { 22 | upsert: true, // Create a new document if no document matches the filter 23 | new: true, // Return the modified document rather than the original 24 | setDefaultsOnInsert: true, // Apply the default values specified in the model schema 25 | }; 26 | await ReferralChannelSchema.findOneAndUpdate(filter, data, options); 27 | } catch (err: any) { 28 | console.log(err); 29 | } 30 | }, 31 | findById: async (props: any) => { 32 | try { 33 | const { id } = props; 34 | const result = await ReferralChannelSchema.findById(id); 35 | 36 | return result; 37 | } catch (err: any) { 38 | throw new Error(err.message); 39 | } 40 | }, 41 | findOne: async (props: any) => { 42 | try { 43 | const filter = props; 44 | const result = await ReferralChannelSchema.findOne(filter); 45 | 46 | return result; 47 | } catch (err: any) { 48 | throw new Error(err.message); 49 | } 50 | }, 51 | findLastOne: async (props: any) => { 52 | try { 53 | const filter = props; 54 | const result = await ReferralChannelSchema.findOne(filter).sort({ 55 | updatedAt: -1, 56 | }); 57 | 58 | return result; 59 | } catch (err: any) { 60 | throw new Error(err.message); 61 | } 62 | }, 63 | find: async (props: any) => { 64 | const filter = props; 65 | try { 66 | const result = await ReferralChannelSchema.find(filter); 67 | 68 | return result; 69 | } catch (err: any) { 70 | throw new Error(err.message); 71 | } 72 | }, 73 | updateOne: async (props: any) => { 74 | const { id } = props; 75 | try { 76 | const result = await ReferralChannelSchema.findByIdAndUpdate(id, props); 77 | return result; 78 | } catch (err: any) { 79 | throw new Error(err.message); 80 | } 81 | }, 82 | deleteOne: async (props: any) => { 83 | try { 84 | const result = await ReferralChannelSchema.findOneAndDelete({ props }); 85 | return result; 86 | } catch (err: any) { 87 | throw new Error(err.message); 88 | } 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | // Wallet Subschema 7 | // const WalletSchema = new Schema({ 8 | // private_key: { 9 | // type: String, 10 | // default: "", 11 | // required: true, 12 | // unique: true, 13 | // }, 14 | // wallet_address: { 15 | // type: String, 16 | // default: "", 17 | // required: true, 18 | // unique: true, 19 | // }, 20 | // nonce: { 21 | // type: Number, 22 | // default: 0, 23 | // }, 24 | // }); 25 | 26 | // Basic Schema 27 | const User = new Schema( 28 | { 29 | // chat id 30 | chat_id: { 31 | type: String, 32 | default: "", 33 | required: true, 34 | }, 35 | first_name: { 36 | type: String, 37 | default: "", 38 | required: true, 39 | }, 40 | last_name: { 41 | type: String, 42 | default: "", 43 | }, 44 | username: { 45 | type: String, 46 | default: "", 47 | required: true, 48 | }, 49 | // wallets: [WalletSchema], // Array of WalletSchema objects 50 | private_key: { 51 | type: String, 52 | default: "", 53 | required: true, 54 | unique: true, 55 | }, 56 | wallet_address: { 57 | type: String, 58 | default: "", 59 | required: true, 60 | unique: true, 61 | }, 62 | preset_setting: { 63 | type: Array, 64 | default: [0.01, 1, 5, 10], 65 | }, 66 | nonce: { 67 | type: Number, 68 | default: 0, 69 | }, 70 | retired: { 71 | type: Boolean, 72 | default: false, 73 | }, 74 | referrer_code: { 75 | type: String, 76 | default: "", 77 | }, 78 | referrer_wallet: { 79 | type: String, 80 | default: "", 81 | }, 82 | referral_code: { 83 | type: String, 84 | default: "", 85 | }, 86 | referral_date: { 87 | type: String, 88 | default: "", 89 | }, 90 | schedule: { 91 | type: String, 92 | default: "60", 93 | }, 94 | burn_fee: { 95 | type: Boolean, 96 | default: false, 97 | }, 98 | auto_buy: { 99 | type: Boolean, 100 | default: false, 101 | }, 102 | auto_buy_amount: { 103 | type: String, 104 | default: "0.1", 105 | }, 106 | auto_sell_amount: { 107 | type: String, 108 | default: "30", 109 | }, 110 | }, 111 | { 112 | timestamps: true, // This option adds createdAt and updatedAt fields 113 | } 114 | ); 115 | 116 | // Create compound index for username, wallet_address, and nonce 117 | User.index({ username: 1, wallet_address: 1, nonce: 1 }, { unique: true }); 118 | 119 | export default mongoose.model("user", User); 120 | -------------------------------------------------------------------------------- /src/services/trade.service.ts: -------------------------------------------------------------------------------- 1 | import { TradeSchema } from "../models/index"; 2 | import redisClient from "./redis"; 3 | 4 | export const TradeService = { 5 | create: async (props: any) => { 6 | try { 7 | return await TradeSchema.create(props); 8 | } catch (err: any) { 9 | console.log(err); 10 | throw new Error(err.message); 11 | } 12 | }, 13 | findById: async (props: any) => { 14 | try { 15 | const { id } = props; 16 | const result = await TradeSchema.findById(id); 17 | 18 | return result; 19 | } catch (err: any) { 20 | throw new Error(err.message); 21 | } 22 | }, 23 | findOne: async (props: any) => { 24 | try { 25 | const filter = props; 26 | const result = await TradeSchema.findOne(filter); 27 | 28 | return result; 29 | } catch (err: any) { 30 | throw new Error(err.message); 31 | } 32 | }, 33 | findLastOne: async (props: any) => { 34 | try { 35 | const filter = props; 36 | const result = await TradeSchema.findOne(filter).sort({ updatedAt: -1 }); 37 | 38 | return result; 39 | } catch (err: any) { 40 | throw new Error(err.message); 41 | } 42 | }, 43 | find: async (props: any) => { 44 | const filter = props; 45 | try { 46 | const result = await TradeSchema.find(filter); 47 | 48 | return result; 49 | } catch (err: any) { 50 | throw new Error(err.message); 51 | } 52 | }, 53 | updateOne: async (props: any) => { 54 | const { id } = props; 55 | try { 56 | const result = await TradeSchema.findByIdAndUpdate(id, props); 57 | return result; 58 | } catch (err: any) { 59 | throw new Error(err.message); 60 | } 61 | }, 62 | deleteOne: async (props: any) => { 63 | try { 64 | const result = await TradeSchema.findOneAndDelete({ props }); 65 | return result; 66 | } catch (err: any) { 67 | throw new Error(err.message); 68 | } 69 | }, 70 | // get mint 71 | getCustomTradeInfo: async (username: string, message_id: number) => { 72 | const key = `${username}${message_id}_trade`; 73 | const data = await redisClient.get(key); 74 | return data; 75 | }, 76 | storeCustomTradeInfo: async (mint: string, username: string, message_id: number) => { 77 | const key = `${username}${message_id}_trade`; 78 | 79 | await redisClient.set(key, mint); 80 | // we remove it for performance 81 | await redisClient.expire(key, 24 * 60 * 60); 82 | }, 83 | getMintDecimal: async (mint: string) => { 84 | const key = `${mint}_decimal`; 85 | const data = await redisClient.get(key); 86 | if (data) return parseInt(data); 87 | return data; 88 | }, 89 | setMintDecimal: async (mint: string, decimal: number) => { 90 | const key = `${mint}_decimal`; 91 | await redisClient.set(key, decimal); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/services/fee.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Keypair, 3 | PublicKey, 4 | SystemProgram, 5 | TransactionInstruction, 6 | } from "@solana/web3.js"; 7 | import bs58 from "bs58"; 8 | import { get_referral_info } from "./referral.service"; 9 | import { RESERVE_WALLET } from "../config"; 10 | import { 11 | TOKEN_2022_PROGRAM_ID, 12 | TOKEN_PROGRAM_ID, 13 | createBurnInstruction, 14 | getAssociatedTokenAddressSync, 15 | } from "@solana/spl-token"; 16 | 17 | export class FeeService { 18 | async getFeeInstructions( 19 | total_fee_in_sol: number, 20 | total_fee_in_token: number, 21 | username: string, 22 | pk: string, 23 | mint: string, 24 | isToken2022: boolean 25 | ) { 26 | try { 27 | const wallet = Keypair.fromSecretKey(bs58.decode(pk)); 28 | let ref_info = await get_referral_info(username); 29 | console.log("🚀 ~ ref_info:", ref_info); 30 | 31 | let referralWallet: PublicKey = RESERVE_WALLET; 32 | if (ref_info && ref_info.referral_address) { 33 | const { referral_address } = ref_info; 34 | console.log("🚀 ~ referral_address:", referral_address); 35 | referralWallet = new PublicKey(ref_info.referral_address); 36 | } 37 | 38 | console.log("🚀 ~ referralWallet:", referralWallet); 39 | const referralFeePercent = ref_info?.referral_option ?? 0; // 25% 40 | 41 | const referralFee = Number( 42 | ((total_fee_in_sol * referralFeePercent) / 100).toFixed(0) 43 | ); 44 | const reserverStakingFee = total_fee_in_sol - referralFee; 45 | 46 | console.log( 47 | "Fee total:", 48 | total_fee_in_sol, 49 | total_fee_in_token, 50 | referralFee, 51 | reserverStakingFee 52 | ); 53 | const instructions: TransactionInstruction[] = []; 54 | if (reserverStakingFee > 0) { 55 | instructions.push( 56 | SystemProgram.transfer({ 57 | fromPubkey: wallet.publicKey, 58 | toPubkey: RESERVE_WALLET, 59 | lamports: reserverStakingFee, 60 | }) 61 | ); 62 | } 63 | 64 | if (referralFee > 0) { 65 | instructions.push( 66 | SystemProgram.transfer({ 67 | fromPubkey: wallet.publicKey, 68 | toPubkey: referralWallet, 69 | lamports: referralFee, 70 | }) 71 | ); 72 | } 73 | 74 | if (total_fee_in_token) { 75 | // Burn 76 | const ata = getAssociatedTokenAddressSync( 77 | new PublicKey(mint), 78 | wallet.publicKey, 79 | true, 80 | isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID 81 | ); 82 | instructions.push( 83 | createBurnInstruction( 84 | ata, 85 | new PublicKey(mint), 86 | wallet.publicKey, 87 | BigInt(total_fee_in_token), 88 | [], 89 | isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID 90 | ) 91 | ); 92 | } 93 | return instructions; 94 | } catch (e) { 95 | console.log("- Fee handler has issue", e); 96 | return []; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/screens/welcome.referral.screen.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot, { 2 | KeyboardButton, 3 | ReplyKeyboardMarkup, 4 | } from "node-telegram-bot-api"; 5 | import { TradeBotID, WELCOME_REFERRAL } from "../bot.opts"; 6 | import { copytoclipboard } from "../utils"; 7 | import { 8 | get_referral_amount, 9 | get_referral_num, 10 | } from "../services/referral.service"; 11 | 12 | export const showWelcomeReferralProgramMessage = async ( 13 | bot: TelegramBot, 14 | chat: TelegramBot.Chat, 15 | uniquecode?: string 16 | ) => { 17 | try { 18 | const chatId = chat.id; 19 | const inlineKeyboards = [ 20 | [ 21 | { 22 | text: "Manage payout 📄", 23 | callback_data: JSON.stringify({ 24 | command: "payout_address", 25 | }), 26 | }, 27 | ], 28 | [ 29 | { 30 | text: "Set up Alert Bot 🤖", 31 | callback_data: JSON.stringify({ 32 | command: "alert_bot", 33 | }), 34 | }, 35 | { 36 | text: `❌ Close`, 37 | callback_data: JSON.stringify({ 38 | command: "dismiss_message", 39 | }), 40 | }, 41 | ], 42 | ]; 43 | if (!uniquecode || uniquecode === "") { 44 | const reply_markup = { 45 | inline_keyboard: [ 46 | [ 47 | { 48 | text: "Create a referral code 💰", 49 | callback_data: JSON.stringify({ 50 | command: "create_referral_code", 51 | }), 52 | }, 53 | ], 54 | ...inlineKeyboards, 55 | ], 56 | }; 57 | 58 | const caption = 59 | `🎉 Welcome to the referral program\n\n` + 60 | `Please create a unique referral code to get started👇.`; 61 | await bot.sendPhoto(chatId, WELCOME_REFERRAL, { 62 | caption: caption, 63 | reply_markup, 64 | parse_mode: "HTML", 65 | }); 66 | } else { 67 | const reply_markup = { 68 | inline_keyboard: inlineKeyboards, 69 | }; 70 | let num = await get_referral_num(uniquecode); 71 | let totalAmount = await get_referral_amount(uniquecode); 72 | const referralLink = `https://t.me/${TradeBotID}?start=${uniquecode}`; 73 | const contents = 74 | "🎉 Welcome to referral program\n\n" + 75 | `Refer your friends and earn 25% of their fees in the first 45 days, 20% in the next 45 days and 15% forever!\n\n` + 76 | `Referred Count: ${num.num}\nSol Earned: ${totalAmount.totalAmount}\n\n` + 77 | `Your referral code 🔖\n${copytoclipboard(uniquecode)}\n\n` + 78 | `Your referral link 🔗\n${copytoclipboard(referralLink)}\n\n` + 79 | // `Note: Don't forget set up payout address to get paid\n\n` + 80 | `- Share your referral link with whoever you want and earn from their swaps 🔁\n` + 81 | `- Check profits, payouts and change the payout address 📄\n`; 82 | 83 | await bot.sendPhoto(chatId, WELCOME_REFERRAL, { 84 | caption: contents, 85 | reply_markup, 86 | parse_mode: "HTML", 87 | }); 88 | } 89 | } catch (e) { 90 | console.log("~ showWelcomeReferralProgramMessage Error ~", e); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { UserSchema } from "../models/index"; 2 | 3 | export const UserService = { 4 | create: async (props: any) => { 5 | try { 6 | return await UserSchema.create(props); 7 | } catch (err: any) { 8 | console.log(err); 9 | throw new Error(err.message); 10 | } 11 | }, 12 | findById: async (props: any) => { 13 | try { 14 | const { id } = props; 15 | const result = await UserSchema.findById(id); 16 | 17 | return result; 18 | } catch (err: any) { 19 | throw new Error(err.message); 20 | } 21 | }, 22 | findOne: async (props: any) => { 23 | try { 24 | const filter = props; 25 | const result = await UserSchema.findOne({ ...filter, retired: false }); 26 | 27 | return result; 28 | } catch (err: any) { 29 | throw new Error(err.message); 30 | } 31 | }, 32 | findLastOne: async (props: any) => { 33 | try { 34 | const filter = props; 35 | const result = await UserSchema.findOne(filter).sort({ updatedAt: -1 }); 36 | 37 | return result; 38 | } catch (err: any) { 39 | throw new Error(err.message); 40 | } 41 | }, 42 | find: async (props: any) => { 43 | const filter = props; 44 | try { 45 | const result = await UserSchema.find(filter); 46 | 47 | return result; 48 | } catch (err: any) { 49 | throw new Error(err.message); 50 | } 51 | }, 52 | findAndSort: async (props: any) => { 53 | const filter = props; 54 | try { 55 | const result = await UserSchema.find(filter).sort({ retired: 1, nonce: 1 }) 56 | .exec(); 57 | 58 | return result; 59 | } catch (err: any) { 60 | throw new Error(err.message); 61 | } 62 | }, 63 | updateOne: async (props: any) => { 64 | const { id } = props; 65 | try { 66 | const result = await UserSchema.findByIdAndUpdate(id, props); 67 | return result; 68 | } catch (err: any) { 69 | throw new Error(err.message); 70 | } 71 | }, 72 | findAndUpdateOne: async (filter: any, props: any) => { 73 | try { 74 | const result = await UserSchema.findOneAndUpdate(filter, props); 75 | return result; 76 | } catch (err: any) { 77 | throw new Error(err.message); 78 | } 79 | }, 80 | updateMany: async (filter: any, props: any) => { 81 | try { 82 | const result = await UserSchema.updateMany(filter, { 83 | $set: props 84 | }); 85 | return result; 86 | } catch (err: any) { 87 | throw new Error(err.message); 88 | } 89 | }, 90 | deleteOne: async (props: any) => { 91 | try { 92 | const result = await UserSchema.findOneAndDelete({ props }); 93 | return result; 94 | } catch (err: any) { 95 | throw new Error(err.message); 96 | } 97 | }, 98 | extractUniqueCode: (text: string): string | null => { 99 | const words = text.split(' '); 100 | return words.length > 1 ? words[1] : null; 101 | }, 102 | 103 | extractPNLdata: (text: string): any => { 104 | const words = text.split(' '); 105 | if(words.length > 1){ 106 | if(words[1].endsWith('png')){ 107 | return words[1].replace('png', '.png'); 108 | } 109 | } 110 | } 111 | 112 | }; 113 | -------------------------------------------------------------------------------- /src/utils/jupiter.transaction.sender.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockhashWithExpiryBlockHeight, 3 | Connection, 4 | TransactionExpiredBlockheightExceededError, 5 | VersionedTransactionResponse, 6 | } from "@solana/web3.js"; 7 | import promiseRetry from "promise-retry"; 8 | import { wait } from "./wait"; 9 | 10 | type TransactionSenderAndConfirmationWaiterArgs = { 11 | connection: Connection; 12 | serializedTransaction: Buffer; 13 | blockhashWithExpiryBlockHeight: BlockhashWithExpiryBlockHeight; 14 | }; 15 | 16 | const SEND_OPTIONS = { 17 | skipPreflight: true, 18 | }; 19 | 20 | export async function transactionSenderAndConfirmationWaiter({ 21 | connection, 22 | serializedTransaction, 23 | blockhashWithExpiryBlockHeight, 24 | }: TransactionSenderAndConfirmationWaiterArgs): Promise { 25 | const txid = await connection.sendRawTransaction( 26 | serializedTransaction, 27 | SEND_OPTIONS 28 | ); 29 | 30 | const controller = new AbortController(); 31 | const abortSignal = controller.signal; 32 | 33 | const abortableResender = async () => { 34 | while (true) { 35 | await wait(2_000); 36 | if (abortSignal.aborted) return; 37 | try { 38 | await connection.sendRawTransaction( 39 | serializedTransaction, 40 | SEND_OPTIONS 41 | ); 42 | } catch (e) { 43 | console.warn(`Failed to resend transaction: ${e}`); 44 | } 45 | } 46 | }; 47 | 48 | try { 49 | abortableResender(); 50 | const lastValidBlockHeight = 51 | blockhashWithExpiryBlockHeight.lastValidBlockHeight - 150; 52 | 53 | // this would throw TransactionExpiredBlockheightExceededError 54 | await Promise.race([ 55 | connection.confirmTransaction( 56 | { 57 | ...blockhashWithExpiryBlockHeight, 58 | lastValidBlockHeight, 59 | signature: txid, 60 | abortSignal, 61 | }, 62 | "confirmed" 63 | ), 64 | new Promise(async (resolve) => { 65 | // in case ws socket died 66 | while (!abortSignal.aborted) { 67 | await wait(2_000); 68 | const tx = await connection.getSignatureStatus(txid, { 69 | searchTransactionHistory: false, 70 | }); 71 | if (tx?.value?.confirmationStatus === "confirmed") { 72 | resolve(tx); 73 | } 74 | } 75 | }), 76 | ]); 77 | } catch (e) { 78 | if (e instanceof TransactionExpiredBlockheightExceededError) { 79 | // we consume this error and getTransaction would return null 80 | return null; 81 | } else { 82 | // invalid state from web3.js 83 | throw e; 84 | } 85 | } finally { 86 | controller.abort(); 87 | } 88 | 89 | // in case rpc is not synced yet, we add some retries 90 | const response = promiseRetry( 91 | async (retry: any) => { 92 | const response = await connection.getTransaction(txid, { 93 | commitment: "confirmed", 94 | maxSupportedTransactionVersion: 0, 95 | }); 96 | if (!response) { 97 | retry(response); 98 | } 99 | return response; 100 | }, 101 | { 102 | retries: 5, 103 | minTimeout: 1e3, 104 | } 105 | ); 106 | 107 | return response; 108 | } -------------------------------------------------------------------------------- /src/utils/v0.transaction.ts: -------------------------------------------------------------------------------- 1 | import { AddressLookupTableProgram, Connection, Keypair, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; 2 | import { getSignature } from "./get.signature"; 3 | import { transactionSenderAndConfirmationWaiter } from "./jupiter.transaction.sender"; 4 | import { wait } from "./wait"; 5 | import { connection } from "../config"; 6 | 7 | const COMMITMENT_LEVEL = 'confirmed'; 8 | 9 | export async function sendTransactionV0( 10 | connection: Connection, 11 | instructions: TransactionInstruction[], 12 | payers: Keypair[], 13 | ): Promise { 14 | let latestBlockhash = await connection 15 | .getLatestBlockhash(COMMITMENT_LEVEL) 16 | 17 | const messageV0 = new TransactionMessage({ 18 | payerKey: payers[0].publicKey, 19 | recentBlockhash: latestBlockhash.blockhash, 20 | instructions, 21 | }).compileToV0Message(); 22 | 23 | const transaction = new VersionedTransaction(messageV0); 24 | transaction.sign(payers); 25 | const signature = getSignature(transaction); 26 | 27 | // We first simulate whether the transaction would be successful 28 | const { value: simulatedTransactionResponse } = 29 | await connection.simulateTransaction(transaction, { 30 | replaceRecentBlockhash: true, 31 | commitment: "processed", 32 | }); 33 | const { err, logs } = simulatedTransactionResponse; 34 | 35 | if (err) { 36 | // Simulation error, we can check the logs for more details 37 | // If you are getting an invalid account error, make sure that you have the input mint account to actually swap from. 38 | console.error("Simulation Error:"); 39 | console.error({ err, logs }); 40 | return null; 41 | } 42 | 43 | const serializedTransaction = Buffer.from(transaction.serialize()); 44 | const blockhash = transaction.message.recentBlockhash; 45 | 46 | const transactionResponse = await transactionSenderAndConfirmationWaiter({ 47 | connection, 48 | serializedTransaction, 49 | blockhashWithExpiryBlockHeight: { 50 | blockhash, 51 | lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, 52 | }, 53 | }); 54 | 55 | // If we are not getting a response back, the transaction has not confirmed. 56 | if (!transactionResponse) { 57 | console.error("Transaction not confirmed"); 58 | return null; 59 | } 60 | 61 | if (transactionResponse.meta?.err) { 62 | console.error(transactionResponse.meta?.err); 63 | return null; 64 | } 65 | 66 | return signature; 67 | } 68 | 69 | export const getSignatureStatus = async (signature: string) => { 70 | try { 71 | const maxRetry = 30; 72 | let retries = 0; 73 | while (retries < maxRetry) { 74 | await wait(1_000); 75 | retries++; 76 | 77 | const tx = await connection.getSignatureStatus(signature, { 78 | searchTransactionHistory: false, 79 | }); 80 | if (tx?.value?.err) { 81 | console.log("JitoTransaction Failed"); 82 | break; 83 | } 84 | if (tx?.value?.confirmationStatus === "confirmed" || tx?.value?.confirmationStatus === "finalized") { 85 | retries = 0; 86 | console.log("JitoTransaction confirmed!!!"); 87 | break; 88 | } 89 | } 90 | 91 | if (retries > 0) return false; 92 | return true; 93 | } catch (e) { 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { BIRDEYE_API_URL, REQUEST_HEADER } from "../config"; 2 | import redisClient from "../services/redis"; 3 | 4 | export function isValidWalletAddress(address: string): boolean { 5 | if (!address) return false; 6 | const pattern: RegExp = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/; 7 | 8 | return pattern.test(address); 9 | } 10 | 11 | export const generateReferralCode = (length: number) => { 12 | let code = ''; 13 | // Convert the Telegram username to a hexadecimal string 14 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 15 | for (let i = 0; i < length; i++) { 16 | code += characters.charAt(Math.floor(Math.random() * characters.length)); 17 | } 18 | return code; 19 | } 20 | 21 | export function formatNumber(number: bigint | string | number) { 22 | if (!number) return "0"; 23 | // Convert the number to a string and add commas using regular expression 24 | return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 25 | } 26 | 27 | export function formatKMB(val: bigint | string | number) { 28 | if (!val) return "0"; 29 | if (Number(val) > 1000000000) { 30 | return `${(Number(val) / 1000000000).toFixed(1)}B`; 31 | } 32 | if (Number(val) > 1000000) { 33 | return `${(Number(val) / 1000000).toFixed(1)}M`; 34 | } 35 | if (Number(val) > 1000) { 36 | return `${(Number(val) / 1000).toFixed(1)}k`; 37 | } 38 | return Number(val).toFixed(3); 39 | } 40 | 41 | export const contractLink = (mint: string) => { 42 | return `Contract`; 43 | } 44 | 45 | export const birdeyeLink = (mint: string) => { 46 | return `Birdeye`; 47 | } 48 | 49 | export const dextoolLink = (mint: string) => { 50 | return `Dextools`; 51 | } 52 | 53 | export const dexscreenerLink = (mint: string) => { 54 | return `Dexscreener`; 55 | } 56 | 57 | export function formatPrice(price: number) { 58 | if (!price) return 0; 59 | if (price <= 0) return 0; 60 | // If the price is less than 1, format it to 6 decimal places 61 | if (price < 1) { 62 | let decimal = 15; 63 | while (1) { 64 | if (price * 10 ** decimal < 1) { 65 | break; 66 | } 67 | decimal--; 68 | } 69 | return price.toFixed(decimal + 3); 70 | } 71 | // If the price is greater than or equal to 1, format it to 3 decimal places 72 | return price.toFixed(2); 73 | } 74 | 75 | export const getPrice = async (mint: string) => { 76 | const key = `${mint}_price`; 77 | const data = await redisClient.get(key); 78 | if (data) { 79 | return Number(data); 80 | } 81 | const options = { method: 'GET', headers: REQUEST_HEADER }; 82 | const response = await fetch(`https://public-api.birdeye.so/defi/price?address=${mint}`, options) 83 | const res = await response.json(); 84 | const price = res.data.value; 85 | await redisClient.set(key, price); 86 | await redisClient.expire(key, 5); // 5 seconds 87 | return Number(price); 88 | }; 89 | 90 | export const copytoclipboard = ( 91 | text: string 92 | ) => { 93 | return `${text}`; 94 | } 95 | 96 | export const isEqual = (a: number, b: number) => { 97 | return Math.abs(b - a) < 0.001; 98 | } 99 | 100 | export const fromWeiToValue = (wei: string | number, decimal: number) => { 101 | return Number(wei) / 10 ** decimal; 102 | } -------------------------------------------------------------------------------- /src/controllers/message.handler.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { isValidWalletAddress } from "../utils"; 3 | import { contractInfoScreenHandler } from "../screens/contract.info.screen"; 4 | import { 5 | AUTO_BUY_TEXT, 6 | BUY_XSOL_TEXT, 7 | PRESET_BUY_TEXT, 8 | SELL_XPRO_TEXT, 9 | SET_GAS_FEE, 10 | SET_JITO_FEE, 11 | SET_SLIPPAGE_TEXT, 12 | WITHDRAW_TOKEN_AMT_TEXT, 13 | WITHDRAW_XTOKEN_TEXT, 14 | } from "../bot.opts"; 15 | import { 16 | buyHandler, 17 | sellHandler, 18 | setSlippageHandler, 19 | } from "../screens/trade.screen"; 20 | import { 21 | withdrawAddressHandler, 22 | withdrawHandler, 23 | } from "../screens/transfer.funds"; 24 | import { 25 | presetBuyBtnHandler, 26 | setCustomAutoBuyAmountHandler, 27 | setCustomBuyPresetHandler, 28 | setCustomFeeHandler, 29 | setCustomJitoFeeHandler, 30 | } from "../screens/settings.screen"; 31 | 32 | export const messageHandler = async ( 33 | bot: TelegramBot, 34 | msg: TelegramBot.Message 35 | ) => { 36 | try { 37 | const messageText = msg.text; 38 | const { reply_to_message } = msg; 39 | 40 | if (!messageText) return; 41 | 42 | if (reply_to_message && reply_to_message.text) { 43 | const { text } = reply_to_message; 44 | // if number, input amount 45 | const regex = /^[0-9]+(\.[0-9]+)?$/; 46 | const isNumber = regex.test(messageText) === true; 47 | const reply_message_id = reply_to_message.message_id; 48 | 49 | if (isNumber) { 50 | const amount = Number(messageText); 51 | 52 | if (text === BUY_XSOL_TEXT.replace(/<[^>]*>/g, "")) { 53 | await buyHandler(bot, msg, amount, reply_message_id); 54 | } else if (text === SELL_XPRO_TEXT.replace(/<[^>]*>/g, "")) { 55 | await sellHandler(bot, msg, amount, reply_message_id); 56 | } else if (text === WITHDRAW_XTOKEN_TEXT.replace(/<[^>]*>/g, "")) { 57 | await withdrawHandler(bot, msg, messageText, reply_message_id); 58 | } else if (text === SET_SLIPPAGE_TEXT.replace(/<[^>]*>/g, "")) { 59 | await setSlippageHandler(bot, msg, amount, reply_message_id); 60 | } else if (text === PRESET_BUY_TEXT.replace(/<[^>]*>/g, "")) { 61 | await setCustomBuyPresetHandler(bot, msg, amount, reply_message_id); 62 | } else if (text === AUTO_BUY_TEXT.replace(/<[^>]*>/g, "")) { 63 | await setCustomAutoBuyAmountHandler( 64 | bot, 65 | msg, 66 | amount, 67 | reply_message_id 68 | ); 69 | } else if (text === SET_GAS_FEE.replace(/<[^>]*>/g, "")) { 70 | await setCustomFeeHandler(bot, msg, amount, reply_message_id); 71 | } else if (text === SET_JITO_FEE.replace(/<[^>]*>/g, "")) { 72 | if (amount > 0.0001) { 73 | await setCustomJitoFeeHandler(bot, msg, amount, reply_message_id); 74 | } else { 75 | await setCustomJitoFeeHandler(bot, msg, 0.0001, reply_message_id); 76 | } 77 | } 78 | } else { 79 | if (text === WITHDRAW_TOKEN_AMT_TEXT.replace(/<[^>]*>/g, "")) { 80 | await withdrawAddressHandler(bot, msg, messageText, reply_message_id); 81 | } 82 | } 83 | return; 84 | } 85 | 86 | // // wallet address 87 | // if (isValidWalletAddress(messageText)) { 88 | // await contractInfoScreenHandler(bot, msg, messageText, 'switch_sell'); 89 | // return; 90 | // } 91 | // wallet address 92 | if (isValidWalletAddress(messageText)) { 93 | await contractInfoScreenHandler(bot, msg, messageText); 94 | return; 95 | } 96 | } catch (e) { 97 | console.log("~messageHandler~", e); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /src/services/position.service.ts: -------------------------------------------------------------------------------- 1 | import { PositionSchema } from "../models/index"; 2 | 3 | export const PositionService = { 4 | create: async (props: any) => { 5 | try { 6 | return await PositionSchema.create(props); 7 | } catch (err: any) { 8 | console.log(err); 9 | throw new Error(err.message); 10 | } 11 | }, 12 | findById: async (props: any) => { 13 | try { 14 | const { id } = props; 15 | const result = await PositionSchema.findById(id); 16 | 17 | return result; 18 | } catch (err: any) { 19 | throw new Error(err.message); 20 | } 21 | }, 22 | findOne: async (props: any) => { 23 | try { 24 | const filter = props; 25 | const result = await PositionSchema.findOne({ ...filter }); 26 | 27 | return result; 28 | } catch (err: any) { 29 | throw new Error(err.message); 30 | } 31 | }, 32 | findLastOne: async (props: any) => { 33 | try { 34 | const filter = props; 35 | const result = await PositionSchema.findOne(filter).sort({ updatedAt: -1 }); 36 | 37 | return result; 38 | } catch (err: any) { 39 | throw new Error(err.message); 40 | } 41 | }, 42 | find: async (props: any) => { 43 | const filter = props; 44 | try { 45 | const result = await PositionSchema.find(filter); 46 | 47 | return result; 48 | } catch (err: any) { 49 | throw new Error(err.message); 50 | } 51 | }, 52 | findAndSort: async (props: any) => { 53 | const filter = props; 54 | try { 55 | const result = await PositionSchema.find(filter).sort({ retired: 1, nonce: 1 }) 56 | .exec(); 57 | 58 | return result; 59 | } catch (err: any) { 60 | throw new Error(err.message); 61 | } 62 | }, 63 | updateOne: async (props: any) => { 64 | const { id } = props; 65 | try { 66 | const result = await PositionSchema.findByIdAndUpdate(id, props); 67 | return result; 68 | } catch (err: any) { 69 | throw new Error(err.message); 70 | } 71 | }, 72 | findAndUpdateOne: async (filter: any, props: any) => { 73 | try { 74 | const result = await PositionSchema.findOneAndUpdate(filter, props); 75 | return result; 76 | } catch (err: any) { 77 | throw new Error(err.message); 78 | } 79 | }, 80 | deleteOne: async (props: any) => { 81 | try { 82 | const result = await PositionSchema.findOneAndDelete({ props }); 83 | return result; 84 | } catch (err: any) { 85 | throw new Error(err.message); 86 | } 87 | }, 88 | updateBuyPosition: async function (params: any) { 89 | const { wallet_address, mint, chat_id, username, volume, amount } = params; 90 | 91 | let position = await PositionSchema.findOne({ wallet_address, mint }); 92 | 93 | if (!position) { 94 | // Create new position entry if it doesn't exist 95 | position = new PositionSchema({ wallet_address, mint, chat_id, username, volume, sol_amount: amount }); 96 | } else { 97 | // Update volume if position already exists 98 | position.volume += volume; 99 | position.sol_amount += amount; 100 | } 101 | await position.save(); 102 | }, 103 | updateSellPosition: async function (params: any) { 104 | const { wallet_address, mint, chat_id, username, percent } = params; 105 | 106 | const position = await this.findOne({ wallet_address, mint, chat_id, username }); 107 | 108 | if (!position) { 109 | // No data found, return null 110 | return null; 111 | } 112 | if (percent >= 100) { 113 | position.sol_amount = 0; 114 | position.volume = 0; 115 | } else { 116 | const rA = position.sol_amount * (100 - percent) / 100; 117 | const rV = position.volume * (100 - percent) / 100; 118 | position.sol_amount = rA; 119 | position.volume = rV; 120 | } 121 | 122 | await position.save(); 123 | 124 | return position; 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /src/raydium/utils/formatClmmKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiClmmPoolsItem, 3 | ApiClmmPoolsItemStatistics, 4 | PoolInfoLayout, 5 | getMultipleAccountsInfoWithCustomFlags, 6 | } from "@raydium-io/raydium-sdk"; 7 | import { AddressLookupTableAccount, PublicKey } from "@solana/web3.js"; 8 | 9 | import { private_connection } from "../../config"; 10 | import { formatClmmConfigs } from "./formatClmmConfigs"; 11 | 12 | export function getApiClmmPoolsItemStatisticsDefault(): ApiClmmPoolsItemStatistics { 13 | return { 14 | volume: 0, 15 | volumeFee: 0, 16 | feeA: 0, 17 | feeB: 0, 18 | feeApr: 0, 19 | rewardApr: { A: 0, B: 0, C: 0 }, 20 | apr: 0, 21 | priceMin: 0, 22 | priceMax: 0, 23 | }; 24 | } 25 | 26 | export async function formatClmmKeys( 27 | programId: string, 28 | findLookupTableAddress: boolean = false 29 | ): Promise { 30 | const filterDefKey = PublicKey.default.toString(); 31 | 32 | const poolAccountInfo = await private_connection.getProgramAccounts( 33 | new PublicKey(programId), 34 | { filters: [{ dataSize: PoolInfoLayout.span }] } 35 | ); 36 | 37 | const configIdToData = await formatClmmConfigs(programId); 38 | 39 | const poolAccountFormat = poolAccountInfo.map((i) => ({ 40 | id: i.pubkey, 41 | ...PoolInfoLayout.decode(i.account.data), 42 | })); 43 | 44 | const allMint = Array.from( 45 | new Set( 46 | poolAccountFormat 47 | .map((i) => [ 48 | i.mintA.toString(), 49 | i.mintB.toString(), 50 | ...i.rewardInfos.map((ii) => ii.tokenMint.toString()), 51 | ]) 52 | .flat() 53 | ) 54 | ) 55 | .filter((i) => i !== filterDefKey) 56 | .map((i) => ({ pubkey: new PublicKey(i) })); 57 | const mintAccount = await getMultipleAccountsInfoWithCustomFlags( 58 | private_connection, 59 | allMint 60 | ); 61 | const mintInfoDict = mintAccount 62 | .filter((i) => i.accountInfo !== null) 63 | .reduce((a, b) => { 64 | a[b.pubkey.toString()] = { programId: b.accountInfo!.owner.toString() }; 65 | return a; 66 | }, {} as { [mint: string]: { programId: string } }); 67 | 68 | const poolInfoDict = poolAccountFormat 69 | .map((i) => { 70 | const mintProgramIdA = mintInfoDict[i.mintA.toString()].programId; 71 | const mintProgramIdB = mintInfoDict[i.mintB.toString()].programId; 72 | const rewardInfos = i.rewardInfos 73 | .filter((i) => !i.tokenMint.equals(PublicKey.default)) 74 | .map((i) => ({ 75 | mint: i.tokenMint.toString(), 76 | programId: mintInfoDict[i.tokenMint.toString()].programId, 77 | })); 78 | 79 | return { 80 | id: i.id.toString(), 81 | mintProgramIdA, 82 | mintProgramIdB, 83 | mintA: i.mintA.toString(), 84 | mintB: i.mintB.toString(), 85 | vaultA: i.vaultA.toString(), 86 | vaultB: i.vaultB.toString(), 87 | mintDecimalsA: i.mintDecimalsA, 88 | mintDecimalsB: i.mintDecimalsB, 89 | ammConfig: configIdToData[i.ammConfig.toString()], 90 | rewardInfos, 91 | tvl: 0, 92 | day: getApiClmmPoolsItemStatisticsDefault(), 93 | week: getApiClmmPoolsItemStatisticsDefault(), 94 | month: getApiClmmPoolsItemStatisticsDefault(), 95 | lookupTableAccount: PublicKey.default.toString(), 96 | }; 97 | }) 98 | .reduce((a, b) => { 99 | a[b.id] = b; 100 | return a; 101 | }, {} as { [id: string]: ApiClmmPoolsItem }); 102 | 103 | if (findLookupTableAddress) { 104 | const ltas = await private_connection.getProgramAccounts( 105 | new PublicKey("AddressLookupTab1e1111111111111111111111111"), 106 | { 107 | filters: [ 108 | { 109 | memcmp: { 110 | offset: 22, 111 | bytes: "RayZuc5vEK174xfgNFdD9YADqbbwbFjVjY4NM8itSF9", 112 | }, 113 | }, 114 | ], 115 | } 116 | ); 117 | for (const itemLTA of ltas) { 118 | const keyStr = itemLTA.pubkey.toString(); 119 | const ltaForamt = new AddressLookupTableAccount({ 120 | key: itemLTA.pubkey, 121 | state: AddressLookupTableAccount.deserialize(itemLTA.account.data), 122 | }); 123 | for (const itemKey of ltaForamt.state.addresses) { 124 | const itemKeyStr = itemKey.toString(); 125 | if (poolInfoDict[itemKeyStr] === undefined) continue; 126 | poolInfoDict[itemKeyStr].lookupTableAccount = keyStr; 127 | } 128 | } 129 | } 130 | 131 | return Object.values(poolInfoDict); 132 | } 133 | -------------------------------------------------------------------------------- /src/services/jito.bundle.ts: -------------------------------------------------------------------------------- 1 | import bs58 from "bs58"; 2 | import axios from "axios"; 3 | import { wait } from "../utils/wait"; 4 | import { JITO_UUID, MAX_CHECK_JITO } from "../config"; 5 | 6 | type Region = "ams" | "ger" | "ny" | "tokyo"; // "default" | 7 | 8 | // Region => Endpoint 9 | export const endpoints = { 10 | ams: "https://amsterdam.mainnet.block-engine.jito.wtf", 11 | // "default": "https://mainnet.block-engine.jito.wtf", 12 | ger: "https://frankfurt.mainnet.block-engine.jito.wtf", 13 | ny: "https://ny.mainnet.block-engine.jito.wtf", 14 | tokyo: "https://tokyo.mainnet.block-engine.jito.wtf", 15 | }; 16 | 17 | const regions = ["ams", "ger", "ny", "tokyo"] as Region[]; // "default", 18 | let idx = 0; 19 | 20 | export const JitoTipAmount = 7_500_00; 21 | 22 | export const tipAccounts = [ 23 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", 24 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 25 | "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", 26 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", 27 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 28 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", 29 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", 30 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", 31 | ]; 32 | 33 | export class JitoBundleService { 34 | endpoint: string; 35 | 36 | // constructor(_region: Region) { 37 | constructor() { 38 | idx = (idx + 1) % regions.length; 39 | const _region = regions[idx]; 40 | 41 | this.endpoint = endpoints[_region]; 42 | console.log("JitoRegion", _region); 43 | } 44 | 45 | updateRegion() { 46 | idx = (idx + 1) % regions.length; 47 | const _region = regions[idx]; 48 | this.endpoint = endpoints[_region]; 49 | // console.log("JitoRegion", _region); 50 | } 51 | async sendBundle(serializedTransaction: Uint8Array) { 52 | const encodedTx = bs58.encode(serializedTransaction); 53 | const jitoURL = `${this.endpoint}/api/v1/bundles?uuid=${JITO_UUID}`; // ?uuid=${JITO_UUID} 54 | const payload = { 55 | jsonrpc: "2.0", 56 | id: 1, 57 | method: "sendBundle", 58 | params: [[encodedTx]], 59 | }; 60 | 61 | try { 62 | const response = await axios.post(jitoURL, payload, { 63 | headers: { "Content-Type": "application/json" }, 64 | }); 65 | return response.data.result; 66 | } catch (error) { 67 | console.error("cannot send!:", error); 68 | return null; 69 | } 70 | } 71 | async sendTransaction(serializedTransaction: Uint8Array) { 72 | const encodedTx = bs58.encode(serializedTransaction); 73 | const jitoURL = `${this.endpoint}/api/v1/transactions`; // ?uuid=${JITO_UUID} 74 | // const jitoURL = `${this.endpoint}/api/v1/bundles?uuid=${JITO_UUID}` 75 | const payload = { 76 | jsonrpc: "2.0", 77 | id: 1, 78 | method: "sendTransaction", 79 | params: [encodedTx], 80 | }; 81 | 82 | try { 83 | const response = await axios.post(jitoURL, payload, { 84 | headers: { "Content-Type": "application/json" }, 85 | }); 86 | return response.data.result; 87 | } catch (error) { 88 | // console.error("Error:", error); 89 | throw new Error("cannot send!"); 90 | } 91 | } 92 | 93 | async getBundleStatus(bundleId: string) { 94 | const payload = { 95 | jsonrpc: "2.0", 96 | id: 1, 97 | method: "getBundleStatuses", 98 | params: [[bundleId]], 99 | }; 100 | 101 | let retries = 0; 102 | while (retries < MAX_CHECK_JITO) { 103 | retries++; 104 | try { 105 | this.updateRegion(); 106 | const jitoURL = `${this.endpoint}/api/v1/bundles?uuid=${JITO_UUID}`; // ?uuid=${JITO_UUID} 107 | // console.log("retries", jitoURL); 108 | 109 | const response = await axios.post(jitoURL, payload, { 110 | headers: { "Content-Type": "application/json" }, 111 | }); 112 | 113 | if (!response || response.data.result.value.length <= 0) { 114 | await wait(1000); 115 | continue; 116 | } 117 | 118 | const bundleResult = response.data.result.value[0]; 119 | if ( 120 | bundleResult.confirmation_status === "confirmed" || 121 | bundleResult.confirmation_status === "finalized" 122 | ) { 123 | retries = 0; 124 | console.log("JitoTransaction confirmed!!!"); 125 | break; 126 | } 127 | } catch (error) { 128 | console.error("GetBundleStatus Failed"); 129 | } 130 | } 131 | if (retries === 0) return true; 132 | return false; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/services/user.trade.setting.service.ts: -------------------------------------------------------------------------------- 1 | import redisClient, { ITradeGasSetting, ITradeJioFeeSetting, ITradeSlippageSetting } from "./redis"; 2 | 3 | export enum GasFeeEnum { 4 | LOW = 'low', 5 | HIGH = 'high', 6 | MEDIUM = 'medium', 7 | CUSTOM = 'custom' 8 | } 9 | 10 | 11 | // - Turbo 0.0075 12 | // - Safe 0.0045 13 | // - Light 0.0015 14 | export enum JitoFeeEnum { 15 | LOW = 'Light', 16 | HIGH = 'Turbo', 17 | MEDIUM = 'Safe', 18 | CUSTOM = 'custom' 19 | } 20 | 21 | 22 | export const UserTradeSettingService = { 23 | // , mint: string 24 | getSlippage: async (username: string) => { 25 | // const key = `${username}_${mint}`; 26 | const key = `${username}_slippage`; 27 | const data = await redisClient.get(key); 28 | if (data) return JSON.parse(data) as ITradeSlippageSetting; 29 | return { 30 | slippage: 20, 31 | slippagebps: 2000, 32 | } as ITradeSlippageSetting 33 | }, 34 | // , mint: string 35 | setSlippage: async (username: string, opts: ITradeSlippageSetting) => { 36 | // const key = `${username}_${mint}`; 37 | const key = `${username}_slippage`; 38 | await redisClient.set(key, JSON.stringify(opts)); 39 | }, 40 | getGasInlineKeyboard: async (gasfee: GasFeeEnum) => { 41 | const keyboards = [ 42 | { text: `${(gasfee === GasFeeEnum.LOW ? "🟢" : "🔴")} Low Gas`, command: 'low_gas' }, 43 | { text: `${(gasfee === GasFeeEnum.MEDIUM ? "🟢" : "🔴")} Medium Gas`, command: 'medium_gas' }, 44 | { text: `${(gasfee === GasFeeEnum.HIGH ? "🟢" : "🔴")} High Gas`, command: 'high_gas' }, 45 | ]; 46 | return keyboards; 47 | }, 48 | getGasValue: (gasSetting: ITradeGasSetting) => { 49 | const { gas, value } = gasSetting; 50 | if (gas === GasFeeEnum.CUSTOM) return value ?? 0.005; 51 | 52 | if (gas === GasFeeEnum.LOW) { 53 | return 0.005; // SOL 54 | } else if (gas === GasFeeEnum.MEDIUM) { 55 | return 0.02; // SOL 56 | } else if (gas === GasFeeEnum.HIGH) { 57 | return 0.05; // SOL 58 | } 59 | return 0.005; // SOL 60 | }, 61 | 62 | setGas: async (username: string, opts: ITradeGasSetting) => { 63 | const key = `${username}_gasfee`; 64 | await redisClient.set(key, JSON.stringify(opts)); 65 | }, 66 | getGas: async (username: string) => { 67 | const key = `${username}_gasfee`; 68 | const data = await redisClient.get(key); 69 | if (data) return JSON.parse(data) as ITradeGasSetting; 70 | return { 71 | gas: GasFeeEnum.LOW, 72 | value: 0.005 73 | } as ITradeGasSetting 74 | }, 75 | getNextGasFeeOption: (option: GasFeeEnum) => { 76 | switch (option) { 77 | case GasFeeEnum.CUSTOM: 78 | return GasFeeEnum.LOW; 79 | case GasFeeEnum.LOW: 80 | return GasFeeEnum.MEDIUM; 81 | case GasFeeEnum.MEDIUM: 82 | return GasFeeEnum.HIGH; 83 | case GasFeeEnum.HIGH: 84 | return GasFeeEnum.LOW; 85 | } 86 | }, 87 | getJitoFeeValue: (gasSetting: ITradeJioFeeSetting) => { 88 | const { jitoOption, value } = gasSetting; 89 | 90 | if (jitoOption === JitoFeeEnum.LOW) { 91 | return 0.0015; // SOL 92 | } else if (jitoOption === JitoFeeEnum.MEDIUM) { 93 | return 0.0045; // SOL 94 | } else if (jitoOption === JitoFeeEnum.HIGH) { 95 | return 0.0075; // SOL 96 | } else { 97 | return value ?? 0.0045; // SOL 98 | } 99 | }, 100 | 101 | setJitoFee: async (username: string, opts: ITradeJioFeeSetting) => { 102 | const key = `${username}_jitofee`; 103 | await redisClient.set(key, JSON.stringify(opts)); 104 | }, 105 | getJitoFee: async (username: string) => { 106 | const key = `${username}_jitofee`; 107 | const data = await redisClient.get(key); 108 | if (data) return JSON.parse(data) as ITradeJioFeeSetting; 109 | return { 110 | jitoOption: JitoFeeEnum.MEDIUM, 111 | value: 0.0045 112 | } as ITradeJioFeeSetting 113 | }, 114 | getNextJitoFeeOption: (option: JitoFeeEnum) => { 115 | switch (option) { 116 | case JitoFeeEnum.CUSTOM: 117 | return JitoFeeEnum.LOW; 118 | case JitoFeeEnum.LOW: 119 | return JitoFeeEnum.MEDIUM; 120 | case JitoFeeEnum.MEDIUM: 121 | return JitoFeeEnum.HIGH; 122 | case JitoFeeEnum.HIGH: 123 | return JitoFeeEnum.LOW; 124 | } 125 | } 126 | } 127 | /** Gas Fee calculation */ 128 | 129 | /** 130 | * lamports_per_signature * number_of_signatures 131 | * 132 | * ❯ solana fees 133 | * Blockhash: 7JgLCFReSYgWNpAB9EMCVv2H4Yv1UV79cp2oQQh2UtvF 134 | * Lamports per signature: 5000 135 | * Last valid block height: 141357699 136 | * 137 | * You can get the fee's for a particular message (serialized transaction) by using the getFeeForMessage method. 138 | * 139 | * getFeeForMessage(message: Message, commitment?: Commitment): Promise> 140 | * 141 | * constants 142 | * 143 | * pub const DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE: u64 = 10_000; 144 | * pub const DEFAULT_TARGET_SIGNATURES_PER_SLOT: u64 = 50 * DEFAULT_MS_PER_SLOT; // 20_000 145 | * 146 | * prioritizationFeeLamports = 1000_000 147 | * fee = 0.001 148 | * 149 | * prioritizationFeeLamports = 25_000_000 150 | * fee = 0.025 151 | * 152 | * prioritizationFeeLamports = 50_000_000 153 | * fee = 0.05 154 | */ 155 | 156 | -------------------------------------------------------------------------------- /src/raydium/utils/formatAmmKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiPoolInfo, 3 | ApiPoolInfoV4, 4 | LIQUIDITY_STATE_LAYOUT_V4, 5 | Liquidity, 6 | MARKET_STATE_LAYOUT_V3, 7 | Market, 8 | } from "@raydium-io/raydium-sdk"; 9 | import { AddressLookupTableAccount, PublicKey } from "@solana/web3.js"; 10 | 11 | import { private_connection } from "../../config"; 12 | 13 | export async function formatAmmKeys( 14 | programId: string, 15 | findLookupTableAddress: boolean = false 16 | ): Promise { 17 | const filterDefKey = PublicKey.default.toString(); 18 | const allAmmAccount = await private_connection.getProgramAccounts( 19 | new PublicKey(programId), 20 | { filters: [{ dataSize: LIQUIDITY_STATE_LAYOUT_V4.span }] } 21 | ); 22 | const amAccountmData = allAmmAccount 23 | .map((i) => ({ 24 | id: i.pubkey, 25 | programId: i.account.owner, 26 | ...LIQUIDITY_STATE_LAYOUT_V4.decode(i.account.data), 27 | })) 28 | .filter((i) => i.marketProgramId.toString() !== filterDefKey); 29 | 30 | const allMarketProgram = Array.from( 31 | new Set(amAccountmData.map((i) => i.marketProgramId.toString())) 32 | ); 33 | 34 | const marketInfo: { 35 | [marketId: string]: { 36 | marketProgramId: string; 37 | marketAuthority: string; 38 | marketBaseVault: string; 39 | marketQuoteVault: string; 40 | marketBids: string; 41 | marketAsks: string; 42 | marketEventQueue: string; 43 | }; 44 | } = {}; 45 | for (const itemMarketProgram of allMarketProgram) { 46 | const allMarketInfo = await private_connection.getProgramAccounts( 47 | new PublicKey(itemMarketProgram), 48 | { filters: [{ dataSize: MARKET_STATE_LAYOUT_V3.span }] } 49 | ); 50 | for (const itemAccount of allMarketInfo) { 51 | const itemMarketInfo = MARKET_STATE_LAYOUT_V3.decode( 52 | itemAccount.account.data 53 | ); 54 | marketInfo[itemAccount.pubkey.toString()] = { 55 | marketProgramId: itemAccount.account.owner.toString(), 56 | marketAuthority: Market.getAssociatedAuthority({ 57 | programId: itemAccount.account.owner, 58 | marketId: itemAccount.pubkey, 59 | }).publicKey.toString(), 60 | marketBaseVault: itemMarketInfo.baseVault.toString(), 61 | marketQuoteVault: itemMarketInfo.quoteVault.toString(), 62 | marketBids: itemMarketInfo.bids.toString(), 63 | marketAsks: itemMarketInfo.asks.toString(), 64 | marketEventQueue: itemMarketInfo.eventQueue.toString(), 65 | }; 66 | } 67 | } 68 | 69 | const ammFormatData = ( 70 | amAccountmData 71 | .map((itemAmm) => { 72 | const itemMarket = marketInfo[itemAmm.marketId.toString()]; 73 | if (itemMarket === undefined) return undefined; 74 | 75 | const format: ApiPoolInfoV4 = { 76 | id: itemAmm.id.toString(), 77 | baseMint: itemAmm.baseMint.toString(), 78 | quoteMint: itemAmm.quoteMint.toString(), 79 | lpMint: itemAmm.lpMint.toString(), 80 | baseDecimals: itemAmm.baseDecimal.toNumber(), 81 | quoteDecimals: itemAmm.quoteDecimal.toNumber(), 82 | lpDecimals: itemAmm.baseDecimal.toNumber(), 83 | version: 4, 84 | programId: itemAmm.programId.toString(), 85 | authority: Liquidity.getAssociatedAuthority({ 86 | programId: itemAmm.programId, 87 | }).publicKey.toString(), 88 | openOrders: itemAmm.openOrders.toString(), 89 | targetOrders: itemAmm.targetOrders.toString(), 90 | baseVault: itemAmm.baseVault.toString(), 91 | quoteVault: itemAmm.quoteVault.toString(), 92 | withdrawQueue: itemAmm.withdrawQueue.toString(), 93 | lpVault: itemAmm.lpVault.toString(), 94 | marketVersion: 3, 95 | marketId: itemAmm.marketId.toString(), 96 | ...itemMarket, 97 | lookupTableAccount: filterDefKey, 98 | }; 99 | return format; 100 | }) 101 | .filter((i) => i !== undefined) as ApiPoolInfoV4[] 102 | ).reduce((a, b) => { 103 | a[b.id] = b; 104 | return a; 105 | }, {} as { [id: string]: ApiPoolInfoV4 }); 106 | 107 | if (findLookupTableAddress) { 108 | const ltas = await private_connection.getProgramAccounts( 109 | new PublicKey("AddressLookupTab1e1111111111111111111111111"), 110 | { 111 | filters: [ 112 | { 113 | memcmp: { 114 | offset: 22, 115 | bytes: "RayZuc5vEK174xfgNFdD9YADqbbwbFjVjY4NM8itSF9", 116 | }, 117 | }, 118 | ], 119 | } 120 | ); 121 | for (const itemLTA of ltas) { 122 | const keyStr = itemLTA.pubkey.toString(); 123 | const ltaForamt = new AddressLookupTableAccount({ 124 | key: itemLTA.pubkey, 125 | state: AddressLookupTableAccount.deserialize(itemLTA.account.data), 126 | }); 127 | for (const itemKey of ltaForamt.state.addresses) { 128 | const itemKeyStr = itemKey.toString(); 129 | if (ammFormatData[itemKeyStr] === undefined) continue; 130 | ammFormatData[itemKeyStr].lookupTableAccount = keyStr; 131 | } 132 | } 133 | } 134 | 135 | return Object.values(ammFormatData); 136 | } 137 | 138 | export async function formatAmmKeysToApi( 139 | programId: string, 140 | findLookupTableAddress: boolean = false 141 | ): Promise { 142 | return { 143 | official: [], 144 | unOfficial: await formatAmmKeys(programId, findLookupTableAddress), 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /src/raydium/liquidity/liquidity.ts: -------------------------------------------------------------------------------- 1 | import { Commitment, Connection, PublicKey } from "@solana/web3.js"; 2 | import { 3 | Liquidity, 4 | LiquidityPoolKeys, 5 | Market, 6 | TokenAccount, 7 | SPL_ACCOUNT_LAYOUT, 8 | publicKey, 9 | struct, 10 | MAINNET_PROGRAM_ID, 11 | LiquidityStateV4, 12 | BNLayout, 13 | } from "@raydium-io/raydium-sdk"; 14 | import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; 15 | import { MinimalMarketLayoutV3 } from "../market"; 16 | 17 | export const RAYDIUM_LIQUIDITY_PROGRAM_ID_CLMM = MAINNET_PROGRAM_ID.CLMM; 18 | export const RAYDIUM_LIQUIDITY_PROGRAM_ID_V4 = MAINNET_PROGRAM_ID.AmmV4; 19 | export const OPENBOOK_PROGRAM_ID = MAINNET_PROGRAM_ID.OPENBOOK_MARKET; 20 | 21 | export const MINIMAL_MARKET_STATE_LAYOUT_V3 = struct([ 22 | publicKey("eventQueue"), 23 | publicKey("bids"), 24 | publicKey("asks"), 25 | ]); 26 | 27 | export function createPoolKeys( 28 | id: PublicKey, 29 | accountData: LiquidityStateV4, 30 | minimalMarketLayoutV3: MinimalMarketLayoutV3 31 | ): LiquidityPoolKeys { 32 | return { 33 | id, 34 | baseMint: accountData.baseMint, 35 | quoteMint: accountData.quoteMint, 36 | lpMint: accountData.lpMint, 37 | baseDecimals: Number(accountData.baseDecimal), // .toNumber(), 38 | quoteDecimals: Number(accountData.quoteDecimal), // .toNumber(), 39 | lpDecimals: 5, 40 | version: 4, 41 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 42 | authority: Liquidity.getAssociatedAuthority({ 43 | programId: RAYDIUM_LIQUIDITY_PROGRAM_ID_V4, 44 | }).publicKey, 45 | openOrders: accountData.openOrders, 46 | targetOrders: accountData.targetOrders, 47 | baseVault: accountData.baseVault, 48 | quoteVault: accountData.quoteVault, 49 | marketVersion: 3, 50 | marketProgramId: accountData.marketProgramId, 51 | marketId: accountData.marketId, 52 | marketAuthority: Market.getAssociatedAuthority({ 53 | programId: accountData.marketProgramId, 54 | marketId: accountData.marketId, 55 | }).publicKey, 56 | marketBaseVault: accountData.baseVault, 57 | marketQuoteVault: accountData.quoteVault, 58 | marketBids: minimalMarketLayoutV3.bids, 59 | marketAsks: minimalMarketLayoutV3.asks, 60 | marketEventQueue: minimalMarketLayoutV3.eventQueue, 61 | withdrawQueue: accountData.withdrawQueue, 62 | lpVault: accountData.lpVault, 63 | lookupTableAccount: PublicKey.default, 64 | }; 65 | } 66 | 67 | export async function getTokenAccounts( 68 | connection: Connection, 69 | owner: PublicKey, 70 | commitment?: Commitment 71 | ) { 72 | const tokenResp = await connection.getTokenAccountsByOwner( 73 | owner, 74 | { 75 | programId: TOKEN_PROGRAM_ID, 76 | }, 77 | commitment 78 | ); 79 | 80 | const accounts: TokenAccount[] = []; 81 | for (const { pubkey, account } of tokenResp.value) { 82 | accounts.push({ 83 | pubkey, 84 | programId: account.owner, 85 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(account.data), 86 | }); 87 | } 88 | 89 | return accounts; 90 | } 91 | 92 | export const convertDBForPoolStateV4 = (poolstate: any) => { 93 | return { 94 | status: poolstate.status, 95 | nonce: poolstate.nonce, 96 | maxOrder: poolstate.maxOrder, 97 | depth: poolstate.depth, 98 | baseDecimal: poolstate.baseDecimal, 99 | quoteDecimal: poolstate.quoteDecimal, 100 | state: poolstate.state, 101 | resetFlag: poolstate.resetFlag, 102 | minSize: poolstate.minSize, 103 | volMaxCutRatio: poolstate.volMaxCutRatio, 104 | amountWaveRatio: poolstate.amountWaveRatio, 105 | baseLotSize: poolstate.baseLotSize, 106 | quoteLotSize: poolstate.quoteLotSize, 107 | minPriceMultiplier: poolstate.minPriceMultiplier, 108 | maxPriceMultiplier: poolstate.maxPriceMultiplier, 109 | systemDecimalValue: poolstate.systemDecimalValue, 110 | minSeparateNumerator: poolstate.minSeparateNumerator, 111 | minSeparateDenominator: poolstate.minSeparateDenominator, 112 | tradeFeeNumerator: poolstate.tradeFeeNumerator, 113 | tradeFeeDenominator: poolstate.tradeFeeDenominator, 114 | pnlNumerator: poolstate.pnlNumerator, 115 | pnlDenominator: poolstate.pnlDenominator, 116 | swapFeeNumerator: poolstate.swapFeeNumerator, 117 | swapFeeDenominator: poolstate.swapFeeDenominator, 118 | baseNeedTakePnl: poolstate.baseNeedTakePnl, 119 | quoteNeedTakePnl: poolstate.quoteNeedTakePnl, 120 | quoteTotalPnl: poolstate.quoteTotalPnl, 121 | baseTotalPnl: poolstate.baseTotalPnl, 122 | poolOpenTime: poolstate.poolOpenTime, 123 | punishPcAmount: poolstate.punishPcAmount, 124 | punishCoinAmount: poolstate.punishCoinAmount, 125 | orderbookToInitTime: poolstate.orderbookToInitTime, 126 | swapBaseInAmount: poolstate.swapBaseInAmount, 127 | swapQuoteOutAmount: poolstate.swapQuoteOutAmount, 128 | swapBase2QuoteFee: poolstate.swapBase2QuoteFee, 129 | swapQuoteInAmount: poolstate.swapQuoteInAmount, 130 | swapBaseOutAmount: poolstate.swapBaseOutAmount, 131 | swapQuote2BaseFee: poolstate.swapQuote2BaseFee, 132 | baseVault: new PublicKey(poolstate.baseVault), 133 | quoteVault: new PublicKey(poolstate.quoteVault), 134 | baseMint: new PublicKey(poolstate.baseMint), 135 | quoteMint: new PublicKey(poolstate.quoteMint), 136 | lpMint: new PublicKey(poolstate.lpMint), 137 | openOrders: new PublicKey(poolstate.openOrders), 138 | marketId: new PublicKey(poolstate.marketId), 139 | marketProgramId: new PublicKey(poolstate.marketProgramId), 140 | targetOrders: new PublicKey(poolstate.targetOrders), 141 | withdrawQueue: new PublicKey(poolstate.withdrawQueue), 142 | lpVault: new PublicKey(poolstate.lpVault), 143 | owner: new PublicKey(poolstate.owner), 144 | lpReserve: poolstate.lpReserve, 145 | } as LiquidityStateV4; 146 | }; 147 | -------------------------------------------------------------------------------- /src/screens/bot.dashboard.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { AlertBotID, WELCOME_REFERRAL } from "../bot.opts"; 3 | import { get_referrer_info } from "../services/referral.service"; 4 | import { ReferralChannelController } from "../controllers/referral.channel"; 5 | import { UserService } from "../services/user.service"; 6 | import { schedule } from "node-cron"; 7 | import { 8 | ReferralChannelService, 9 | ReferralPlatform, 10 | } from "../services/referral.channel.service"; 11 | 12 | export const openAlertBotDashboard = async ( 13 | bot: TelegramBot, 14 | chat: TelegramBot.Chat 15 | ) => { 16 | try { 17 | const chatId = chat.id; 18 | const username = chat.username; 19 | 20 | if (!username) return; 21 | const refdata = await get_referrer_info(username); 22 | if (!refdata) { 23 | bot.sendMessage( 24 | chat.id, 25 | "You have no referral code. Please create a referral code first." 26 | ); 27 | return; 28 | } 29 | 30 | const { schedule, uniquecode } = refdata; 31 | 32 | const channels = await ReferralChannelController.find({ 33 | referral_code: uniquecode, 34 | }); 35 | 36 | const inlineKeyboards = [ 37 | [ 38 | { 39 | text: "Alert Schedule 🕓", 40 | callback_data: JSON.stringify({ 41 | command: "alert_schedule", 42 | }), 43 | }, 44 | { 45 | text: "Invite AlertBot 🤖", 46 | // callback_data: JSON.stringify({ 47 | // 'command': 'dummy_button' 48 | // }) 49 | url: `https://t.me/${AlertBotID}?startgroup=tradebot`, 50 | }, 51 | ], 52 | [ 53 | { 54 | text: "Refresh bot info", 55 | callback_data: JSON.stringify({ 56 | command: "refresh_alert_bot", 57 | }), 58 | }, 59 | { 60 | text: "Back", 61 | callback_data: JSON.stringify({ 62 | command: "back_from_ref", 63 | }), 64 | }, 65 | ], 66 | ]; 67 | 68 | const reply_markup = { 69 | inline_keyboard: inlineKeyboards, 70 | }; 71 | 72 | let channelList = ``; 73 | for (const channel of channels) { 74 | const { channel_name } = channel; 75 | channelList += `🟢 ${channel_name}\n`; 76 | } 77 | 78 | if (channels.length <= 0) { 79 | channelList += 80 | "You have not invited our alert bot into any channel yet.\n"; 81 | } 82 | const contents = 83 | `AlertBot Configuration\n\n` + 84 | `Channels\n` + 85 | `${channelList}\n` + 86 | "Alert schedule\n" + 87 | `Bot will send alerts every ${schedule ?? 30} minutes.\n\n` + 88 | `Once you setup at least one group, you can then invite @${AlertBotID} into your group-chat.\n\n` + 89 | `If you want to update the settings, you can do it through the menu down below 👇🏼`; 90 | await bot.sendPhoto(chatId, WELCOME_REFERRAL, { 91 | caption: contents, 92 | reply_markup, 93 | parse_mode: "HTML", 94 | }); 95 | } catch (e) { 96 | console.log("~ openAlertBotDashboard Error ~", e); 97 | } 98 | }; 99 | 100 | export const sendMsgForAlertScheduleHandler = async ( 101 | bot: TelegramBot, 102 | chat: TelegramBot.Chat 103 | ) => { 104 | try { 105 | if (!chat.username) return; 106 | const msgText = `Please, set a time alert frequency 👇🏼.`; 107 | 108 | const inlineKeyboards = [ 109 | [ 110 | { 111 | text: "10mins", 112 | callback_data: JSON.stringify({ 113 | command: "schedule_time_10", 114 | }), 115 | }, 116 | { 117 | text: "30mins", 118 | callback_data: JSON.stringify({ 119 | command: "schedule_time_30", 120 | }), 121 | }, 122 | ], 123 | [ 124 | { 125 | text: "1h", 126 | callback_data: JSON.stringify({ 127 | command: "schedule_time_60", 128 | }), 129 | }, 130 | { 131 | text: "3h", 132 | callback_data: JSON.stringify({ 133 | command: "schedule_time_180", 134 | }), 135 | }, 136 | { 137 | text: "4h", 138 | callback_data: JSON.stringify({ 139 | command: "schedule_time_240", 140 | }), 141 | }, 142 | { 143 | text: "6h", 144 | callback_data: JSON.stringify({ 145 | command: "schedule_time_360", 146 | }), 147 | }, 148 | ], 149 | ]; 150 | 151 | const reply_markup = { 152 | inline_keyboard: inlineKeyboards, 153 | }; 154 | 155 | await bot.sendMessage(chat.id, msgText, { 156 | reply_markup, 157 | parse_mode: "HTML", 158 | }); 159 | } catch (e) { 160 | console.log("sendMsgForChannelIDHandler", e); 161 | } 162 | }; 163 | 164 | export const updateSchedule = async ( 165 | bot: TelegramBot, 166 | chat: TelegramBot.Chat, 167 | scheduleTime: string 168 | ) => { 169 | try { 170 | const chatId = chat.id; 171 | const username = chat.username; 172 | if (!username) return; 173 | // update channel DB as well 174 | const referralChannelService = new ReferralChannelService(); 175 | const result = await referralChannelService.updateReferralChannel({ 176 | creator: username, 177 | platform: ReferralPlatform.TradeBot, 178 | schedule: scheduleTime, 179 | }); 180 | console.log(result); 181 | if (!result) return; 182 | // post 183 | const res = await UserService.updateMany( 184 | { username: username }, 185 | { schedule: scheduleTime } 186 | ); 187 | if (res) { 188 | bot.sendMessage(chatId, "Successfully updated!"); 189 | } else { 190 | bot.sendMessage(chatId, "Update schedule failed! Try it again"); 191 | } 192 | } catch (e) { 193 | console.log("updateSchedule", e); 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { PNL_IMG_GENERATOR_API, TELEGRAM_BOT_API_TOKEN } from "./config"; 3 | import { AlertBotID, BotMenu } from "./bot.opts"; 4 | import { WelcomeScreenHandler } from "./screens/welcome.screen"; 5 | import { callbackQueryHandler } from "./controllers/callback.handler"; 6 | import { messageHandler } from "./controllers/message.handler"; 7 | import { positionScreenHandler } from "./screens/position.screen"; 8 | import { UserService } from "./services/user.service"; 9 | import { 10 | alertBot, 11 | runAlertBotForChannel, 12 | runAlertBotSchedule, 13 | } from "./cron/alert.bot.cron"; 14 | import { 15 | newReferralChannelHandler, 16 | removeReferralChannelHandler, 17 | } from "./services/alert.bot.module"; 18 | import { runSOLPriceUpdateSchedule } from "./cron/sol.price.cron"; 19 | import { settingScreenHandler } from "./screens/settings.screen"; 20 | import { 21 | ReferralChannelService, 22 | ReferralPlatform, 23 | } from "./services/referral.channel.service"; 24 | import { ReferrerListService } from "./services/referrer.list.service"; 25 | import { runListener } from "./raydium"; 26 | import { wait } from "./utils/wait"; 27 | import { runOpenmarketCronSchedule } from "./cron/remove.openmarket.cron"; 28 | 29 | const token = TELEGRAM_BOT_API_TOKEN; 30 | 31 | if (!token) { 32 | throw new Error( 33 | "TELEGRAM_BOT API_KEY is not defined in the environment variables" 34 | ); 35 | } 36 | 37 | export interface ReferralIdenticalType { 38 | referrer: string; 39 | chatId: string; 40 | messageId: string; 41 | channelName: string; 42 | } 43 | 44 | const startTradeBot = () => { 45 | const bot = new TelegramBot(token, { polling: true }); 46 | // 47 | runOpenmarketCronSchedule(); 48 | // Listen Raydium POOL creation 49 | runListener(); 50 | // bot menu 51 | runAlertBotSchedule(); 52 | // Later: runAlertBotForChannel(); 53 | runSOLPriceUpdateSchedule(); 54 | bot.setMyCommands(BotMenu); 55 | 56 | // bot callback 57 | bot.on( 58 | "callback_query", 59 | async function onCallbackQuery(callbackQuery: TelegramBot.CallbackQuery) { 60 | callbackQueryHandler(bot, callbackQuery); 61 | } 62 | ); 63 | 64 | // bot message 65 | bot.on("message", async (msg: TelegramBot.Message) => { 66 | messageHandler(bot, msg); 67 | }); 68 | 69 | // bot commands 70 | bot.onText(/\/start/, async (msg: TelegramBot.Message) => { 71 | // Need to remove "/start" text 72 | bot.deleteMessage(msg.chat.id, msg.message_id); 73 | 74 | await WelcomeScreenHandler(bot, msg); 75 | const referralcode = UserService.extractUniqueCode(msg.text ?? ""); 76 | if (referralcode && referralcode !== "") { 77 | // store info 78 | const chat = msg.chat; 79 | if (chat.username) { 80 | const data = await UserService.findLastOne({ username: chat.username }); 81 | if (data && data.referral_code && data.referral_code !== "") return; 82 | await UserService.updateMany( 83 | { username: chat.username }, 84 | { 85 | referral_code: referralcode, 86 | referral_date: new Date(), 87 | } 88 | ); 89 | } 90 | } 91 | }); 92 | bot.onText(/\/position/, async (msg: TelegramBot.Message) => { 93 | await positionScreenHandler(bot, msg); 94 | }); 95 | 96 | bot.onText(/\/settings/, async (msg: TelegramBot.Message) => { 97 | await settingScreenHandler(bot, msg); 98 | }); 99 | 100 | alertBot.onText(/\/start/, async (msg: TelegramBot.Message) => { 101 | const { from, chat, text, message_id } = msg; 102 | console.log("AlertBotStart", `/start@${AlertBotID}`); 103 | if (text && text.includes(`/start@${AlertBotID}`)) { 104 | console.log("AlertBotStart Delete", Date.now()); 105 | await wait(3000); 106 | console.log("AlertBotStart Delete", Date.now()); 107 | 108 | try { 109 | alertBot.deleteMessage(chat.id, message_id); 110 | } catch (e) {} 111 | if (!from) return; 112 | if (!text.includes(" ")) return; 113 | const referrerInfo = await ReferrerListService.findLastOne({ 114 | referrer: from.username, 115 | chatId: chat.id.toString(), 116 | }); 117 | if (!referrerInfo) return; 118 | // for (const referrerInfo of referrerList) { 119 | const { referrer, chatId, channelName } = referrerInfo; 120 | // if (referrer === from.username && chat.id.toString() === chatId) { 121 | const parts = text.split(" "); 122 | if (parts.length < 1) { 123 | return; 124 | } 125 | if (parts[0] !== `/start@${AlertBotID}`) { 126 | return; 127 | } 128 | const botType = parts[1]; 129 | if (botType === "tradebot") { 130 | const referralChannelService = new ReferralChannelService(); 131 | await referralChannelService.addReferralChannel({ 132 | creator: referrer, 133 | platform: ReferralPlatform.TradeBot, 134 | chat_id: chatId, 135 | channel_name: channelName, 136 | }); 137 | } else if (botType === "bridgebot") { 138 | const referralChannelService = new ReferralChannelService(); 139 | await referralChannelService.addReferralChannel({ 140 | creator: referrer, 141 | platform: ReferralPlatform.BridgeBot, 142 | chat_id: chatId, 143 | channel_name: channelName, 144 | }); 145 | } 146 | // } 147 | // } 148 | } 149 | }); 150 | alertBot.on("new_chat_members", async (msg: TelegramBot.Message) => { 151 | console.log("new Members", msg); 152 | const data = await newReferralChannelHandler(msg); 153 | if (!data) return; 154 | 155 | try { 156 | console.log("New member created"); 157 | await ReferrerListService.create(data); 158 | console.log("New member added ended"); 159 | } catch (e) {} 160 | }); 161 | alertBot.on("left_chat_member", async (msg: TelegramBot.Message) => { 162 | await removeReferralChannelHandler(msg); 163 | }); 164 | }; 165 | 166 | export default startTradeBot; 167 | -------------------------------------------------------------------------------- /src/services/pnl.service.ts: -------------------------------------------------------------------------------- 1 | import { PositionService } from "./position.service"; 2 | import { isEqual } from "../utils"; 3 | import { QuoteRes } from "./jupiter.service"; 4 | import { amount } from "@metaplex-foundation/js"; 5 | import { waitFlagForBundleVerify } from "./redis.service"; 6 | import { PNL_IMG_GENERATOR_API } from "../config"; 7 | 8 | export class PNLService { 9 | wallet_address: string; 10 | mint: string; 11 | // username: string; 12 | quote: QuoteRes | undefined | null; // input: mint, output: SOL 13 | 14 | constructor( 15 | _wallet_address: string, 16 | _mint: string, 17 | // _username: string, 18 | _quote?: QuoteRes | undefined | null 19 | ) { 20 | this.wallet_address = _wallet_address; 21 | this.mint = _mint; 22 | // this.username = _username; 23 | this.quote = _quote; 24 | } 25 | 26 | // if wallet has SPL balance but there is no any record in position database 27 | // In this case, we need to synchorize 28 | // This happens only in case of relaunch 29 | async initialize() { 30 | const myposition = await PositionService.findLastOne({ 31 | mint: this.mint, 32 | wallet_address: this.wallet_address 33 | }); 34 | if (!this.quote) return; 35 | if (!myposition) { 36 | const { 37 | inAmount, 38 | outAmount 39 | } = this.quote; 40 | const ts = Date.now(); 41 | 42 | await PositionService.create({ 43 | // username: this.username, 44 | // chat_id: this.chat_id, 45 | mint: this.mint, 46 | wallet_address: this.wallet_address, 47 | volume: 0, 48 | sol_amount: outAmount, 49 | amount: inAmount, 50 | received_sol_amount: 0, 51 | creation_time: ts 52 | }) 53 | } else if ((myposition.sol_amount <= 0 && myposition.amount <= 0)) { 54 | const waitForBundle = await waitFlagForBundleVerify(this.wallet_address); 55 | if (waitForBundle) return; 56 | 57 | const { 58 | inAmount, 59 | outAmount 60 | } = this.quote; 61 | 62 | const filter = { 63 | wallet_address: this.wallet_address, 64 | mint: this.mint, 65 | }; 66 | const data = { 67 | $set: { 68 | sol_amount: outAmount, 69 | amount: inAmount 70 | } 71 | }; 72 | await PositionService.findAndUpdateOne(filter, data); 73 | } 74 | return; 75 | } 76 | 77 | async getPNLInfo() { 78 | const position = await PositionService.findLastOne({ 79 | wallet_address: this.wallet_address, 80 | mint: this.mint, 81 | }); 82 | if (!position) return null; 83 | if (!this.quote) return; 84 | const { sol_amount, received_sol_amount } = position; 85 | const { outAmount } = this.quote; 86 | const profitInSOL = outAmount + received_sol_amount - sol_amount; 87 | const percent = profitInSOL * 100 / sol_amount; 88 | return { profitInSOL, percent }; 89 | } 90 | 91 | async getPNLCard(pnlData: any): Promise { 92 | const url = PNL_IMG_GENERATOR_API + '/create' 93 | const res = await fetch(url, { 94 | method: 'POST', 95 | headers: { 96 | 'Content-Type': 'application/json' 97 | }, 98 | body: JSON.stringify(pnlData) }) 99 | 100 | if(res){ 101 | const data = await res.json(); 102 | if(res.status === 200) 103 | { 104 | // console.log(data.pplUrl) 105 | const urls = data.pplUrl.split('/') 106 | return { pnlCard: urls[urls.length - 1].replace('.png', 'png'), pnlUrl: data.pplUrl } 107 | } 108 | } 109 | return null; 110 | } 111 | 112 | async getBoughtAmount() { 113 | const position = await PositionService.findLastOne({ 114 | wallet_address: this.wallet_address, 115 | mint: this.mint, 116 | }); 117 | if (!position) return null; 118 | 119 | return position.sol_amount; 120 | } 121 | /** 122 | * inAmount: SOL amount spent 123 | * outAmount: mint amount received 124 | */ 125 | async afterBuy(inAmount: number, outAmount: number) { 126 | const filter = { 127 | wallet_address: this.wallet_address, 128 | mint: this.mint, 129 | }; 130 | const res = await PositionService.findLastOne(filter); 131 | if (!res) { 132 | const ts = Date.now(); 133 | return await PositionService.create({ 134 | wallet_address: this.wallet_address, 135 | mint: this.mint, 136 | volume: 0, 137 | sol_amount: inAmount, 138 | amount: outAmount, 139 | received_sol_amount: 0.0, 140 | creation_time: ts 141 | }) 142 | } 143 | const data = { 144 | $inc: { 145 | sol_amount: inAmount, 146 | amount: outAmount 147 | } 148 | }; 149 | return await PositionService.findAndUpdateOne(filter, data); 150 | } 151 | 152 | /** 153 | * Update position after sell 154 | * @param outAmount received SOL amount 155 | * @param sellPercent sell percent 156 | * @returns 157 | */ 158 | async afterSell(outAmount: number, sellPercent: number) { 159 | const filter = { 160 | wallet_address: this.wallet_address, 161 | mint: this.mint, 162 | }; 163 | if (isEqual(sellPercent, 100)) { 164 | const position = await PositionService.findLastOne(filter); 165 | if (!position) return; 166 | const { sol_amount, received_sol_amount } = position; 167 | console.log("OutProfit", outAmount); 168 | const profit = outAmount + received_sol_amount - sol_amount; 169 | const data = { 170 | $inc: { 171 | volume: profit, 172 | }, 173 | $set: { 174 | sol_amount: 0.0, 175 | received_sol_amount: 0.0, 176 | amount: 0.0, 177 | } 178 | } 179 | return await PositionService.findAndUpdateOne(filter, data); 180 | } else { 181 | const data = { 182 | $inc: { 183 | received_sol_amount: outAmount, 184 | } 185 | }; 186 | return await PositionService.findAndUpdateOne(filter, data); 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /src/screens/payout.screen.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { copytoclipboard } from "../utils"; 3 | import { INPUT_SOL_ADDRESS } from "../bot.opts"; 4 | import { isValidWalletAddress } from "../utils"; 5 | import { OpenReferralWindowHandler } from "./referral.link.handler"; 6 | import { UserService } from "../services/user.service"; 7 | 8 | export const sendPayoutAddressManageScreen = async ( 9 | bot: TelegramBot, 10 | chat: TelegramBot.Chat, 11 | message_id: number 12 | ) => { 13 | try { 14 | if (!chat.username) return; 15 | // fetch payout address list 16 | // const refdata = await get_referral_info(chat.username); 17 | // if (!refdata) { 18 | // bot.sendMessage(chat.id, 'You have no referral code. Please create a referral code first.'); 19 | // return; 20 | // } 21 | 22 | // const { busdpayout, solpayout, uniquecode } = refdata; 23 | 24 | // const profitdata = await get_profits(uniquecode); 25 | const userInfo = await UserService.findOne({ username: chat.username }); 26 | const payout_wallet = userInfo?.referrer_wallet ?? ""; 27 | 28 | // `Your profits 💰\n` + 29 | // // `Total profits: $${profitdata?.total_profit.toFixed(3) ?? "0"}\n` + 30 | // // `Available profits: $${profitdata?.available_profit.toFixed(3) ?? "0"}` + 31 | // `\n\n` + 32 | const caption = 33 | "Payout address👇\n" + 34 | `SOL wallet (Solana) 🔹\n${copytoclipboard(payout_wallet)}`; 35 | // \n\n` + 36 | // `Current referral percentage: First Month: 25%` 37 | 38 | // `USDT wallet (BNB-chain) 🔸\n${copytoclipboard(busdpayout)}\n\n` + 39 | // `Note: Payouts can be requests when profits reach a value of 20$.` 40 | 41 | const reply_markup = { 42 | inline_keyboard: [ 43 | [ 44 | { 45 | text: "Update SOL address", 46 | callback_data: JSON.stringify({ 47 | command: "set_sol_address", 48 | }), 49 | }, 50 | ], 51 | // [{ 52 | // text: 'Payout history', 53 | // callback_data: JSON.stringify({ 54 | // 'command': 'get_payout_history' 55 | // }) 56 | // }], 57 | [ 58 | { 59 | text: "Refresh", 60 | callback_data: JSON.stringify({ 61 | command: "refresh_payout", 62 | }), 63 | }, 64 | { 65 | text: "Back", 66 | callback_data: JSON.stringify({ 67 | command: "back_from_ref", 68 | }), 69 | }, 70 | ], 71 | ], 72 | }; 73 | await bot.editMessageCaption(caption, { 74 | chat_id: chat.id, 75 | message_id, 76 | parse_mode: "HTML", 77 | reply_markup, 78 | }); 79 | } catch (e) { 80 | console.log("sendPayoutAddressManageScreen Error", e); 81 | } 82 | }; 83 | 84 | export const setSOLPayoutAddressHandler = async ( 85 | bot: TelegramBot, 86 | chat: TelegramBot.Chat 87 | ) => { 88 | try { 89 | if (!chat.username) return; 90 | const solAddressMsg = await bot.sendMessage(chat.id, INPUT_SOL_ADDRESS, { 91 | parse_mode: "HTML", 92 | }); 93 | const textEventHandler = async (msg: TelegramBot.Message) => { 94 | const receivedChatId = msg.chat.id; 95 | const receivedText = msg.text; 96 | const receivedMessageId = msg.message_id; 97 | const receivedTextSender = msg.chat.username; 98 | // Check if the received message ID matches the original message ID 99 | if ( 100 | receivedText && 101 | receivedChatId === chat.id && 102 | receivedMessageId === solAddressMsg.message_id + 1 103 | ) { 104 | // message should be same user 105 | if (receivedTextSender === chat.username) { 106 | // update address 107 | updateSOLaddressForPayout( 108 | bot, 109 | chat, 110 | solAddressMsg.message_id, 111 | receivedText 112 | ); 113 | } 114 | bot.removeListener("text", textEventHandler); 115 | } 116 | }; 117 | // Add the 'text' event listener 118 | bot.on("text", textEventHandler); 119 | } catch (e) { 120 | console.log("setSOLPayoutAddressHandler", e); 121 | } 122 | }; 123 | 124 | const updateSOLaddressForPayout = async ( 125 | bot: TelegramBot, 126 | chat: TelegramBot.Chat, 127 | old_message_id: number, 128 | address: string 129 | ) => { 130 | try { 131 | const chatId = chat.id; 132 | // validate first 133 | if (!isValidWalletAddress(address)) { 134 | bot.deleteMessage(chatId, old_message_id); 135 | const message = await bot.sendMessage( 136 | chatId, 137 | "Invalid wallet address. Try it again" 138 | ); 139 | setTimeout(() => { 140 | bot.deleteMessage(chatId, message.message_id); 141 | }, 3000); 142 | setSOLPayoutAddressHandler(bot, chat); 143 | return; 144 | } 145 | const username = chat.username; 146 | if (!username) return; 147 | // post 148 | const res = await UserService.updateMany( 149 | { username: username }, 150 | { 151 | referrer_wallet: address, 152 | } 153 | ); 154 | // const res = await update_payout_address( 155 | // username, 156 | // undefined, 157 | // address, 158 | // ) 159 | if (true) { 160 | const sentMsg = await bot.sendMessage(chatId, "Successfully updated!"); 161 | setTimeout(() => { 162 | bot.deleteMessage(chatId, sentMsg.message_id); 163 | bot.deleteMessage(chatId, old_message_id + 1); 164 | bot.deleteMessage(chatId, old_message_id); 165 | }, 2000); 166 | } 167 | } catch (e) { 168 | console.log("updateSOLaddressForPayout", e); 169 | } 170 | }; 171 | 172 | export const backToReferralHomeScreenHandler = async ( 173 | bot: TelegramBot, 174 | chat: TelegramBot.Chat, 175 | msg: TelegramBot.Message 176 | ) => { 177 | if (!chat.username) return; 178 | bot.deleteMessage(chat.id, msg.message_id); 179 | OpenReferralWindowHandler(bot, msg); 180 | }; 181 | 182 | export const refreshPayoutHandler = async ( 183 | bot: TelegramBot, 184 | msg: TelegramBot.Message 185 | ) => { 186 | const chat = msg.chat; 187 | if (!chat.username) return; 188 | await sendPayoutAddressManageScreen(bot, chat, msg.message_id); 189 | }; 190 | -------------------------------------------------------------------------------- /src/services/referral.service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosResponse, isAxiosError } from 'axios'; 2 | import { UserService } from './user.service'; 3 | import { schedule } from 'node-cron'; 4 | import referralHistory from '../models/referral.history'; 5 | import { ReferralHistoryControler } from '../controllers/referral.history'; 6 | import { PublicKey } from '@solana/web3.js'; 7 | import { connection } from '../config'; 8 | import { wait } from '../utils/wait'; 9 | import { ReferralChannelService } from './referral.channel.service'; 10 | 11 | export type ReferralData = { 12 | username: string, 13 | uniquecode: string, 14 | busdpayout: string, 15 | solpayout: string, 16 | schedule: number, 17 | } 18 | 19 | export type ReferralProfits = { 20 | total_profit: number, 21 | available_profit: number, 22 | total_txns: number 23 | } 24 | 25 | export const get_referral_info = async (username: string) => { 26 | let userInfo = await UserService.findOne({ username: username }); 27 | let referral_code = userInfo?.referral_code; 28 | 29 | if (referral_code == '') { 30 | return ({ referral: false }); 31 | } 32 | 33 | let referral_date = userInfo?.referral_date ?? new Date(); 34 | let inputDate = new Date(referral_date); 35 | 36 | // Calculate the current date after 45 days 37 | let currentDateAfter45Days = inputDate.getTime() + (45 * 24 * 60 * 60 * 1000); 38 | 39 | // Calculate the current date after 90 days 40 | let currentDateAfter90Days = inputDate.getTime() + (90 * 24 * 60 * 60 * 1000); 41 | 42 | // Determine the referral_option based on the conditions 43 | let referral_option = 15; 44 | if (Date.now() < currentDateAfter45Days) { 45 | referral_option = 25; 46 | } else if (Date.now() < currentDateAfter90Days) { 47 | referral_option = 20; 48 | } else { 49 | referral_option = 15; 50 | } 51 | 52 | let referrerInfo = await UserService.findOne({ referrer_code: referral_code }); 53 | 54 | return { 55 | referral: true, 56 | referral_option: referral_option, 57 | uniquecode: userInfo?.referral_code ?? "", 58 | referral_address: referrerInfo?.referrer_wallet == "" ? referrerInfo?.wallet_address : referrerInfo?.referrer_wallet 59 | }; 60 | } 61 | export const get_referrer_info = async (username: string) => { 62 | let userInfo = await UserService.findOne({ username: username }); 63 | 64 | return { 65 | uniquecode: userInfo?.referrer_code ?? "", 66 | schedule: userInfo?.schedule ?? "60" 67 | }; 68 | } 69 | 70 | export const get_referral_num = async (uniquecode: string) => { 71 | let userList = await UserService.find({ referral_code: uniquecode }); 72 | return { num: userList.length } 73 | } 74 | 75 | export const get_referral_amount = async (uniquecode: string) => { 76 | let referalList = await referralHistory.find({ uniquecode: uniquecode }); 77 | let totalAmount = 0; 78 | for (let index = 0; index < referalList.length; index++) { 79 | totalAmount += referalList[index].amount; 80 | } 81 | return { totalAmount: totalAmount / 100000 / 10000 } 82 | } 83 | 84 | export const getReferralList = async () => { 85 | try { 86 | const referralChannelService = new ReferralChannelService(); 87 | const result = await referralChannelService.getAllReferralChannels(); 88 | return result; 89 | } catch (e) { 90 | return null; 91 | } 92 | } 93 | 94 | // export const getReferralList = async () => { 95 | // let userInfo = await UserService.find({}); 96 | // let res = userInfo?.map((item) => ({ 97 | // username: item.username, 98 | // uniquecode: item.referrer_code, 99 | // schedule: item.schedule ?? "60" 100 | // })) 101 | // return res; 102 | // } 103 | 104 | export const update_channel_id = async (username: string, idx: number, channelId: string) => { 105 | try { 106 | const refdata = { 107 | username, 108 | arrayIdx: idx, 109 | channelId 110 | } 111 | return null; 112 | } catch (error) { 113 | return null; 114 | } 115 | } 116 | 117 | export const checkReferralFeeSent = async ( 118 | total_fee_in_sol: number, 119 | username: string, 120 | // signature: string 121 | ) => { 122 | try { 123 | console.log("Calculate Referral Fee to wallet starts"); 124 | // const maxRetry = 30; 125 | // let retries = 0; 126 | // while (retries < maxRetry) { 127 | // await wait(2_000); 128 | // retries++; 129 | 130 | // const tx = await connection.getSignatureStatus(signature, { 131 | // searchTransactionHistory: false, 132 | // }); 133 | // if (tx?.value?.err) { 134 | // console.log("Transaction not confirmed: Failed"); 135 | // retries = maxRetry * 2; 136 | // break; 137 | // } 138 | // if (tx?.value?.confirmationStatus === "confirmed") { 139 | // retries = 0; 140 | // console.log("Transaction confirmed!!!"); 141 | // break; 142 | // } 143 | // } 144 | 145 | // if (retries == maxRetry * 2) return null; 146 | // if (retries > 0) return undefined; 147 | 148 | // connection.getSignatureStatus(signature, { 149 | // searchTransactionHistory: false, 150 | // }) 151 | let ref_info = await get_referral_info(username); 152 | if (!ref_info) return undefined; 153 | 154 | const { 155 | referral_option, 156 | referral_address 157 | } = ref_info; 158 | if (!referral_address) return undefined; 159 | 160 | const referralFeePercent = referral_option ?? 0; // 25% 161 | const referralFee = Number((total_fee_in_sol * referralFeePercent / 100).toFixed(0)); 162 | const referralWallet = new PublicKey(referral_address); 163 | 164 | if (referralFee > 0) { 165 | // If referral amount exist, you can store this data into the database 166 | // to calculate total revenue.. 167 | await ReferralHistoryControler.create({ 168 | username: username, 169 | uniquecode: ref_info.uniquecode, 170 | referrer_address: referralWallet, 171 | amount: referralFee 172 | }) 173 | console.log("Calculate Referral Fee to wallet ends"); 174 | } 175 | return true; 176 | } catch (e) { 177 | console.log("CheckReferralFeeSent Failed"); 178 | } 179 | } -------------------------------------------------------------------------------- /src/models/token.model.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import mongoose from "mongoose"; 4 | const Schema = mongoose.Schema; 5 | 6 | const AmmPoolKeysSchema = new Schema( 7 | { 8 | id: { 9 | type: String, 10 | default: "", 11 | }, 12 | baseMint: { 13 | type: String, 14 | default: "", 15 | }, 16 | quoteMint: { 17 | type: String, 18 | default: "", 19 | }, 20 | lpMint: { 21 | type: String, 22 | default: "", 23 | }, 24 | baseDecimals: { 25 | type: Number, 26 | default: 0, 27 | }, 28 | quoteDecimals: { 29 | type: Number, 30 | default: 0, 31 | }, 32 | lpDecimals: { 33 | type: Number, 34 | default: 0, 35 | }, 36 | version: { 37 | type: Number, 38 | default: 0, 39 | }, 40 | programId: { 41 | type: String, 42 | default: "", 43 | }, 44 | authority: { 45 | type: String, 46 | default: "", 47 | }, 48 | openOrders: { 49 | type: String, 50 | default: "", 51 | }, 52 | targetOrders: { 53 | type: String, 54 | default: "", 55 | }, 56 | baseVault: { 57 | type: String, 58 | default: "", 59 | }, 60 | quoteVault: { 61 | type: String, 62 | default: "", 63 | }, 64 | withdrawQueue: { 65 | type: String, 66 | default: "", 67 | }, 68 | lpVault: { 69 | type: String, 70 | default: "", 71 | }, 72 | marketVersion: { 73 | type: Number, 74 | default: 0, 75 | }, 76 | marketProgramId: { 77 | type: String, 78 | default: "", 79 | }, 80 | marketId: { 81 | type: String, 82 | default: "", 83 | }, 84 | marketAuthority: { 85 | type: String, 86 | default: "", 87 | }, 88 | marketBaseVault: { 89 | type: String, 90 | default: "", 91 | }, 92 | marketQuoteVault: { 93 | type: String, 94 | default: "", 95 | }, 96 | marketBids: { 97 | type: String, 98 | default: "", 99 | }, 100 | marketAsks: { 101 | type: String, 102 | default: "", 103 | }, 104 | marketEventQueue: { 105 | type: String, 106 | default: "", 107 | }, 108 | lookupTableAccount: { 109 | type: String, 110 | default: "", 111 | }, 112 | }, 113 | { _id: false } 114 | ); 115 | 116 | const ApiClmmConfigItemSchema = new Schema( 117 | { 118 | id: { 119 | type: String, 120 | default: "", 121 | }, 122 | index: { 123 | type: Number, 124 | default: 0, 125 | }, 126 | protocolFeeRate: { 127 | type: Number, 128 | default: 0, 129 | }, 130 | tradeFeeRate: { 131 | type: Number, 132 | default: 0, 133 | }, 134 | tickSpacing: { 135 | type: Number, 136 | default: 0, 137 | }, 138 | fundFeeRate: { 139 | type: Number, 140 | default: 0, 141 | }, 142 | fundOwner: { 143 | type: String, 144 | default: "", 145 | }, 146 | description: { 147 | type: String, 148 | default: "", 149 | }, 150 | }, 151 | { _id: false } 152 | ); 153 | 154 | const ApiClmmPoolsItemStatisticSchema = new Schema( 155 | { 156 | volume: { 157 | type: Number, 158 | default: 0, 159 | }, 160 | volumeFee: { 161 | type: Number, 162 | default: 0, 163 | }, 164 | feeA: { 165 | type: Number, 166 | default: 0, 167 | }, 168 | feeB: { 169 | type: Number, 170 | default: 0, 171 | }, 172 | feeApr: { 173 | type: Number, 174 | default: 0, 175 | }, 176 | rewardApr: { 177 | A: { 178 | type: Number, 179 | default: 0, 180 | }, 181 | B: { 182 | type: Number, 183 | default: 0, 184 | }, 185 | C: { 186 | type: Number, 187 | default: 0, 188 | }, 189 | }, 190 | apr: { 191 | type: Number, 192 | default: 0, 193 | }, 194 | priceMin: { 195 | type: Number, 196 | default: 0, 197 | }, 198 | priceMax: { 199 | type: Number, 200 | default: 0, 201 | }, 202 | }, 203 | { _id: false } 204 | ); 205 | 206 | const RewardInfoSchema = new Schema( 207 | { 208 | mint: { 209 | type: String, 210 | default: "", 211 | }, 212 | programId: { 213 | type: String, 214 | default: "", 215 | }, 216 | }, 217 | { _id: false } 218 | ); 219 | 220 | const clmmPoolKeys = new Schema({ 221 | id: { 222 | type: String, 223 | default: "", 224 | }, 225 | mintProgramIdA: { 226 | type: String, 227 | default: "", 228 | }, 229 | mintProgramIdB: { 230 | type: String, 231 | default: "", 232 | }, 233 | mintA: { 234 | type: String, 235 | default: "", 236 | }, 237 | mintB: { 238 | type: String, 239 | default: "", 240 | }, 241 | vaultA: { 242 | type: String, 243 | default: "", 244 | }, 245 | vaultB: { 246 | type: String, 247 | default: "", 248 | }, 249 | mintDecimalsA: { 250 | type: Number, 251 | default: 0, 252 | }, 253 | mintDecimalsB: { 254 | type: Number, 255 | default: 0, 256 | }, 257 | ammConfig: { 258 | type: ApiClmmConfigItemSchema, 259 | }, 260 | rewardInfos: { 261 | type: [RewardInfoSchema], 262 | default: [], 263 | }, 264 | tvl: { 265 | type: Number, 266 | default: 0, 267 | }, 268 | day: { 269 | type: ApiClmmPoolsItemStatisticSchema, 270 | }, 271 | week: { 272 | type: ApiClmmPoolsItemStatisticSchema, 273 | }, 274 | month: { 275 | type: ApiClmmPoolsItemStatisticSchema, 276 | }, 277 | lookupTableAccount: { 278 | type: String, 279 | default: "", 280 | }, 281 | }); 282 | 283 | // Token Schema 284 | const Token = new Schema( 285 | { 286 | name: { 287 | type: String, 288 | default: "", 289 | }, 290 | symbol: { 291 | type: String, 292 | default: "", 293 | }, 294 | mint: { 295 | type: String, 296 | default: "", 297 | required: true, 298 | }, 299 | isAmm: { 300 | type: Boolean, 301 | default: false, 302 | required: true, 303 | }, 304 | poolId: { 305 | type: String, 306 | default: "", 307 | required: true, 308 | unique: true, 309 | }, 310 | creation_ts: { 311 | type: Number, 312 | required: true, 313 | }, 314 | ammKeys: { 315 | type: AmmPoolKeysSchema, 316 | }, 317 | clmmKeys: { 318 | type: clmmPoolKeys, 319 | }, 320 | }, 321 | { 322 | timestamps: true, // This option adds createdAt and updatedAt fields 323 | } 324 | ); 325 | 326 | // Create compound index for username, wallet_address, and nonce 327 | export default mongoose.model("token", Token); 328 | -------------------------------------------------------------------------------- /src/screens/welcome.screen.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { UserService } from "../services/user.service"; 3 | import { Keypair } from "@solana/web3.js"; 4 | import bs58 from "bs58"; 5 | import { GrowTradeVersion } from "../config"; 6 | import { copytoclipboard } from "../utils"; 7 | import { TokenService } from "../services/token.metadata"; 8 | import { contractInfoScreenHandler } from "./contract.info.screen"; 9 | 10 | const MAX_RETRIES = 5; 11 | export const welcomeKeyboardList = [ 12 | // [{ text: '🏦 Buy/Sell', command: 'buysell' }], 13 | // snipe_token, my_position 14 | [ 15 | { text: "🎯 Sniper [Soon]", command: "dummy_button" }, 16 | { text: "📊 Positions", command: "position" }, 17 | ], // position 18 | // [{ text: '♻️ Withdraw', command: 'transfer_funds' }], 19 | [{ text: "Burn: Off ♨️", command: `burn_switch` }], 20 | [ 21 | { text: "⛓ Bridge", command: "bridge" }, 22 | { text: "🛠 Settings & Tools", command: "settings" }, 23 | ], 24 | [{ text: "🎁 Referral Program", command: "referral" }], 25 | [{ text: "❌ Close", command: "dismiss_message" }], 26 | ]; 27 | 28 | export const WelcomeScreenHandler = async ( 29 | bot: TelegramBot, 30 | msg: TelegramBot.Message 31 | ) => { 32 | try { 33 | const { username, id: chat_id, first_name, last_name } = msg.chat; 34 | // check if bot 35 | if (!username) { 36 | bot.sendMessage( 37 | chat_id, 38 | "⚠️ You have no telegram username. Please take at least one and try it again." 39 | ); 40 | return; 41 | } 42 | const user = await UserService.findOne({ username }); 43 | // if new user, create one 44 | if (!user) { 45 | const res = await newUserHandler(bot, msg); 46 | if (!res) return; 47 | } 48 | // send welcome guide 49 | await welcomeGuideHandler(bot, msg); 50 | // await bot.deleteMessage(chat_id, msg.message_id); 51 | } catch (error) { 52 | console.log("-WelcomeScreenHandler-", error); 53 | } 54 | }; 55 | 56 | const newUserHandler = async (bot: TelegramBot, msg: TelegramBot.Message) => { 57 | const { username, id: chat_id, first_name, last_name } = msg.chat; 58 | 59 | let retries = 0; 60 | let userdata: any = null; 61 | let private_key = ""; 62 | let wallet_address = ""; 63 | 64 | // find unique private_key 65 | do { 66 | const keypair = Keypair.generate(); 67 | private_key = bs58.encode(keypair.secretKey); 68 | wallet_address = keypair.publicKey.toString(); 69 | 70 | const wallet = await UserService.findOne({ wallet_address }); 71 | if (!wallet) { 72 | // add 73 | const newUser = { 74 | chat_id, 75 | username, 76 | first_name, 77 | last_name, 78 | wallet_address, 79 | private_key, 80 | }; 81 | userdata = await UserService.create(newUser); // true; // 82 | } else { 83 | retries++; 84 | } 85 | } while (retries < MAX_RETRIES && !userdata); 86 | 87 | // impossible to create 88 | if (!userdata) { 89 | await bot.sendMessage( 90 | chat_id, 91 | "Sorry, we cannot create your account. Please contact support team" 92 | ); 93 | return false; 94 | } 95 | 96 | // send private key & wallet address 97 | const caption = 98 | `👋 Welcome to GrowTradeBot!\n\n` + 99 | `A new wallet has been generated for you. This is your wallet address\n\n` + 100 | `${wallet_address}\n\n` + 101 | `Save this private key below❗\n\n` + 102 | `${private_key}\n\n` + 103 | `To get started, please read our docs`; 104 | 105 | await bot.sendMessage(chat_id, caption, { 106 | parse_mode: "HTML", 107 | disable_web_page_preview: true, 108 | reply_markup: { 109 | inline_keyboard: [ 110 | [ 111 | { 112 | text: "* Dismiss message", 113 | callback_data: JSON.stringify({ 114 | command: "dismiss_message", 115 | }), 116 | }, 117 | ], 118 | ], 119 | }, 120 | }); 121 | return true; 122 | }; 123 | 124 | export const welcomeGuideHandler = async ( 125 | bot: TelegramBot, 126 | msg: TelegramBot.Message, 127 | replaceId?: number 128 | ) => { 129 | const { id: chat_id, username } = msg.chat; 130 | const user = await UserService.findOne({ username }); 131 | 132 | if (!user) return; 133 | const solbalance = await TokenService.getSOLBalance(user.wallet_address); 134 | const caption = 135 | `Welcome to GrowTrade | Beta Version\n\n` + 136 | `The Unique Solana Trading Bot. Snipe, trade and keep track of your positions with GrowTrade.\n\n` + 137 | `⬩ A never seen unique Burn Mechanism 🔥\n` + 138 | `⬩ Revenue Share through Buybacks on GrowSol ($GRW)\n\n` + 139 | `💳 My Wallet:\n${copytoclipboard(user.wallet_address)}\n\n` + 140 | `💳 Balance: ${solbalance} SOL\n\n` + 141 | `View on Explorer\n\n` + 142 | `Part of GrowSol's Ecosystem\n\n` + 143 | // `-----------------------\n` + 144 | // `📖 Docs\n` + 145 | // `🌍 Website\n\n` + 146 | `Paste a contract address to trigger the Buy/Sell Menu or pick an option to get started.`; 147 | 148 | // const textEventHandler = async (msg: TelegramBot.Message) => { 149 | // const receivedChatId = msg.chat.id; 150 | // const receivedText = msg.text; 151 | // const receivedMessageId = msg.message_id; 152 | // const receivedTextSender = msg.chat.username; 153 | // // Check if the received message ID matches the original message ID 154 | // if (receivedText && receivedChatId === chat_id) { 155 | // // message should be same user 156 | // if (receivedTextSender === username) { 157 | // await contractInfoScreenHandler(bot, msg, receivedText, 'switch_sell'); 158 | // } 159 | // setTimeout(() => { bot.deleteMessage(receivedChatId, receivedMessageId) }, 2000) 160 | // } 161 | // console.log("Removed"); 162 | // bot.removeListener('text', textEventHandler); 163 | // } 164 | 165 | // // Add the 'text' event listener 166 | // bot.on('text', textEventHandler); 167 | 168 | const burn_fee = user.burn_fee; 169 | const reply_markup = { 170 | inline_keyboard: welcomeKeyboardList.map((rowItem) => 171 | rowItem.map((item) => { 172 | if (item.command.includes("bridge")) { 173 | return { 174 | text: item.text, 175 | url: "https://t.me/growbridge_bot", 176 | }; 177 | } 178 | if (item.text.includes("Burn")) { 179 | const burnText = `${burn_fee ? "Burn: On 🔥" : "Burn: Off ♨️"}`; 180 | return { 181 | text: burnText, 182 | callback_data: JSON.stringify({ 183 | command: item.command, 184 | }), 185 | }; 186 | } 187 | return { 188 | text: item.text, 189 | callback_data: JSON.stringify({ 190 | command: item.command, 191 | }), 192 | }; 193 | }) 194 | ), 195 | }; 196 | 197 | if (replaceId) { 198 | bot.editMessageText(caption, { 199 | message_id: replaceId, 200 | chat_id, 201 | parse_mode: "HTML", 202 | disable_web_page_preview: true, 203 | reply_markup, 204 | }); 205 | } else { 206 | await bot.sendMessage(chat_id, caption, { 207 | parse_mode: "HTML", 208 | disable_web_page_preview: true, 209 | reply_markup, 210 | }); 211 | } 212 | }; 213 | -------------------------------------------------------------------------------- /src/screens/position.screen.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot, { InlineKeyboardButton } from "node-telegram-bot-api"; 2 | import { TokenService } from "../services/token.metadata"; 3 | import { copytoclipboard } from "../utils"; 4 | import { UserService } from "../services/user.service"; 5 | import { sendUsernameRequiredNotification } from "./common.screen"; 6 | import { GrowTradeVersion, PNL_SHOW_THRESHOLD_USD } from "../config"; 7 | import { PositionService } from "../services/position.service"; 8 | import { JupiterService } from "../services/jupiter.service"; 9 | import { NATIVE_MINT } from "@solana/spl-token"; 10 | import { PNLService } from "../services/pnl.service"; 11 | 12 | export const positionScreenHandler = async ( 13 | bot: TelegramBot, 14 | msg: TelegramBot.Message, 15 | replaceId?: number 16 | ) => { 17 | try { 18 | const { chat } = msg; 19 | const { id: chat_id, username } = chat; 20 | if (!username) { 21 | await sendUsernameRequiredNotification(bot, msg); 22 | return; 23 | } 24 | 25 | const user = await UserService.findOne({ username }); 26 | if (!user) return; 27 | 28 | const temp = 29 | `GrowTrade ${GrowTradeVersion}\n💳 Your wallet address\n` + 30 | `${copytoclipboard(user.wallet_address)}\n\n` + 31 | `Loading...\n`; 32 | 33 | const reply_markup = { 34 | inline_keyboard: [ 35 | [ 36 | { 37 | text: "❌ Close", 38 | callback_data: JSON.stringify({ 39 | command: "dismiss_message", 40 | }), 41 | }, 42 | ], 43 | ], 44 | }; 45 | 46 | let replaceIdtemp = replaceId; 47 | if (replaceId) { 48 | await bot.editMessageText(temp, { 49 | message_id: replaceId, 50 | chat_id, 51 | parse_mode: "HTML", 52 | disable_web_page_preview: true, 53 | reply_markup, 54 | }); 55 | } else { 56 | const sentMessage = await bot.sendMessage(chat_id, temp, { 57 | parse_mode: "HTML", 58 | disable_web_page_preview: true, 59 | reply_markup, 60 | }); 61 | replaceIdtemp = sentMessage.message_id; 62 | } 63 | 64 | const tokenaccounts = await TokenService.getTokenAccounts( 65 | user.wallet_address 66 | ); 67 | // const solprice = await TokenService.getSOLPrice(); 68 | const solbalance = await TokenService.getSOLBalance(user.wallet_address); 69 | 70 | let caption = 71 | `GrowTrade ${GrowTradeVersion}\n💳 Your wallet address\n` + 72 | `${copytoclipboard(user.wallet_address)}\n\n` + 73 | `💳 Balance: ${solbalance} SOL\n\n` + 74 | `Please choose a token to buy/sell.\n`; 75 | 76 | // Initialize the transferInlineKeyboards array with an empty array 77 | const transferInlineKeyboards: InlineKeyboardButton[][] = []; 78 | // const positions = await PositionService.find({ wallet_address: user.wallet_address }); 79 | let idx = 0; 80 | let discount = 0; 81 | for (const item of tokenaccounts) { 82 | const { mint: mintAddress, amount: tokenBalance, symbol } = item; 83 | if (symbol === "SOL" || tokenBalance < 0.000005) { 84 | discount -= 1; 85 | continue; 86 | } 87 | // if (price && price * tokenBalance < 1) { 88 | // discount -= 1; 89 | // continue; 90 | // } 91 | caption += `\n- Token: ${symbol}\nAmount: ${tokenBalance}\n`; 92 | // const position = positions.filter(ps => ps.mint === mintAddress); 93 | // const splvalue = tokenBalance * price; 94 | 95 | // If value is over 5$. 96 | // const jupiterService = new JupiterService(); 97 | // const quote = splvalue > PNL_SHOW_THRESHOLD_USD ? await jupiterService.getQuote( 98 | // mintAddress, 99 | // NATIVE_MINT.toString(), 100 | // tokenBalance, 101 | // decimals, 102 | // 9 103 | // ) : null; 104 | // if (quote) { 105 | // const { wallet_address } = user; 106 | // const pnlService = new PNLService( 107 | // wallet_address, 108 | // mintAddress, 109 | // quote 110 | // ) 111 | // await pnlService.initialize(); 112 | // const pnldata = await pnlService.getPNLInfo(); 113 | // if (pnldata) { 114 | // const { profitInSOL, percent } = pnldata; 115 | // const profitInUSD = profitInSOL * Number(solprice); 116 | // if (profitInSOL < 0) { 117 | // caption += `PNL: ${percent.toFixed(3)}% [${profitInSOL.toFixed(3)} Sol | ${profitInUSD.toFixed(2)}$] 🟥\n` 118 | // } else { 119 | // caption += `PNL: +${percent.toFixed(3)}% [${profitInSOL.toFixed(3)} Sol | ${profitInUSD.toFixed(2)}$] 🟩\n` 120 | // } 121 | // } 122 | // } else { 123 | // caption += `PNL: 0%\n` 124 | // } 125 | // if (sol_amount > 0) { 126 | // let pnl = (price / solprice * tokenBalance * 100) / sol_amount; 127 | // if (transferFeeEnable && transferFeeData) { 128 | // const feerate = 1 - transferFeeData.newer_transfer_fee.transfer_fee_basis_points / 10000.0; 129 | // pnl *= feerate; 130 | // } 131 | // if (pnl >= 100) { 132 | // let pnl_sol = ((pnl - 100) * sol_amount / 100).toFixed(4); 133 | // let pnl_dollar = ((pnl - 100) * sol_amount * solprice / 100).toFixed(2) 134 | // caption += `PNL: +${(pnl - 100).toFixed(2)}% [${pnl_sol} Sol | +${pnl_dollar}$] 🟩\n\n` 135 | // } else { 136 | // let pnl_sol = ((100 - pnl) * sol_amount / 100).toFixed(4); 137 | // let pnl_dollar = ((100 - pnl) * sol_amount * solprice / 100).toFixed(2) 138 | // caption += `PNL: -${(100 - pnl).toFixed(2)}% [${pnl_sol} Sol | -${pnl_dollar}$] 🟥\n\n` 139 | // } 140 | // } 141 | caption += `${copytoclipboard(mintAddress)}\n`; 142 | // Check if the current nested array exists 143 | if (!transferInlineKeyboards[Math.floor(idx / 3)]) { 144 | transferInlineKeyboards.push([]); 145 | } 146 | 147 | // Push the new inline keyboard button to the appropriate nested array 148 | transferInlineKeyboards[Math.floor(idx / 3)].push({ 149 | text: `${symbol ? symbol : mintAddress}`, 150 | callback_data: JSON.stringify({ 151 | command: `SPS_${mintAddress}`, 152 | }), 153 | }); 154 | 155 | idx++; 156 | } 157 | 158 | if (tokenaccounts.length + discount <= 0) { 159 | transferInlineKeyboards.push([]); 160 | caption += `\nYou don't hold any tokens in this wallet`; 161 | } 162 | transferInlineKeyboards.push([]); 163 | transferInlineKeyboards[ 164 | Math.ceil((tokenaccounts.length + discount) / 3) 165 | ].push( 166 | ...[ 167 | { 168 | text: "🔄 Refresh", 169 | callback_data: JSON.stringify({ 170 | command: "pos_ref", 171 | }), 172 | }, 173 | { 174 | text: "❌ Close", 175 | callback_data: JSON.stringify({ 176 | command: "dismiss_message", 177 | }), 178 | }, 179 | ] 180 | ); 181 | 182 | const new_reply_markup = { 183 | inline_keyboard: transferInlineKeyboards, 184 | }; 185 | const sentmessage = await bot.editMessageText(caption, { 186 | message_id: replaceIdtemp, 187 | chat_id, 188 | parse_mode: "HTML", 189 | disable_web_page_preview: true, 190 | reply_markup: new_reply_markup, 191 | }); 192 | } catch (e) { 193 | console.log("~ positionScreenHandler~", e); 194 | } 195 | }; 196 | -------------------------------------------------------------------------------- /src/services/alert.bot.module.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import redisClient from "./redis"; 3 | import { 4 | ALERT_GB_IMAGE, 5 | ALERT_GT_IMAGE, 6 | AlertBotID, 7 | BridgeBotID, 8 | TradeBotID, 9 | } from "../bot.opts"; 10 | // import { ReferralChannelController } from "../controllers/referral.channel"; 11 | import { 12 | getReferralList, 13 | get_referrer_info, 14 | update_channel_id, 15 | } from "./referral.service"; 16 | import { ReferralIdenticalType } from "../main"; 17 | import { 18 | ReferralChannelService, 19 | ReferralPlatform, 20 | } from "./referral.channel.service"; 21 | 22 | type ReferralChannel = { 23 | chat_id: string; // channel id 24 | channel_name: string; 25 | }; 26 | export type ReferralData = { 27 | channels: ReferralChannel[]; 28 | referral_code: string; 29 | creator: string; 30 | platform: ReferralPlatform; // TradeBot, BridgeBot 31 | schedule: string; 32 | }; 33 | 34 | export const alertbotModule = async (bot: TelegramBot) => { 35 | try { 36 | const referrals = await getReferralList(); 37 | 38 | if (!referrals) return; 39 | for (const referral of referrals) { 40 | processReferral(referral, bot); 41 | } 42 | } catch (e) { 43 | console.log("alertbotModule", e); 44 | } 45 | }; 46 | 47 | const processReferral = async (referral: ReferralData, bot: TelegramBot) => { 48 | try { 49 | const { creator, referral_code, channels, platform, schedule } = referral; 50 | const scheduleInseconds = parseInt(schedule) * 60; 51 | const isValid = await validateSchedule(referral_code, scheduleInseconds); 52 | if (!isValid) return; 53 | 54 | const isTradeBot = Number(platform) === ReferralPlatform.TradeBot; 55 | for (let idx = 0; idx < channels.length; idx++) { 56 | const { chat_id } = channels[idx]; 57 | 58 | sendAlert(bot, chat_id, referral_code, creator, idx, isTradeBot); 59 | } 60 | } catch (e) { 61 | console.log("processReferral", e); 62 | } 63 | }; 64 | 65 | const sendAlert = async ( 66 | bot: TelegramBot, 67 | channelChatId: string, 68 | referral_code: string, 69 | creator: string, 70 | idx: number, 71 | isTradeBot: boolean 72 | ) => { 73 | try { 74 | if (!channelChatId || channelChatId === "") return; 75 | await bot.getChat(channelChatId); 76 | 77 | const botId = isTradeBot ? TradeBotID : BridgeBotID; 78 | const botImg = isTradeBot ? ALERT_GT_IMAGE : ALERT_GB_IMAGE; 79 | const txt = isTradeBot ? "Try GrowTrade Now" : "Try GrowBridge Now"; 80 | const referralLink = `https://t.me/${botId}?start=${referral_code}`; 81 | 82 | const inline_keyboard = [ 83 | [ 84 | { 85 | text: txt, 86 | url: referralLink, 87 | }, 88 | ], 89 | ]; 90 | if (isTradeBot) { 91 | inline_keyboard.push([ 92 | { 93 | text: "Trade with us 📈", 94 | url: "https://t.me/GrowTradeOfficial", 95 | }, 96 | ]); 97 | } 98 | 99 | bot.sendPhoto(channelChatId, botImg, { 100 | caption: "", 101 | reply_markup: { 102 | inline_keyboard, 103 | }, 104 | parse_mode: "HTML", 105 | }); 106 | } catch (error) { 107 | console.log("sendAlert Error:", channelChatId, referral_code); 108 | await handleError(error, creator, idx, channelChatId); 109 | } 110 | }; 111 | const handleError = async ( 112 | error: any, 113 | creator: string, 114 | idx: number, 115 | channelChatId: string 116 | ) => { 117 | try { 118 | const errMsg = error.response.body.description; 119 | if (errMsg.includes("chat not found")) { 120 | const lastNum: string | null = await redisClient.get(channelChatId); 121 | if (!lastNum) { 122 | await redisClient.set(channelChatId, "0"); 123 | return; 124 | } 125 | const retryCounter = parseInt(lastNum) + 1; 126 | if (retryCounter <= 3) { 127 | await redisClient.set(channelChatId, retryCounter.toFixed(0)); 128 | return; 129 | } 130 | await redisClient.del(channelChatId); 131 | 132 | const res = await update_channel_id(creator, idx, "delete"); 133 | if (!res) { 134 | console.log("ServerError: cannot remove channel", creator, idx); 135 | } 136 | } 137 | } catch (e) { 138 | return; 139 | } 140 | }; 141 | const validateSchedule = async (referral_code: string, schedule: number) => { 142 | try { 143 | const last_ts: string | null = await redisClient.get(referral_code); 144 | const timestamp = Date.now() / 1000; 145 | if (!last_ts) { 146 | await redisClient.set(referral_code, timestamp.toFixed(0)); 147 | return true; 148 | } 149 | const last_timestamp = Number(last_ts); 150 | if (timestamp - last_timestamp > schedule) { 151 | await redisClient.set(referral_code, timestamp.toFixed(0)); 152 | return true; 153 | } 154 | return false; 155 | } catch (e) { 156 | console.log("validateSchedule", e); 157 | return false; 158 | } 159 | }; 160 | 161 | export const newReferralChannelHandler = async (msg: TelegramBot.Message) => { 162 | try { 163 | const { chat, from, new_chat_members } = msg; 164 | if (from && new_chat_members && from.username) { 165 | // if bot added me, return 166 | if (from.is_bot) return; 167 | // if me is bot??, 168 | const alertbotInfo = new_chat_members.find( 169 | (member) => member.username === AlertBotID 170 | ); 171 | if (!alertbotInfo) return; 172 | 173 | const creator = from.username; 174 | // const refdata = await get_referrer_info(creator); 175 | // if (!refdata) return; 176 | // const referral_code = refdata.referral_code; 177 | const chat_id = chat.id; 178 | const channel_name = chat.title ?? ""; 179 | 180 | return { 181 | chatId: chat_id.toString(), 182 | referrer: creator, 183 | channelName: channel_name, 184 | messageId: msg.message_id.toString(), 185 | } as ReferralIdenticalType; 186 | // await ReferralChannelController.create( 187 | // creator, 188 | // channel_name, 189 | // chat_id.toString(), 190 | // referral_code 191 | // ) 192 | } 193 | return null; 194 | } catch (e) { 195 | console.log("newReferralChannelHandler", e); 196 | return null; 197 | } 198 | }; 199 | 200 | export const removeReferralChannelHandler = async ( 201 | msg: TelegramBot.Message 202 | ) => { 203 | try { 204 | const { chat, from, left_chat_member } = msg; 205 | if (from && left_chat_member && from.username) { 206 | // if bot added me, return 207 | if (from.is_bot) return; 208 | 209 | // if me is bot??, 210 | const alertbotInfo = left_chat_member.username === AlertBotID; 211 | if (!alertbotInfo) return; 212 | 213 | const creator = from.username; 214 | // const refdata = await get_referrer_info(creator); 215 | // if (!refdata) return; 216 | // const referral_code = refdata; 217 | const chat_id = chat.id; 218 | 219 | const referralChannelService = new ReferralChannelService(); 220 | await referralChannelService.deleteReferralChannel({ 221 | creator, 222 | // platform: ReferralPlatform.TradeBot, 223 | chat_id: chat_id.toString(), 224 | channel_name: chat.title, 225 | }); 226 | // await ReferralChannelController.deleteOne({ 227 | // chat_id, 228 | // referral_code 229 | // }) 230 | } 231 | } catch (e) { 232 | console.log("newReferralChannelHandler", e); 233 | } 234 | }; 235 | 236 | export const sendAlertForOurChannel = async (alertBot: TelegramBot) => { 237 | try { 238 | const chat_id = "-1002138253167"; 239 | await alertBot.getChat(chat_id); 240 | await alertBot.sendPhoto(chat_id, ALERT_GT_IMAGE, { 241 | caption: "", 242 | reply_markup: { 243 | inline_keyboard: [ 244 | [ 245 | { 246 | text: "Try GrowBridge now!", 247 | url: `https://t.me/${TradeBotID}`, 248 | }, 249 | ], 250 | // [{ 251 | // text: 'Start Trading With GrowTrade', 252 | // url: "https://t.me/growtradeapp_bot" 253 | // }], 254 | ], 255 | }, 256 | parse_mode: "HTML", 257 | }); 258 | } catch (e) { 259 | console.log("Channel Error", e); 260 | } 261 | }; 262 | -------------------------------------------------------------------------------- /src/controllers/callback.handler.ts: -------------------------------------------------------------------------------- 1 | import TelegramBot from "node-telegram-bot-api"; 2 | import { 3 | contractInfoScreenHandler, 4 | refreshHandler, 5 | } from "../screens/contract.info.screen"; 6 | import { GasFeeEnum } from "../services/user.trade.setting.service"; 7 | import { 8 | buyCustomAmountScreenHandler, 9 | buyHandler, 10 | sellCustomAmountScreenHandler, 11 | sellHandler, 12 | setSlippageScreenHandler, 13 | } from "../screens/trade.screen"; 14 | import { 15 | cancelWithdrawHandler, 16 | transferFundScreenHandler, 17 | withdrawButtonHandler, 18 | withdrawCustomAmountScreenHandler, 19 | withdrawHandler, 20 | } from "../screens/transfer.funds"; 21 | import { 22 | WelcomeScreenHandler, 23 | welcomeGuideHandler, 24 | } from "../screens/welcome.screen"; 25 | import { 26 | autoBuyAmountScreenHandler, 27 | changeGasFeeHandler, 28 | changeJitoTipFeeHandler, 29 | generateNewWalletHandler, 30 | pnlCardHandler, 31 | presetBuyAmountScreenHandler, 32 | presetBuyBtnHandler, 33 | revealWalletPrivatekyHandler, 34 | setCustomAutoBuyAmountHandler, 35 | setCustomFeeScreenHandler, 36 | setCustomJitoFeeScreenHandler, 37 | settingScreenHandler, 38 | switchAutoBuyOptsHandler, 39 | switchBurnOptsHandler, 40 | switchWalletHandler, 41 | walletViewHandler, 42 | } from "../screens/settings.screen"; 43 | import { positionScreenHandler } from "../screens/position.screen"; 44 | import { OpenReferralWindowHandler } from "../screens/referral.link.handler"; 45 | import { 46 | openAlertBotDashboard, 47 | sendMsgForAlertScheduleHandler, 48 | updateSchedule, 49 | } from "../screens/bot.dashboard"; 50 | import { 51 | backToReferralHomeScreenHandler, 52 | refreshPayoutHandler, 53 | sendPayoutAddressManageScreen, 54 | setSOLPayoutAddressHandler, 55 | } from "../screens/payout.screen"; 56 | 57 | export const callbackQueryHandler = async ( 58 | bot: TelegramBot, 59 | callbackQuery: TelegramBot.CallbackQuery 60 | ) => { 61 | try { 62 | const { data: callbackData, message: callbackMessage } = callbackQuery; 63 | if (!callbackData || !callbackMessage) return; 64 | 65 | const data = JSON.parse(callbackData); 66 | const opts = { 67 | chat_id: callbackMessage.chat.id, 68 | message_id: callbackMessage.message_id, 69 | }; 70 | if (data.command.includes("dismiss_message")) { 71 | bot.deleteMessage(opts.chat_id, opts.message_id); 72 | return; 73 | } 74 | 75 | if (data.command.includes("cancel_withdraw")) { 76 | await cancelWithdrawHandler(bot, callbackMessage); 77 | return; 78 | } 79 | 80 | if (data.command.includes("dummy_button")) { 81 | return; 82 | } 83 | 84 | if (data.command.includes("position")) { 85 | // const replaceId = callbackMessage.message_id; 86 | await positionScreenHandler(bot, callbackMessage); 87 | return; 88 | } 89 | 90 | if (data.command.includes("burn_switch")) { 91 | await switchBurnOptsHandler(bot, callbackMessage); 92 | return; 93 | } 94 | 95 | if (data.command.includes("autobuy_switch")) { 96 | await switchAutoBuyOptsHandler(bot, callbackMessage); 97 | return; 98 | } 99 | 100 | if (data.command.includes("autobuy_amount")) { 101 | const replaceId = callbackMessage.message_id; 102 | await autoBuyAmountScreenHandler(bot, callbackMessage, replaceId); 103 | return; 104 | } 105 | 106 | if (data.command === "pos_ref") { 107 | const replaceId = callbackMessage.message_id; 108 | await positionScreenHandler(bot, callbackMessage, replaceId); 109 | return; 110 | } 111 | 112 | // click on mint symbol from position 113 | if (data.command.includes("SPS_")) { 114 | const mint = data.command.slice(4); 115 | await contractInfoScreenHandler( 116 | bot, 117 | callbackMessage, 118 | mint, 119 | "switch_buy", 120 | true 121 | ); 122 | return; 123 | } 124 | 125 | if (data.command.includes("BS_")) { 126 | const mint = data.command.slice(3); 127 | await contractInfoScreenHandler(bot, callbackMessage, mint, "switch_buy"); 128 | return; 129 | } 130 | 131 | if (data.command.includes("SS_")) { 132 | const mint = data.command.slice(3); 133 | await contractInfoScreenHandler( 134 | bot, 135 | callbackMessage, 136 | mint, 137 | "switch_sell" 138 | ); 139 | return; 140 | } 141 | 142 | if (data.command.includes("transfer_funds")) { 143 | const replaceId = callbackMessage.message_id; 144 | await transferFundScreenHandler(bot, callbackMessage, replaceId); 145 | return; 146 | } 147 | 148 | if (data.command.includes("TF_")) { 149 | const mint = data.command.slice(3); 150 | await withdrawButtonHandler(bot, callbackMessage, mint); 151 | return; 152 | } 153 | 154 | if (data.command.includes("withdrawtoken_custom")) { 155 | await withdrawCustomAmountScreenHandler(bot, callbackMessage); 156 | return; 157 | } 158 | 159 | const withdrawstr = "withdraw_"; 160 | if (data.command.includes(withdrawstr)) { 161 | const percent = data.command.slice(withdrawstr.length); 162 | await withdrawHandler(bot, callbackMessage, percent); 163 | return; 164 | } 165 | 166 | if (data.command.includes("settings")) { 167 | console.log("Settings"); 168 | const replaceId = callbackMessage.message_id; 169 | await settingScreenHandler(bot, callbackMessage, replaceId); 170 | } 171 | 172 | if (data.command.includes("generate_wallet")) { 173 | await generateNewWalletHandler(bot, callbackMessage); 174 | } 175 | 176 | const pkstr = "revealpk_"; 177 | if (data.command.includes(pkstr)) { 178 | const nonce = Number(data.command.slice(pkstr.length)); 179 | await revealWalletPrivatekyHandler(bot, callbackMessage, nonce); 180 | } 181 | 182 | const usewalletstr = "usewallet_"; 183 | if (data.command.includes(usewalletstr)) { 184 | const nonce = Number(data.command.slice(usewalletstr.length)); 185 | await switchWalletHandler(bot, callbackMessage, nonce); 186 | } 187 | 188 | if (data.command.includes("back_home")) { 189 | const replaceId = callbackMessage.message_id; 190 | await welcomeGuideHandler(bot, callbackMessage, replaceId); 191 | } 192 | 193 | if (data.command === "switch_gas") { 194 | await changeGasFeeHandler(bot, callbackMessage, GasFeeEnum.LOW); 195 | return; 196 | } 197 | if (data.command === "custom_gas") { 198 | await setCustomFeeScreenHandler(bot, callbackMessage); 199 | return; 200 | } 201 | 202 | if (data.command === "switch_mev") { 203 | await changeJitoTipFeeHandler(bot, callbackMessage); 204 | return; 205 | } 206 | 207 | if (data.command === "custom_jitofee") { 208 | await setCustomJitoFeeScreenHandler(bot, callbackMessage); 209 | return; 210 | } 211 | 212 | const buyTokenStr = "buytoken_"; 213 | if (data.command.includes(buyTokenStr)) { 214 | const buyAmount = Number(data.command.slice(buyTokenStr.length)); 215 | await buyHandler(bot, callbackMessage, buyAmount); 216 | return; 217 | } 218 | const sellTokenStr = "selltoken_"; 219 | if (data.command.includes(sellTokenStr)) { 220 | const sellPercent = Number(data.command.slice(sellTokenStr.length)); 221 | await sellHandler(bot, callbackMessage, sellPercent); 222 | return; 223 | } 224 | if (data.command.includes("buy_custom")) { 225 | await buyCustomAmountScreenHandler(bot, callbackMessage); 226 | return; 227 | } 228 | if (data.command.includes("sell_custom")) { 229 | await sellCustomAmountScreenHandler(bot, callbackMessage); 230 | return; 231 | } 232 | if (data.command.includes("set_slippage")) { 233 | await setSlippageScreenHandler(bot, callbackMessage); 234 | return; 235 | } 236 | if (data.command.includes("preset_setting")) { 237 | await presetBuyBtnHandler(bot, callbackMessage); 238 | return; 239 | } 240 | if (data.command.includes("wallet_view")) { 241 | await walletViewHandler(bot, callbackMessage); 242 | return; 243 | } 244 | if (data.command === "referral") { 245 | await OpenReferralWindowHandler(bot, callbackMessage); 246 | } 247 | // Open payout dashboard 248 | if (data.command === "payout_address") { 249 | await sendPayoutAddressManageScreen( 250 | bot, 251 | callbackMessage.chat, 252 | callbackMessage.message_id 253 | ); 254 | } 255 | // Update SOL address 256 | if (data.command === "set_sol_address") { 257 | await setSOLPayoutAddressHandler(bot, callbackMessage.chat); 258 | } else if (data.command === "refresh_payout") { 259 | await refreshPayoutHandler(bot, callbackMessage); 260 | } 261 | // Alert Bot 262 | if (data.command === "alert_bot" || data.command === "refresh_alert_bot") { 263 | bot.deleteMessage(callbackMessage.chat.id, callbackMessage.message_id); 264 | await openAlertBotDashboard(bot, callbackMessage.chat); 265 | } 266 | // Schedule 267 | else if (data.command.includes("alert_schedule")) { 268 | await sendMsgForAlertScheduleHandler(bot, callbackMessage.chat); 269 | } 270 | // Back home 271 | else if (data.command === "back_from_ref") { 272 | await backToReferralHomeScreenHandler( 273 | bot, 274 | callbackMessage.chat, 275 | callbackMessage 276 | ); 277 | } else if (data.command.includes("schedule_time_")) { 278 | const scheduleTime = data.command.slice(14); 279 | 280 | await updateSchedule(bot, callbackMessage.chat, scheduleTime); 281 | } 282 | const presetBuyStr = "preset_buy_"; 283 | if (data.command.includes(presetBuyStr)) { 284 | const preset_index = parseInt(data.command.slice(presetBuyStr.length)); 285 | await presetBuyAmountScreenHandler(bot, callbackMessage, preset_index); 286 | return; 287 | } 288 | if (data.command === "refresh") { 289 | await refreshHandler(bot, callbackMessage); 290 | return; 291 | } 292 | 293 | if (data.command === "pnl_card") { 294 | await pnlCardHandler(bot, callbackMessage); 295 | return; 296 | } 297 | } catch (e) { 298 | console.log(e); 299 | } 300 | }; 301 | -------------------------------------------------------------------------------- /src/pump/swap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComputeBudgetProgram, 3 | Connection, 4 | Keypair, 5 | LAMPORTS_PER_SOL, 6 | PublicKey, 7 | SystemProgram, 8 | Transaction, 9 | TransactionInstruction, 10 | TransactionMessage, 11 | VersionedTransaction, 12 | clusterApiUrl, 13 | } from "@solana/web3.js"; 14 | import { 15 | getAssociatedTokenAddress, 16 | createAssociatedTokenAccountInstruction, 17 | TOKEN_PROGRAM_ID, 18 | createAssociatedTokenAccountIdempotentInstruction, 19 | createSyncNativeInstruction, 20 | NATIVE_MINT, 21 | getAssociatedTokenAddressSync, 22 | createCloseAccountInstruction, 23 | } from "@solana/spl-token"; 24 | import { 25 | getKeyPairFromPrivateKey, 26 | createTransaction, 27 | sendAndConfirmTransactionWrapper, 28 | bufferFromUInt64, 29 | } from "./utils"; 30 | import { getCoinData } from "./api"; 31 | import { 32 | GLOBAL, 33 | FEE_RECIPIENT, 34 | SYSTEM_PROGRAM_ID, 35 | RENT, 36 | PUMP_FUN_ACCOUNT, 37 | PUMP_FUN_PROGRAM, 38 | ASSOC_TOKEN_ACC_PROG, 39 | } from "./constants"; 40 | import { JitoBundleService, tipAccounts } from "../services/jito.bundle"; 41 | import { calculateMicroLamports } from "../raydium/raydium.service"; 42 | import { FeeService } from "../services/fee.service"; 43 | import { getSignature } from "../utils/get.signature"; 44 | import base58 from "bs58"; 45 | import { private_connection } from "../config"; 46 | import { UserTradeSettingService } from "../services/user.trade.setting.service"; 47 | 48 | export async function pumpFunSwap( 49 | payerPrivateKey: string, 50 | mintStr: string, 51 | decimal: number, 52 | is_buy: boolean, 53 | _amount: number, 54 | gasFee: number, 55 | _slippage: number, 56 | isFeeBurn: boolean, 57 | username: string, 58 | isToken2022: boolean 59 | ) { 60 | try { 61 | const coinData = await getCoinData(mintStr); 62 | if (!coinData) { 63 | console.error("Failed to retrieve coin data..."); 64 | return; 65 | } 66 | 67 | // JitoFee 68 | const jitoFeeSetting = await UserTradeSettingService.getJitoFee(username); 69 | const jitoFeeValue = 70 | UserTradeSettingService.getJitoFeeValue(jitoFeeSetting); 71 | const jitoFeeValueWei = BigInt((jitoFeeValue * 10 ** 9).toFixed()); 72 | 73 | const txBuilder = new Transaction(); 74 | 75 | const payer = await getKeyPairFromPrivateKey(payerPrivateKey); 76 | const owner = payer.publicKey; 77 | const mint = new PublicKey(mintStr); 78 | const slippage = _slippage / 100; 79 | let total_fee_in_sol = 0; 80 | let total_fee_in_token = 0; 81 | let total_fee_percent = 0.01; // 1% 82 | let total_fee_percent_in_sol = 0.01; // 1% 83 | let total_fee_percent_in_token = 0; 84 | if (isFeeBurn) { 85 | total_fee_percent_in_sol = 0.0075; 86 | total_fee_percent_in_token = total_fee_percent - total_fee_percent_in_sol; 87 | } 88 | const fee = 89 | _amount * 90 | (is_buy ? total_fee_percent_in_sol : total_fee_percent_in_token); 91 | const inDecimal = is_buy ? 9 : decimal; 92 | const outDecimal = is_buy ? decimal : 9; 93 | const amount = Number(((_amount - fee) * 10 ** inDecimal).toFixed(0)); 94 | 95 | const tokenAccountIn = getAssociatedTokenAddressSync( 96 | is_buy ? NATIVE_MINT : mint, 97 | owner, 98 | true 99 | ); 100 | const tokenAccountOut = getAssociatedTokenAddressSync( 101 | is_buy ? mint : NATIVE_MINT, 102 | owner, 103 | true 104 | ); 105 | 106 | const tokenAccountAddress = await getAssociatedTokenAddress( 107 | mint, 108 | owner, 109 | false 110 | ); 111 | const keys = [ 112 | { pubkey: GLOBAL, isSigner: false, isWritable: false }, 113 | { pubkey: FEE_RECIPIENT, isSigner: false, isWritable: true }, 114 | { pubkey: mint, isSigner: false, isWritable: false }, 115 | { 116 | pubkey: new PublicKey(coinData["bonding_curve"]), 117 | isSigner: false, 118 | isWritable: true, 119 | }, 120 | { 121 | pubkey: new PublicKey(coinData["associated_bonding_curve"]), 122 | isSigner: false, 123 | isWritable: true, 124 | }, 125 | { pubkey: tokenAccountAddress, isSigner: false, isWritable: true }, 126 | { pubkey: owner, isSigner: false, isWritable: true }, 127 | { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, 128 | { 129 | pubkey: is_buy ? TOKEN_PROGRAM_ID : ASSOC_TOKEN_ACC_PROG, 130 | isSigner: false, 131 | isWritable: false, 132 | }, 133 | { 134 | pubkey: is_buy ? RENT : TOKEN_PROGRAM_ID, 135 | isSigner: false, 136 | isWritable: false, 137 | }, 138 | { pubkey: PUMP_FUN_ACCOUNT, isSigner: false, isWritable: false }, 139 | { pubkey: PUMP_FUN_PROGRAM, isSigner: false, isWritable: false }, 140 | ]; 141 | 142 | let data: Buffer; 143 | let quoteAmount = 0; 144 | 145 | if (is_buy) { 146 | const tokenOut = Math.floor( 147 | (amount * coinData["virtual_token_reserves"]) / 148 | coinData["virtual_sol_reserves"] 149 | ); 150 | const solInWithSlippage = amount * (1 + slippage); 151 | const maxSolCost = Math.floor(solInWithSlippage * LAMPORTS_PER_SOL); 152 | 153 | data = Buffer.concat([ 154 | bufferFromUInt64("16927863322537952870"), 155 | bufferFromUInt64(tokenOut), 156 | bufferFromUInt64(maxSolCost), 157 | ]); 158 | 159 | quoteAmount = tokenOut; 160 | total_fee_in_sol = Number((fee * 10 ** inDecimal).toFixed(0)); 161 | total_fee_in_token = Number( 162 | (quoteAmount * total_fee_percent_in_token).toFixed(0) 163 | ); 164 | } else { 165 | const minSolOutput = Math.floor( 166 | (amount! * (1 - slippage) * coinData["virtual_sol_reserves"]) / 167 | coinData["virtual_token_reserves"] 168 | ); 169 | data = Buffer.concat([ 170 | bufferFromUInt64("12502976635542562355"), 171 | bufferFromUInt64(amount), 172 | bufferFromUInt64(minSolOutput), 173 | ]); 174 | quoteAmount = minSolOutput; 175 | total_fee_in_token = Number((fee * 10 ** inDecimal).toFixed(0)); 176 | total_fee_in_sol = Number( 177 | (Number(quoteAmount) * total_fee_percent_in_sol).toFixed(0) 178 | ); 179 | } 180 | 181 | const instruction = new TransactionInstruction({ 182 | keys: keys, 183 | programId: PUMP_FUN_PROGRAM, 184 | data: data, 185 | }); 186 | txBuilder.add(instruction); 187 | 188 | // const jitoInstruction = await createTransaction(private_connection, txBuilder.instructions, payer.publicKey); 189 | const jitoInstruction = txBuilder.instructions; 190 | // console.log(instruction) 191 | 192 | const cu = 1_000_000; 193 | const microLamports = calculateMicroLamports(gasFee, cu); 194 | console.log("Is_BUY", is_buy); 195 | const instructions: TransactionInstruction[] = is_buy 196 | ? [ 197 | ComputeBudgetProgram.setComputeUnitPrice({ 198 | microLamports: microLamports, 199 | }), 200 | ComputeBudgetProgram.setComputeUnitLimit({ units: cu }), 201 | SystemProgram.transfer({ 202 | fromPubkey: owner, 203 | toPubkey: new PublicKey(tipAccounts[0]), 204 | lamports: jitoFeeValueWei, 205 | }), 206 | createAssociatedTokenAccountIdempotentInstruction( 207 | owner, 208 | tokenAccountIn, 209 | owner, 210 | NATIVE_MINT 211 | ), 212 | SystemProgram.transfer({ 213 | fromPubkey: owner, 214 | toPubkey: tokenAccountIn, 215 | lamports: amount, 216 | }), 217 | createSyncNativeInstruction(tokenAccountIn, TOKEN_PROGRAM_ID), 218 | createAssociatedTokenAccountIdempotentInstruction( 219 | owner, 220 | tokenAccountOut, 221 | owner, 222 | new PublicKey(mint) 223 | ), 224 | ...jitoInstruction, 225 | // Unwrap WSOL for SOL 226 | createCloseAccountInstruction(tokenAccountIn, owner, owner), 227 | ] 228 | : [ 229 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports }), 230 | ComputeBudgetProgram.setComputeUnitLimit({ units: cu }), 231 | SystemProgram.transfer({ 232 | fromPubkey: owner, 233 | toPubkey: new PublicKey(tipAccounts[0]), 234 | lamports: jitoFeeValueWei, 235 | }), 236 | createAssociatedTokenAccountIdempotentInstruction( 237 | owner, 238 | tokenAccountOut, 239 | owner, 240 | NATIVE_MINT 241 | ), 242 | ...jitoInstruction, 243 | // Unwrap WSOL for SOL 244 | createCloseAccountInstruction(tokenAccountOut, owner, owner), 245 | ]; 246 | 247 | // Referral Fee, ReserverStaking Fee, Burn Token 248 | console.log("Before Fee: ", Date.now()); 249 | const feeInstructions = await new FeeService().getFeeInstructions( 250 | total_fee_in_sol, 251 | total_fee_in_token, 252 | username, 253 | payerPrivateKey, 254 | is_buy ? mintStr : NATIVE_MINT.toString(), 255 | isToken2022 256 | ); 257 | instructions.push(...feeInstructions); 258 | console.log("After Fee: ", Date.now()); 259 | 260 | const { blockhash, lastValidBlockHeight } = 261 | await private_connection.getLatestBlockhash(); 262 | 263 | const messageV0 = new TransactionMessage({ 264 | payerKey: owner, 265 | recentBlockhash: blockhash, 266 | instructions, 267 | }).compileToV0Message(); 268 | 269 | const transaction = new VersionedTransaction(messageV0); 270 | // transaction.sign([wallet]); 271 | transaction.sign([payer]); 272 | // Sign the transaction 273 | const signature = getSignature(transaction); 274 | 275 | // We first simulate whether the transaction would be successful 276 | const { value: simulatedTransactionResponse } = 277 | await private_connection.simulateTransaction(transaction, { 278 | replaceRecentBlockhash: true, 279 | commitment: "processed", 280 | }); 281 | const { err, logs } = simulatedTransactionResponse; 282 | 283 | console.log("🚀 Simulate ~", Date.now()); 284 | // if (!err) return; 285 | 286 | if (err) { 287 | // Simulation error, we can check the logs for more details 288 | // If you are getting an invalid account error, make sure that you have the input mint account to actually swap from. 289 | console.error("Simulation Error:"); 290 | console.error({ err, logs }); 291 | return; 292 | } 293 | 294 | const rawTransaction = transaction.serialize(); 295 | 296 | // Netherland 297 | // const jitoBundleInstance = new JitoBundleService("ams"); 298 | const jitoBundleInstance = new JitoBundleService(); 299 | const bundleId = await jitoBundleInstance.sendBundle(rawTransaction); 300 | // const status = await getSignatureStatus(signature); 301 | if (!bundleId) return; 302 | console.log("BundleID", bundleId); 303 | console.log(`https://solscan.io/tx/${signature}`); 304 | const quote = { inAmount: amount, outAmount: quoteAmount }; 305 | return { 306 | quote, 307 | signature, 308 | total_fee_in_sol, 309 | total_fee_in_token, 310 | bundleId, 311 | }; 312 | 313 | // txBuilder.add(instruction); 314 | 315 | // const transaction = await createTransaction(connection, txBuilder.instructions, payer.publicKey, priorityFeeInSol); 316 | 317 | // if (transactionMode === TransactionMode.Execution) { 318 | // const signature = await sendAndConfirmTransactionWrapper(connection, transaction, [payer]); 319 | // console.log(`${isBuy ? 'Buy' : 'Sell'} transaction confirmed:`, signature); 320 | // } else if (transactionMode === TransactionMode.Simulation) { 321 | // const simulatedResult = await connection.simulateTransaction(transaction); 322 | // console.log(simulatedResult); 323 | // } 324 | } catch (error) { 325 | console.log(" - Swap pump token is failed", error); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/services/token.metadata.ts: -------------------------------------------------------------------------------- 1 | import { ASSOCIATED_TOKEN_PROGRAM_ID, AccountLayout, ExtensionType, NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, getExtensionData, getExtensionTypes, getMetadataPointerState, getMint } from "@solana/spl-token"; 2 | import { COMMITMENT_LEVEL, MAINNET_RPC, connection } from "../config"; 3 | import { Metaplex, Metadata } from "@metaplex-foundation/js"; 4 | import { Connection, GetProgramAccountsFilter, PublicKey } from "@solana/web3.js"; 5 | import redisClient from "./redis"; 6 | import { formatNumber, getPrice } from "../utils"; 7 | import { BirdEyeAPIService, TokenOverviewDataType, TokenSecurityInfoDataType } from "./birdeye.api.service"; 8 | import { min } from "bn.js"; 9 | export interface ITokenAccountInfo { 10 | mint: string, 11 | amount: number, 12 | name: string, 13 | symbol: string 14 | } 15 | 16 | export interface MintExtention { 17 | extension: string, // transferFeeConfig, 18 | state: { 19 | newerTransferFee: { 20 | epoch: number, // 580, 21 | maximumFee: number, // 2328306436538696000, 22 | transferFeeBasisPoints: number, // 800 23 | }, 24 | olderTransferFee: { 25 | epoch: number, // 580, 26 | maximumFee: number, // 2328306436538696000, 27 | transferFeeBasisPoints: number, // 800 28 | }, 29 | transferFeeConfigAuthority: string, 30 | withdrawWithheldAuthority: string, 31 | withheldAmount: number, // 284998271699445 32 | } 33 | } 34 | 35 | export interface MintParsedInfo { 36 | decimals: number, 37 | freezeAuthority: string | null, 38 | isInitialized: Boolean, 39 | mintAuthority: string | null, 40 | supply: string, 41 | extension?: MintExtention[], 42 | } 43 | 44 | 45 | 46 | export interface MintParsed { 47 | info: MintParsedInfo, 48 | type: string, // 'mint' 49 | } 50 | 51 | export interface MintData { 52 | program: string; // 'spl-token', 'spl-token-2022' 53 | parsed: MintParsed, 54 | space: number, 55 | } 56 | 57 | const knownMints = [ 58 | { 59 | mint: "FPymkKgpg1sLFbVao4JMk4ip8xb8C8uKqfMdARMobHaw", 60 | tokenName: "GrowSol", 61 | tokenSymbol: "$GRW", 62 | }, 63 | { 64 | mint: "HKYX2jvwkdjbkbSdirAiQHqTCPQa3jD2DVRkAFHgFXXT", 65 | tokenName: "Print Protocol", 66 | tokenSymbol: "$PRINT", 67 | }, 68 | 69 | ] 70 | 71 | export const TokenService = { 72 | getMintInfo: async (mint: string) => { 73 | try { 74 | const overview = await TokenService.getTokenOverview(mint); 75 | if (!overview) return null; 76 | 77 | const secureinfo = await TokenService.getTokenSecurity(mint); 78 | const resdata = { 79 | overview, 80 | secureinfo 81 | } as { 82 | overview: TokenOverviewDataType, 83 | secureinfo: TokenSecurityInfoDataType 84 | } 85 | 86 | return resdata; 87 | } catch (e) { 88 | return null; 89 | } 90 | }, 91 | getTokenSecurity: async (mint: string) => { 92 | const key = `${mint}_security`; 93 | const redisdata = await redisClient.get(key); 94 | if (redisdata) { 95 | return JSON.parse(redisdata); 96 | } 97 | const secureinfo = await BirdEyeAPIService.getTokenSecurity(mint); 98 | await redisClient.set(key, JSON.stringify(secureinfo)) 99 | await redisClient.expire(key, 30); 100 | return secureinfo; 101 | }, 102 | getTokenOverview: async (mint: string) => { 103 | const key = `${mint}_overview`; 104 | const redisdata = await redisClient.get(key); 105 | if (redisdata) { 106 | return JSON.parse(redisdata); 107 | } 108 | 109 | const overview = await BirdEyeAPIService.getTokenOverview(mint) as TokenOverviewDataType; 110 | if (!overview || !overview.address) { 111 | return null; 112 | } 113 | 114 | await redisClient.set(key, JSON.stringify(overview)) 115 | await redisClient.expire(key, 10); 116 | return overview; 117 | }, 118 | fetchSecurityInfo: async (mint: PublicKey) => { 119 | try { 120 | const key = `${mint}_security`; 121 | await redisClient.expire(key, 0); 122 | const data = await redisClient.get(key); 123 | if (data) return JSON.parse(data); 124 | 125 | let mintInfo: any; 126 | let token2022 = false; 127 | // spltoken and token2022 128 | try { 129 | mintInfo = await getMint( 130 | connection, 131 | mint 132 | ); 133 | } catch (error) { 134 | token2022 = true; 135 | mintInfo = await getMint( 136 | connection, 137 | mint, 138 | COMMITMENT_LEVEL, 139 | TOKEN_2022_PROGRAM_ID 140 | ) 141 | } 142 | const mintdata = { 143 | token2022, 144 | address: mintInfo.address.toString(), 145 | mintAuthority: mintInfo.mintAuthority, 146 | supply: mintInfo.supply.toString(), 147 | decimals: mintInfo.decimals, 148 | isInitialized: mintInfo.isInitialized, 149 | freezeAuthority: mintInfo.freezeAuthority, 150 | } 151 | await redisClient.set(key, JSON.stringify(mintdata)); 152 | if (mintInfo.freezeAuthority || mintInfo.freezeAuthority) { 153 | await redisClient.expire(key, 60); 154 | } else { 155 | await redisClient.expire(key, 24 * 3600); 156 | } 157 | return mintInfo; 158 | } catch (error) { 159 | console.log(error); 160 | return null; 161 | } 162 | }, 163 | fetchMetadataInfo: async (mint: PublicKey) => { 164 | try { 165 | const filteredMints = knownMints.filter((item) => item.mint === mint.toString()); 166 | if (filteredMints && filteredMints.length > 0) { 167 | const filteredMint = filteredMints[0]; 168 | return { 169 | tokenName: filteredMint.tokenName, 170 | tokenSymbol: filteredMint.tokenSymbol, 171 | website: "", 172 | twitter: "", 173 | telegram: "" 174 | } 175 | } 176 | const key = `${mint}_metadata`; 177 | const data = await redisClient.get(key); 178 | if (data) return JSON.parse(data); 179 | const metaplex = Metaplex.make(connection); 180 | 181 | const mintAddress = new PublicKey(mint); 182 | 183 | let tokenName: any; 184 | let tokenSymbol: any; 185 | let website: any; 186 | let twitter: any; 187 | let telegram: any; 188 | 189 | const metadataAccount = metaplex 190 | .nfts() 191 | .pdas() 192 | .metadata({ mint: mintAddress }); 193 | 194 | const metadataAccountInfo = await connection.getAccountInfo(metadataAccount); 195 | 196 | if (metadataAccountInfo) { 197 | const token = await metaplex.nfts().findByMint({ mintAddress: mintAddress }); 198 | tokenName = token.name; 199 | tokenSymbol = token.symbol; 200 | 201 | if (token.json && token.json.extensions) { 202 | website = (token.json.extensions as any).website ?? ""; 203 | twitter = (token.json.extensions as any).twitter ?? ""; 204 | telegram = (token.json.extensions as any).twitter ?? ""; 205 | } 206 | } 207 | await redisClient.set(key, JSON.stringify({ 208 | tokenName, 209 | tokenSymbol, 210 | website, 211 | twitter, 212 | telegram 213 | })) 214 | return { 215 | tokenName, 216 | tokenSymbol, 217 | website, 218 | twitter, 219 | telegram 220 | } 221 | } catch (e) { 222 | return { 223 | tokenName: "", 224 | tokenSymbol: "", 225 | website: "", 226 | twitter: "", 227 | telegram: "" 228 | } 229 | } 230 | }, 231 | getSPLBalance: async (mint: string, owner: string, isToken2022: boolean, isLive: boolean = false) => { 232 | let tokenBalance = 0; 233 | try { 234 | const key = `${owner}${mint}_balance`; 235 | const data = await redisClient.get(key); 236 | if (data && !isLive) return Number(data); 237 | const ata = getAssociatedTokenAddressSync( 238 | new PublicKey(mint), 239 | new PublicKey(owner), 240 | true, 241 | isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID, 242 | ASSOCIATED_TOKEN_PROGRAM_ID 243 | ) 244 | 245 | const balance = await connection.getTokenAccountBalance(ata); 246 | if (balance.value.uiAmount) { 247 | tokenBalance = balance.value.uiAmount; 248 | await redisClient.set(key, tokenBalance); 249 | await redisClient.expire(key, 10); 250 | } 251 | } catch (e) { 252 | tokenBalance = 0; 253 | } 254 | return tokenBalance; 255 | }, 256 | fetchSimpleMetaData: async (mint: PublicKey) => { 257 | try { 258 | const metaPlex = new Metaplex(connection); 259 | const metadata = await metaPlex 260 | .nfts() 261 | .findByMint({ mintAddress: mint }) 262 | const tokenName = metadata.name; 263 | const tokenSymbol = metadata.symbol; 264 | return { 265 | name: tokenName, 266 | symbol: tokenSymbol, 267 | }; 268 | } catch (e) { 269 | return { 270 | name: "", 271 | symbol: "", 272 | }; 273 | } 274 | }, 275 | getMintMetadata: async ( 276 | connection: Connection, 277 | mint: PublicKey 278 | ): Promise => { 279 | try { 280 | const key = `raymintmeta_${mint}`; 281 | const res = await redisClient.get(key); 282 | if (res) { 283 | return JSON.parse(res) as MintData; 284 | } 285 | const mintdata = await connection.getParsedAccountInfo(mint); 286 | if (!mintdata || !mintdata.value) { 287 | return; 288 | } 289 | 290 | const data = mintdata.value.data as MintData; 291 | await redisClient.set(key, JSON.stringify(data)); 292 | await redisClient.expire(key, 30); 293 | return data; 294 | } catch (e) { 295 | return undefined; 296 | } 297 | }, 298 | getSOLBalance: async (owner: string, isLive: boolean = false) => { 299 | let solBalance = 0; 300 | try { 301 | const key = `${owner}_solbalance`; 302 | const data = await redisClient.get(key); 303 | if (data && !isLive) return Number(data); 304 | const sol = await connection.getBalance(new PublicKey(owner)); 305 | 306 | if (sol) { 307 | solBalance = sol / 10 ** 9; 308 | await redisClient.set(key, solBalance); 309 | await redisClient.expire(key, 10); 310 | } 311 | } catch (e) { 312 | solBalance = 0; 313 | } 314 | return solBalance; 315 | }, 316 | getSOLPrice: async () => { 317 | return getPrice(NATIVE_MINT.toString()); 318 | }, 319 | getSPLPrice: async (mint: string) => { 320 | return getPrice(mint); 321 | }, 322 | getTokenAccounts: async (wallet: string): Promise> => { 323 | try { 324 | const key = `${wallet}_tokenaccounts`; 325 | const data = await redisClient.get(key); 326 | if (data) return JSON.parse(data); 327 | 328 | const results: Array = await getaccounts(wallet); 329 | 330 | await redisClient.set(key, JSON.stringify(results)); 331 | await redisClient.expire(key, 30); 332 | return results; 333 | } catch (e) { return [] } 334 | }, 335 | } 336 | 337 | const getaccounts = async (owner: string) => { 338 | const results: Array = []; 339 | const response = await fetch(MAINNET_RPC, { 340 | method: "POST", 341 | headers: { 342 | "Content-Type": "application/json", 343 | }, 344 | body: JSON.stringify({ 345 | jsonrpc: "2.0", 346 | method: "getTokenAccounts", 347 | id: "helius-test", 348 | params: { 349 | page: 1, 350 | limit: 100, 351 | "displayOptions": { 352 | "showZeroBalance": false, 353 | }, 354 | owner: owner, 355 | }, 356 | }), 357 | }); 358 | const data = await response.json(); 359 | 360 | if (!data.result) { 361 | console.error("No result in the response", data); 362 | return []; 363 | } 364 | 365 | for (const account of data.result.token_accounts as Array) { 366 | const { mint, amount } = account; 367 | let tokenName 368 | let tokenSymbol 369 | const { name, symbol } = await TokenService.fetchSimpleMetaData(new PublicKey(mint)) 370 | tokenName = name 371 | tokenSymbol = symbol 372 | if(name ==='' && symbol ===''){ 373 | const res = await TokenService.getMintInfo(mint); 374 | tokenName = res?.overview.name as string 375 | tokenSymbol = res?.overview.symbol as string 376 | } 377 | results.push({ mint, amount, name: tokenName, symbol: tokenSymbol }) 378 | } 379 | return results; 380 | } --------------------------------------------------------------------------------