├── .dockerignore ├── .env.template ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── schema.sql ├── src ├── data │ ├── example-pumpfun-swap.json │ ├── example-token-tx.json │ └── pumpfun-idl.json ├── discord.ts ├── index.ts └── lib │ ├── jito.ts │ ├── postgres.ts │ ├── pumpfun.ts │ ├── raydium.ts │ ├── snipe.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | node_modules/ 3 | .env 4 | payer.json -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | KEYPAIR_ENCRYPTION_KEY= 2 | KEYPAIR_ENCRYPTION_IV= 3 | DATABASE_URL= 4 | DISCORD_BOT_TOKEN= 5 | TELEGRAM_BOT_TOKEN= 6 | RPC_URL= 7 | JITO_PAYER_KEYPAIR= 8 | API_PORT= 9 | HTTPS_KEY_PATH= 10 | HTTPS_CERT_PATH= 11 | NEXTBLOCK_API_KEY= 12 | SNIPING_WALLET_PUBLIC_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | node_modules/ 3 | .env 4 | test.ts 5 | payer.json 6 | wallets/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others’ private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [your contact information]. All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 74 | 75 | Community Impact Guidelines were inspired by [Mozilla’s code of conduct enforcement ladder](https://github.com/mozilla/diversity). 76 | 77 | [homepage]: https://www.contributor-covenant.org 78 | 79 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Moonbot 2 | 3 | Thank you for considering contributing to Moonbot! We welcome contributions from everyone. By participating in this project, you agree to abide by our code of conduct. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork the repository. 8 | 2. Create a new branch (`git checkout -b feature-branch`). 9 | 3. Make your changes. 10 | 4. Commit your changes (`git commit -m 'Add some feature'`). 11 | 5. Push to the branch (`git push origin feature-branch`). 12 | 6. Open a pull request. 13 | 14 | ## Code of Conduct 15 | 16 | Please read our [Code of Conduct](CODE_OF_CONDUCT.md) for details on our code of conduct. 17 | 18 | ## Reporting Issues 19 | 20 | If you find a bug or have a feature request, please open an issue on GitHub. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 The Valkar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moonbot Token Sniper Bot 2 | 3 | ![Moonbot Logo](https://www.mooners.xyz/mooners480.png) 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Features](#features) 9 | - [Architecture](#architecture) 10 | - [Prerequisites](#prerequisites) 11 | - [Installation](#installation) 12 | - [Configuration](#configuration) 13 | - [Usage](#usage) 14 | - [Contributing](#contributing) 15 | - [Code of Conduct](#code-of-conduct) 16 | - [License](#license) 17 | 18 | ## Introduction 19 | 20 | Moonbot Token Sniper Bot is a sophisticated tool designed for the Solana blockchain ecosystem. It monitors transactions in real-time, detects potential token purchases, and executes automated buy operations based on predefined criteria. Additionally, it integrates with Discord and Telegram to provide real-time notifications and manage user interactions through invite codes. 21 | 22 | ## Features 23 | 24 | - **Real-Time Transaction Monitoring:** Listens to Solana transactions via POST requests (a webhook) and processes them to identify token purchases. 25 | - **Automated Token Sniping:** Executes buy operations for tokens that meet specific criteria such as Fairly Distributed Value (FDV) thresholds. 26 | - **Discord Integration:** Provides a Discord bot for managing invite codes, enabling/disabling access, and sending notifications to designated channels. 27 | - **Telegram Integration:** Sends real-time alerts to Telegram threads corresponding to different transaction sources. 28 | - **Database Management:** Utilizes PostgreSQL to store and manage token data, signals, and user invite codes. 29 | - **Encryption:** Encrypts sensitive information such as keypairs using AES-256-CBC encryption for enhanced security. 30 | - **Retry Mechanism:** Implements robust retry logic for transaction submissions to handle network or API failures gracefully. 31 | 32 | ## Architecture 33 | 34 | The project is structured into several key components: 35 | 36 | 1. **Transaction Handler (`src/index.ts`):** Listens to incoming transactions, parses them, and determines whether to execute buy operations or send notifications. 37 | 2. **Utilities (`src/lib/utils.ts`):** Contains helper functions for interacting with the Solana blockchain, fetching token data, calculating prices, and managing retries. 38 | 3. **Database Interface (`src/lib/postgres.ts`):** Manages all interactions with the PostgreSQL database, including querying and inserting data. 39 | 4. **Discord Bot (`src/discord.ts`):** Handles Discord interactions, command processing, and sending notifications to Discord channels. 40 | 5. **Configuration (`.env.template`):** Manages environment variables required for secure operations. 41 | 42 | ## Prerequisites 43 | 44 | Before setting up Moonbot, ensure you have the following: 45 | 46 | - **Node.js** (v14 or later) 47 | - **npm** or **yarn** 48 | - **PostgreSQL** database 49 | - **Solana Wallet** with necessary permissions 50 | - **Discord Account** for bot integration 51 | - **Telegram Account** for bot notifications 52 | 53 | ## Installation 54 | 55 | 1. **Clone the Repository** 56 | 57 | ```bash 58 | git clone https://github.com/thevalkar/moonbot.git 59 | cd moonbot 60 | ``` 61 | 62 | 2. **Install Dependencies** 63 | 64 | Using npm: 65 | 66 | ```bash 67 | npm install 68 | ``` 69 | 70 | Using yarn: 71 | 72 | ```bash 73 | yarn install 74 | ``` 75 | 76 | 3. **Set Up the Database** 77 | 78 | Ensure PostgreSQL is installed and running. Create a new database for the project. 79 | 80 | ```bash 81 | createdb moonbot_db 82 | ``` 83 | 84 | Run the schema setup using the `schema.sql` file: 85 | 86 | ```bash 87 | psql -d moonbot_db -f schema.sql 88 | ``` 89 | 90 | This command will execute the SQL statements in `schema.sql` to set up the necessary tables and structures in your database. 91 | 92 | ## Configuration 93 | 94 | 1. **Environment Variables** 95 | 96 | Create a `.env` file in the root directory based on the provided `.env.template`. 97 | 98 | ```bash 99 | cp .env.template .env 100 | ``` 101 | 102 | Populate the `.env` file with your configuration: 103 | 104 | ```env 105 | KEYPAIR_ENCRYPTION_KEY= 106 | KEYPAIR_ENCRYPTION_IV= 107 | DATABASE_URL=postgres://postgres:password@localhost:5432/database 108 | DISCORD_BOT_TOKEN= 109 | TELEGRAM_BOT_TOKEN= 110 | RPC_URL=https://mainnet.helius-rpc.com/?api-key=api_key 111 | JITO_PAYER_KEYPAIR=[...,...,...] 112 | API_PORT=443 113 | HTTPS_KEY_PATH=/etc/letsencrypt/archive/server.com/privkey1.pem 114 | HTTPS_CERT_PATH=/etc/letsencrypt/archive/server.com/fullchain1.pem 115 | ``` 116 | - **KEYPAIR_ENCRYPTION_KEY:** Hex-encoded key for encrypting keypairs. 117 | - **KEYPAIR_ENCRYPTION_IV:** Hex-encoded initialization vector for encryption. 118 | - **DATABASE_URL:** PostgreSQL connection string. 119 | - **DISCORD_BOT_TOKEN:** Token for Discord bot integration. 120 | - **TELEGRAM_BOT_TOKEN:** Token for Telegram bot integration. 121 | - **RPC_URL:** RPC endpoint for Solana interactions. 122 | - **JITO_PAYER_KEYPAIR:** Jito payer keypair for sending bundles. 123 | - **API_PORT:** Port to run the API 124 | - **HTTPS_KEY_PATH:** Absolute path for HTTPS cert key 125 | - **HTTPS_CERT_PATH:** Absolute path for HTTPS cert 126 | 127 | 2. **Configure Discord Bot** 128 | 129 | Ensure your Discord bot has the necessary permissions and is added to your server. Update the `GUILD_ID`, `ROLE_ID`, and channel IDs in the code if necessary. 130 | 131 | 3. **Configure Telegram Bot** 132 | 133 | Set up a Telegram bot via BotFather and obtain the bot token. Update the `TELEGRAM_BOT_TOKEN` in your `.env` file. 134 | 135 | ## Usage 136 | 137 | 1. **Start the Server** 138 | 139 | Using npm: 140 | 141 | ```bash 142 | npm start 143 | ``` 144 | 145 | Using yarn: 146 | 147 | ```bash 148 | yarn start 149 | ``` 150 | 151 | The server will start on the specified port (default is 45000). 152 | 153 | 2. **Interacting via Discord** 154 | 155 | - **Enable/Disable Moonbot:** Use the `/enable` command in Discord to toggle access. 156 | - **Notifications:** Receive real-time notifications in designated Discord channels when potential token purchases are detected. 157 | 158 | 3. **Interacting via Telegram** 159 | 160 | Receive alerts and updates in corresponding Telegram threads based on transaction sources like Raydium, Pumpfun, and Moonshot. 161 | 162 | 4. **Webhook Integration** 163 | 164 | The Express server listens for incoming webhook transactions. Send a POST request to the server's root endpoint (`/`) with transaction data to trigger processing. 165 | 166 | ## Contributing 167 | 168 | Contributions are welcome! Please follow these steps: 169 | 170 | 1. **Fork the Repository** 171 | 172 | 2. **Create a Feature Branch** 173 | 174 | ```bash 175 | git checkout -b feature/YourFeature 176 | ``` 177 | 178 | 3. **Commit Your Changes** 179 | 180 | ```bash 181 | git commit -m "Add some feature" 182 | ``` 183 | 184 | 4. **Push to the Branch** 185 | 186 | ```bash 187 | git push origin feature/YourFeature 188 | ``` 189 | 190 | 5. **Open a Pull Request** 191 | 192 | ## Code of Conduct 193 | 194 | Please read our [Code of Conduct](./CODE_OF_CONDUCT.md) to understand the standards we expect from our community members. 195 | 196 | ## License 197 | 198 | This project is licensed under the [MIT License](./LICENSE). 199 | 200 | --- 201 | 202 | **Disclaimer:** Use this bot responsibly. The developers are not liable for any losses or damages caused by the use of this bot. 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moonbot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production tsx src/index.ts & tsx src/discord.ts", 8 | "dev": "tsx src/index.ts & tsx src/discord.ts", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@coral-xyz/anchor": "0.29", 16 | "@metaplex-foundation/mpl-token-metadata": "^3.2.1", 17 | "@metaplex-foundation/umi": "^0.9.2", 18 | "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", 19 | "@raydium-io/raydium-sdk": "^1.3.1-beta.58", 20 | "@solana/web3.js": "^1.95.3", 21 | "bn.js": "^5.2.1", 22 | "bs58": "^6.0.0", 23 | "chalk": "^5.3.0", 24 | "discord.js": "^14.16.2", 25 | "dotenv": "^16.4.5", 26 | "express": "^4.21.0", 27 | "grammy": "^1.30.0", 28 | "jito-js-rpc": "^0.1.0", 29 | "postgres": "^3.4.4", 30 | "socket.io": "^4.8.1", 31 | "tsx": "^4.19.1", 32 | "vercel": "^37.6.0" 33 | }, 34 | "devDependencies": { 35 | "@types/bn.js": "^5.1.6", 36 | "@types/express": "^4.17.21", 37 | "typescript": "^5.6.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 14.13 (Homebrew) 6 | -- Dumped by pg_dump version 14.13 (Homebrew) 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | SET default_tablespace = ''; 20 | 21 | SET default_table_access_method = heap; 22 | 23 | -- 24 | -- Name: moonbot_invite_codes; Type: TABLE; Schema: public; Owner: postgres 25 | -- 26 | 27 | CREATE TABLE public.moonbot_invite_codes ( 28 | code character varying(64), 29 | keypair character varying(255), 30 | for_user character varying(64), 31 | enabled boolean DEFAULT false NOT NULL 32 | ); 33 | 34 | 35 | ALTER TABLE public.moonbot_invite_codes OWNER TO postgres; 36 | 37 | -- 38 | -- Name: pairs; Type: TABLE; Schema: public; Owner: postgres 39 | -- 40 | 41 | CREATE TABLE public.pairs ( 42 | address character varying(80) NOT NULL, 43 | source character varying(32), 44 | token_mint character varying(80), 45 | data jsonb 46 | ); 47 | 48 | 49 | ALTER TABLE public.pairs OWNER TO postgres; 50 | 51 | -- 52 | -- Name: pool_addresses; Type: TABLE; Schema: public; Owner: postgres 53 | -- 54 | 55 | CREATE TABLE public.pool_addresses ( 56 | token character varying(80), 57 | pool_address character varying(80) 58 | ); 59 | 60 | 61 | ALTER TABLE public.pool_addresses OWNER TO postgres; 62 | 63 | -- 64 | -- Name: signal_token_prices; Type: TABLE; Schema: public; Owner: postgres 65 | -- 66 | 67 | CREATE TABLE public.signal_token_prices ( 68 | token_mint character varying(80), 69 | price double precision, 70 | "timestamp" bigint, 71 | token_amount double precision, 72 | sol_amount double precision, 73 | txid character varying 74 | ); 75 | 76 | 77 | ALTER TABLE public.signal_token_prices OWNER TO postgres; 78 | 79 | -- 80 | -- Name: token_entries; Type: TABLE; Schema: public; Owner: postgres 81 | -- 82 | 83 | CREATE TABLE public.token_entries ( 84 | token_mint character varying(80) NOT NULL, 85 | price double precision, 86 | buyer character varying(80), 87 | "timestamp" bigint, 88 | source character varying(80), 89 | token_insights jsonb, 90 | amount double precision 91 | ); 92 | 93 | 94 | ALTER TABLE public.token_entries OWNER TO postgres; 95 | 96 | -- 97 | -- Name: token_price; Type: TABLE; Schema: public; Owner: postgres 98 | -- 99 | 100 | CREATE TABLE public.token_price ( 101 | token_mint character varying(80), 102 | price double precision, 103 | updated_timestamp character varying(80) 104 | ); 105 | 106 | 107 | ALTER TABLE public.token_price OWNER TO postgres; 108 | 109 | -- 110 | -- Name: token_prices; Type: TABLE; Schema: public; Owner: postgres 111 | -- 112 | 113 | CREATE TABLE public.token_prices ( 114 | token_mint character varying(80), 115 | price double precision, 116 | "timestamp" bigint 117 | ); 118 | 119 | 120 | ALTER TABLE public.token_prices OWNER TO postgres; 121 | 122 | -- 123 | -- Name: token_signals; Type: TABLE; Schema: public; Owner: postgres 124 | -- 125 | 126 | CREATE TABLE public.token_signals ( 127 | token_mint character varying(80), 128 | pair_address character varying(80), 129 | price double precision, 130 | source character varying(80), 131 | "timestamp" bigint, 132 | buyers integer, 133 | amount double precision, 134 | buyer character varying(80) 135 | ); 136 | 137 | 138 | ALTER TABLE public.token_signals OWNER TO postgres; 139 | 140 | -- 141 | -- Name: tokens; Type: TABLE; Schema: public; Owner: postgres 142 | -- 143 | 144 | CREATE TABLE public.tokens ( 145 | mint character varying(80) NOT NULL, 146 | data jsonb, 147 | pair_address character varying(80) 148 | ); 149 | 150 | 151 | ALTER TABLE public.tokens OWNER TO postgres; 152 | 153 | -- 154 | -- Name: user_config; Type: TABLE; Schema: public; Owner: postgres 155 | -- 156 | 157 | CREATE TABLE public.user_config ( 158 | discordid character varying(80) NOT NULL, 159 | strategy character varying(80), 160 | entry double precision, 161 | min_times_bought integer 162 | ); 163 | 164 | 165 | ALTER TABLE public.user_config OWNER TO postgres; 166 | 167 | -- 168 | -- Name: pairs pairs_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres 169 | -- 170 | 171 | ALTER TABLE ONLY public.pairs 172 | ADD CONSTRAINT pairs_pkey PRIMARY KEY (address); 173 | 174 | 175 | -- 176 | -- Name: token_price token_price_token_mint_key; Type: CONSTRAINT; Schema: public; Owner: postgres 177 | -- 178 | 179 | ALTER TABLE ONLY public.token_price 180 | ADD CONSTRAINT token_price_token_mint_key UNIQUE (token_mint); 181 | 182 | 183 | -- 184 | -- Name: tokens tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres 185 | -- 186 | 187 | ALTER TABLE ONLY public.tokens 188 | ADD CONSTRAINT tokens_pkey PRIMARY KEY (mint); 189 | 190 | 191 | -- 192 | -- Name: moonbot_invite_codes uq_code; Type: CONSTRAINT; Schema: public; Owner: postgres 193 | -- 194 | 195 | ALTER TABLE ONLY public.moonbot_invite_codes 196 | ADD CONSTRAINT uq_code UNIQUE (code); 197 | 198 | 199 | -- 200 | -- Name: user_config user_config_discordid_key; Type: CONSTRAINT; Schema: public; Owner: postgres 201 | -- 202 | 203 | ALTER TABLE ONLY public.user_config 204 | ADD CONSTRAINT user_config_discordid_key UNIQUE (discordid); 205 | 206 | 207 | -- 208 | -- Name: idx_token_mint_timestamp; Type: INDEX; Schema: public; Owner: postgres 209 | -- 210 | 211 | CREATE INDEX idx_token_mint_timestamp ON public.token_prices USING btree (token_mint, "timestamp"); 212 | 213 | 214 | -- 215 | -- Name: idx_token_prices_token_mint_price_timestamp; Type: INDEX; Schema: public; Owner: postgres 216 | -- 217 | 218 | CREATE INDEX idx_token_prices_token_mint_price_timestamp ON public.token_prices USING btree (token_mint, price, "timestamp"); 219 | 220 | 221 | -- 222 | -- Name: ixs_token_buyers; Type: INDEX; Schema: public; Owner: postgres 223 | -- 224 | 225 | CREATE INDEX ixs_token_buyers ON public.token_signals USING btree (token_mint, buyers); 226 | 227 | 228 | -- 229 | -- Name: pairs pair_token_mint; Type: FK CONSTRAINT; Schema: public; Owner: postgres 230 | -- 231 | 232 | ALTER TABLE ONLY public.pairs 233 | ADD CONSTRAINT pair_token_mint FOREIGN KEY (token_mint) REFERENCES public.tokens(mint); 234 | 235 | 236 | -- 237 | -- Name: token_price price_token_mint; Type: FK CONSTRAINT; Schema: public; Owner: postgres 238 | -- 239 | 240 | ALTER TABLE ONLY public.token_price 241 | ADD CONSTRAINT price_token_mint FOREIGN KEY (token_mint) REFERENCES public.tokens(mint); 242 | 243 | 244 | -- 245 | -- PostgreSQL database dump complete 246 | -- 247 | 248 | -------------------------------------------------------------------------------- /src/data/example-pumpfun-swap.json: -------------------------------------------------------------------------------- 1 | { 2 | "accountData": [ 3 | { 4 | "account": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 5 | "nativeBalanceChange": -55577355, 6 | "tokenBalanceChanges": [] 7 | }, 8 | { 9 | "account": "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 10 | "nativeBalanceChange": 3000000, 11 | "tokenBalanceChanges": [] 12 | }, 13 | { 14 | "account": "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 15 | "nativeBalanceChange": 2039280, 16 | "tokenBalanceChanges": [ 17 | { 18 | "mint": "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 19 | "rawTokenAmount": { "decimals": 6, "tokenAmount": "1071707852766" }, 20 | "tokenAccount": "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 21 | "userAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M" 22 | } 23 | ] 24 | }, 25 | { 26 | "account": "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", 27 | "nativeBalanceChange": 500000, 28 | "tokenBalanceChanges": [] 29 | }, 30 | { 31 | "account": "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh", 32 | "nativeBalanceChange": 50000000, 33 | "tokenBalanceChanges": [] 34 | }, 35 | { 36 | "account": "DT8Q5LsLoVUMUocCekfwaq18NSDXP6FQowBovF9t2oZA", 37 | "nativeBalanceChange": 0, 38 | "tokenBalanceChanges": [ 39 | { 40 | "mint": "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 41 | "rawTokenAmount": { "decimals": 6, "tokenAmount": "-1071707852766" }, 42 | "tokenAccount": "DT8Q5LsLoVUMUocCekfwaq18NSDXP6FQowBovF9t2oZA", 43 | "userAccount": "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh" 44 | } 45 | ] 46 | }, 47 | { 48 | "account": "ComputeBudget111111111111111111111111111111", 49 | "nativeBalanceChange": 0, 50 | "tokenBalanceChanges": [] 51 | }, 52 | { 53 | "account": "11111111111111111111111111111111", 54 | "nativeBalanceChange": 0, 55 | "tokenBalanceChanges": [] 56 | }, 57 | { 58 | "account": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", 59 | "nativeBalanceChange": 0, 60 | "tokenBalanceChanges": [] 61 | }, 62 | { 63 | "account": "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 64 | "nativeBalanceChange": 0, 65 | "tokenBalanceChanges": [] 66 | }, 67 | { 68 | "account": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 69 | "nativeBalanceChange": 0, 70 | "tokenBalanceChanges": [] 71 | }, 72 | { 73 | "account": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P", 74 | "nativeBalanceChange": 0, 75 | "tokenBalanceChanges": [] 76 | }, 77 | { 78 | "account": "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf", 79 | "nativeBalanceChange": 0, 80 | "tokenBalanceChanges": [] 81 | }, 82 | { 83 | "account": "SysvarRent111111111111111111111111111111111", 84 | "nativeBalanceChange": 0, 85 | "tokenBalanceChanges": [] 86 | }, 87 | { 88 | "account": "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1", 89 | "nativeBalanceChange": 0, 90 | "tokenBalanceChanges": [] 91 | } 92 | ], 93 | "description": "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh transferred 1071707.852766 dilly to 2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M.", 94 | "events": {}, 95 | "fee": 38075, 96 | "feePayer": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 97 | "instructions": [ 98 | { 99 | "accounts": [], 100 | "data": "JC3gyu", 101 | "innerInstructions": [], 102 | "programId": "ComputeBudget111111111111111111111111111111" 103 | }, 104 | { 105 | "accounts": [ 106 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 107 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe" 108 | ], 109 | "data": "3Bxs4Z6oyhaczjLK", 110 | "innerInstructions": [], 111 | "programId": "11111111111111111111111111111111" 112 | }, 113 | { 114 | "accounts": [], 115 | "data": "3wx6fzy1Zykb", 116 | "innerInstructions": [], 117 | "programId": "ComputeBudget111111111111111111111111111111" 118 | }, 119 | { 120 | "accounts": [ 121 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 122 | "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 123 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 124 | "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 125 | "11111111111111111111111111111111", 126 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 127 | ], 128 | "data": "", 129 | "innerInstructions": [ 130 | { 131 | "accounts": ["2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump"], 132 | "data": "84eT", 133 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 134 | }, 135 | { 136 | "accounts": [ 137 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 138 | "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM" 139 | ], 140 | "data": "11119os1e9qSs2u7TsThXqkBSRVFxhmYaFKFZ1waB2X7armDmvK3p5GmLdUxYdg3h7QSrL", 141 | "programId": "11111111111111111111111111111111" 142 | }, 143 | { 144 | "accounts": ["AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM"], 145 | "data": "P", 146 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 147 | }, 148 | { 149 | "accounts": [ 150 | "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 151 | "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump" 152 | ], 153 | "data": "6NZq8pgi7R26gwXPT6JRpHGD6Td9wetLS4wz6jqFPvJVK", 154 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 155 | } 156 | ], 157 | "programId": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 158 | }, 159 | { 160 | "accounts": [ 161 | "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf", 162 | "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM", 163 | "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 164 | "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh", 165 | "DT8Q5LsLoVUMUocCekfwaq18NSDXP6FQowBovF9t2oZA", 166 | "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 167 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 168 | "11111111111111111111111111111111", 169 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 170 | "SysvarRent111111111111111111111111111111111", 171 | "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1", 172 | "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 173 | ], 174 | "data": "AJTQ2h9DXrC6ZagiKedixTj7u7E7tFXG3", 175 | "innerInstructions": [ 176 | { 177 | "accounts": [ 178 | "DT8Q5LsLoVUMUocCekfwaq18NSDXP6FQowBovF9t2oZA", 179 | "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 180 | "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh" 181 | ], 182 | "data": "3rbqarN8GGw1", 183 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" 184 | }, 185 | { 186 | "accounts": [ 187 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 188 | "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh" 189 | ], 190 | "data": "3Bxs4NRZ15a54oAf", 191 | "programId": "11111111111111111111111111111111" 192 | }, 193 | { 194 | "accounts": [ 195 | "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 196 | "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" 197 | ], 198 | "data": "3Bxs46KChmhFZqno", 199 | "programId": "11111111111111111111111111111111" 200 | }, 201 | { 202 | "accounts": ["Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"], 203 | "data": "2K7nL28PxCW8ejnyCeuMpbVpAhMEzCzBa6J3urLKxubdP9GUV2FNTSUodZeBiaTZsyizBWRZvquuQk2aB5P1jEoNnziFHzw3G27SqVV36TwhkcN1wVmuNgrygvPXitGWBhW82cemHMDGaGKqPJcNmmtfQiSoc6eLYcP7R2ijSa35hK8NWTfsyk4DVRNX", 204 | "programId": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 205 | } 206 | ], 207 | "programId": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 208 | } 209 | ], 210 | "nativeTransfers": [ 211 | { 212 | "amount": 3000000, 213 | "fromUserAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 214 | "toUserAccount": "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe" 215 | }, 216 | { 217 | "amount": 2039280, 218 | "fromUserAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 219 | "toUserAccount": "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM" 220 | }, 221 | { 222 | "amount": 50000000, 223 | "fromUserAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 224 | "toUserAccount": "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh" 225 | }, 226 | { 227 | "amount": 500000, 228 | "fromUserAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 229 | "toUserAccount": "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" 230 | } 231 | ], 232 | "signature": "2atC6R6YW3a3myqXvbBpqKEnJYvaFMqytiw9nLinpGpuvjeAcahcqbyfrFQETkUDmaNTx6ePGgn3t6Hf67trSQTB", 233 | "slot": 278671220, 234 | "source": "SYSTEM_PROGRAM", 235 | "timestamp": 1721497600, 236 | "tokenTransfers": [ 237 | { 238 | "fromTokenAccount": "DT8Q5LsLoVUMUocCekfwaq18NSDXP6FQowBovF9t2oZA", 239 | "fromUserAccount": "9ePVoPdwXs98La8V3xNKrt9vUipyGRrJhXXtfFYXanzh", 240 | "mint": "2Ya8S3FPkaqu8ji6PaxKMSpJEdYYkbyax8eZMDkvpump", 241 | "toTokenAccount": "AcMKzvejA5S4jhxMd92PeCLz18CgQzV7bhGX5GUJBKCM", 242 | "toUserAccount": "2S8TtVBK8Aprg3Hw19EUoHmhxja8F94jN2pFFFZadA4M", 243 | "tokenAmount": 1071707.852766, 244 | "tokenStandard": "Fungible" 245 | } 246 | ], 247 | "transactionError": null, 248 | "type": "TRANSFER" 249 | } 250 | -------------------------------------------------------------------------------- /src/data/example-token-tx.json: -------------------------------------------------------------------------------- 1 | { 2 | "blockTime": 1710661161, 3 | "meta": { 4 | "computeUnitsConsumed": 66424, 5 | "err": null, 6 | "fee": 20005000, 7 | "innerInstructions": [ 8 | { 9 | "index": 2, 10 | "instructions": [ 11 | { 12 | "parsed": { 13 | "info": { 14 | "extensionTypes": ["immutableOwner"], 15 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso" 16 | }, 17 | "type": "getAccountDataSize" 18 | }, 19 | "program": "spl-token", 20 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 21 | "stackHeight": 2 22 | }, 23 | { 24 | "parsed": { 25 | "info": { 26 | "lamports": 2039280, 27 | "newAccount": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 28 | "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 29 | "source": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 30 | "space": 165 31 | }, 32 | "type": "createAccount" 33 | }, 34 | "program": "system", 35 | "programId": "11111111111111111111111111111111", 36 | "stackHeight": 2 37 | }, 38 | { 39 | "parsed": { 40 | "info": { 41 | "account": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS" 42 | }, 43 | "type": "initializeImmutableOwner" 44 | }, 45 | "program": "spl-token", 46 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 47 | "stackHeight": 2 48 | }, 49 | { 50 | "parsed": { 51 | "info": { 52 | "account": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 53 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 54 | "owner": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 55 | }, 56 | "type": "initializeAccount3" 57 | }, 58 | "program": "spl-token", 59 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 60 | "stackHeight": 2 61 | } 62 | ] 63 | }, 64 | { 65 | "index": 3, 66 | "instructions": [ 67 | { 68 | "parsed": { 69 | "info": { 70 | "amount": "4950000000", 71 | "authority": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 72 | "destination": "7KFnjT2QA3uDFJUGeyb9hpYAfGUgLuSXXuhSjKEhyv5W", 73 | "source": "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4" 74 | }, 75 | "type": "transfer" 76 | }, 77 | "program": "spl-token", 78 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 79 | "stackHeight": 2 80 | }, 81 | { 82 | "parsed": { 83 | "info": { 84 | "amount": "2679024146408", 85 | "authority": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 86 | "destination": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 87 | "source": "941esAZEhsLkmBxovSaJs4Y9nrDvuhZ4UEfLuGeja3vP" 88 | }, 89 | "type": "transfer" 90 | }, 91 | "program": "spl-token", 92 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 93 | "stackHeight": 2 94 | } 95 | ] 96 | } 97 | ], 98 | "logMessages": [ 99 | "Program 11111111111111111111111111111111 invoke [1]", 100 | "Program 11111111111111111111111111111111 success", 101 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", 102 | "Program log: Instruction: InitializeAccount", 103 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3443 of 1399850 compute units", 104 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 105 | "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]", 106 | "Program log: Create", 107 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 108 | "Program log: Instruction: GetAccountDataSize", 109 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1622 of 1383540 compute units", 110 | "Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=", 111 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 112 | "Program 11111111111111111111111111111111 invoke [2]", 113 | "Program 11111111111111111111111111111111 success", 114 | "Program log: Initialize the associated token account", 115 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 116 | "Program log: Instruction: InitializeImmutableOwner", 117 | "Program log: Please upgrade to SPL Token 2022 for immutable owner support", 118 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 1376900 compute units", 119 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 120 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 121 | "Program log: Instruction: InitializeAccount3", 122 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4241 of 1373018 compute units", 123 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 124 | "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 27913 of 1396407 compute units", 125 | "Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success", 126 | "Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [1]", 127 | "Program log: ray_log: A4ABCycBAAAAjtv9lkABAAABAAAAAAAAAIABCycBAAAAYXWOzoqCEgDG15aduggAAOhXSMJvAgAA", 128 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 129 | "Program log: Instruction: Transfer", 130 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 1350106 compute units", 131 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 132 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]", 133 | "Program log: Instruction: Transfer", 134 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 1342389 compute units", 135 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 136 | "Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 31553 of 1368494 compute units", 137 | "Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success", 138 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]", 139 | "Program log: Instruction: CloseAccount", 140 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2915 of 1336941 compute units", 141 | "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success", 142 | "Program 11111111111111111111111111111111 invoke [1]", 143 | "Program 11111111111111111111111111111111 success", 144 | "Program 11111111111111111111111111111111 invoke [1]", 145 | "Program 11111111111111111111111111111111 success", 146 | "Program ComputeBudget111111111111111111111111111111 invoke [1]", 147 | "Program ComputeBudget111111111111111111111111111111 success" 148 | ], 149 | "postBalances": [ 150 | 9020802230, 0, 2039280, 6124800, 23357760, 16258560, 2039280, 151 | 9602552887094, 3591360, 101977920, 101977920, 79594560, 2039280, 2039280, 152 | 7841203151219, 20946560, 1, 934087680, 487602132782, 731913600, 1461600, 153 | 1141440, 2440705783, 0, 1, 1009200, 1141440 154 | ], 155 | "postTokenBalances": [ 156 | { 157 | "accountIndex": 2, 158 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 159 | "owner": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 160 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 161 | "uiTokenAmount": { 162 | "amount": "2679024146408", 163 | "decimals": 6, 164 | "uiAmount": 2679024.146408, 165 | "uiAmountString": "2679024.146408" 166 | } 167 | }, 168 | { 169 | "accountIndex": 6, 170 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 171 | "owner": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 172 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 173 | "uiTokenAmount": { 174 | "amount": "5207403239185785", 175 | "decimals": 6, 176 | "uiAmount": 5207403239.185785, 177 | "uiAmountString": "5207403239.185785" 178 | } 179 | }, 180 | { 181 | "accountIndex": 7, 182 | "mint": "So11111111111111111111111111111111111111112", 183 | "owner": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 184 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 185 | "uiTokenAmount": { 186 | "amount": "9602550847814", 187 | "decimals": 9, 188 | "uiAmount": 9602.550847814, 189 | "uiAmountString": "9602.550847814" 190 | } 191 | }, 192 | { 193 | "accountIndex": 12, 194 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 195 | "owner": "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 196 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 197 | "uiTokenAmount": { 198 | "amount": "0", 199 | "decimals": 6, 200 | "uiAmount": null, 201 | "uiAmountString": "0" 202 | } 203 | }, 204 | { 205 | "accountIndex": 13, 206 | "mint": "So11111111111111111111111111111111111111112", 207 | "owner": "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 208 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 209 | "uiTokenAmount": { 210 | "amount": "0", 211 | "decimals": 9, 212 | "uiAmount": null, 213 | "uiAmountString": "0" 214 | } 215 | } 216 | ], 217 | "preBalances": [ 218 | 14062846510, 0, 0, 6124800, 23357760, 16258560, 2039280, 9597602887094, 219 | 3591360, 101977920, 101977920, 79594560, 2039280, 2039280, 7841153151219, 220 | 946560, 1, 934087680, 487602132782, 731913600, 1461600, 1141440, 221 | 2440705783, 0, 1, 1009200, 1141440 222 | ], 223 | "preTokenBalances": [ 224 | { 225 | "accountIndex": 6, 226 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 227 | "owner": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 228 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 229 | "uiTokenAmount": { 230 | "amount": "5210082263332193", 231 | "decimals": 6, 232 | "uiAmount": 5210082263.332193, 233 | "uiAmountString": "5210082263.332193" 234 | } 235 | }, 236 | { 237 | "accountIndex": 7, 238 | "mint": "So11111111111111111111111111111111111111112", 239 | "owner": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 240 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 241 | "uiTokenAmount": { 242 | "amount": "9597600847814", 243 | "decimals": 9, 244 | "uiAmount": 9597.600847814, 245 | "uiAmountString": "9597.600847814" 246 | } 247 | }, 248 | { 249 | "accountIndex": 12, 250 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 251 | "owner": "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 252 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 253 | "uiTokenAmount": { 254 | "amount": "0", 255 | "decimals": 6, 256 | "uiAmount": null, 257 | "uiAmountString": "0" 258 | } 259 | }, 260 | { 261 | "accountIndex": 13, 262 | "mint": "So11111111111111111111111111111111111111112", 263 | "owner": "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 264 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 265 | "uiTokenAmount": { 266 | "amount": "0", 267 | "decimals": 9, 268 | "uiAmount": null, 269 | "uiAmountString": "0" 270 | } 271 | } 272 | ], 273 | "rewards": [], 274 | "status": { 275 | "Ok": null 276 | } 277 | }, 278 | "slot": 254682129, 279 | "transaction": { 280 | "message": { 281 | "accountKeys": [ 282 | { 283 | "pubkey": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 284 | "signer": true, 285 | "source": "transaction", 286 | "writable": true 287 | }, 288 | { 289 | "pubkey": "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4", 290 | "signer": false, 291 | "source": "transaction", 292 | "writable": true 293 | }, 294 | { 295 | "pubkey": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 296 | "signer": false, 297 | "source": "transaction", 298 | "writable": true 299 | }, 300 | { 301 | "pubkey": "DAfbUFUYHbCmZT25eu6XB3GRSjWzZWJuN7c4SL6btPXc", 302 | "signer": false, 303 | "source": "transaction", 304 | "writable": true 305 | }, 306 | { 307 | "pubkey": "9WvWfYuq6SjWznJtJV3PUrEYifyZaiBfJJBcGrSgKhhe", 308 | "signer": false, 309 | "source": "transaction", 310 | "writable": true 311 | }, 312 | { 313 | "pubkey": "14DPBLokL8FiBhkSd8m1pBRkx6ZXAziyoYVw4kqaj4zt", 314 | "signer": false, 315 | "source": "transaction", 316 | "writable": true 317 | }, 318 | { 319 | "pubkey": "941esAZEhsLkmBxovSaJs4Y9nrDvuhZ4UEfLuGeja3vP", 320 | "signer": false, 321 | "source": "transaction", 322 | "writable": true 323 | }, 324 | { 325 | "pubkey": "7KFnjT2QA3uDFJUGeyb9hpYAfGUgLuSXXuhSjKEhyv5W", 326 | "signer": false, 327 | "source": "transaction", 328 | "writable": true 329 | }, 330 | { 331 | "pubkey": "9ff7MjVEjrZbMpzyKmKXxQhSrHxG6uftq7EQstPm3hjp", 332 | "signer": false, 333 | "source": "transaction", 334 | "writable": true 335 | }, 336 | { 337 | "pubkey": "FUvSMTE9Dtj7FvraUa4m7ewzmkDBExXy3iMDe5i3AQEP", 338 | "signer": false, 339 | "source": "transaction", 340 | "writable": true 341 | }, 342 | { 343 | "pubkey": "BzKb2ties2gKioDxUN1DcqaBqnEBwmKvuKdRAiuuL7X8", 344 | "signer": false, 345 | "source": "transaction", 346 | "writable": true 347 | }, 348 | { 349 | "pubkey": "6kdd9KshnhCK1v2mHgxjxphjKGBumjVZ9UsXWGKzR6k1", 350 | "signer": false, 351 | "source": "transaction", 352 | "writable": true 353 | }, 354 | { 355 | "pubkey": "3eMAVW9sawfSi1umKhsb1BJmq6NG9xjVhB8uhGiqgWQD", 356 | "signer": false, 357 | "source": "transaction", 358 | "writable": true 359 | }, 360 | { 361 | "pubkey": "A5k6EFoaNc8bB1DRpUKPj1q3WA8U4BySgziCpP9aqH2f", 362 | "signer": false, 363 | "source": "transaction", 364 | "writable": true 365 | }, 366 | { 367 | "pubkey": "AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH", 368 | "signer": false, 369 | "source": "transaction", 370 | "writable": true 371 | }, 372 | { 373 | "pubkey": "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 374 | "signer": false, 375 | "source": "transaction", 376 | "writable": true 377 | }, 378 | { 379 | "pubkey": "11111111111111111111111111111111", 380 | "signer": false, 381 | "source": "transaction", 382 | "writable": false 383 | }, 384 | { 385 | "pubkey": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 386 | "signer": false, 387 | "source": "transaction", 388 | "writable": false 389 | }, 390 | { 391 | "pubkey": "So11111111111111111111111111111111111111112", 392 | "signer": false, 393 | "source": "transaction", 394 | "writable": false 395 | }, 396 | { 397 | "pubkey": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", 398 | "signer": false, 399 | "source": "transaction", 400 | "writable": false 401 | }, 402 | { 403 | "pubkey": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 404 | "signer": false, 405 | "source": "transaction", 406 | "writable": false 407 | }, 408 | { 409 | "pubkey": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", 410 | "signer": false, 411 | "source": "transaction", 412 | "writable": false 413 | }, 414 | { 415 | "pubkey": "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 416 | "signer": false, 417 | "source": "transaction", 418 | "writable": false 419 | }, 420 | { 421 | "pubkey": "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 422 | "signer": false, 423 | "source": "transaction", 424 | "writable": false 425 | }, 426 | { 427 | "pubkey": "ComputeBudget111111111111111111111111111111", 428 | "signer": false, 429 | "source": "transaction", 430 | "writable": false 431 | }, 432 | { 433 | "pubkey": "SysvarRent111111111111111111111111111111111", 434 | "signer": false, 435 | "source": "lookupTable", 436 | "writable": false 437 | }, 438 | { 439 | "pubkey": "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX", 440 | "signer": false, 441 | "source": "lookupTable", 442 | "writable": false 443 | } 444 | ], 445 | "addressTableLookups": [ 446 | { 447 | "accountKey": "2immgwYNHBbyVQKVGCEkgWpi53bLwWNRMB5G2nbgYV17", 448 | "readonlyIndexes": [5, 11], 449 | "writableIndexes": [] 450 | } 451 | ], 452 | "instructions": [ 453 | { 454 | "parsed": { 455 | "info": { 456 | "base": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 457 | "lamports": 4952039280, 458 | "newAccount": "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4", 459 | "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 460 | "seed": "3B5YKKArrozGCux8Dk2Nf3Gabdeic7cd", 461 | "source": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 462 | "space": 165 463 | }, 464 | "type": "createAccountWithSeed" 465 | }, 466 | "program": "system", 467 | "programId": "11111111111111111111111111111111", 468 | "stackHeight": null 469 | }, 470 | { 471 | "parsed": { 472 | "info": { 473 | "account": "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4", 474 | "mint": "So11111111111111111111111111111111111111112", 475 | "owner": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 476 | "rentSysvar": "SysvarRent111111111111111111111111111111111" 477 | }, 478 | "type": "initializeAccount" 479 | }, 480 | "program": "spl-token", 481 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 482 | "stackHeight": null 483 | }, 484 | { 485 | "parsed": { 486 | "info": { 487 | "account": "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 488 | "mint": "12UP9cSwe1tDzQg3KSEx1BpSS9nkpT5VkmDe4fSz4Hso", 489 | "source": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 490 | "systemProgram": "11111111111111111111111111111111", 491 | "tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 492 | "wallet": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 493 | }, 494 | "type": "create" 495 | }, 496 | "program": "spl-associated-token-account", 497 | "programId": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", 498 | "stackHeight": null 499 | }, 500 | { 501 | "accounts": [ 502 | "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 503 | "DAfbUFUYHbCmZT25eu6XB3GRSjWzZWJuN7c4SL6btPXc", 504 | "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", 505 | "9WvWfYuq6SjWznJtJV3PUrEYifyZaiBfJJBcGrSgKhhe", 506 | "14DPBLokL8FiBhkSd8m1pBRkx6ZXAziyoYVw4kqaj4zt", 507 | "941esAZEhsLkmBxovSaJs4Y9nrDvuhZ4UEfLuGeja3vP", 508 | "7KFnjT2QA3uDFJUGeyb9hpYAfGUgLuSXXuhSjKEhyv5W", 509 | "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX", 510 | "9ff7MjVEjrZbMpzyKmKXxQhSrHxG6uftq7EQstPm3hjp", 511 | "FUvSMTE9Dtj7FvraUa4m7ewzmkDBExXy3iMDe5i3AQEP", 512 | "BzKb2ties2gKioDxUN1DcqaBqnEBwmKvuKdRAiuuL7X8", 513 | "6kdd9KshnhCK1v2mHgxjxphjKGBumjVZ9UsXWGKzR6k1", 514 | "3eMAVW9sawfSi1umKhsb1BJmq6NG9xjVhB8uhGiqgWQD", 515 | "A5k6EFoaNc8bB1DRpUKPj1q3WA8U4BySgziCpP9aqH2f", 516 | "B2gpsCasUDDcVHtjt73AgFv1ynJ5EDNeYH1m9yXaxkRs", 517 | "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4", 518 | "Fnux2fh76WvP6oR6PbgWezeKeFptMPGzHVrENgcVESXS", 519 | "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 520 | ], 521 | "data": "6BKE5M3EVvm14Q17foao4PR", 522 | "programId": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8", 523 | "stackHeight": null 524 | }, 525 | { 526 | "parsed": { 527 | "info": { 528 | "account": "HSK294jYwvagsFiGVyedYsJCBb2nyDGVXG9VKzh4cvz4", 529 | "destination": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL", 530 | "owner": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 531 | }, 532 | "type": "closeAccount" 533 | }, 534 | "program": "spl-token", 535 | "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", 536 | "stackHeight": null 537 | }, 538 | { 539 | "parsed": { 540 | "info": { 541 | "destination": "AVUCZyuT35YSuj4RH7fwiyPu82Djn2Hfg7y2ND2XcnZH", 542 | "lamports": 50000000, 543 | "source": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 544 | }, 545 | "type": "transfer" 546 | }, 547 | "program": "system", 548 | "programId": "11111111111111111111111111111111", 549 | "stackHeight": null 550 | }, 551 | { 552 | "parsed": { 553 | "info": { 554 | "destination": "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 555 | "lamports": 20000000, 556 | "source": "BDXHMF8Vy76G7YsZCvjUjgRCPYVBUnPb2DcXm1j8jWsL" 557 | }, 558 | "type": "transfer" 559 | }, 560 | "program": "system", 561 | "programId": "11111111111111111111111111111111", 562 | "stackHeight": null 563 | }, 564 | { 565 | "accounts": [], 566 | "data": "3e3VMPiYzXbV", 567 | "programId": "ComputeBudget111111111111111111111111111111", 568 | "stackHeight": null 569 | } 570 | ], 571 | "recentBlockhash": "38eF5Nyj48F89Gmww22e6KmLMMabtuV4ik1whF33VfwY" 572 | }, 573 | "signatures": [ 574 | "4QX7HTbyj64YBgNB4p4Ga94Br4hi2c9teKYQVzS3f5EQJ2BiGz6JPAfkfdpidJFa8o2W57YtgzUsdDrmfGrekh9w" 575 | ] 576 | }, 577 | "version": 0 578 | } 579 | -------------------------------------------------------------------------------- /src/data/pumpfun-idl.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "name": "pump", 4 | "instructions": [ 5 | { 6 | "name": "initialize", 7 | "docs": ["Creates the global state."], 8 | "accounts": [ 9 | { 10 | "name": "global", 11 | "isMut": true, 12 | "isSigner": false 13 | }, 14 | { 15 | "name": "user", 16 | "isMut": true, 17 | "isSigner": true 18 | }, 19 | { 20 | "name": "systemProgram", 21 | "isMut": false, 22 | "isSigner": false 23 | } 24 | ], 25 | "args": [] 26 | }, 27 | { 28 | "name": "setParams", 29 | "docs": ["Sets the global state parameters."], 30 | "accounts": [ 31 | { 32 | "name": "global", 33 | "isMut": true, 34 | "isSigner": false 35 | }, 36 | { 37 | "name": "user", 38 | "isMut": true, 39 | "isSigner": true 40 | }, 41 | { 42 | "name": "systemProgram", 43 | "isMut": false, 44 | "isSigner": false 45 | }, 46 | { 47 | "name": "eventAuthority", 48 | "isMut": false, 49 | "isSigner": false 50 | }, 51 | { 52 | "name": "program", 53 | "isMut": false, 54 | "isSigner": false 55 | } 56 | ], 57 | "args": [ 58 | { 59 | "name": "feeRecipient", 60 | "type": "publicKey" 61 | }, 62 | { 63 | "name": "initialVirtualTokenReserves", 64 | "type": "u64" 65 | }, 66 | { 67 | "name": "initialVirtualSolReserves", 68 | "type": "u64" 69 | }, 70 | { 71 | "name": "initialRealTokenReserves", 72 | "type": "u64" 73 | }, 74 | { 75 | "name": "tokenTotalSupply", 76 | "type": "u64" 77 | }, 78 | { 79 | "name": "feeBasisPoints", 80 | "type": "u64" 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "create", 86 | "docs": ["Creates a new coin and bonding curve."], 87 | "accounts": [ 88 | { 89 | "name": "mint", 90 | "isMut": true, 91 | "isSigner": true 92 | }, 93 | { 94 | "name": "mintAuthority", 95 | "isMut": false, 96 | "isSigner": false 97 | }, 98 | { 99 | "name": "bondingCurve", 100 | "isMut": true, 101 | "isSigner": false 102 | }, 103 | { 104 | "name": "associatedBondingCurve", 105 | "isMut": true, 106 | "isSigner": false 107 | }, 108 | { 109 | "name": "global", 110 | "isMut": false, 111 | "isSigner": false 112 | }, 113 | { 114 | "name": "mplTokenMetadata", 115 | "isMut": false, 116 | "isSigner": false 117 | }, 118 | { 119 | "name": "metadata", 120 | "isMut": true, 121 | "isSigner": false 122 | }, 123 | { 124 | "name": "user", 125 | "isMut": true, 126 | "isSigner": true 127 | }, 128 | { 129 | "name": "systemProgram", 130 | "isMut": false, 131 | "isSigner": false 132 | }, 133 | { 134 | "name": "tokenProgram", 135 | "isMut": false, 136 | "isSigner": false 137 | }, 138 | { 139 | "name": "associatedTokenProgram", 140 | "isMut": false, 141 | "isSigner": false 142 | }, 143 | { 144 | "name": "rent", 145 | "isMut": false, 146 | "isSigner": false 147 | }, 148 | { 149 | "name": "eventAuthority", 150 | "isMut": false, 151 | "isSigner": false 152 | }, 153 | { 154 | "name": "program", 155 | "isMut": false, 156 | "isSigner": false 157 | } 158 | ], 159 | "args": [ 160 | { 161 | "name": "name", 162 | "type": "string" 163 | }, 164 | { 165 | "name": "symbol", 166 | "type": "string" 167 | }, 168 | { 169 | "name": "uri", 170 | "type": "string" 171 | } 172 | ] 173 | }, 174 | { 175 | "name": "buy", 176 | "docs": ["Buys tokens from a bonding curve."], 177 | "accounts": [ 178 | { 179 | "name": "global", 180 | "isMut": false, 181 | "isSigner": false 182 | }, 183 | { 184 | "name": "feeRecipient", 185 | "isMut": true, 186 | "isSigner": false 187 | }, 188 | { 189 | "name": "mint", 190 | "isMut": false, 191 | "isSigner": false 192 | }, 193 | { 194 | "name": "bondingCurve", 195 | "isMut": true, 196 | "isSigner": false 197 | }, 198 | { 199 | "name": "associatedBondingCurve", 200 | "isMut": true, 201 | "isSigner": false 202 | }, 203 | { 204 | "name": "associatedUser", 205 | "isMut": true, 206 | "isSigner": false 207 | }, 208 | { 209 | "name": "user", 210 | "isMut": true, 211 | "isSigner": true 212 | }, 213 | { 214 | "name": "systemProgram", 215 | "isMut": false, 216 | "isSigner": false 217 | }, 218 | { 219 | "name": "tokenProgram", 220 | "isMut": false, 221 | "isSigner": false 222 | }, 223 | { 224 | "name": "rent", 225 | "isMut": false, 226 | "isSigner": false 227 | }, 228 | { 229 | "name": "eventAuthority", 230 | "isMut": false, 231 | "isSigner": false 232 | }, 233 | { 234 | "name": "program", 235 | "isMut": false, 236 | "isSigner": false 237 | } 238 | ], 239 | "args": [ 240 | { 241 | "name": "amount", 242 | "type": "u64" 243 | }, 244 | { 245 | "name": "maxSolCost", 246 | "type": "u64" 247 | } 248 | ] 249 | }, 250 | { 251 | "name": "sell", 252 | "docs": ["Sells tokens into a bonding curve."], 253 | "accounts": [ 254 | { 255 | "name": "global", 256 | "isMut": false, 257 | "isSigner": false 258 | }, 259 | { 260 | "name": "feeRecipient", 261 | "isMut": true, 262 | "isSigner": false 263 | }, 264 | { 265 | "name": "mint", 266 | "isMut": false, 267 | "isSigner": false 268 | }, 269 | { 270 | "name": "bondingCurve", 271 | "isMut": true, 272 | "isSigner": false 273 | }, 274 | { 275 | "name": "associatedBondingCurve", 276 | "isMut": true, 277 | "isSigner": false 278 | }, 279 | { 280 | "name": "associatedUser", 281 | "isMut": true, 282 | "isSigner": false 283 | }, 284 | { 285 | "name": "user", 286 | "isMut": true, 287 | "isSigner": true 288 | }, 289 | { 290 | "name": "systemProgram", 291 | "isMut": false, 292 | "isSigner": false 293 | }, 294 | { 295 | "name": "associatedTokenProgram", 296 | "isMut": false, 297 | "isSigner": false 298 | }, 299 | { 300 | "name": "tokenProgram", 301 | "isMut": false, 302 | "isSigner": false 303 | }, 304 | { 305 | "name": "eventAuthority", 306 | "isMut": false, 307 | "isSigner": false 308 | }, 309 | { 310 | "name": "program", 311 | "isMut": false, 312 | "isSigner": false 313 | } 314 | ], 315 | "args": [ 316 | { 317 | "name": "amount", 318 | "type": "u64" 319 | }, 320 | { 321 | "name": "minSolOutput", 322 | "type": "u64" 323 | } 324 | ] 325 | }, 326 | { 327 | "name": "withdraw", 328 | "docs": [ 329 | "Allows the admin to withdraw liquidity for a migration once the bonding curve completes" 330 | ], 331 | "accounts": [ 332 | { 333 | "name": "global", 334 | "isMut": false, 335 | "isSigner": false 336 | }, 337 | { 338 | "name": "lastWithdraw", 339 | "isMut": true, 340 | "isSigner": false 341 | }, 342 | { 343 | "name": "mint", 344 | "isMut": false, 345 | "isSigner": false 346 | }, 347 | { 348 | "name": "bondingCurve", 349 | "isMut": true, 350 | "isSigner": false 351 | }, 352 | { 353 | "name": "associatedBondingCurve", 354 | "isMut": true, 355 | "isSigner": false 356 | }, 357 | { 358 | "name": "associatedUser", 359 | "isMut": true, 360 | "isSigner": false 361 | }, 362 | { 363 | "name": "user", 364 | "isMut": true, 365 | "isSigner": true 366 | }, 367 | { 368 | "name": "systemProgram", 369 | "isMut": false, 370 | "isSigner": false 371 | }, 372 | { 373 | "name": "tokenProgram", 374 | "isMut": false, 375 | "isSigner": false 376 | }, 377 | { 378 | "name": "rent", 379 | "isMut": false, 380 | "isSigner": false 381 | }, 382 | { 383 | "name": "eventAuthority", 384 | "isMut": false, 385 | "isSigner": false 386 | }, 387 | { 388 | "name": "program", 389 | "isMut": false, 390 | "isSigner": false 391 | } 392 | ], 393 | "args": [] 394 | } 395 | ], 396 | "accounts": [ 397 | { 398 | "name": "Global", 399 | "type": { 400 | "kind": "struct", 401 | "fields": [ 402 | { 403 | "name": "initialized", 404 | "type": "bool" 405 | }, 406 | { 407 | "name": "authority", 408 | "type": "publicKey" 409 | }, 410 | { 411 | "name": "feeRecipient", 412 | "type": "publicKey" 413 | }, 414 | { 415 | "name": "initialVirtualTokenReserves", 416 | "type": "u64" 417 | }, 418 | { 419 | "name": "initialVirtualSolReserves", 420 | "type": "u64" 421 | }, 422 | { 423 | "name": "initialRealTokenReserves", 424 | "type": "u64" 425 | }, 426 | { 427 | "name": "tokenTotalSupply", 428 | "type": "u64" 429 | }, 430 | { 431 | "name": "feeBasisPoints", 432 | "type": "u64" 433 | } 434 | ] 435 | } 436 | }, 437 | { 438 | "name": "LastWithdraw", 439 | "type": { 440 | "kind": "struct", 441 | "fields": [ 442 | { 443 | "name": "lastWithdrawTimestamp", 444 | "type": "i64" 445 | } 446 | ] 447 | } 448 | }, 449 | { 450 | "name": "BondingCurve", 451 | "type": { 452 | "kind": "struct", 453 | "fields": [ 454 | { 455 | "name": "virtualTokenReserves", 456 | "type": "u64" 457 | }, 458 | { 459 | "name": "virtualSolReserves", 460 | "type": "u64" 461 | }, 462 | { 463 | "name": "realTokenReserves", 464 | "type": "u64" 465 | }, 466 | { 467 | "name": "realSolReserves", 468 | "type": "u64" 469 | }, 470 | { 471 | "name": "tokenTotalSupply", 472 | "type": "u64" 473 | }, 474 | { 475 | "name": "complete", 476 | "type": "bool" 477 | } 478 | ] 479 | } 480 | } 481 | ], 482 | "events": [ 483 | { 484 | "name": "CreateEvent", 485 | "fields": [ 486 | { 487 | "name": "name", 488 | "type": "string", 489 | "index": false 490 | }, 491 | { 492 | "name": "symbol", 493 | "type": "string", 494 | "index": false 495 | }, 496 | { 497 | "name": "uri", 498 | "type": "string", 499 | "index": false 500 | }, 501 | { 502 | "name": "mint", 503 | "type": "publicKey", 504 | "index": false 505 | }, 506 | { 507 | "name": "bondingCurve", 508 | "type": "publicKey", 509 | "index": false 510 | }, 511 | { 512 | "name": "user", 513 | "type": "publicKey", 514 | "index": false 515 | } 516 | ] 517 | }, 518 | { 519 | "name": "TradeEvent", 520 | "fields": [ 521 | { 522 | "name": "mint", 523 | "type": "publicKey", 524 | "index": false 525 | }, 526 | { 527 | "name": "solAmount", 528 | "type": "u64", 529 | "index": false 530 | }, 531 | { 532 | "name": "tokenAmount", 533 | "type": "u64", 534 | "index": false 535 | }, 536 | { 537 | "name": "isBuy", 538 | "type": "bool", 539 | "index": false 540 | }, 541 | { 542 | "name": "user", 543 | "type": "publicKey", 544 | "index": false 545 | }, 546 | { 547 | "name": "timestamp", 548 | "type": "i64", 549 | "index": false 550 | }, 551 | { 552 | "name": "virtualSolReserves", 553 | "type": "u64", 554 | "index": false 555 | }, 556 | { 557 | "name": "virtualTokenReserves", 558 | "type": "u64", 559 | "index": false 560 | }, 561 | { 562 | "name": "realSolReserves", 563 | "type": "u64", 564 | "index": false 565 | }, 566 | { 567 | "name": "realTokenReserves", 568 | "type": "u64", 569 | "index": false 570 | } 571 | ] 572 | }, 573 | { 574 | "name": "CompleteEvent", 575 | "fields": [ 576 | { 577 | "name": "user", 578 | "type": "publicKey", 579 | "index": false 580 | }, 581 | { 582 | "name": "mint", 583 | "type": "publicKey", 584 | "index": false 585 | }, 586 | { 587 | "name": "bondingCurve", 588 | "type": "publicKey", 589 | "index": false 590 | }, 591 | { 592 | "name": "timestamp", 593 | "type": "i64", 594 | "index": false 595 | } 596 | ] 597 | }, 598 | { 599 | "name": "SetParamsEvent", 600 | "fields": [ 601 | { 602 | "name": "feeRecipient", 603 | "type": "publicKey", 604 | "index": false 605 | }, 606 | { 607 | "name": "initialVirtualTokenReserves", 608 | "type": "u64", 609 | "index": false 610 | }, 611 | { 612 | "name": "initialVirtualSolReserves", 613 | "type": "u64", 614 | "index": false 615 | }, 616 | { 617 | "name": "initialRealTokenReserves", 618 | "type": "u64", 619 | "index": false 620 | }, 621 | { 622 | "name": "tokenTotalSupply", 623 | "type": "u64", 624 | "index": false 625 | }, 626 | { 627 | "name": "feeBasisPoints", 628 | "type": "u64", 629 | "index": false 630 | } 631 | ] 632 | } 633 | ], 634 | "errors": [ 635 | { 636 | "code": 6000, 637 | "name": "NotAuthorized", 638 | "msg": "The given account is not authorized to execute this instruction." 639 | }, 640 | { 641 | "code": 6001, 642 | "name": "AlreadyInitialized", 643 | "msg": "The program is already initialized." 644 | }, 645 | { 646 | "code": 6002, 647 | "name": "TooMuchSolRequired", 648 | "msg": "slippage: Too much SOL required to buy the given amount of tokens." 649 | }, 650 | { 651 | "code": 6003, 652 | "name": "TooLittleSolReceived", 653 | "msg": "slippage: Too little SOL received to sell the given amount of tokens." 654 | }, 655 | { 656 | "code": 6004, 657 | "name": "MintDoesNotMatchBondingCurve", 658 | "msg": "The mint does not match the bonding curve." 659 | }, 660 | { 661 | "code": 6005, 662 | "name": "BondingCurveComplete", 663 | "msg": "The bonding curve has completed and liquidity migrated to raydium." 664 | }, 665 | { 666 | "code": 6006, 667 | "name": "BondingCurveNotComplete", 668 | "msg": "The bonding curve has not completed." 669 | }, 670 | { 671 | "code": 6007, 672 | "name": "NotInitialized", 673 | "msg": "The program is not initialized." 674 | }, 675 | { 676 | "code": 6008, 677 | "name": "WithdrawTooFrequent", 678 | "msg": "Withdraw too frequent" 679 | } 680 | ] 681 | } 682 | -------------------------------------------------------------------------------- /src/discord.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Client, 3 | Events, 4 | GatewayIntentBits, 5 | Message, 6 | TextChannel, 7 | SlashCommandBuilder, 8 | REST, 9 | Routes, 10 | ActivityType, 11 | } from "discord.js" 12 | import sql from "./lib/postgres" 13 | import { configDotenv } from "dotenv" 14 | import { decrypt, getSolanaPrice, heliusRpcUrl } from "./lib/utils" 15 | import { Connection, Keypair } from "@solana/web3.js" 16 | import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes" 17 | import { TOKEN_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token" 18 | configDotenv() 19 | 20 | const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN as string 21 | 22 | const GUILD_ID = "1215040919313580132" 23 | const ROLE_ID = "1262242684224147487" 24 | const LOGS_CHANNEL_ID = "1219020566640726076" 25 | 26 | const client = new Client({ 27 | intents: [ 28 | GatewayIntentBits.Guilds, 29 | GatewayIntentBits.GuildMessages, 30 | GatewayIntentBits.DirectMessages, 31 | GatewayIntentBits.DirectMessageReactions, 32 | GatewayIntentBits.GuildMembers, 33 | GatewayIntentBits.GuildMessageReactions, 34 | GatewayIntentBits.MessageContent, 35 | ], 36 | }) 37 | client.login(DISCORD_BOT_TOKEN) 38 | 39 | if (!client) process.exit(1) 40 | 41 | client.on("ready", () => { 42 | console.log(`Logged in as ${client.user?.tag}!`) 43 | }) 44 | 45 | const connection = new Connection(process.env.RPC_URL as string) 46 | // Handle messages 47 | // client.on(Events.MessageCreate, async (message: Message) => { 48 | // console.log(message.content) 49 | 50 | // const guild = message.guild 51 | // const user = message.author // Example: Getting the message author. Adjust as needed. 52 | 53 | // // verify if its not a bot 54 | // if (user.bot || !message.member) return 55 | 56 | // if (!message.member.roles.cache.has(ROLE_ID)) { 57 | // try { 58 | // await message.member.roles.add(ROLE_ID) 59 | // // send a message to another channel 60 | // const channel = guild?.channels.cache.get(LOGS_CHANNEL_ID) as TextChannel 61 | 62 | // channel.send(`Added the role to ${user.username}`) 63 | // } catch (e) { 64 | // // Handle errors here 65 | // console.error(e) 66 | 67 | // const channel = guild?.channels.cache.get(LOGS_CHANNEL_ID) as TextChannel 68 | 69 | // channel.send(`Failed to add the role to ${user.username}`) 70 | // } 71 | // } else { 72 | // console.log(`User ${user.username} already has the role`) 73 | // } 74 | // }) 75 | 76 | // const usersToGiveRole = [ 77 | // ] 78 | 79 | // give role to users 80 | // client.on(Events.ClientReady, async () => { 81 | // const guild = client.guilds.cache.get(GUILD_ID) 82 | 83 | // if (!guild) { 84 | // console.error("Guild not found") 85 | // return 86 | // } 87 | 88 | // const role = guild.roles.cache.get(ROLE_ID) 89 | 90 | // if (!role) { 91 | // console.error("Role not found") 92 | // return 93 | // } 94 | 95 | // await guild.members.fetch() 96 | // console.log(guild.members.cache.size) 97 | // for (const username of usersToGiveRole) { 98 | // const member = guild.members.cache.find( 99 | // (member) => member.user.username === username 100 | // ) 101 | 102 | // if (!member) { 103 | // console.error(`User ${username} not found`) 104 | // continue 105 | // } 106 | 107 | // if (!member.roles.cache.has(ROLE_ID)) { 108 | // try { 109 | // await member.roles.add(ROLE_ID) 110 | // console.log(`Added the role to ${username}`) 111 | // } catch (e) { 112 | // // Handle errors here 113 | // console.error(e) 114 | // console.log(`Failed to add the role to ${username}`) 115 | // } 116 | // } else { 117 | // console.log(`User ${username} already has the role`) 118 | // } 119 | // } 120 | // }) 121 | 122 | // client.on(Events.ClientReady, async () => { 123 | // const guild = client.guilds.cache.get(GUILD_ID) 124 | 125 | // if (!guild) { 126 | // console.error("Guild not found") 127 | // return 128 | // } 129 | 130 | // const OG_ROLE = "1262242684224147487" 131 | // const role = guild.roles.cache.get(OG_ROLE) 132 | 133 | // if (!role) { 134 | // console.error("Role not found") 135 | // return 136 | // } 137 | 138 | // await guild.members.fetch() 139 | // console.log(guild.members.cache.size) 140 | 141 | // const ogMembers = [] 142 | 143 | // for (const [, { user }] of guild.members.cache) { 144 | // const member = guild.members.cache.find( 145 | // (member) => member.user.id === user.id 146 | // ) 147 | 148 | // if (!member) { 149 | // console.error(`User ${user.id} not found`) 150 | // continue 151 | // } 152 | 153 | // if (member.roles.cache.has(OG_ROLE)) { 154 | // ogMembers.push(user.username) 155 | // } 156 | // // if (!member.roles.cache.has(ROLE_ID)) { 157 | // // try { 158 | // // await member.roles.add(ROLE_ID) 159 | // // console.log(`Added the role to ${username}`) 160 | // // } catch (e) { 161 | // // // Handle errors here 162 | // // console.error(e) 163 | // // console.log(`Failed to add the role to ${username}`) 164 | // // } 165 | // // } else { 166 | // // console.log(`User ${username} already has the role`) 167 | // // } 168 | // } 169 | 170 | // console.log(ogMembers) 171 | // }) 172 | 173 | // FAQ msg 174 | // client.on(Events.ClientReady, async () => { 175 | // const guild = client.guilds.cache.get(GUILD_ID) 176 | 177 | // const faqChannelId = "1268638132262146151" 178 | // const channel = guild?.channels.cache.get(faqChannelId) as TextChannel 179 | // channel.send({ 180 | // embeds: [ 181 | // { 182 | // title: "FAQ", 183 | // description: "Please read the FAQ before asking questions", 184 | // fields: [ 185 | // { 186 | // name: "Q: How does Moonbot work?", 187 | // value: 188 | // "A: Simply deposit some SOL into your Moonbot wallet. Once deposited, type `/enable` in the Discord server and the bot will start sniping tokens automatically. Type `/enable` again to disable. The bot's algorithm handles both buying and selling of tokens without any manual intervention.", 189 | // }, 190 | // { 191 | // name: "Q: What is the recommended budget?", 192 | // value: `A: Recommended budget: 1-2 SOL per week, or 0.15-0.3 SOL per day, to start. 193 | 194 | // The recommended budget is the amount of SOL you should deposit to your Moonbot wallet to snipe tokens. It is calculated based on the entry size and the average number of tokens the bot buys per day. 195 | 196 | // If you deposit less than the recommended amount, the bot will have less chance of profiting.`, 197 | // }, 198 | // { 199 | // name: "Q: What is the Moonbot wallet?", 200 | // value: 201 | // "A: The Moonbot wallet is your personal wallet. You can deposit and withdraw SOL just like any other wallet. It uses AES256 encryption for security but should be treated as a hot wallet. Withdraw your SOL once you have enough and keep only what you want to spend with the bot. The wallet is not stored in the database and is used in runtime with powerful encryption.", 202 | // }, 203 | // { 204 | // name: "Q: How do I activate the bot?", 205 | // value: 206 | // "A: If you have a balance in your wallet, type `/enable` in the Discord server and the bot will start sniping tokens. If your wallet is empty, deposit SOL to enable the bot. Your Moonbot wallet address can be copied for deposits.", 207 | // }, 208 | // // { 209 | // // name: "Q: What are the fees and plans available?", 210 | // // value: 211 | // // "A: **Trial Plan:** No upfront payment, fixed settings, lasts for a week. The minimum entry size is 0.002 SOL to avoid excessive token account rent fees.\n**Premium Plan:** No usage fee besides membership payment, allows adjustable settings such as entry size, and includes additional features.", 212 | // // }, 213 | // // { 214 | // // name: "Q: How are profits and losses estimated?", 215 | // // value: 216 | // // "A: Moonbot's benchmarks show an average of 70% profit per week, with potential for higher gains up to 150% in good weeks. The maximum estimated loss in a bad week is around 20%. We have enough data to support these estimates, and the bot's early buying strategy ensures that even in bad weeks, some value is retained. Also, you'll never lose your whole investment, with the maximum risk estimated at 20% per week.", 217 | // // }, 218 | // // { 219 | // // name: "Q: What is the token account rent fee?", 220 | // // value: 221 | // // "A: For each token bought, a 0.002 SOL fee is incurred for the token account rent from the Solana Blockchain, not from Moonbot. This fee will be returned when the token is sold and the token account is closed. While this can decrease your wallet SOL throughout the week, you shouldn't worry as you will get it back. This fee is accounted for in our weekly profit and loss estimations.", 222 | // // }, 223 | // // { 224 | // // name: "Q: How does the sell strategy work?", 225 | // // value: 226 | // // "A: Moonbot uses an algorithm to sell tokens automatically. After the tokens are sold, the token accounts are closed, and the account rent fee is returned. This ensures you get back the SOL spent on token account rent.", 227 | // // }, 228 | // { 229 | // name: "Q: Why does my SOL balance decrease during the week?", 230 | // value: 231 | // "A: You may notice your wallet SOL decreasing throughout the week due to the token account rent fees (0.002 SOL per token) from the Solana Blockchain. This is normal and shouldn't be a concern as these fees are returned when the bot sells the tokens and closes the token accounts. Don't worry if your balance starts going down; it's normal and lots of SOL can be redeemed back once the bot starts selling.", 232 | // }, 233 | // // { 234 | // // name: "Q: Can I deposit tokens into my Moonbot wallet?", 235 | // // value: 236 | // // "A: Yes, you can deposit tokens into your Moonbot wallet, but it is generally unnecessary. The main function of the wallet is to facilitate SOL transactions for sniping tokens.", 237 | // // }, 238 | // // { 239 | // // name: "Q: What are the default settings in the trial plan?", 240 | // // value: 241 | // // "A: In the trial plan, settings are fixed. The buy strategy is automatic with a default entry size of 0.002 SOL per token purchase. The recommended budget is 3 SOL per week, or 0.7 SOL per day. These settings are designed to provide an optimal balance between cost and performance.", 242 | // // }, 243 | // { 244 | // name: "Q: What is the sell strategy?", 245 | // value: 246 | // "A: The sell strategy is automatic and based on benchmark data. While you can sell tokens on your own, it is recommended to follow the automatic strategy for optimal results.", 247 | // }, 248 | // // { 249 | // // name: "Q: What are the benefits of the premium plan?", 250 | // // value: 251 | // // "A: The premium plan, which will be available soon, allows you to adjust settings such as entry size and removes the per-use fee in favor of a membership payment. It also includes additional features as the product matures.", 252 | // // }, 253 | // { 254 | // name: "Q: How should I manage my wallet security?", 255 | // value: 256 | // "A: Even though the Moonbot wallet uses AES256 encryption, treat it as a hot wallet. Only keep the SOL you intend to use with the bot in this wallet. Regularly withdraw any excess SOL to ensure your funds are secure.", 257 | // }, 258 | // // { 259 | // // name: "Q: How can I ensure the bot doesn't run out of balance?", 260 | // // value: 261 | // // "A: It's important not to let the bot run out of SOL balance. The bot could miss a highly profitable token if it runs out of funds at a critical time. Make sure you leave enough balance for the day or, even better, for the whole week as the recommended budget suggests. If your balance starts going down, it's mostly because of the Solana fees, which can be redeemed. For those with a low budget, we are working on improving the sell script to redeem SOL daily.", 262 | // // }, 263 | // { 264 | // name: "Q: What should I do if I have more questions?", 265 | // value: 266 | // "A: Feel free to ask any questions you may have about Moonbot, its wallet, plans, profits, or any other details. We're here to help!", 267 | // }, 268 | // ], 269 | // author: { 270 | // name: "Moonbot", 271 | // icon_url: "https://www.mooners.xyz/moonbot480.png", 272 | // }, 273 | // thumbnail: { 274 | // url: "https://i.imgur.com/NJeRMPu.jpeg", 275 | // width: 64, 276 | // height: 64, 277 | // }, 278 | // }, 279 | // ], 280 | // }) 281 | // }) 282 | 283 | // const DISCORD_APPLICATION_ID = "1219008065718714429" 284 | // ;(async () => { 285 | // const rest = new REST().setToken(DISCORD_BOT_TOKEN) 286 | 287 | // const command = new SlashCommandBuilder() 288 | // .setName("wallet") 289 | // .setDescription("Get information about your Moonbot wallet") 290 | // console.log(command.toJSON()) 291 | 292 | // const command2 = new SlashCommandBuilder() 293 | // .setName("enable") 294 | // .setDescription("Enable or disable Moonbot") 295 | // console.log(command2.toJSON()) 296 | 297 | // await rest.put( 298 | // Routes.applicationGuildCommands(DISCORD_APPLICATION_ID, GUILD_ID), 299 | // { body: [command.toJSON(), command2.toJSON()] } 300 | // ) 301 | // })() 302 | 303 | const MOONBOTTER_ROLE_ID = "1266172172402036848" 304 | client.on(Events.InteractionCreate, async (interaction) => { 305 | if (!interaction.isChatInputCommand()) return 306 | await interaction.deferReply({ ephemeral: true }) 307 | 308 | try { 309 | const userId = interaction.user.id 310 | 311 | const allUsers = await sql`SELECT for_user from moonbot_invite_codes` 312 | const forUser = allUsers.find( 313 | (user) => user.for_user.indexOf(userId) !== -1 314 | )?.for_user 315 | 316 | if (!forUser) { 317 | interaction.editReply({ 318 | content: "You need to have an invite code to use Moonbot!", 319 | }) 320 | return true 321 | } 322 | 323 | switch (interaction.commandName) { 324 | case "enable": 325 | const isEnabled = ( 326 | await sql` 327 | SELECT enabled from moonbot_invite_codes where for_user = ${forUser} 328 | ` 329 | )[0]?.enabled 330 | 331 | console.log(forUser, "isEnabled", isEnabled, "will enable", !isEnabled) 332 | // If enabled, disabled it. If disabled, enable it 333 | if (isEnabled) { 334 | await sql` 335 | UPDATE moonbot_invite_codes 336 | SET enabled = false 337 | WHERE for_user = ${forUser} 338 | ` 339 | 340 | // Remove role 341 | const guild = client.guilds.cache.get(GUILD_ID) 342 | const member = guild?.members.cache.get(userId) 343 | member?.roles.remove(MOONBOTTER_ROLE_ID) 344 | interaction.editReply({ 345 | content: "Moonbot has been disabled for your account!", 346 | }) 347 | } else { 348 | await sql` 349 | UPDATE moonbot_invite_codes 350 | SET enabled = true 351 | WHERE for_user = ${forUser} 352 | ` 353 | // Add role 354 | const guild = client.guilds.cache.get(GUILD_ID) 355 | const member = guild?.members.cache.get(userId) 356 | member?.roles.add(MOONBOTTER_ROLE_ID) 357 | interaction.editReply({ 358 | content: "Moonbot has been enabled for your account!", 359 | }) 360 | } 361 | 362 | return true 363 | 364 | case "wallet": 365 | const solanaPrice = await getSolanaPrice() 366 | // Fetch the user's wallet information 367 | const userWallet = await sql` 368 | SELECT keypair FROM moonbot_invite_codes WHERE for_user = ${forUser} AND enabled = true 369 | ` 370 | 371 | if (!userWallet.length) { 372 | interaction.editReply({ 373 | content: "No active wallet found for your account!", 374 | }) 375 | return true 376 | } 377 | 378 | const decryptedKeypair = await decrypt(userWallet[0].keypair) 379 | const kp = Keypair.fromSecretKey(bs58.decode(decryptedKeypair)) 380 | const solBalance = (await connection.getBalance(kp.publicKey)) / 1e9 381 | 382 | const walletTokenAccounts = 383 | await connection.getParsedTokenAccountsByOwner(kp.publicKey, { 384 | programId: TOKEN_PROGRAM_ID, 385 | }) 386 | 387 | const tokens: { mint: string; amount: number }[] = [] 388 | walletTokenAccounts.value.forEach((tokenAccount) => { 389 | const { info } = tokenAccount.account.data.parsed 390 | const amount = info.tokenAmount.uiAmount 391 | if (amount > 0) { 392 | tokens.push({ mint: info.mint, amount }) 393 | } 394 | }) 395 | 396 | const rentValue = walletTokenAccounts.value.length * 0.002 397 | 398 | const tokenMintsArray = tokens.map((token) => token.mint) 399 | const tokenPrices = await sql< 400 | { token_mint: string; price: number }[] 401 | >`SELECT DISTINCT ON (token_mint) token_mint, price FROM token_prices WHERE token_mint IN ${sql( 402 | tokenMintsArray 403 | )} ORDER BY token_mint, timestamp DESC` 404 | 405 | const tokenPricesByMint: { [mint: string]: number } = {} 406 | tokenPrices.forEach(({ token_mint, price }) => { 407 | tokenPricesByMint[token_mint] = price 408 | }) 409 | 410 | let tokenValue = 0 411 | tokens.forEach((token) => { 412 | const price = tokenPricesByMint[token.mint] || 0 413 | tokenValue += token.amount * price 414 | }) 415 | 416 | const totalBalance = solBalance + rentValue + tokenValue 417 | 418 | const balanceInUsd = totalBalance * solanaPrice 419 | 420 | // Sort tokens by value and get top 5 421 | const topTokens = tokens 422 | .map((token) => ({ 423 | ...token, 424 | value: token.amount * (tokenPricesByMint[token.mint] || 0), 425 | })) 426 | .sort((a, b) => b.value - a.value) 427 | .slice(0, 5) 428 | 429 | // Respond with the wallet's value information 430 | interaction.editReply({ 431 | embeds: [ 432 | { 433 | color: 0x0099ff, 434 | title: "💼 Wallet Information", 435 | description: "Here are the details of your wallet:", 436 | fields: [ 437 | { 438 | name: "Address", 439 | value: `${kp.publicKey.toBase58()}`, 440 | inline: false, 441 | }, 442 | { 443 | name: "💰 SOL Balance", 444 | value: `${solBalance.toFixed(2)} SOL`, 445 | inline: true, 446 | }, 447 | { 448 | name: "🪙 Token Value", 449 | value: `${tokenValue.toFixed(2)} SOL`, 450 | inline: true, 451 | }, 452 | { 453 | name: "🏠 Rent Value", 454 | value: `${rentValue.toFixed(2)} SOL`, 455 | inline: true, 456 | }, 457 | { 458 | name: "🔢 Total Balance", 459 | value: `**${totalBalance.toFixed(2)} SOL (${Intl.NumberFormat( 460 | "en-US", 461 | { 462 | style: "currency", 463 | currency: "USD", 464 | notation: "compact", 465 | } 466 | ).format(balanceInUsd)})**`, 467 | inline: true, 468 | }, 469 | { 470 | name: "🏆 Top 5 Tokens", 471 | value: 472 | topTokens 473 | .map( 474 | (token, index) => 475 | `**${index + 1}.** \`${token.mint 476 | }\`- **${token.value.toFixed( 477 | 2 478 | )} SOL** (${Intl.NumberFormat("en-US", { 479 | style: "currency", 480 | currency: "USD", 481 | notation: "compact", 482 | }).format(token.value * solanaPrice)})` 483 | ) 484 | .join("\n") || "No tokens", 485 | inline: false, 486 | }, 487 | ], 488 | }, 489 | ], 490 | }) 491 | return true 492 | 493 | default: 494 | break 495 | } 496 | } catch (error) { 497 | console.error(error) 498 | if (interaction.replied || interaction.deferred) { 499 | interaction.editReply({ 500 | content: "There was an error while executing this command!", 501 | }) 502 | } else { 503 | interaction.editReply({ 504 | content: "There was an error while executing this command!", 505 | }) 506 | } 507 | } 508 | }) 509 | // Map through all members and add moonbotter role if they have it enabled 510 | // client.on(Events.ClientReady, async () => { 511 | // const guild = client.guilds.cache.get(GUILD_ID) 512 | // if (!guild) { 513 | // console.error("Guild not found") 514 | // return 515 | // } 516 | 517 | // const allUsers = 518 | // await sql`SELECT for_user from moonbot_invite_codes WHERE enabled = true` 519 | // console.log(allUsers.length, "enabled users") 520 | // for (const user of allUsers) { 521 | // const isEnabled = user.enabled 522 | // console.log(user.for_user, isEnabled) 523 | // const allMembers = await guild.members.fetch() 524 | // const member = allMembers.find( 525 | // (member) => user.for_user.indexOf(member.user.id) !== -1 526 | // ) 527 | // if (member) { 528 | // member.roles.add(MOONBOTTER_ROLE_ID) 529 | // } 530 | // } 531 | // }) 532 | 533 | // Access msg 534 | // client.on(Events.ClientReady, async () => { 535 | // const guild = client.guilds.cache.get(GUILD_ID) 536 | 537 | // const accessChannelId = "1273034410526244984" 538 | // const channel = guild?.channels.cache.get(accessChannelId) as TextChannel 539 | // channel.send({ 540 | // embeds: [ 541 | // { 542 | // title: "Access", 543 | // // description: "Please read the FAQ before asking questions", 544 | // fields: [ 545 | // { 546 | // name: "Mooners Signals", 547 | // value: 548 | // "Discord | [Telegram](https://t.me/findmooners) | [Website](https://www.mooners.xyz/app)", 549 | // }, 550 | // { 551 | // name: "Moonbot (Sniper Bot)", 552 | // value: 553 | // "[Join the Waitlist](https://tally.so/r/3ELko4) (Private Access)", 554 | // }, 555 | // ], 556 | // author: { 557 | // name: "Mooners", 558 | // icon_url: "https://www.mooners.xyz/mooners480.png", 559 | // }, 560 | // thumbnail: { 561 | // url: "https://symbl-world.akamaized.net/i/webp/1c/fa12713b68f7c9c6eb58882a0e40f8.webp", 562 | // width: 64, 563 | // height: 64, 564 | // }, 565 | // }, 566 | // ], 567 | // }) 568 | // }) 569 | 570 | // // Links message 571 | // client.on(Events.ClientReady, async () => { 572 | // const guild = client.guilds.cache.get(GUILD_ID) 573 | 574 | // const accessChannelId = "1262358090586521640" 575 | // const channel = guild?.channels.cache.get(accessChannelId) as TextChannel 576 | // channel.send({ 577 | // embeds: [ 578 | // { 579 | // title: "Links", 580 | // // description: "Please read the FAQ before asking questions", 581 | // fields: [ 582 | // { 583 | // name: "Website", 584 | // value: "[mooners.xyz](https://www.mooners.xyz/)", 585 | // }, 586 | // { 587 | // name: "Telegram", 588 | // value: "[t.me/findmooners](https://t.me/findmooners)", 589 | // }, 590 | // { 591 | // name: "Twitter/X", 592 | // value: "[x.com/thevalkar](https://x.com/thevalkar)", 593 | // }, 594 | // { 595 | // name: "Moonbot (Sniper Bot) Waitlist", 596 | // value: "[Join the Waitlist](https://tally.so/r/3ELko4)", 597 | // }, 598 | // ], 599 | // author: { 600 | // name: "Mooners", 601 | // icon_url: "https://www.mooners.xyz/mooners480.png", 602 | // }, 603 | // thumbnail: { 604 | // url: "https://images.emojiterra.com/google/android-12l/512px/1f517.png", 605 | // width: 64, 606 | // height: 64, 607 | // }, 608 | // }, 609 | // ], 610 | // }) 611 | // }) 612 | 613 | // client.on(Events.ClientReady, async () => { 614 | // console.log("Client ready") 615 | // client.user?.setActivity({ 616 | // name: "Moonbot intern", 617 | // type: ActivityType.Custom, 618 | // }) 619 | // }) 620 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from "express" 2 | import chalk from "chalk" 3 | import { Connection, PublicKey, Keypair } from "@solana/web3.js" 4 | import { Bot } from "grammy" 5 | import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor" 6 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet" 7 | import { readFileSync } from "fs" 8 | 9 | import examplePumpfun from "@/data/example-pumpfun-swap.json" 10 | 11 | import { 12 | getBuyRaydiumTokenTransaction, 13 | fetchPoolAndMarketAccounts, 14 | } from "@/lib/raydium" 15 | import { 16 | MOONSHOT_PROGRAM_ID, 17 | PUMPFUN_PROGRAM_ID, 18 | RAYDIUM_AUTHORITY, 19 | RAYDIUM_PROGRAM_ID, 20 | fetchMintAsset, 21 | getMintData, 22 | getTransactionInstructionByProgramId, 23 | getWalletTokenBalance, 24 | getSolanaPrice, 25 | getTransactionDataFromWebhookTransaction, 26 | heliusRpcUrl, 27 | } from "@/lib/utils" 28 | import sql, { 29 | getTokenSignals, 30 | insertToken, 31 | insertTokenEntry, 32 | insertTokenSignal, 33 | selectTokenEntriesUniqueBuyers, 34 | } from "./lib/postgres" 35 | import idl from "@/data/pumpfun-idl.json" 36 | import { getBuyPumpfunTokenTransaction } from "@/lib/pumpfun" 37 | import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes" 38 | import { decrypt } from "@/lib/utils" 39 | import { createServer } from "http" 40 | import { configDotenv } from "dotenv" 41 | import { sendJitoBundle } from "./lib/jito" 42 | import { Server } from "socket.io" 43 | import { snipeAnyCoinGuaranteed } from "./lib/snipe" 44 | configDotenv() 45 | 46 | const isSignaling: { 47 | [token: string]: boolean 48 | } = {} 49 | const isProcessing: { 50 | [token: string]: { [buyer: string]: boolean } 51 | } = {} 52 | 53 | const isWalletBuyingToken: { 54 | [transactionWallet: string]: { 55 | [token: string]: boolean 56 | } 57 | } = {} 58 | 59 | const MIN_BUYERS_RAYDIUM = 1 60 | const MIN_BUYERS_PUMPFUN = 1 61 | 62 | // Whenever a transaction happens, buy and signal a potential mooner. 63 | const onTransactionBuyAndSignalToken = async ( 64 | transaction: WebhookEnhancedTransaction 65 | ) => { 66 | console.log( 67 | `${chalk.blueBright( 68 | "Transaction" 69 | )} ${` https://solscan.io/tx/${transaction.signature}`} ${chalk.blueBright( 70 | "received" 71 | )}` 72 | ) 73 | const data = await getParsedTokenAndTransactionDataFromTransaction( 74 | transaction 75 | ) 76 | 77 | if (!data) return true 78 | 79 | const { 80 | transactionSource, 81 | tokenPrice, 82 | tokenVault, 83 | tokenPairAddress, 84 | tokenMint, 85 | transactionBuyAmount, 86 | transactionWallet, 87 | tokenTopFiveHoldersPercentage, 88 | tokenLpPercentage, 89 | tokenIsRenounced, 90 | tokenFdv, 91 | transactionTimestamp, 92 | tokenData, 93 | } = data 94 | 95 | const isTopFiveGood = tokenTopFiveHoldersPercentage <= 32 96 | const shouldCreateEntry = isTopFiveGood 97 | 98 | if (!shouldCreateEntry) { 99 | console.log( 100 | chalk.red("Potential rug detected") + ` tokenVault: ${tokenVault}`, 101 | `${tokenMint}` 102 | ) 103 | return true 104 | } 105 | 106 | try { 107 | if (isProcessing[tokenMint]?.[transactionWallet]) { 108 | console.log( 109 | `${chalk.red( 110 | "[SWAP_WEBHOOK]" 111 | )} ${transactionWallet}/${tokenMint} is already being processed. Skipping...` 112 | ) 113 | return 114 | } else { 115 | if (!isProcessing[tokenMint]) { 116 | isProcessing[tokenMint] = { 117 | [transactionWallet]: true, 118 | } 119 | } else { 120 | isProcessing[tokenMint][transactionWallet] = true 121 | } 122 | } 123 | 124 | const entry = await insertTokenEntry( 125 | tokenMint, 126 | transactionWallet, 127 | tokenPrice, 128 | transactionTimestamp, 129 | transactionSource, 130 | transactionBuyAmount, 131 | { 132 | lpPercentage: tokenLpPercentage, 133 | isRenounced: tokenIsRenounced, 134 | fdv: tokenFdv, 135 | } 136 | ) 137 | 138 | const uniqueBuyersSqlRes = await selectTokenEntriesUniqueBuyers(tokenMint) 139 | const uniqueBuyersCount = Number(uniqueBuyersSqlRes.length) || 1 140 | 141 | console.log( 142 | chalk.green(`Potential mooner detected (${uniqueBuyersCount} buyers)`), 143 | `${tokenMint}`, 144 | 145 | transactionSource === "Raydium" 146 | ? `https://dexscreener.com/solana/${tokenMint}` 147 | : `https://pump.fun/${tokenMint}`, 148 | ` https://solscan.io/tx/${`${transaction.signature}`}`, 149 | new Date(Date.now()).toLocaleString() 150 | ) 151 | 152 | socketConnection.emit("entry", entry) 153 | console.log(`${chalk.blueBright("Emitted entry")} for `, tokenMint) 154 | 155 | const BUYERS_AMOUNT_FOR_SIGNAL = 156 | transactionSource === "Raydium" ? MIN_BUYERS_RAYDIUM : MIN_BUYERS_PUMPFUN 157 | 158 | if (uniqueBuyersCount >= BUYERS_AMOUNT_FOR_SIGNAL) { 159 | let shouldBuy = false 160 | 161 | // if (shouldBuy) { 162 | // const codes = await sql< 163 | // { 164 | // // bs58 encoded keypair 165 | // keypair: string 166 | // code: string 167 | // enabled: boolean 168 | // }[] 169 | // >`select keypair, code, enabled from moonbot_invite_codes` 170 | 171 | // const keypairs = ( 172 | // await Promise.all( 173 | // codes.map(async ({ keypair, code, enabled }) => { 174 | // if (!enabled) return null 175 | 176 | // try { 177 | // const decrypted = await decrypt(keypair) 178 | // if (!decrypted) throw new Error("Decryption failed for " + code) 179 | // const kp = Keypair.fromSecretKey(bs58.decode(decrypted)) 180 | 181 | // if ( 182 | // kp.publicKey.toString() !== 183 | // (process.env.SNIPING_WALLET_PUBLIC_KEY as string) 184 | // ) 185 | // return null 186 | 187 | // return kp 188 | // // return await getSnipeTransaction(kp, data) 189 | // } catch (e) { 190 | // console.log(`Error buying for ${code}: ` + e) 191 | // } 192 | // return null 193 | // }) 194 | // ) 195 | // ).filter((kp) => kp instanceof Keypair) 196 | 197 | // ;(async () => { 198 | // try { 199 | // snipeAnyCoinGuaranteed(tokenMint, keypairs) 200 | // } catch (e) { 201 | // console.error(e) 202 | // } 203 | // })() 204 | // } else { 205 | // const solanaPrice = await getSolanaPrice() 206 | // const marketCapInUsd = solanaPrice * tokenFdv 207 | 208 | // console.log( 209 | // chalk.yellow( 210 | // `Token FDV is too high (${Intl.NumberFormat("en-US", { 211 | // style: "currency", 212 | // currency: "USD", 213 | // notation: "compact", 214 | // maximumFractionDigits: 1, 215 | // }).format(marketCapInUsd)}). Not buying.` 216 | // ) 217 | // ) 218 | // } 219 | 220 | const token = await insertToken( 221 | tokenMint, 222 | tokenData, 223 | tokenPairAddress, 224 | transactionSource, 225 | data.tokenPumpfunBondingCurveAta 226 | ) 227 | 228 | const signals = await getTokenSignals(tokenMint) 229 | const signalExists = signals.length > 0 230 | 231 | const isSignalEntry = 232 | !signalExists && uniqueBuyersCount >= BUYERS_AMOUNT_FOR_SIGNAL 233 | 234 | const buyersLastSignal = 235 | signals[signals.length - 1] 236 | ?.buyers /** Fallback for data that doesn't exist on some rows on the updated db table */ || 237 | BUYERS_AMOUNT_FOR_SIGNAL 238 | 239 | // This is used to send signal 1 unique buyer 240 | const isBumpEntry = uniqueBuyersCount > buyersLastSignal 241 | 242 | if ( 243 | (isSignalEntry || isBumpEntry) && 244 | !isSignaling[tokenMint] 245 | // && tokenFdv < 5000 246 | ) { 247 | try { 248 | isSignaling[tokenMint] = true 249 | 250 | await insertTokenSignal( 251 | tokenMint, 252 | tokenPrice, 253 | tokenPairAddress, 254 | transactionTimestamp, 255 | transactionSource, 256 | uniqueBuyersCount, 257 | transactionWallet, 258 | transactionBuyAmount 259 | ) 260 | 261 | const DISCORD_SHRIMP_CHANNEL_ID = "1246368892688011356" 262 | const DISCORD_DOLPHIN_CHANNEL_ID = "1243899842263126157" 263 | const DISCORD_WHALE_CHANNEL_ID = "1297547599544324107" 264 | const DISCORD_EXPERIMENTAL_CHANNEL_ID = "1299041170952818698" 265 | 266 | const TELEGRAM_SHRIMP_THREAD_ID = 7554 267 | const TELEGRAM_DOLPHIN_THREAD_ID = 7553 268 | const TELEGRAM_WHALE_THREAD_ID = 50378 269 | const TELEGRAM_EXPERIMENTAL_THREAD_ID = 50789 270 | 271 | let channelIdDiscord, threadIdTelegram 272 | 273 | if (tokenFdv < 1000) { 274 | channelIdDiscord = DISCORD_SHRIMP_CHANNEL_ID 275 | threadIdTelegram = TELEGRAM_SHRIMP_THREAD_ID 276 | } else if (tokenFdv < 6300) { 277 | channelIdDiscord = DISCORD_DOLPHIN_CHANNEL_ID 278 | threadIdTelegram = TELEGRAM_DOLPHIN_THREAD_ID 279 | } else { 280 | channelIdDiscord = DISCORD_WHALE_CHANNEL_ID 281 | threadIdTelegram = TELEGRAM_WHALE_THREAD_ID 282 | } 283 | 284 | await sendSocialsNotification( 285 | data, 286 | shouldBuy, 287 | channelIdDiscord, 288 | threadIdTelegram 289 | ) 290 | } catch (e) { 291 | console.log(e) 292 | } finally { 293 | isSignaling[tokenMint] = false 294 | } 295 | } 296 | } 297 | } catch (e) { 298 | console.log(e) 299 | } finally { 300 | if (isProcessing[tokenMint]) { 301 | isProcessing[tokenMint][transactionWallet] = false 302 | } 303 | } 304 | } 305 | 306 | const getSnipeTransaction = async ( 307 | keypair: Keypair, 308 | data: Exclude< 309 | Awaited>, 310 | null 311 | > 312 | ) => { 313 | const { 314 | tokenMint, 315 | tokenRaydiumPoolKeys, 316 | tokenPairAddress, 317 | transactionSource, 318 | tokenPumpfunGlobalAddress, 319 | tokenPumpfunBondingCurveAta, 320 | } = data 321 | const walletBalance = await connection.getBalance(keypair.publicKey) 322 | 323 | if (walletBalance / 1e9 < 0.05) { 324 | console.log( 325 | `${chalk.red( 326 | "[SNIPING_BOT]" 327 | )} ${keypair.publicKey.toString()}: Wallet balance is too low | ${new Date().toUTCString()}` 328 | ) 329 | return 330 | } 331 | // Find token account balance 332 | const tokenBalance = await getWalletTokenBalance( 333 | connection, 334 | keypair.publicKey, 335 | new PublicKey(tokenMint) 336 | ) 337 | 338 | // Buy only if the wallet has no balance 339 | if (tokenBalance?.value.uiAmount) { 340 | console.log( 341 | `${chalk.greenBright( 342 | "[SNIPING_BOT]" 343 | )} ${keypair.publicKey.toString()}: Already has balance for ${tokenMint} | ${new Date().toUTCString()}` 344 | ) 345 | 346 | return 347 | } 348 | 349 | try { 350 | // Make sure the token isn't being processed yet 351 | if (isWalletBuyingToken[keypair.publicKey.toString()]?.[tokenMint]) { 352 | console.log( 353 | `${chalk.red( 354 | "[SWAP_WEBHOOK]" 355 | )} Wallet ${keypair.publicKey.toString()} is already processing ${tokenMint}. Skipping...` 356 | ) 357 | return 358 | } else { 359 | if (!isWalletBuyingToken[keypair.publicKey.toString()]) { 360 | isWalletBuyingToken[keypair.publicKey.toString()] = { 361 | [tokenMint]: true, 362 | } 363 | } else { 364 | isWalletBuyingToken[keypair.publicKey.toString()][tokenMint] = true 365 | } 366 | } 367 | 368 | // const dynamicEntrySize = walletBalance / 1e9 / 3 / 80 369 | const amountToBuyInSol = 0.01 370 | // dynamicEntrySize < 0.005 371 | // ? 0.005 372 | // : dynamicEntrySize > 0.05 373 | // ? 0.05 374 | // : dynamicEntrySize 375 | 376 | console.log( 377 | `${chalk.greenBright( 378 | "[SNIPING_BOT]" 379 | )} ${keypair.publicKey.toString()}: Buying ${tokenMint} | ${new Date().toUTCString()}` 380 | ) 381 | 382 | if (transactionSource === "Raydium") { 383 | const tx = await getBuyRaydiumTokenTransaction( 384 | connection, 385 | keypair, 386 | tokenMint, 387 | tokenPairAddress, 388 | amountToBuyInSol, 389 | 0.0005, 390 | tokenRaydiumPoolKeys 391 | ) 392 | 393 | return tx 394 | } else if (transactionSource === "Pumpfun") { 395 | const pumpfunTx = await getBuyPumpfunTokenTransaction( 396 | connection, 397 | keypair, 398 | new PublicKey(tokenMint), 399 | new PublicKey(tokenPairAddress), 400 | amountToBuyInSol 401 | ) 402 | 403 | return pumpfunTx 404 | } 405 | } catch (e) { 406 | console.error("Error buying token", e) 407 | } finally { 408 | if (keypair.publicKey.toString() in isWalletBuyingToken) { 409 | isWalletBuyingToken[keypair.publicKey.toString()][tokenMint] = false 410 | } 411 | } 412 | } 413 | 414 | const expressApp = express().use(express.json()) 415 | 416 | // const options = { 417 | // key: readFileSync(process.env.HTTPS_KEY_PATH as string), 418 | // cert: readFileSync(process.env.HTTPS_CERT_PATH as string), 419 | // } 420 | const server = createServer(expressApp) 421 | 422 | const socketConnection = new Server(server, { 423 | cors: { 424 | origin: "*", 425 | methods: ["GET", "POST"], 426 | }, 427 | }) 428 | 429 | expressApp.post("/", async (req, res) => { 430 | try { 431 | await onTransactionBuyAndSignalToken(req.body[0]) 432 | } catch (e: any) { 433 | console.log(e + "") 434 | } finally { 435 | return res.status(200).send("ok") 436 | } 437 | }) 438 | 439 | const port = process.env.API_PORT || 443 440 | server.listen(port, () => console.log(`App is running on port ${port}`)) 441 | process.on("SIGINT", () => { 442 | console.log("Server closed on SIGINT") 443 | server.close() 444 | 445 | process.exit(0) 446 | }) 447 | 448 | const connection = new Connection(heliusRpcUrl, { 449 | confirmTransactionInitialTimeout: 1 * 80 * 1000, 450 | commitment: "processed", 451 | }) 452 | 453 | const pumpfunProgram = new Program( 454 | idl as Idl, 455 | new PublicKey(PUMPFUN_PROGRAM_ID), 456 | new AnchorProvider( 457 | connection, 458 | new NodeWallet(Keypair.generate()), 459 | AnchorProvider.defaultOptions() 460 | ) 461 | ) 462 | 463 | const telegramBot = new Bot(process.env.TELEGRAM_BOT_TOKEN as string) 464 | type WebhookEnhancedTransaction = typeof examplePumpfun 465 | 466 | // Parse transaction into readable data 467 | const getParsedTokenAndTransactionDataFromTransaction = async ( 468 | transaction: WebhookEnhancedTransaction 469 | ) => { 470 | const isRaydium = !!transaction.accountData.find( 471 | (data) => data.account === RAYDIUM_PROGRAM_ID 472 | ) 473 | 474 | const isPumpFun = !!transaction.accountData.find( 475 | (data) => data.account === PUMPFUN_PROGRAM_ID 476 | ) 477 | 478 | const isJup = !!transaction.accountData.find( 479 | (data) => data.account === "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" 480 | ) 481 | 482 | const isMoonShot = !!transaction.accountData.find( 483 | (data) => data.account === MOONSHOT_PROGRAM_ID 484 | ) 485 | 486 | const isRaydiumCAMM = !!transaction.accountData.find( 487 | (data) => data.account === "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" 488 | ) 489 | 490 | const isMeteora = !!transaction.accountData.find( 491 | (data) => data.account === "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB" 492 | ) 493 | 494 | if ( 495 | !isRaydium && 496 | !isPumpFun && 497 | !isJup && 498 | !isMoonShot && 499 | !isRaydiumCAMM && 500 | !isMeteora 501 | ) { 502 | console.log( 503 | chalk.red("Transaction is not Raydium, PumpFun, Jup or Moonshot") + 504 | ` https://solscan.io/tx/${transaction.signature}` 505 | ) 506 | return null 507 | } 508 | 509 | const data = getTransactionDataFromWebhookTransaction(transaction) 510 | 511 | if (!data) return null 512 | 513 | const { tokenMint, isBuy, isSell, solAmount, tokenAmount, price } = data 514 | 515 | const transactionWallet = transaction.feePayer 516 | 517 | if (isSell || !isBuy) return null 518 | if (!tokenMint) throw new Error("No token mint found") 519 | 520 | let transactionSource: "Pumpfun" | "Raydium", 521 | tokenVault, 522 | tokenPairAddress, 523 | tokenLiquidity, 524 | tokenRaydiumPoolKeys, 525 | tokenPumpfunGlobalAddress, 526 | tokenPumpfunBondingCurveAta 527 | if (isPumpFun) { 528 | const pumpFunIx = getTransactionInstructionByProgramId( 529 | transaction, 530 | PUMPFUN_PROGRAM_ID 531 | ) 532 | 533 | if (!pumpFunIx) { 534 | throw new Error( 535 | "Transaction is PumpFun, but no PumpFun instruction found " + 536 | ` https://solscan.io/tx/${transaction.signature}` 537 | ) 538 | } 539 | 540 | transactionSource = "Pumpfun" 541 | tokenPairAddress = pumpFunIx.accounts[3] 542 | tokenPumpfunGlobalAddress = pumpFunIx.accounts[0] 543 | tokenPumpfunBondingCurveAta = pumpFunIx.accounts[4] 544 | tokenVault = tokenPairAddress 545 | } else if (isRaydium) { 546 | const raydiumIx = getTransactionInstructionByProgramId( 547 | transaction, 548 | RAYDIUM_PROGRAM_ID 549 | ) 550 | 551 | if (!raydiumIx) { 552 | throw new Error( 553 | "Transaction is Raydium, but no Raydium instruction found " + 554 | ` https://solscan.io/tx/${transaction.signature}` 555 | ) 556 | } 557 | 558 | transactionSource = "Raydium" 559 | tokenVault = RAYDIUM_AUTHORITY 560 | 561 | tokenPairAddress = raydiumIx.accounts[1] 562 | 563 | tokenRaydiumPoolKeys = ( 564 | await fetchPoolAndMarketAccounts(connection, tokenPairAddress) 565 | ).poolKeys 566 | } else if (isMoonShot) { 567 | return null 568 | // const moonshotIx = getTransactionInstructionByProgramId( 569 | // transaction, 570 | // MOONSHOT_PROGRAM_ID 571 | // ) 572 | // if (!moonshotIx) { 573 | // throw new Error( 574 | // "Transaction is Moonshot, but no Moonshot instruction found " + 575 | // ` https://solscan.io/tx/${transaction.signature}` 576 | // ) 577 | // } 578 | 579 | // console.log( 580 | // chalk.yellowBright("moonshot found") + 581 | // ` https://solscan.io/tx/${transaction.signature}` 582 | // ) 583 | // tokenVault = moonshotIx.accounts[3] 584 | // tokenPairAddress = tokenVault 585 | // transactionSource = "Moonshot" 586 | // tokenLiquidity = 587 | // (await connection.getBalance(new PublicKey(tokenVault))) / 1e9 588 | } else { 589 | return null 590 | } 591 | if (!price) { 592 | throw new Error( 593 | "Token price not found " + 594 | ` https://solscan.io/tx/${transaction.signature}` 595 | ) 596 | } 597 | const { 598 | tokenSupply, 599 | topFivePercentage: tokenTopFiveHoldersPercentage, 600 | lpPercentage: tokenLpPercentage, 601 | isRenounced: tokenIsRenounced, 602 | } = await getMintData(connection, new PublicKey(tokenMint), tokenVault) 603 | 604 | const tokenFdv = 605 | (Number(price) * Number(tokenSupply.value.amount)) / 606 | 10 ** tokenSupply.value.decimals 607 | 608 | const transactionTimestamp = transaction.timestamp * 1000 609 | 610 | const asset = await fetchMintAsset(tokenMint) 611 | 612 | if (!asset) throw new Error("Digital Asset not found") 613 | 614 | return { 615 | transactionSource, 616 | tokenPrice: price, 617 | tokenVault, 618 | tokenPairAddress, 619 | tokenLiquidity, 620 | tokenMint, 621 | transactionBuyAmount: undefined, 622 | transactionWallet, 623 | tokenTopFiveHoldersPercentage, 624 | tokenLpPercentage, 625 | tokenIsRenounced, 626 | tokenFdv, 627 | transactionTimestamp, 628 | 629 | tokenData: asset, 630 | tokenRaydiumPoolKeys, 631 | tokenPumpfunGlobalAddress, 632 | tokenPumpfunBondingCurveAta, 633 | } 634 | } 635 | 636 | const getSocialsSignalMessage = async ( 637 | tokenData: Awaited>, 638 | transactionSource: string, 639 | tokenFdv: number, 640 | tokenPrice: number, 641 | tokenIsRenounced: boolean, 642 | tokenLpPercentage: number, 643 | tokenTopFiveHoldersPercentage: number, 644 | isSniped: boolean 645 | ) => { 646 | const solanaPrice = await getSolanaPrice() 647 | const marketCapInUsd = solanaPrice * tokenFdv 648 | const priceInUsd = solanaPrice * tokenPrice 649 | const isTopFiveGood = tokenTopFiveHoldersPercentage <= 32 650 | 651 | const isLpPercentageGood = 652 | tokenLpPercentage !== null && (tokenLpPercentage as number) >= 10 653 | 654 | const uniqueBuyersSqlRes = await selectTokenEntriesUniqueBuyers( 655 | tokenData.mint.publicKey.toString() 656 | ) 657 | 658 | // Diminishing Factors: The factors array [1, 0.5, 0.25] 659 | // ensures that the first buyer has full impact, the second buyer contributes half of their score, the third contributes a quarter, and so on. 660 | const diminishingFactors = [1, 0.5] // Factors to ensure around or less than half is summed after the first buyer 661 | 662 | const score = Array.from(uniqueBuyersSqlRes).reduce((acc, buyer, index) => { 663 | // const walletScore = (walletsInfo as any)[buyer]?.score 664 | 665 | // if (walletScore) { 666 | // // Use diminishing factors based on the index 667 | // const factor = diminishingFactors[index] || 0.3 668 | // const adjustedScore = walletScore * factor 669 | // return acc + adjustedScore 670 | // } 671 | 672 | return acc 673 | }, 0) 674 | 675 | let sourceName, sourceLink 676 | switch (transactionSource) { 677 | case "Raydium": 678 | sourceName = "Raydium" 679 | sourceLink = `https://dexscreener.com/solana/${tokenData.mint.publicKey.toString()}` 680 | 681 | break 682 | case "Pumpfun": 683 | sourceName = "Pumpfun" 684 | sourceLink = `https://pump.fun/${tokenData.mint.publicKey.toString()}` 685 | break 686 | case "Moonshot": 687 | sourceName = "Moonshot" 688 | sourceLink = `https://dexscreener.com/solana/${tokenData.mint.publicKey.toString()}` 689 | break 690 | default: 691 | sourceName = "Unknown" 692 | sourceLink = "" 693 | break 694 | } 695 | 696 | return ` 697 | ${isSniped ? "🟢" : "⚪"} $\`${tokenData.metadata.symbol}\` \\(\`${ 698 | tokenData.metadata.name 699 | }\`\\) bought by the cabal on ${transactionSource} 700 | 701 | \`${tokenData.mint.publicKey.toString()}\` 702 | 703 | 📈 Price: $\`${priceInUsd.toFixed(7)}\` 704 | 💰 MC: \`${Intl.NumberFormat("en-US", { 705 | style: "currency", 706 | currency: "USD", 707 | notation: "compact", 708 | maximumFractionDigits: 1, 709 | }).format(marketCapInUsd)}\` 710 | ──────────── 711 | ℹ️ Renounced: ${tokenIsRenounced ? "☑️ Yes" : "⚠️ No"} 712 | ℹ️ Top 5 holders: ${ 713 | isTopFiveGood ? "☑️" : "⚠️" 714 | } ${tokenTopFiveHoldersPercentage.toFixed(0)}% \\(${ 715 | isTopFiveGood ? "Good" : "High" 716 | }\\) 717 | ${ 718 | // `ℹ️ LP supply: ${isLpPercentageGood ? "☑️" : "⚠️"} ${ 719 | // tokenLpPercentage ? tokenLpPercentage?.toFixed(0) : "Low" 720 | // }% \\(${isLpPercentageGood ? "Good" : "low"}\\)` 721 | `` 722 | } 723 | 🔗 [[${ 724 | sourceName === "Raydium" ? "DexScreener" : sourceName 725 | }]](${sourceLink}) \\| [[BonkBot]](https://t.me/bonkbot_bot?start=ref_1ncf2_ca_${tokenData.mint.publicKey.toString()}) \\| [[Trojan]](https://t.me/paris_trojanbot?start=r-edceds-${tokenData.mint.publicKey.toString()}) \\| [[Photon]](https://photon-sol.tinyastro.io/en/lp/${tokenData.mint.publicKey.toString()}?handle=19437044e66753b1e4627) \\| [[Pepeboost]](https://t.me/pepeboost_sol_bot?start=ref_0261rz_ca_${tokenData.mint.publicKey.toString()}) \\| [[BullX]](https://bullx.io/terminal?chainId=1399811149&address=${tokenData.mint.publicKey.toString()}) [[NeoBullX]](https://neo.bullx.io/terminal?chainId=1399811149&address=${tokenData.mint.publicKey.toString()})` 726 | } 727 | const sendSocialsNotification = async ( 728 | data: Exclude< 729 | Awaited>, 730 | null 731 | >, 732 | isSniped: boolean, 733 | discordChannel: string, 734 | telegramThreadId?: number 735 | ) => { 736 | const msg = await getSocialsSignalMessage( 737 | data.tokenData, 738 | data.transactionSource, 739 | data.tokenFdv, 740 | data.tokenPrice, 741 | data.tokenIsRenounced, 742 | data.tokenLpPercentage, 743 | data.tokenTopFiveHoldersPercentage, 744 | isSniped 745 | ) 746 | 747 | try { 748 | // Send to Telegram 749 | await telegramBot.api.sendMessage("-1002246150675", msg, { 750 | parse_mode: "MarkdownV2", 751 | message_thread_id: telegramThreadId, 752 | }) 753 | } catch (e) { 754 | console.error("Couldn't send Telegram message " + e) 755 | } 756 | 757 | try { 758 | await sendDiscordMessage(msg, discordChannel, data.tokenData) 759 | } catch (e) { 760 | console.error( 761 | "Couldn't send Discord message " + 762 | e + 763 | JSON.stringify(msg) + 764 | JSON.stringify(data.tokenData) + 765 | new Date().toLocaleString() 766 | ) 767 | } 768 | } 769 | 770 | const sendDiscordMessage = async ( 771 | msg: string, 772 | channelId: string, 773 | tokenData: Awaited> 774 | ) => { 775 | const color = stringToColour(tokenData.mint.publicKey.toString()) 776 | 777 | const res = await fetch( 778 | `https://discord.com/api/v10/channels/${channelId}/messages`, 779 | { 780 | body: JSON.stringify({ 781 | embeds: [ 782 | { 783 | title: ``, 784 | description: msg, 785 | color: parseInt(color, 16), 786 | // thumbnail: { 787 | // url: tokenData.metadata.offchain.image, 788 | // width: 64, 789 | // height: 64, 790 | // }, 791 | }, 792 | ], 793 | }), 794 | method: "POST", 795 | headers: { 796 | "Content-Type": "application/json", 797 | Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 798 | }, 799 | } 800 | ) 801 | 802 | if (!res.ok) { 803 | throw new Error("Invalid request") 804 | } 805 | } 806 | 807 | const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN 808 | 809 | const stringToColour = (str: string) => { 810 | let hash = 0 811 | str.split("").forEach((char) => { 812 | hash = char.charCodeAt(0) + ((hash << 5) - hash) 813 | }) 814 | let colour = "" 815 | for (let i = 0; i < 3; i++) { 816 | const value = (hash >> (i * 8)) & 0xff 817 | colour += value.toString(16).padStart(2, "0") 818 | } 819 | return colour 820 | } 821 | 822 | // @ts-ignore 823 | BigInt.prototype["toJSON"] = function () { 824 | return parseInt(this.toString()) 825 | } 826 | -------------------------------------------------------------------------------- /src/lib/jito.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | PublicKey, 4 | Transaction, 5 | SystemProgram, 6 | Keypair, 7 | TransactionInstruction, 8 | LAMPORTS_PER_SOL, 9 | } from "@solana/web3.js" 10 | // @ts-ignore 11 | import { JitoJsonRpcClient } from "jito-js-rpc" 12 | import bs58 from "bs58" 13 | import { heliusRpcUrl } from "./utils" 14 | 15 | const connection = new Connection(heliusRpcUrl) 16 | 17 | const payerKeypair = Keypair.fromSecretKey( 18 | Uint8Array.from(JSON.parse(process.env.JITO_PAYER_KEYPAIR as string)) 19 | ) 20 | export async function sendJitoBundle( 21 | transactions: Uint8Array[], 22 | jitoTipAmountInSol = 0.000269858 23 | ) { 24 | try { 25 | const base58EncodedTransactions = transactions.map((tx) => bs58.encode(tx)) 26 | const jitoClient = new JitoJsonRpcClient( 27 | "https://mainnet.block-engine.jito.wtf/api/v1", 28 | "" 29 | ) 30 | 31 | const randomTipAccount = "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49" 32 | const jitoTipAccount = new PublicKey(randomTipAccount) 33 | const jitoTipAmount = jitoTipAmountInSol * LAMPORTS_PER_SOL 34 | 35 | const memoProgramId = new PublicKey( 36 | "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" 37 | ) 38 | 39 | const { blockhash } = await connection.getLatestBlockhash() 40 | 41 | // Split transactions into batches 42 | const batchSize = 4 43 | const batches = [] 44 | for (let i = 0; i < base58EncodedTransactions.length; i += batchSize) { 45 | const batch = base58EncodedTransactions.slice(i, i + batchSize) 46 | batches.push(batch) 47 | } 48 | 49 | for (const batch of batches) { 50 | const jitoTransaction = new Transaction() 51 | jitoTransaction.add( 52 | SystemProgram.transfer({ 53 | fromPubkey: payerKeypair.publicKey, 54 | toPubkey: jitoTipAccount, 55 | lamports: jitoTipAmount, 56 | }) 57 | ) 58 | 59 | const memoInstruction = new TransactionInstruction({ 60 | keys: [], 61 | programId: memoProgramId, 62 | data: Buffer.from(Keypair.generate().publicKey.toString()), 63 | }) 64 | jitoTransaction.add(memoInstruction) 65 | 66 | jitoTransaction.recentBlockhash = blockhash 67 | jitoTransaction.feePayer = payerKeypair.publicKey 68 | jitoTransaction.sign(payerKeypair) 69 | 70 | const serializedJitoTransaction = jitoTransaction.serialize({ 71 | verifySignatures: false, 72 | }) 73 | 74 | const base58EncodedJitoTransaction = bs58.encode( 75 | serializedJitoTransaction 76 | ) 77 | 78 | try { 79 | const res = await jitoClient.sendBundle([ 80 | [base58EncodedJitoTransaction].concat(batch), 81 | ]) 82 | 83 | // Let's ignore confirming for now since we can't do anything about it. 84 | // @TODO later when sniping we should check if balance changed, if not changed we have to re-snipe the coin. 85 | // @TODO also some coins we can't snipe it because too much volume probably. Let's query the `coins` table and find `volume` and `created. If `volume` is high, and `created` is < 15 min, we can increase bribery. 86 | 87 | // const bundleId = res.result 88 | // console.log("Bundle ID:", bundleId) 89 | // const inflightStatus = await jitoClient.confirmInflightBundle( 90 | // bundleId, 91 | // ) 92 | 93 | // if (inflightStatus.status === "Landed" || inflightStatus.confirmation_status === "confirmed" || inflightStatus.confirmation_status === "finalized") { 94 | // console.log( 95 | // `Batch successfully confirmed on-chain at slot ${inflightStatus.slot}` 96 | // ) 97 | 98 | // return bundleId 99 | // } else { 100 | // console.log(inflightStatus) 101 | 102 | // throw new Error( 103 | // "Batch processing failed: " + 104 | // JSON.stringify(inflightStatus) + 105 | // "" 106 | // ) 107 | // } 108 | } catch (e: any) { 109 | // console.error("Error sending batch:", e) 110 | // if (e.response && e.response.data) { 111 | // console.error("Server response:", e.response.data) 112 | // } 113 | } 114 | 115 | await new Promise((resolve) => setTimeout(resolve, 2100)) 116 | } 117 | } catch (e) { 118 | console.error(e + new Date().toLocaleString()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/lib/postgres.ts: -------------------------------------------------------------------------------- 1 | import { Keypair } from "@solana/web3.js" 2 | import postgres from "postgres" 3 | import { DigitalAsset } from "@metaplex-foundation/mpl-token-metadata" 4 | import dotenv from "dotenv" 5 | 6 | dotenv.config() 7 | 8 | export const sql = postgres(process.env.DATABASE_URL as string) 9 | 10 | export const getCoinsPairs = async (mints: string[]) => { 11 | const results = await sql< 12 | { pair: string, mint: string, source: string }[]>` 13 | select pair, mint, source from coins where mint IN ${sql(mints)} 14 | ` 15 | 16 | const pairByMint: Record = {} 17 | if (results.length) { 18 | for (const result of results) { 19 | const { pair, mint, source } = result 20 | pairByMint[mint] = { 21 | pair, source, mint 22 | } 23 | } 24 | 25 | } 26 | 27 | return pairByMint 28 | } 29 | 30 | 31 | export type PairSqlRow = { 32 | token_mint: string 33 | address: string 34 | source: string 35 | data: { 36 | creationDateUtc: string 37 | creationDateTimestamp: number 38 | associatedBondingCurve?: string 39 | initialPrice: number 40 | } 41 | } 42 | 43 | export type TokenSqlRow = { 44 | mint: string 45 | data: DigitalAsset & { 46 | metadata: { 47 | name: string 48 | offchain: any 49 | } 50 | } 51 | pair_address: string 52 | } 53 | 54 | export type UserConfig = { 55 | entry: number 56 | strategy: "automatic" | "default" 57 | discordid: string 58 | min_times_bought: number 59 | } 60 | 61 | export type Signal = { 62 | token_mint: string 63 | price: number 64 | timestamp: number 65 | source: string 66 | buyers: number 67 | pair_address: string 68 | amount: number 69 | } 70 | 71 | export type SignalWithEntry = { 72 | token_mint: string 73 | signal_price: number 74 | signal_timestamp: number 75 | signal_source: string 76 | buyers: number 77 | amount: number 78 | entry_price: number 79 | entry_buyer: string 80 | entry_source: string 81 | } 82 | 83 | 84 | export const getSignals = async (maxHoursOld = 24) => { 85 | console.time("signals query") 86 | const signalsWithEntry = await sql` 87 | WITH RankedSignals AS ( 88 | SELECT 89 | ts.token_mint, 90 | ts.price AS signal_price, 91 | ts.timestamp AS signal_timestamp, 92 | ts.source AS signal_source, 93 | ts.buyers, 94 | ts.amount, 95 | te.price AS entry_price, 96 | te.buyer AS entry_buyer, 97 | te.source AS entry_source, 98 | ROW_NUMBER() OVER (PARTITION BY ts.token_mint ORDER BY ts.timestamp DESC) AS rn 99 | FROM 100 | token_signals ts 101 | LEFT JOIN 102 | token_entries te 103 | ON 104 | ts.token_mint = te.token_mint 105 | AND 106 | ts.timestamp = te.timestamp 107 | AND 108 | ts.price = te.price 109 | WHERE ts.timestamp > ${Date.now() - maxHoursOld * 60 * 60 * 1000} 110 | ) 111 | SELECT 112 | token_mint, 113 | signal_price, 114 | signal_timestamp, 115 | signal_source, 116 | buyers, 117 | amount, 118 | entry_price, 119 | entry_buyer, 120 | entry_source 121 | FROM 122 | RankedSignals 123 | WHERE 124 | rn = 1 125 | ORDER BY 126 | signal_timestamp DESC; 127 | ` 128 | 129 | const tokensToFetch = new Set() 130 | signalsWithEntry.forEach((signal) => { 131 | tokensToFetch.add(signal.token_mint) 132 | }) 133 | 134 | console.timeEnd("signals query") 135 | 136 | const tokens = await getTokens(Array.from(tokensToFetch)) 137 | const tokensMap = tokens.reduce<{ 138 | [tokenMint: string]: TokenSqlRow 139 | }>((acc, row) => { 140 | acc[row.mint] = row 141 | return acc 142 | }, {}) 143 | 144 | const signalsWithToken = signalsWithEntry.map((signal) => { 145 | const token = tokensMap[signal.token_mint] 146 | 147 | return { 148 | ...signal, 149 | token, 150 | } 151 | }) 152 | 153 | return signalsWithToken 154 | } 155 | 156 | export const getTokens = async (mints?: string[]) => { 157 | const tokens: TokenSqlRow[] = mints 158 | ? await sql`SELECT mint, data, pair_address FROM tokens WHERE mint in ${sql( 159 | mints 160 | )}` 161 | : await sql`SELECT mint, data, pair_address FROM tokens` 162 | 163 | return tokens 164 | } 165 | 166 | export type PriceSqlRow = { 167 | price: number 168 | timestamp: string 169 | token_mint: string 170 | } 171 | 172 | export const getTokenSignals = async (mint: string) => { 173 | const res = await sql` 174 | SELECT token_mint, buyers from token_signals where token_mint=${mint} 175 | ` 176 | 177 | return res 178 | } 179 | export const insertTokenSignal = async ( 180 | mint: string, 181 | price: number, 182 | pair: string, 183 | timestamp: number, 184 | source: string, 185 | buyers: number, 186 | buyer: string, 187 | amount?: number 188 | ) => { 189 | const res = await sql` 190 | INSERT INTO token_signals (token_mint, price, pair_address, timestamp, source, buyers, buyer, amount) 191 | VALUES (${mint}, ${price}, ${pair}, ${timestamp}, ${source}, ${buyers}, ${buyer}, ${amount || null 192 | }) 193 | RETURNING * 194 | ` 195 | return res 196 | } 197 | 198 | export const selectTokenEntriesUniqueBuyers = async (token_mint: string) => { 199 | const res = await sql< 200 | { unique_buyers: string[] }[] 201 | >`select array_agg(distinct buyer) as unique_buyers from token_entries where token_mint=${token_mint};` 202 | 203 | return res[0]?.unique_buyers 204 | } 205 | 206 | export const insertTokenEntry = async ( 207 | mint: string, 208 | buyer: string, 209 | price: number, 210 | timestamp: number, 211 | source: string, 212 | amount?: number, 213 | tokenInsights?: { 214 | lpPercentage: number 215 | isRenounced: boolean 216 | fdv: number 217 | } 218 | ) => { 219 | const res = 220 | await sql`INSERT INTO token_entries (token_mint, buyer, price, amount, timestamp, source, token_insights) VALUES(${mint}, ${buyer}, ${price},${amount || null 221 | }, ${timestamp},${source},${tokenInsights as never}) 222 | RETURNING *` 223 | 224 | return res[0] 225 | } 226 | 227 | export const insertTokenPrice = async ( 228 | mint: string, 229 | price: number, 230 | pairAddress: string 231 | ) => { 232 | const res = 233 | await sql`INSERT INTO token_price (token_mint, price, pair_address) VALUES(${mint}, ${price}, ${pairAddress}) 234 | ON CONFLICT (pair_address) DO UPDATE SET price = ${price} 235 | RETURNING *` 236 | 237 | return res 238 | } 239 | 240 | export const insertToken = async ( 241 | mint: string, 242 | data: DigitalAsset | null = null, 243 | pairAddress: string, 244 | pairSource: "Raydium" | "Pumpfun", 245 | tokenPumpfunBondingCurveAta: string | null = null 246 | ) => { 247 | const res = await sql< 248 | TokenSqlRow[] 249 | >`INSERT INTO tokens (mint, data, pair_address, pair_source, pumpfun_bonding_curve_ata) VALUES(${mint}, ${data as never 250 | }, ${pairAddress}, ${pairSource}, ${tokenPumpfunBondingCurveAta}) 251 | ON CONFLICT (mint) DO UPDATE SET pair_address = ${pairAddress}, pair_source = ${pairSource}, ${data ? sql`data = ${data as never},` : sql`` 252 | } pumpfun_bonding_curve_ata = ${tokenPumpfunBondingCurveAta} 253 | RETURNING *` 254 | 255 | return res[0] 256 | } 257 | 258 | export const insertPair = async ( 259 | address: string, 260 | source: "RAYDIUM" | "PUMPFUN", 261 | tokenMint: string, 262 | data: unknown = null 263 | ) => { 264 | const res = sql`INSERT INTO pairs (address, source, token_mint, data) VALUES(${address}, ${source}, ${tokenMint}, ${data as never 265 | }) 266 | 267 | RETURNING *` 268 | 269 | return res 270 | } 271 | 272 | export const insertPoolAddress = async (token: string, poolAddress: string) => { 273 | const res = await sql` 274 | INSERT INTO pool_addresses (token, pool_address) 275 | VALUES (${token}, ${poolAddress}) 276 | ` 277 | 278 | return res[0] 279 | } 280 | 281 | export default sql 282 | -------------------------------------------------------------------------------- /src/lib/pumpfun.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComputeBudgetProgram, 3 | Connection, 4 | Keypair, 5 | LAMPORTS_PER_SOL, 6 | PublicKey, 7 | SYSVAR_RENT_PUBKEY, 8 | SystemProgram, 9 | TransactionInstruction, 10 | TransactionMessage, 11 | VersionedTransaction, 12 | } from "@solana/web3.js" 13 | import { PUMPFUN_PROGRAM_ID, heliusRpcUrl } from "./utils" 14 | 15 | import { AnchorProvider, BN, Idl, Program } from "@coral-xyz/anchor" 16 | import idl from "../data/pumpfun-idl.json" 17 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet" 18 | import { 19 | TOKEN_PROGRAM_ID, 20 | createAssociatedTokenAccountInstruction, 21 | getAssociatedTokenAddressSync, 22 | } from "@solana/spl-token" 23 | import { 24 | ASSOCIATED_TOKEN_PROGRAM_ID, 25 | MEMO_PROGRAM_ID, 26 | } from "@raydium-io/raydium-sdk" 27 | import chalk from "chalk" 28 | 29 | const connection = new Connection(heliusRpcUrl, { 30 | confirmTransactionInitialTimeout: 1 * 80 * 1000, 31 | commitment: "confirmed", 32 | }) 33 | 34 | const program = new Program( 35 | idl as Idl, 36 | new PublicKey(PUMPFUN_PROGRAM_ID), 37 | new AnchorProvider( 38 | connection, 39 | new NodeWallet(Keypair.generate()), 40 | AnchorProvider.defaultOptions() 41 | ) 42 | ) 43 | const feeRecipient = "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" 44 | const EVENT_AUTH = "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" 45 | 46 | const globalState = new PublicKey( 47 | "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" 48 | ) 49 | 50 | const jitoPayerKeypair = Keypair.fromSecretKey( 51 | Uint8Array.from(JSON.parse(process.env.JITO_PAYER_KEYPAIR as string)) 52 | ) 53 | 54 | export const getBuyPumpfunTokenTransaction = async ( 55 | connection: Connection, 56 | keypair: Keypair, 57 | tokenMint: PublicKey, 58 | bondingCurve: PublicKey, 59 | amountInSol = 0.003, 60 | feesInSol = 0.000011 61 | ) => { 62 | let bought = false 63 | let tries = 1 64 | const priorityFee = Number((feesInSol * LAMPORTS_PER_SOL).toFixed(0)) 65 | 66 | const [associatedBondingCurve] = PublicKey.findProgramAddressSync( 67 | [ 68 | bondingCurve.toBuffer(), 69 | TOKEN_PROGRAM_ID.toBuffer(), 70 | tokenMint.toBuffer(), 71 | ], 72 | ASSOCIATED_TOKEN_PROGRAM_ID 73 | ) 74 | 75 | while (!bought && tries < 5) { 76 | try { 77 | const user = keypair.publicKey 78 | const userAta = getAssociatedTokenAddressSync(tokenMint, user, true) 79 | const signerTokenAccount = getAssociatedTokenAddressSync( 80 | tokenMint, 81 | user, 82 | true, 83 | TOKEN_PROGRAM_ID 84 | ) 85 | 86 | let bondingCurveData, 87 | mintData, 88 | account, 89 | fetchTries = 1 90 | while ((!bondingCurveData || !mintData) && fetchTries < 5) { 91 | try { 92 | ;[bondingCurveData, mintData, account] = await Promise.all([ 93 | program.account.bondingCurve.fetch(bondingCurve), 94 | connection.getParsedAccountInfo(tokenMint), 95 | connection.getAccountInfo(signerTokenAccount, "confirmed"), 96 | ]) 97 | } catch (e) { 98 | console.log( 99 | `${chalk.redBright( 100 | "[SNIPING_BOT]" 101 | )} Failed to get bonding curve data for ${tokenMint.toString()} for ${keypair.publicKey.toString()} | ${fetchTries} tries | ${new Date().toUTCString()}` 102 | ) 103 | console.log(bondingCurveData) 104 | } finally { 105 | await new Promise((resolve) => setTimeout(resolve, 2500)) 106 | fetchTries++ 107 | } 108 | } 109 | 110 | if (!bondingCurveData || !mintData) { 111 | throw new Error( 112 | `Failed to get bonding curve data for ${tokenMint.toString()} for ${keypair.publicKey.toString()} | ${fetchTries} tries | ${new Date().toUTCString()}` 113 | ) 114 | } 115 | 116 | //@ts-ignore 117 | const decimals = mintData.value?.data.parsed.info.decimals 118 | const virtualTokenReserves = ( 119 | bondingCurveData.virtualTokenReserves as any 120 | ).toNumber() 121 | const virtualSolReserves = ( 122 | bondingCurveData.virtualSolReserves as any 123 | ).toNumber() 124 | 125 | const adjustedVirtualTokenReserves = virtualTokenReserves / 10 ** decimals 126 | const adjustedVirtualSolReserves = virtualSolReserves / LAMPORTS_PER_SOL 127 | 128 | const virtualTokenPrice = 129 | adjustedVirtualSolReserves / adjustedVirtualTokenReserves 130 | 131 | const maxSolCost = amountInSol * 1.51 132 | const finalAmount = amountInSol / virtualTokenPrice 133 | 134 | const ixs = [] 135 | if (!account) { 136 | ixs.push( 137 | createAssociatedTokenAccountInstruction( 138 | user, 139 | signerTokenAccount, 140 | user, 141 | tokenMint 142 | ) 143 | ) 144 | } 145 | 146 | if (!finalAmount || isNaN(finalAmount)) { 147 | throw new Error( 148 | `Failed to get final amount for ${tokenMint.toString()} for ${keypair.publicKey.toString()} | ${new Date().toUTCString()}` 149 | ) 150 | } 151 | 152 | const snipeIx = await program.methods 153 | .buy( 154 | new BN(finalAmount * 10 ** decimals), 155 | new BN(maxSolCost * LAMPORTS_PER_SOL) 156 | ) 157 | .accounts({ 158 | global: globalState, 159 | feeRecipient: feeRecipient, 160 | mint: tokenMint, 161 | bondingCurve: bondingCurve, 162 | associatedBondingCurve, 163 | associatedUser: userAta, 164 | user: user, 165 | systemProgram: SystemProgram.programId, 166 | tokenProgram: TOKEN_PROGRAM_ID, 167 | rent: SYSVAR_RENT_PUBKEY, 168 | eventAuthority: EVENT_AUTH, 169 | program: program.programId, 170 | }) 171 | .instruction() 172 | ixs.push(snipeIx) 173 | 174 | const memoix = new TransactionInstruction({ 175 | programId: new PublicKey(MEMO_PROGRAM_ID), 176 | keys: [], 177 | data: Buffer.from(getRandomNumber().toString(), "utf8"), 178 | }) 179 | ixs.push(memoix) 180 | 181 | ixs.push( 182 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee }) 183 | ) 184 | 185 | // const feesWallet = jitoPayerKeypair.publicKey 186 | 187 | ixs.push( 188 | SystemProgram.transfer({ 189 | fromPubkey: keypair.publicKey, 190 | toPubkey: new PublicKey( 191 | "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" 192 | ), 193 | lamports: 0.001 * 1e9, 194 | }) 195 | ) 196 | 197 | console.log( 198 | `${chalk.green( 199 | "[SNIPING_BOT]" 200 | )} Attempt ${tries} to buy ${tokenMint} for ${keypair.publicKey.toString()} | ${new Date().toUTCString()}` 201 | ) 202 | 203 | let latestBlockHash = await connection.getLatestBlockhashAndContext( 204 | "confirmed" 205 | ) 206 | const versionedTransaction = new VersionedTransaction( 207 | new TransactionMessage({ 208 | payerKey: keypair.publicKey, 209 | recentBlockhash: latestBlockHash.value.blockhash, 210 | instructions: ixs, 211 | }).compileToV0Message() 212 | ) 213 | 214 | versionedTransaction.sign([keypair]) 215 | 216 | return versionedTransaction.serialize() 217 | 218 | // const txid = await connection.sendRawTransaction( 219 | // versionedTransaction.serialize(), 220 | // { 221 | // skipPreflight: true, 222 | // preflightCommitment: "processed", 223 | // // minContextSlot: latestBlockHash.context.slot, 224 | // // maxRetries: 0, 225 | // } 226 | // ) 227 | 228 | // sendAndRetryTransaction(connection, versionedTransaction, latestBlockHash) 229 | 230 | // await connection.confirmTransaction(txid, "processed") 231 | 232 | // bought = true 233 | 234 | // console.log( 235 | // `${chalk.yellowBright( 236 | // "[SNIPING_BOT]" 237 | // )} Bought ${tokenMint} for ${keypair.publicKey.toString()} | https://solscan.io/tx/${txid} | ${new Date().toUTCString()}` 238 | // ) 239 | 240 | // return txid 241 | } catch (e) { 242 | console.log(e) 243 | } finally { 244 | tries++ 245 | await new Promise((resolve) => setTimeout(resolve, 2500)) 246 | } 247 | } 248 | 249 | if (!bought) { 250 | console.log( 251 | `${chalk.redBright( 252 | "[SNIPING_BOT]" 253 | )} Failed to buy ${tokenMint} for ${keypair.publicKey.toString()} | ${tries} tries | ${new Date().toUTCString()}` 254 | ) 255 | } 256 | } 257 | 258 | export const getSellPumpfunTokenTransaction = async ( 259 | connection: Connection, 260 | keypair: Keypair, 261 | tokenMint: PublicKey, 262 | bondingCurve: PublicKey, 263 | bondingCurveAta: PublicKey, 264 | globalState: PublicKey, 265 | amount: number, 266 | feesInSol = 0.000011 267 | ) => { 268 | let tries = 1 269 | const priorityFee = feesInSol * LAMPORTS_PER_SOL 270 | 271 | while (tries <= 5) { 272 | try { 273 | const user = keypair.publicKey 274 | const userAta = getAssociatedTokenAddressSync(tokenMint, user, true) 275 | const signerTokenAccount = getAssociatedTokenAddressSync( 276 | tokenMint, 277 | user, 278 | true, 279 | TOKEN_PROGRAM_ID 280 | ) 281 | 282 | const [bondingCurveData, mintData, account] = await Promise.all([ 283 | program.account.bondingCurve.fetch(bondingCurve), 284 | connection.getParsedAccountInfo(tokenMint), 285 | connection.getAccountInfo(signerTokenAccount, "confirmed"), 286 | ]) 287 | 288 | //@ts-ignore 289 | const decimals = mintData.value?.data.parsed.info.decimals 290 | const virtualTokenReserves = ( 291 | bondingCurveData.virtualTokenReserves as any 292 | ).toNumber() 293 | const virtualSolReserves = ( 294 | bondingCurveData.virtualSolReserves as any 295 | ).toNumber() 296 | 297 | const adjustedVirtualTokenReserves = virtualTokenReserves / 10 ** decimals 298 | const adjustedVirtualSolReserves = virtualSolReserves / LAMPORTS_PER_SOL 299 | 300 | const virtualTokenPrice = 301 | adjustedVirtualSolReserves / adjustedVirtualTokenReserves 302 | 303 | const minSolOutput = amount * virtualTokenPrice * 0.95 304 | 305 | const ixs = [] 306 | if (!account) { 307 | ixs.push( 308 | createAssociatedTokenAccountInstruction( 309 | user, 310 | signerTokenAccount, 311 | user, 312 | tokenMint 313 | ) 314 | ) 315 | } 316 | 317 | const memoix = new TransactionInstruction({ 318 | programId: new PublicKey(MEMO_PROGRAM_ID), 319 | keys: [], 320 | data: Buffer.from(getRandomNumber().toString(), "utf8"), 321 | }) 322 | ixs.push(memoix) 323 | 324 | ixs.push( 325 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee }) 326 | ) 327 | 328 | const snipeIx = await program.methods 329 | .sell( 330 | new BN(amount * 10 ** decimals), 331 | new BN(minSolOutput * LAMPORTS_PER_SOL) 332 | ) 333 | .accounts({ 334 | global: globalState, 335 | feeRecipient: feeRecipient, 336 | mint: tokenMint, 337 | bondingCurve: bondingCurve, 338 | associatedBondingCurve: bondingCurveAta, 339 | associatedUser: userAta, 340 | user: user, 341 | systemProgram: SystemProgram.programId, 342 | tokenProgram: TOKEN_PROGRAM_ID, 343 | rent: SYSVAR_RENT_PUBKEY, 344 | eventAuthority: EVENT_AUTH, 345 | program: program.programId, 346 | }) 347 | .instruction() 348 | ixs.push(snipeIx) 349 | 350 | console.log( 351 | `${chalk.green( 352 | "[SNIPING_BOT]" 353 | )} Attempt ${tries} to sell ${tokenMint} for ${keypair.publicKey.toString()} | ${new Date().toUTCString()}` 354 | ) 355 | 356 | let latestBlockHash = await connection.getLatestBlockhash("confirmed") 357 | const versionedTransaction = new VersionedTransaction( 358 | new TransactionMessage({ 359 | payerKey: keypair.publicKey, 360 | recentBlockhash: latestBlockHash.blockhash, 361 | instructions: ixs, 362 | }).compileToV0Message() 363 | ) 364 | 365 | versionedTransaction.sign([keypair]) 366 | 367 | const simulated = await connection.simulateTransaction( 368 | versionedTransaction 369 | ) 370 | if (simulated.value.err) { 371 | throw new Error( 372 | `Invalid tx for ${tokenMint.toString()} ` + 373 | JSON.stringify(simulated.value.err) 374 | ) 375 | } else { 376 | return versionedTransaction.serialize() 377 | } 378 | } catch (e) { 379 | console.log(e) 380 | tries++ 381 | } 382 | } 383 | } 384 | 385 | export function getRandomNumber() { 386 | // Generate a random number between 0 and 1 387 | var randomNumber = Math.random() 388 | 389 | // Scale the random number to the desired range (1 to 5000) 390 | var scaledNumber = Math.floor(randomNumber * 5000) + 1 391 | 392 | return scaledNumber 393 | } 394 | -------------------------------------------------------------------------------- /src/lib/raydium.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TransactionMessage, 3 | VersionedTransaction, 4 | Connection, 5 | Keypair, 6 | PublicKey, 7 | SystemProgram, 8 | } from "@solana/web3.js" 9 | 10 | import { 11 | LIQUIDITY_STATE_LAYOUT_V4, 12 | LiquidityPoolKeys, 13 | MARKET_STATE_LAYOUT_V3, 14 | Market, 15 | Liquidity, 16 | Percent, 17 | SPL_ACCOUNT_LAYOUT, 18 | TOKEN_PROGRAM_ID, 19 | Token, 20 | TokenAmount, 21 | LiquidityPoolInfo, 22 | } from "@raydium-io/raydium-sdk" 23 | import { getWalletTokenBalance, sendAndRetryTransaction } from "./utils" 24 | import chalk from "chalk" 25 | import { configDotenv } from "dotenv" 26 | configDotenv() 27 | 28 | const jitoPayerKeypair = Keypair.fromSecretKey( 29 | Uint8Array.from(JSON.parse(process.env.JITO_PAYER_KEYPAIR as string)) 30 | ) 31 | 32 | export const getBuyRaydiumTokenTransaction = async ( 33 | connection: Connection, 34 | keypair: Keypair, 35 | tokenMint: string, 36 | poolId: string, 37 | amountInSol: number = 0.01, 38 | feesInSol = 0.0001, 39 | poolKeys?: Awaited>["poolKeys"] 40 | ) => { 41 | let tries = 1 42 | 43 | while (tries < 5) { 44 | try { 45 | if (!poolKeys) { 46 | try { 47 | poolKeys = (await fetchPoolAndMarketAccounts(connection, poolId)) 48 | .poolKeys 49 | } catch (e) { 50 | console.error(e) 51 | } 52 | 53 | if (!poolKeys) { 54 | console.error("Couldn't find pool keys or info.") 55 | return false 56 | } 57 | } 58 | 59 | const ixsRes = await getSwapInstructions( 60 | connection, 61 | keypair, 62 | poolKeys, 63 | "buy", 64 | amountInSol, 65 | 51, 66 | feesInSol 67 | ) 68 | 69 | if (!ixsRes) { 70 | throw new Error("No swap instructions found") 71 | } 72 | 73 | let latestBlockHash = await connection.getLatestBlockhashAndContext( 74 | "confirmed" 75 | ) 76 | 77 | const ixs = [...ixsRes.instructions] 78 | 79 | // const feesWallet = jitoPayerKeypair.publicKey 80 | 81 | // Transfer tip to Nextblock 82 | ixs.push( 83 | SystemProgram.transfer({ 84 | fromPubkey: keypair.publicKey, 85 | toPubkey: new PublicKey( 86 | "nextBLoCkPMgmG8ZgJtABeScP35qLa2AMCNKntAP7Xc" 87 | ), 88 | lamports: 0.001 * 1e9, 89 | }) 90 | ) 91 | 92 | const versionedTransaction = new VersionedTransaction( 93 | new TransactionMessage({ 94 | payerKey: keypair.publicKey, 95 | recentBlockhash: latestBlockHash.value.blockhash, 96 | instructions: ixs, 97 | }).compileToV0Message() 98 | ) 99 | 100 | versionedTransaction.sign([keypair]) 101 | 102 | return versionedTransaction.serialize() 103 | } catch (e) { 104 | console.log(e) 105 | tries++ 106 | } 107 | } 108 | } 109 | 110 | export const fetchPoolAndMarketAccounts = async ( 111 | connection: Connection, 112 | poolId: string 113 | ) => { 114 | let pool: ReturnType | undefined = 115 | undefined, 116 | market: MarketData | undefined = undefined 117 | let error: boolean | string = true 118 | let retries = 0 119 | const MAX_RETRIES = 30 120 | 121 | while (error && retries < MAX_RETRIES) { 122 | try { 123 | const poolAccountInfo = await connection.getAccountInfo( 124 | new PublicKey(poolId) 125 | ) 126 | 127 | if (!poolAccountInfo) throw new Error("Pool account not found") 128 | 129 | pool = LIQUIDITY_STATE_LAYOUT_V4.decode(poolAccountInfo.data) 130 | const { marketId } = pool 131 | 132 | const marketAccountInfo = await connection.getAccountInfo( 133 | new PublicKey(marketId) 134 | ) 135 | 136 | if (!marketAccountInfo) throw new Error("Market account not found") 137 | 138 | market = MARKET_STATE_LAYOUT_V3.decode(marketAccountInfo.data) 139 | 140 | market.marketAuthority = Market.getAssociatedAuthority({ 141 | programId: marketAccountInfo?.owner, 142 | marketId: new PublicKey(marketId), 143 | }).publicKey.toString() 144 | error = false 145 | } catch (e: any) { 146 | error = e 147 | retries++ 148 | await new Promise((resolve) => setTimeout(resolve, 300)) 149 | } 150 | } 151 | 152 | if (!pool || !market) throw new Error("Pool or market accounts not found") 153 | 154 | const poolKeys = getPoolKeysFromAccountsData(poolId, pool, market) 155 | 156 | return { poolKeys, pool, market } 157 | } 158 | 159 | export const fetchPoolInfo = async ( 160 | connection: Connection, 161 | poolKeys: LiquidityPoolKeys 162 | ) => { 163 | let poolInfo: LiquidityPoolInfo | undefined = undefined 164 | let error: boolean | string = true 165 | let retries = 0 166 | const MAX_RETRIES = 5 167 | 168 | while (error && retries < MAX_RETRIES) { 169 | try { 170 | poolInfo = await Liquidity.fetchInfo({ connection, poolKeys }) 171 | 172 | error = false 173 | } catch (e: any) { 174 | error = e 175 | retries++ 176 | await new Promise((resolve) => setTimeout(resolve, 1000)) 177 | } 178 | } 179 | 180 | if (!poolInfo) throw new Error("Pool info not found") 181 | 182 | return poolInfo 183 | } 184 | 185 | // This is necessary because Raydium library sucks. 186 | export const getPoolKeysFromAccountsData = ( 187 | poolId: string, 188 | pool: ReturnType, 189 | market: MarketData 190 | ) => { 191 | const { marketId } = pool 192 | const poolKeys = { 193 | id: new PublicKey(poolId), 194 | baseMint: pool.baseMint, 195 | quoteMint: pool.quoteMint, 196 | lpMint: pool.lpMint, 197 | baseDecimals: Number(pool.baseDecimal.toString().slice(0, 9)), 198 | quoteDecimals: Number(pool.quoteDecimal.toString().slice(0, 9)), 199 | lpDecimals: Number(pool.baseDecimal.toString().slice(0, 9)), 200 | version: 4 as 4, 201 | programId: new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), 202 | authority: new PublicKey("5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1"), 203 | openOrders: pool.openOrders, 204 | targetOrders: pool.targetOrders, 205 | baseVault: pool.baseVault, 206 | quoteVault: pool.quoteVault, 207 | withdrawQueue: new PublicKey("11111111111111111111111111111111"), 208 | lpVault: new PublicKey("11111111111111111111111111111111"), 209 | marketVersion: 3 as 3, 210 | marketProgramId: new PublicKey( 211 | "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" 212 | ), 213 | marketId: new PublicKey(marketId), 214 | marketAuthority: new PublicKey(!market.marketAuthority), 215 | marketBaseVault: market.baseVault, 216 | marketQuoteVault: market.quoteVault, 217 | marketBids: market.bids, 218 | marketAsks: market.asks, 219 | marketEventQueue: market.eventQueue, 220 | lookupTableAccount: new PublicKey("11111111111111111111111111111111"), 221 | } 222 | 223 | return poolKeys 224 | } 225 | 226 | export async function calcAmountOut( 227 | connection: Connection, 228 | poolKeys: Awaited>["poolKeys"], 229 | rawAmountIn: number, 230 | currencyInMint: PublicKey, 231 | currencyOutMint: PublicKey, 232 | slippage = 50 233 | ) { 234 | const liquidityPoolInfo = await Liquidity.fetchInfo({ connection, poolKeys }) 235 | let currencyInDecimals = 236 | currencyInMint.toString() === poolKeys.baseMint.toString() 237 | ? liquidityPoolInfo.baseDecimals 238 | : liquidityPoolInfo.quoteDecimals 239 | let currencyOutDecimals = 240 | currencyOutMint.toString() === poolKeys.baseMint.toString() 241 | ? liquidityPoolInfo.baseDecimals 242 | : liquidityPoolInfo.quoteDecimals 243 | 244 | const currencyIn = new Token( 245 | new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), 246 | currencyInMint, 247 | currencyInDecimals 248 | ) 249 | const amountIn = new TokenAmount(currencyIn, rawAmountIn, false) 250 | 251 | const currencyOut = new Token( 252 | new PublicKey("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"), 253 | currencyOutMint, 254 | currencyOutDecimals 255 | ) 256 | const slippagePercent = new Percent(slippage, 100) 257 | 258 | const { 259 | amountOut, 260 | minAmountOut, 261 | currentPrice, 262 | executionPrice, 263 | priceImpact, 264 | fee, 265 | } = Liquidity.computeAmountOut({ 266 | poolKeys, 267 | poolInfo: liquidityPoolInfo, 268 | amountIn, 269 | currencyOut, 270 | slippage: slippagePercent, 271 | }) 272 | 273 | return { 274 | amountIn, 275 | amountOut, 276 | minAmountOut, 277 | currentPrice, 278 | executionPrice, 279 | priceImpact, 280 | fee, 281 | currencyInMint, 282 | currencyOutMint, 283 | poolInfo: liquidityPoolInfo, 284 | } 285 | } 286 | 287 | export const getOwnerTokenAccounts = async ( 288 | connection: Connection, 289 | publicKey: PublicKey 290 | ) => { 291 | const walletTokenAccount = await connection.getTokenAccountsByOwner( 292 | publicKey, 293 | { 294 | programId: TOKEN_PROGRAM_ID, 295 | } 296 | ) 297 | 298 | return walletTokenAccount.value.map((i) => ({ 299 | pubkey: i.pubkey, 300 | programId: i.account.owner, 301 | accountInfo: SPL_ACCOUNT_LAYOUT.decode(i.account.data), 302 | })) 303 | } 304 | 305 | export const SOL_MINT = "So11111111111111111111111111111111111111112" 306 | 307 | // Computes amount out and makes instructions 308 | export const getSwapInstructions = async ( 309 | connection: Connection, 310 | keypair: Keypair, 311 | poolKeys: Awaited>["poolKeys"], 312 | swapType: "buy" | "sell" = "buy", 313 | amount = 0.001, 314 | slippage?: number, 315 | feesInSol?: number 316 | ) => { 317 | // Determine the currency in and out. "in" means we're buying the token with SOL, "out" means we're selling the token for SOL 318 | let currencyInMint: PublicKey, currencyOutMint: PublicKey 319 | const isBaseSol = poolKeys.baseMint.toString() === SOL_MINT 320 | 321 | switch (swapType) { 322 | case "buy": 323 | currencyInMint = new PublicKey(SOL_MINT) 324 | currencyOutMint = isBaseSol ? poolKeys.quoteMint : poolKeys.baseMint 325 | 326 | break 327 | case "sell": 328 | currencyInMint = isBaseSol ? poolKeys.quoteMint : poolKeys.baseMint 329 | currencyOutMint = new PublicKey(SOL_MINT) 330 | 331 | break 332 | } 333 | 334 | let error: boolean | string = true 335 | let retries = 0 336 | const MAX_RETRIES = swapType === "sell" ? 1 : 5 337 | 338 | while (error && retries < MAX_RETRIES) { 339 | try { 340 | const { amountIn, minAmountOut, currentPrice } = await calcAmountOut( 341 | connection, 342 | poolKeys, 343 | amount, 344 | currencyInMint, 345 | currencyOutMint, 346 | slippage || swapType === "buy" ? 51 : 11 347 | ) 348 | 349 | if (Number(minAmountOut.toExact()) <= 0) { 350 | throw new Error("Not swapping: No min amount out") 351 | } 352 | 353 | const tokenAccounts = await getOwnerTokenAccounts( 354 | connection, 355 | keypair.publicKey 356 | ) 357 | const swapTransaction = await Liquidity.makeSwapInstructionSimple({ 358 | connection: connection, 359 | makeTxVersion: 1, 360 | poolKeys, 361 | userKeys: { 362 | tokenAccounts, 363 | owner: keypair.publicKey, 364 | }, 365 | amountIn: amountIn, 366 | amountOut: minAmountOut, 367 | // in means quote is the destination token, and base is SOL. out means quote is SOL and base is the destination token 368 | // fixedSide: isBaseSol ? "in" : "out", 369 | fixedSide: "in", 370 | config: { 371 | bypassAssociatedCheck: false, 372 | }, 373 | computeBudgetConfig: { 374 | microLamports: (feesInSol || 0.000011) * 10 ** 9, 375 | }, 376 | }) 377 | 378 | const instructions = 379 | swapTransaction.innerTransactions[0].instructions.filter(Boolean) 380 | 381 | error = false 382 | 383 | return { instructions, minAmountOut, currentPrice } 384 | } catch (e: any) { 385 | console.error(e) 386 | error = e 387 | retries++ 388 | await new Promise((resolve) => setTimeout(resolve, 100)) 389 | } 390 | } 391 | 392 | if (error) { 393 | throw new Error(`Failed to ${swapType}. ` + error) 394 | } 395 | } 396 | 397 | export type MarketData = ReturnType & { 398 | marketAuthority?: string 399 | } 400 | -------------------------------------------------------------------------------- /src/lib/snipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | SystemProgram, 6 | TransactionInstruction, 7 | TransactionMessage, 8 | VersionedTransaction, 9 | } from "@solana/web3.js" 10 | import { getCoinsPairs } from "./postgres" 11 | import { getBuyRaydiumTokenTransaction } from "./raydium" 12 | // import kp from "../../payer.json" 13 | import { sendJitoBundle } from "./jito" 14 | import { getWalletTokenBalance, sendAndRetryTransaction } from "./utils" 15 | import { configDotenv } from "dotenv" 16 | import chalk from "chalk" 17 | import { getBuyPumpfunTokenTransaction } from "./pumpfun" 18 | import { TOKEN_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token" 19 | import { ASSOCIATED_TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk" 20 | import { readFileSync } from "fs" 21 | configDotenv() 22 | 23 | // const mainKeypair = Keypair.fromSecretKey(Uint8Array.from(kp)) 24 | 25 | const heliusConnection = new Connection( 26 | process.env.HELIUS_RPC_URL as string, 27 | "processed" 28 | ) 29 | 30 | const AMOUNT_TO_BUY_IN_SOL = 0.01 31 | const TX_FEES_IN_SOL = 0.000269858 32 | 33 | export const snipeAnyCoinGuaranteed = async ( 34 | coin: string, 35 | keypairs: Keypair[], 36 | attempt: number = 0 37 | ) => { 38 | try { 39 | // We only want wallets that don't have the coin yet. 40 | const snipers = ( 41 | await Promise.all( 42 | keypairs.map(async (keypair) => { 43 | // Check if bought 44 | const walletCoinBalance = await getWalletTokenBalance( 45 | heliusConnection, 46 | keypair.publicKey, 47 | new PublicKey(coin) 48 | ) 49 | 50 | if (walletCoinBalance?.value.uiAmount) { 51 | return false 52 | } 53 | 54 | const walletBalance = await heliusConnection.getBalance( 55 | keypair.publicKey 56 | ) 57 | 58 | if (walletBalance / 1e9 < 0.05) { 59 | return false 60 | } 61 | 62 | return keypair 63 | }) 64 | ) 65 | ).filter((sniper) => sniper instanceof Keypair) 66 | 67 | if (keypairs.length > 0 && snipers.length === 0) { 68 | console.log( 69 | `${chalk.green("Success")} All snipers have already bought ${coin}` 70 | ) 71 | return true 72 | } 73 | 74 | console.log(`Sniping ${coin} with ${snipers.length} wallets`) 75 | const pairByMint = await getCoinsPairs([coin]) 76 | 77 | const result = pairByMint[coin] 78 | 79 | if (!result) { 80 | throw new Error(`Couldn't find pair address for ${coin}`) 81 | } 82 | 83 | const { mint, pair, source } = result 84 | 85 | const txByWallet: Record = {} 86 | const txs = await Promise.all( 87 | snipers.map(async (keypair) => { 88 | try { 89 | let tx 90 | 91 | if (source === "Raydium") { 92 | tx = await getBuyRaydiumTokenTransaction( 93 | heliusConnection, 94 | keypair, 95 | mint, 96 | pair, 97 | AMOUNT_TO_BUY_IN_SOL, 98 | TX_FEES_IN_SOL 99 | ) 100 | } else { 101 | tx = await getBuyPumpfunTokenTransaction( 102 | heliusConnection, 103 | keypair, 104 | new PublicKey(coin), 105 | new PublicKey(pair), 106 | AMOUNT_TO_BUY_IN_SOL, 107 | TX_FEES_IN_SOL 108 | ) 109 | } 110 | 111 | if (!tx || !(tx instanceof Uint8Array)) { 112 | // Retry mounting tx again 113 | snipeAnyCoinGuaranteed(coin, [keypair], attempt + 1) 114 | return false 115 | } 116 | 117 | txByWallet[keypair.publicKey.toString()] = tx 118 | 119 | return tx 120 | } catch (e) { 121 | console.log(e) 122 | return false 123 | } 124 | }) 125 | ) 126 | 127 | if (!Object.values(txByWallet).length) { 128 | console.log("No transactions to send") 129 | return true 130 | } 131 | 132 | console.log(`Sending ${Object.values(txByWallet).length} transactions...`) 133 | 134 | // const buyTxs = txs.filter( 135 | // (tx): tx is Uint8Array => !!tx && tx instanceof Uint8Array 136 | // ) 137 | // try { 138 | // // Since we can't retry sending a Jito bundle, just send it. 139 | // sendJitoBundle(buyTxs) 140 | // } catch (e) { 141 | // console.log(e) 142 | // } 143 | 144 | const blockhashAndContext = 145 | await heliusConnection.getLatestBlockhashAndContext("processed") 146 | 147 | const bought: Record = {} 148 | await Promise.all( 149 | snipers.map(async (keypair) => { 150 | if (!txByWallet[keypair.publicKey.toString()]) return null 151 | 152 | let tries = 0, 153 | isBlockheightValid = true 154 | 155 | // Max time to send tx before mounting a new one 156 | const timeout = Date.now() + 1000 * 10 // Timeout of 10s 157 | 158 | // Try forever until bought never give up 159 | while ( 160 | !bought[keypair.publicKey.toString()] && 161 | isBlockheightValid && 162 | Date.now() < timeout 163 | ) { 164 | try { 165 | // Check if bought 166 | const walletCoinBalance = await getWalletTokenBalance( 167 | heliusConnection, 168 | keypair.publicKey, 169 | new PublicKey(coin) 170 | ) 171 | 172 | if (walletCoinBalance?.value.uiAmount) { 173 | console.log( 174 | `${chalk.green( 175 | "Success" 176 | )} Bought ${coin} for ${keypair.publicKey.toString()} | ${new Date().toUTCString()}` 177 | ) 178 | 179 | bought[keypair.publicKey.toString()] = true 180 | break 181 | } 182 | 183 | console.log(Date.now()) 184 | // Send to Nextblock first 185 | const res = await fetch("https://ny.nextblock.io/api/v2/submit", { 186 | method: "POST", 187 | body: JSON.stringify({ 188 | transaction: { 189 | content: Buffer.from( 190 | txByWallet[keypair.publicKey.toString()] 191 | ).toString("base64"), 192 | }, 193 | frontRunningProtection: true, 194 | }), 195 | 196 | headers: { 197 | Authorization: process.env.NEXTBLOCK_API_KEY as string, 198 | }, 199 | }) 200 | 201 | console.log(await res.json()) 202 | 203 | // Start sending to regular RPC 204 | // await heliusConnection.sendRawTransaction( 205 | // txByWallet[keypair.publicKey.toString()], 206 | // { 207 | // preflightCommitment: "processed", 208 | // // minContextSlot: blockhashAndContext.context.slot, 209 | // maxRetries: 0, 210 | // // skipPreflight: true, 211 | // } 212 | // ) 213 | } catch (e) { 214 | console.log(e) 215 | } finally { 216 | await new Promise((resolve) => setTimeout(resolve, 1200)) 217 | 218 | const blockHeight = await heliusConnection.getBlockHeight( 219 | "processed" 220 | ) 221 | isBlockheightValid = 222 | blockHeight <= blockhashAndContext.value.lastValidBlockHeight 223 | 224 | // // Too much time passed. Start again 225 | // if ( 226 | // !bought[keypair.publicKey.toString()] && 227 | // (!isBlockheightValid || Date.now() > timeout) 228 | // ) { 229 | // console.log( 230 | // `${chalk.yellow( 231 | // `Too much time passed. Start again to buy ${coin} for ${keypair.publicKey.toString()}` 232 | // )}` 233 | // ) 234 | // break 235 | // } 236 | 237 | tries++ 238 | } 239 | } 240 | 241 | if (!bought[keypair.publicKey.toString()]) { 242 | if (attempt < 5) { 243 | snipeAnyCoinGuaranteed(coin, [keypair], attempt + 1) 244 | } else { 245 | console.log( 246 | `${chalk.red( 247 | `Failed to buy ${coin} for ${keypair.publicKey.toString()} after 5 attempts` 248 | )}` 249 | ) 250 | } 251 | } 252 | 253 | return true 254 | }) 255 | ) 256 | } catch (e) { 257 | console.error(e) 258 | } 259 | } 260 | 261 | // Test mode only 262 | // ; (async () => { 263 | // const keypairs = [] 264 | // for (let i = 1; i <= 20; i++) { 265 | // const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(readFileSync('./wallets/' + i + ".json", 'utf-8')))) 266 | 267 | // keypairs.push(keypair) 268 | 269 | // // const balance = await heliusConnection.getBalance(keypair.publicKey) 270 | // // const balanceInSol = balance / 1e9 271 | // // console.log(balanceInSol) 272 | // // if (balanceInSol >= 0.01) { 273 | // // keypairs.push(keypair) 274 | // // } else { 275 | // // let latestBlockHash = await heliusConnection.getLatestBlockhashAndContext( 276 | // // "processed" 277 | // // ) 278 | // // console.log('from ', mainKeypair.publicKey.toString(), 'to ', keypair.publicKey.toString()) 279 | // // const tx = new VersionedTransaction( 280 | // // new TransactionMessage({ 281 | // // payerKey: mainKeypair.publicKey, 282 | // // recentBlockhash: latestBlockHash.value.blockhash, 283 | // // instructions: [SystemProgram.transfer({ 284 | // // fromPubkey: mainKeypair.publicKey, 285 | // // toPubkey: keypair.publicKey, 286 | // // lamports: ((0.01 * 1e9) - (balanceInSol * 1e9)), 287 | // // })], 288 | // // }).compileToV0Message() 289 | // // ) 290 | 291 | // // tx.sign([mainKeypair]) 292 | // // keypairs.push(keypair) 293 | // // const { txid } = await sendAndRetryTransaction(heliusConnection, tx.serialize()) 294 | // // console.log(txid) 295 | // // } 296 | // } 297 | 298 | // snipeAnyCoinGuaranteed(coin, keypairs) 299 | 300 | // })() 301 | // ;(async () => { 302 | // await snipeAnyCoinGuaranteed("ECWeqP3MnmVgKk7UHn3t6LREbiCM6vQu35gP4cU1pump", [ 303 | // mainKeypair, 304 | // ]) 305 | // })() 306 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MintLayout, 3 | TOKEN_PROGRAM_ID, 4 | getAssociatedTokenAddress, 5 | getAssociatedTokenAddressSync, 6 | } from "@solana/spl-token" 7 | import { 8 | Connection, 9 | Keypair, 10 | LAMPORTS_PER_SOL, 11 | ParsedAccountData, 12 | ParsedInstruction, 13 | ParsedTransactionWithMeta, 14 | PartiallyDecodedInstruction, 15 | PublicKey, 16 | RpcResponseAndContext, 17 | TokenAmount, 18 | VersionedTransaction, 19 | } from "@solana/web3.js" 20 | 21 | import { 22 | jsonInfo2PoolKeys, 23 | LIQUIDITY_STATE_LAYOUT_V4, 24 | } from "@raydium-io/raydium-sdk" 25 | import { writeFileSync, readFileSync, readdirSync } from "fs" 26 | import BN from "bn.js" 27 | import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor" 28 | import idl from "../data/pumpfun-idl.json" 29 | import { 30 | fetchDigitalAsset, 31 | mplTokenMetadata, 32 | } from "@metaplex-foundation/mpl-token-metadata" 33 | import { createUmi } from "@metaplex-foundation/umi-bundle-defaults" 34 | import { publicKey } from "@metaplex-foundation/umi" 35 | import { insertToken } from "./postgres" 36 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet" 37 | import example from "@/data/example-pumpfun-swap.json" 38 | import exampleTokenTx from "@/data/example-token-tx.json" 39 | import chalk from "chalk" 40 | import { token } from "@coral-xyz/anchor/dist/cjs/utils" 41 | import crypto from "crypto" 42 | import dotenv from "dotenv" 43 | import { SOL_MINT } from "./raydium" 44 | dotenv.config() 45 | 46 | export const PUMPFUN_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 47 | export const RAYDIUM_PROGRAM_ID = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" 48 | export const MOONSHOT_PROGRAM_ID = "MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG" 49 | 50 | export const heliusRpcUrl = process.env.HELIUS_RPC_URL as string 51 | 52 | const umi = createUmi(heliusRpcUrl).use(mplTokenMetadata()) 53 | 54 | const encryptionKey = Buffer.from( 55 | process.env.KEYPAIR_ENCRYPTION_KEY as string, 56 | "hex" 57 | ) 58 | const iv = Buffer.from(process.env.KEYPAIR_ENCRYPTION_IV as string, "hex") 59 | 60 | export async function encrypt(text: string) { 61 | const cipher = crypto.createCipheriv("aes-256-cbc", encryptionKey, iv) 62 | const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]) 63 | return iv.toString("hex") + ":" + encrypted.toString("hex") 64 | } 65 | 66 | export async function decrypt(text: string) { 67 | const [ivHex, encryptedText] = text.split(":") 68 | const ivBuffer = Buffer.from(ivHex, "hex") 69 | const encryptedBuffer = Buffer.from(encryptedText, "hex") 70 | const decipher = crypto.createDecipheriv( 71 | "aes-256-cbc", 72 | encryptionKey, 73 | ivBuffer 74 | ) 75 | const decrypted = Buffer.concat([ 76 | decipher.update(encryptedBuffer), 77 | decipher.final(), 78 | ]) 79 | return decrypted.toString("utf8") 80 | } 81 | 82 | export function findTokenMintAddressFromTransaction( 83 | transactionData: ParsedTransactionWithMeta 84 | ): string | null { 85 | if (!transactionData.meta) return null 86 | 87 | const { innerInstructions, postTokenBalances } = transactionData.meta 88 | 89 | if (!innerInstructions) return null 90 | 91 | // Helper function to find the mint address from postTokenBalances 92 | const findMintFromBalances = (): string | null => { 93 | if (!postTokenBalances) return null 94 | for (const balance of postTokenBalances) { 95 | if ( 96 | balance.uiTokenAmount.amount !== "0" && 97 | balance.mint !== "So11111111111111111111111111111111111111112" 98 | ) { 99 | return balance.mint 100 | } 101 | } 102 | return null 103 | } 104 | 105 | for (const instructionGroup of innerInstructions) { 106 | for (const instruction of instructionGroup.instructions) { 107 | const parsed = (instruction as ParsedInstruction).parsed 108 | if (!parsed) continue 109 | 110 | if ( 111 | parsed.type === "initializeAccount" || 112 | parsed.type === "initializeAccount3" 113 | ) { 114 | const { mint } = parsed.info 115 | if (mint && mint !== "So11111111111111111111111111111111111111112") { 116 | return mint 117 | } 118 | } 119 | 120 | if (parsed.type === "transfer") { 121 | const { destination } = parsed.info 122 | if (destination) { 123 | const mint = findMintFromBalances() 124 | if (mint && mint !== "So11111111111111111111111111111111111111112") { 125 | return mint 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | // If no mint address is found in the instructions, check postTokenBalances 133 | return findMintFromBalances() 134 | } 135 | 136 | export function getTokenPriceInSolFromTransaction( 137 | transactionData: ParsedTransactionWithMeta, 138 | // Wheter to account for sales. If false, it will only return the price if the wallet is buying tokens. 139 | accountForSales: boolean = false 140 | ) { 141 | const isDexSwap = isTransactionDexSwap(transactionData) 142 | if (!isDexSwap || !transactionData.meta) return null 143 | const { 144 | preBalances, 145 | postBalances, 146 | preTokenBalances, 147 | postTokenBalances, 148 | innerInstructions, 149 | } = transactionData.meta 150 | const { 151 | transaction: { 152 | message: { instructions }, 153 | }, 154 | } = transactionData 155 | 156 | // Find the mint address to focus on the specific token 157 | const tokenMintAddress = findTokenMintAddressFromTransaction(transactionData) 158 | 159 | if (!tokenMintAddress) return null 160 | 161 | // Filter out if the wallet is sending coins to another wallet. It's not a purchase. 162 | const signerWallet = transactionData.transaction.message.accountKeys[0].pubkey 163 | const tokenAta = getAssociatedTokenAddressSync( 164 | new PublicKey(tokenMintAddress), 165 | new PublicKey(signerWallet) 166 | ) 167 | 168 | const isTokenTransfer = instructions.some( 169 | (ix) => 170 | (ix as ParsedInstruction).parsed?.info?.source === tokenAta.toString() && 171 | (ix as ParsedInstruction).parsed?.info?.mint === tokenMintAddress 172 | ) 173 | 174 | if (isTokenTransfer) return null 175 | 176 | // Check if any other token was used in the transaction 177 | const otherTokenUsed = preTokenBalances?.some((preBalance, index) => { 178 | const postBalance = postTokenBalances?.find( 179 | (postBalance) => postBalance.mint === preBalance.mint 180 | ) 181 | if (!postBalance) return false 182 | const preAmount = Number(preBalance.uiTokenAmount.uiAmount) 183 | const postAmount = Number(postBalance.uiTokenAmount.uiAmount) 184 | return ( 185 | preAmount !== postAmount && 186 | preBalance.mint !== tokenMintAddress && 187 | preBalance.mint !== "So11111111111111111111111111111111111111112" 188 | ) 189 | }) 190 | 191 | if (otherTokenUsed) return null 192 | 193 | let solSpent 194 | 195 | if (innerInstructions) { 196 | for (const innexIxs of innerInstructions) { 197 | for (const instruction of innexIxs.instructions) { 198 | const parsed = (instruction as ParsedInstruction).parsed 199 | if (!parsed) continue 200 | 201 | if ( 202 | instruction.programId.toString() === TOKEN_PROGRAM_ID.toString() && 203 | parsed.info.authority === signerWallet.toString() && 204 | parsed.type === "transfer" 205 | ) { 206 | const amount = Number(parsed.info.amount) / 1e9 207 | 208 | solSpent = amount 209 | } 210 | } 211 | } 212 | } 213 | 214 | if (!solSpent) { 215 | solSpent = (preBalances[0] - postBalances[0]) / 1000000000 // Convert lamports to SOL 216 | } 217 | 218 | // Get SOL spent by comparing pre and post balances of the main account 219 | 220 | // Get the token amount received by comparing pre and post token balances 221 | let tokensReceived = 0 222 | 223 | if (!postTokenBalances) return null 224 | for (let i = 0; i < postTokenBalances.length; i++) { 225 | if ( 226 | postTokenBalances[i].mint === tokenMintAddress && 227 | postTokenBalances[i].owner === signerWallet.toString() 228 | ) { 229 | if (!preTokenBalances) continue 230 | const preTokenBalance = preTokenBalances.find( 231 | (balance) => 232 | balance.mint === tokenMintAddress && 233 | balance.owner === signerWallet.toString() 234 | ) 235 | const preAmount = preTokenBalance 236 | ? Number(preTokenBalance.uiTokenAmount.uiAmount) 237 | : 0 238 | const postAmount = Number(postTokenBalances[i].uiTokenAmount.uiAmount) 239 | tokensReceived = postAmount - preAmount 240 | break 241 | } 242 | } 243 | 244 | if (!accountForSales) { 245 | if (tokensReceived <= 0) return null 246 | } 247 | 248 | // Calculate the price per token in SOL 249 | const pricePerToken = solSpent / tokensReceived 250 | return { solSpent, pricePerToken, tokensReceived } 251 | } 252 | 253 | export function isTransactionDexSwap(tx: ParsedTransactionWithMeta) { 254 | const raydiumIx = tx.transaction.message.accountKeys.find( 255 | (key) => 256 | key.pubkey.toString() === "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" 257 | ) 258 | 259 | const pumpIx = tx.transaction.message.accountKeys.find( 260 | (key) => 261 | key.pubkey.toString() === "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" 262 | ) 263 | 264 | const jupIx = tx.transaction.message.accountKeys.find( 265 | (key) => 266 | key.pubkey.toString() === "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" 267 | ) 268 | 269 | const moonshotIx = tx.transaction.message.accountKeys.find( 270 | (key) => 271 | key.pubkey.toString() === "MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG" 272 | ) 273 | 274 | const meteoraIx = tx.transaction.message.accountKeys.find( 275 | (key) => 276 | key.pubkey.toString() === "Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB" 277 | ) 278 | 279 | return !!raydiumIx || !!pumpIx || !!jupIx || !!moonshotIx || !!meteoraIx 280 | } 281 | 282 | export const getTokensTransactionsFromFiles = (tokensFilter?: string[]) => { 283 | const files = readdirSync("./transactions/") 284 | const transactionsFiles = files.filter( 285 | (file) => file.indexOf("transactions") > -1 286 | ) 287 | 288 | const transactionsPerToken: { 289 | [token: string]: (typeof exampleTokenTx)[] 290 | } = {} 291 | for (const file of transactionsFiles) { 292 | if (!file.match(/^[a-zA-Z0-9]+-transactions\.json$/)) continue 293 | const path = `./transactions/${file}` 294 | const token = file.split("-")[0] 295 | if (tokensFilter && !tokensFilter.includes(token)) continue 296 | const content = JSON.parse(readFileSync(path, "utf-8")) 297 | console.log(path) 298 | transactionsPerToken[token] = content 299 | } 300 | 301 | // Heap memory 302 | // writeFileSync( 303 | // "./data/transactionsPerToken.json", 304 | // JSON.stringify(transactionsPerToken, null, 2) 305 | // ) 306 | 307 | return transactionsPerToken 308 | } 309 | 310 | export const getSolanaPrice = async () => { 311 | const priceRes = (await ( 312 | await fetch( 313 | "https://hermes.pyth.network/v2/updates/price/latest?ids%5B%5D=0xef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d" 314 | ) 315 | ).json()) as { 316 | parsed: { 317 | price: { 318 | price: string 319 | } 320 | }[] 321 | } 322 | 323 | const solanaPrice = Number(priceRes.parsed[0].price.price) / 1e8 324 | 325 | return solanaPrice 326 | } 327 | 328 | export const getTransactionDataFromWebhookTransaction = ( 329 | transaction: typeof example 330 | ) => { 331 | const pumpFunIx = getTransactionInstructionByProgramId( 332 | transaction, 333 | PUMPFUN_PROGRAM_ID 334 | ) 335 | 336 | const raydiumIx = getTransactionInstructionByProgramId( 337 | transaction, 338 | RAYDIUM_PROGRAM_ID 339 | ) 340 | 341 | const moonshotIx = getTransactionInstructionByProgramId( 342 | transaction, 343 | MOONSHOT_PROGRAM_ID 344 | ) 345 | 346 | const programIx = pumpFunIx || raydiumIx || moonshotIx 347 | 348 | if (!programIx) 349 | throw new Error("Program instruction not found " + transaction.signature) 350 | 351 | const tokenVault = 352 | pumpFunIx || moonshotIx ? programIx.accounts[3] : programIx.accounts[5] 353 | 354 | const payer = transaction.feePayer 355 | 356 | const { tokenTransfers, nativeTransfers } = transaction 357 | 358 | let isBuy = false, 359 | isSell = false, 360 | tokenMint, 361 | tokenAmount, 362 | solAmount 363 | 364 | for (const transfer of tokenTransfers) { 365 | const isSol = transfer.mint === SOL_MINT 366 | const isToUser = transfer.toUserAccount === payer 367 | const isFromUser = transfer.fromUserAccount === payer 368 | let isToVault, isFromVault 369 | 370 | if (raydiumIx) { 371 | isToVault = transfer.toUserAccount === RAYDIUM_AUTHORITY 372 | isFromVault = transfer.fromUserAccount === RAYDIUM_AUTHORITY 373 | } else { 374 | isToVault = transfer.toUserAccount === tokenVault 375 | isFromVault = transfer.fromUserAccount === tokenVault 376 | } 377 | 378 | if ( 379 | !isSol && 380 | !tokenMint && 381 | ((isFromUser && isToVault) || (isFromVault && isToUser)) 382 | ) { 383 | tokenMint = transfer.mint 384 | } 385 | 386 | if ( 387 | !tokenAmount && 388 | !isSol && 389 | isToUser && 390 | transfer.mint === tokenMint && 391 | isFromVault 392 | ) { 393 | isBuy = true 394 | tokenAmount = transfer.tokenAmount 395 | } else if (!solAmount && isSol && isToUser && isFromVault) { 396 | isSell = true 397 | solAmount = transfer.tokenAmount 398 | } else if (!solAmount && isSol && isFromUser && isToVault) { 399 | isBuy = true 400 | solAmount = transfer.tokenAmount 401 | } else if ( 402 | !tokenAmount && 403 | !isSol && 404 | isFromUser && 405 | transfer.mint === tokenMint && 406 | isToVault 407 | ) { 408 | isSell = true 409 | tokenAmount = transfer.tokenAmount 410 | } 411 | } 412 | 413 | if (!solAmount && pumpFunIx) { 414 | const bondingCurveSolChange = transaction.accountData.find( 415 | (accData) => accData.account === tokenVault && accData.nativeBalanceChange 416 | )?.nativeBalanceChange 417 | solAmount = bondingCurveSolChange ? bondingCurveSolChange / 1e9 : undefined 418 | } 419 | 420 | if (solAmount && tokenAmount && tokenMint) { 421 | const price = Math.abs(solAmount) / Math.abs(tokenAmount) 422 | 423 | return { 424 | solAmount: Math.abs(solAmount), 425 | tokenAmount: Math.abs(tokenAmount), 426 | tokenMint, 427 | isBuy, 428 | isSell, 429 | tokenVault, 430 | price, 431 | } 432 | } else { 433 | console.log( 434 | "Price not found", 435 | transaction.signature, 436 | tokenMint, 437 | "solAmount", 438 | solAmount, 439 | "tokenAmount", 440 | tokenAmount, 441 | isBuy, 442 | isSell 443 | ) 444 | return 445 | } 446 | } 447 | 448 | export const getTransactionInstructionByProgramId = ( 449 | tx: typeof example, 450 | programId: string 451 | ) => { 452 | let instruction 453 | instruction = tx.instructions.find((ix) => ix.programId === programId) 454 | 455 | if (!instruction) { 456 | tx.instructions.forEach((ix) => { 457 | if (ix.innerInstructions) { 458 | const found = ix.innerInstructions.find( 459 | (innerIx) => innerIx.programId === programId 460 | ) 461 | if (found) instruction = found 462 | } 463 | }) 464 | } 465 | 466 | return instruction 467 | } 468 | 469 | // @TODO there MUST be a better way to find price for a token. 470 | export const getTokenPrice = async ( 471 | connection: Connection, 472 | mint: string, 473 | pairAddress: string 474 | ) => { 475 | let tokenPriceInSol 476 | let tries = 1 477 | 478 | while (!tokenPriceInSol && tries < 5) { 479 | try { 480 | const raydiumPrice = await getRaydiumTokenPrice( 481 | connection, 482 | new PublicKey(pairAddress) 483 | ) 484 | 485 | if (raydiumPrice) { 486 | tokenPriceInSol = raydiumPrice 487 | } 488 | } catch (e) { 489 | await new Promise((resolve) => setTimeout(resolve, 1000)) 490 | try { 491 | const pumpFunPrice = await getPumpFunTokenPrice(pairAddress) 492 | 493 | if (pumpFunPrice) { 494 | tokenPriceInSol = Number(pumpFunPrice.price.toFixed(9)) 495 | } 496 | } catch (e) { 497 | await new Promise((resolve) => setTimeout(resolve, 1000)) 498 | } 499 | } finally { 500 | tries++ 501 | await new Promise((resolve) => setTimeout(resolve, 1000)) 502 | 503 | if (tries === 5) 504 | throw new Error( 505 | chalk.red(`Price not found for token ${mint} after 5 tries.`) 506 | ) 507 | } 508 | } 509 | 510 | return tokenPriceInSol 511 | } 512 | 513 | export const getTokenPairAddressAndAsset = async ( 514 | tokenMint: string, 515 | fetchAsset = false 516 | ) => { 517 | try { 518 | let pairAddress 519 | 520 | // Try raydium first. If the token is from Pump.fun, we don't want to save the Pump.fun pair address, because it will only have the pump.fun liquidity. 521 | const apiRes = await fetch( 522 | "https://api.dexscreener.com/latest/dex/tokens/" + tokenMint 523 | ) 524 | 525 | if (apiRes.ok) { 526 | const { pairs }: { pairs: PairData[] } = await apiRes.json() 527 | 528 | const raydiumPair = pairs?.find((pair) => pair.dexId === "raydium") 529 | 530 | if (raydiumPair) { 531 | const { 532 | priceNative, 533 | pairAddress: raydiumPairAddress, 534 | liquidity: { usd }, 535 | } = raydiumPair 536 | 537 | pairAddress = raydiumPairAddress 538 | } 539 | } else { 540 | console.log( 541 | chalk.red("Dexscreener API error:"), 542 | await apiRes.text(), 543 | token 544 | ) 545 | if (apiRes.status === 429) { 546 | throw new Error(chalk.red("Dexscreener API error: Rate limit reached.")) 547 | } 548 | } 549 | 550 | // Try pump.fun if raydium didn't work 551 | if (!pairAddress) { 552 | const res = await fetch( 553 | `https://frontend-api.pump.fun/coins/${tokenMint}` 554 | ) 555 | 556 | if (!res.ok) { 557 | if (res.status === 429) { 558 | throw new Error(chalk.red("Pump.fun API error: Rate limit reached.")) 559 | } 560 | throw new Error(chalk.red("Pump.fun API response not ok")) 561 | } 562 | 563 | const { associated_bonding_curve, bonding_curve } = await res.json() 564 | 565 | if (!bonding_curve) { 566 | throw new Error( 567 | chalk.red("Pump.fun API response, but no bonding curve found") 568 | ) 569 | } 570 | 571 | pairAddress = bonding_curve 572 | } 573 | 574 | if (pairAddress) { 575 | let asset 576 | if (fetchAsset) { 577 | asset = await fetchMintAsset(tokenMint) 578 | if (!asset) throw new Error("Digital Asset not found") 579 | } 580 | 581 | return { 582 | tokenMint, 583 | asset, 584 | pairAddress, 585 | } 586 | } else { 587 | throw `${chalk.red( 588 | "No API response and no bonding curve" 589 | )} for ${tokenMint}: No API Response and no bonding curve` 590 | } 591 | } catch (e) { 592 | throw e 593 | } 594 | } 595 | 596 | export const fetchMintAsset = async ( 597 | mint: string, 598 | fetchOffchainJson: boolean = false 599 | ) => { 600 | let withMetadata 601 | const maxRetries = 5 602 | let retries = 0 603 | let error 604 | 605 | while (!withMetadata && retries < maxRetries) { 606 | try { 607 | const asset = await fetchDigitalAsset(umi, publicKey(mint)) 608 | 609 | if (fetchOffchainJson) { 610 | const res = await fetch(asset.metadata.uri) 611 | if (!res.ok) 612 | throw new Error( 613 | "Offchain metadata not found " + mint + ` ${asset.metadata.uri}` 614 | ) 615 | 616 | const metadata: Metadata = await res.json() 617 | 618 | withMetadata = { 619 | ...asset, 620 | metadata: { 621 | ...asset.metadata, 622 | offchain: metadata, 623 | }, 624 | } 625 | } else { 626 | withMetadata = asset 627 | } 628 | } catch (e) { 629 | retries++ 630 | console.log("Retrying...") 631 | await new Promise((resolve) => setTimeout(resolve, 1500)) 632 | if (retries === maxRetries) { 633 | console.log("Max retries reached, asset not found" + e) 634 | error = e 635 | } 636 | } 637 | } 638 | 639 | if (!withMetadata) throw error 640 | 641 | return withMetadata 642 | } 643 | 644 | type Metadata = { 645 | name: string 646 | symbol: string 647 | description: string 648 | image: string 649 | showName: string 650 | createdOn: string 651 | telegram: string 652 | twitter: string 653 | } 654 | 655 | const pumpFunProgram = new Program( 656 | idl as Idl, 657 | new PublicKey(PUMPFUN_PROGRAM_ID), 658 | new AnchorProvider( 659 | new Connection(heliusRpcUrl), 660 | new NodeWallet(Keypair.generate()), 661 | {} 662 | ) 663 | ) 664 | 665 | export const getPumpFunTokenPrice = async ( 666 | bondingCurve: string, 667 | program: Program = pumpFunProgram 668 | ) => { 669 | const bondingCurveData = (await program.account.bondingCurve.fetch( 670 | bondingCurve 671 | )) as { 672 | virtualTokenReserves: BN 673 | virtualSolReserves: BN 674 | } 675 | 676 | const decimals = 6 677 | const virtualTokenReserves = bondingCurveData.virtualTokenReserves.toNumber() 678 | const virtualSolReserves = bondingCurveData.virtualSolReserves.toNumber() 679 | 680 | const parsedVirtualTokenReserves = virtualTokenReserves / 10 ** decimals 681 | const parsedVirtualSolReserves = virtualSolReserves / LAMPORTS_PER_SOL 682 | 683 | const price = parsedVirtualSolReserves / parsedVirtualTokenReserves 684 | 685 | return { price, solReserve: parsedVirtualSolReserves } 686 | } 687 | 688 | export const RAYDIUM_AUTHORITY = "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1" 689 | 690 | export const getMintData = async ( 691 | connection: Connection, 692 | mint: PublicKey, 693 | vault: string 694 | ) => { 695 | const token = await connection.getAccountInfo(mint) 696 | if (!token) throw new Error("Token not found") 697 | const parsed = MintLayout.decode(token.data) 698 | 699 | const isRenounced = !parsed.mintAuthorityOption 700 | const tokenSupply = await connection.getTokenSupply(mint) 701 | 702 | const largest = await connection.getTokenLargestAccounts(mint) 703 | 704 | const accountInfos = await connection.getMultipleParsedAccounts( 705 | largest.value.slice(0, 5).map((acc) => acc.address), 706 | { 707 | commitment: "confirmed", 708 | } 709 | ) 710 | 711 | let lpSupply: number | undefined = undefined 712 | 713 | // Figure out how many tokens percentage the top 5 holders hold 714 | const topFiveHoldersHold = largest.value 715 | .slice(0, 6) 716 | .reduce((acc, curr, index) => { 717 | const accountInfo = accountInfos.value[index] 718 | 719 | if (!accountInfo) return acc 720 | const owner = (accountInfo.data as ParsedAccountData).parsed.info.owner 721 | 722 | if (owner === vault && curr.uiAmount) { 723 | lpSupply = curr.uiAmount 724 | 725 | return acc 726 | } 727 | 728 | return curr.uiAmount 729 | ? acc + curr.uiAmount 730 | : acc + Number(curr.amount) / 10 ** curr.decimals 731 | }, 0) 732 | 733 | const topFivePercentage = 734 | (topFiveHoldersHold * 100) / Number(tokenSupply.value.uiAmount) 735 | 736 | const lpPercentage = (lpSupply! * 100) / Number(tokenSupply.value.uiAmount) 737 | 738 | return { 739 | topFivePercentage, 740 | isRenounced, 741 | tokenSupply, 742 | lpSupply, 743 | lpPercentage, 744 | } 745 | } 746 | 747 | export async function getRaydiumTokenPrice( 748 | connection: Connection, 749 | pairAddress: PublicKey = new PublicKey( 750 | "2STjwk3LhwGT6T57RfiX381A4bDxcnTx7b6Mg6yLEC9m" 751 | ) 752 | ) { 753 | const poolAccountInfo = await connection.getAccountInfo( 754 | new PublicKey(pairAddress) 755 | ) 756 | 757 | if (!poolAccountInfo) throw new Error("Invalid pair address") 758 | 759 | const pool = LIQUIDITY_STATE_LAYOUT_V4.decode(poolAccountInfo.data) 760 | 761 | if (!pool) throw new Error("Pool account not found") 762 | 763 | const { baseVault, baseMint, quoteVault, quoteMint } = pool 764 | 765 | const [quoteReserve, baseReserve] = await Promise.all([ 766 | connection.getTokenAccountBalance(quoteVault), 767 | connection.getTokenAccountBalance(baseVault), 768 | ]) 769 | 770 | if (!quoteReserve || !quoteReserve) { 771 | throw new Error("Could not find token account balance for pool reserves") 772 | } 773 | 774 | if (!quoteReserve.value.uiAmount || !baseReserve.value.uiAmount) { 775 | throw new Error("Reserve token accounts found but no uiAmount") 776 | } 777 | const price = 778 | baseMint.toString() === SOL_MINT 779 | ? baseReserve.value.uiAmount / quoteReserve.value.uiAmount 780 | : quoteReserve.value.uiAmount / baseReserve.value.uiAmount 781 | 782 | const toFixed = Number(price.toFixed(9)) 783 | 784 | return toFixed 785 | } 786 | 787 | export interface DasApiTokenResponse { 788 | interface: "FungibleToken" 789 | id: string 790 | content: { 791 | $schema: string 792 | json_uri: string 793 | files: [object] // You might want to define a more specific type for files 794 | metadata: { 795 | description: string 796 | name: string 797 | symbol: string 798 | token_standard: string 799 | } 800 | links: { 801 | image: string 802 | } 803 | } 804 | authorities: [ 805 | { 806 | address: string 807 | scopes: string[] 808 | } 809 | ] 810 | compression: { 811 | eligible: boolean 812 | compressed: boolean 813 | data_hash: string 814 | creator_hash: string 815 | asset_hash: string 816 | tree: string 817 | seq: number 818 | leaf_id: number 819 | } 820 | grouping: any[] // You might want to define a more specific type for grouping 821 | royalty: { 822 | royalty_model: string 823 | target: null 824 | percent: number 825 | basis_points: number 826 | primary_sale_happened: boolean 827 | locked: boolean 828 | } 829 | creators: any[] // You might want to define a more specific type for creators 830 | ownership: { 831 | frozen: boolean 832 | delegated: boolean 833 | delegate: null 834 | ownership_model: string 835 | owner: string 836 | } 837 | supply: null 838 | mutable: boolean 839 | burnt: boolean 840 | token_info: { 841 | supply: number | null 842 | decimals: number 843 | token_program: string 844 | } 845 | } 846 | 847 | export const getAssetData = async (id: string) => { 848 | const response = await fetch(heliusRpcUrl, { 849 | method: "POST", 850 | headers: { 851 | "Content-Type": "application/json", 852 | }, 853 | body: JSON.stringify({ 854 | jsonrpc: "2.0", 855 | id: "my-id", 856 | method: "getAsset", 857 | params: { 858 | id, 859 | displayOptions: { 860 | showFungible: true, //return details about a fungible token 861 | }, 862 | }, 863 | }), 864 | }) 865 | const { result } = await response.json() 866 | 867 | return result as DasApiTokenResponse 868 | } 869 | 870 | export const getWalletTokenBalance = async ( 871 | connection: Connection, 872 | publicKey: PublicKey, 873 | mint: PublicKey 874 | ) => { 875 | let tokenAccount: PublicKey | undefined = undefined, 876 | balance: RpcResponseAndContext | undefined = undefined 877 | 878 | let error: boolean | string = true 879 | let retries = 0 880 | const MAX_RETRIES = 10 881 | 882 | while (error && retries < MAX_RETRIES) { 883 | try { 884 | tokenAccount = await getAssociatedTokenAddress(mint, publicKey) 885 | balance = await connection.getTokenAccountBalance(tokenAccount) 886 | error = false 887 | } catch (e: any) { 888 | error = e 889 | retries++ 890 | await new Promise((resolve) => setTimeout(resolve, 200)) 891 | } 892 | } 893 | 894 | return balance 895 | } 896 | 897 | // @ts-ignore 898 | BigInt.prototype["toJSON"] = function () { 899 | return parseInt(this.toString()) 900 | } 901 | 902 | export const sendAndRetryTransaction = async ( 903 | connection: Connection, 904 | transaction: Uint8Array 905 | ) => { 906 | let blockHeight 907 | let txid 908 | let confirmed = false 909 | 910 | const blockhashAndContext = await connection.getLatestBlockhashAndContext( 911 | "processed" 912 | ) 913 | 914 | try { 915 | blockHeight = await connection.getBlockHeight("processed") 916 | txid = await connection.sendRawTransaction(transaction, { 917 | // skipPreflight: true, 918 | preflightCommitment: "processed", 919 | // minContextSlot: blockhashAndContext.context.slot, 920 | // maxRetries: 0, 921 | }) 922 | 923 | connection 924 | .confirmTransaction(txid, "processed") 925 | .then(() => { 926 | confirmed = true 927 | }) 928 | .catch((e) => {}) 929 | } catch (e) { 930 | console.error(e) 931 | 932 | return {} 933 | } 934 | 935 | // while ( 936 | // blockHeight < blockhashAndContext.value.lastValidBlockHeight && 937 | // !confirmed 938 | // ) { 939 | // try { 940 | // txid = await connection.sendRawTransaction(transaction, { 941 | // skipPreflight: true, 942 | // preflightCommitment: "processed", 943 | // // minContextSlot: blockhashAndContext.context.slot, 944 | // // maxRetries: 0, 945 | // }) 946 | // blockHeight = await connection.getBlockHeight("processed") 947 | // await new Promise(resolve => setTimeout(resolve, 2500)) 948 | // } catch (e) { } 949 | // } 950 | 951 | return { txid } 952 | } 953 | 954 | type TokenInfo = { 955 | address: string 956 | name: string 957 | symbol: string 958 | } 959 | 960 | type TxnsData = { 961 | buys: number 962 | sells: number 963 | } 964 | 965 | type VolumeData = { 966 | h24: number 967 | h6: number 968 | h1: number 969 | m5: number 970 | } 971 | 972 | type PriceChangeData = { 973 | m5: number 974 | h1: number 975 | h6: number 976 | h24: number 977 | } 978 | 979 | type LiquidityData = { 980 | usd: number 981 | base: number 982 | quote: number 983 | } 984 | 985 | type Website = { 986 | label: string 987 | url: string 988 | } 989 | 990 | type Social = { 991 | type: string 992 | url: string 993 | } 994 | 995 | type TokenImage = { 996 | imageUrl: string 997 | websites: Website[] 998 | socials: Social[] 999 | } 1000 | 1001 | export type PairData = { 1002 | chainId: string 1003 | dexId: string 1004 | url: string 1005 | pairAddress: string 1006 | baseToken: TokenInfo 1007 | quoteToken: TokenInfo 1008 | priceNative: string 1009 | priceUsd: string 1010 | txns: { 1011 | m5: TxnsData 1012 | h1: TxnsData 1013 | h6: TxnsData 1014 | h24: TxnsData 1015 | } 1016 | volume: VolumeData 1017 | priceChange: PriceChangeData 1018 | liquidity: LiquidityData 1019 | fdv: number 1020 | pairCreatedAt: number 1021 | info: TokenImage 1022 | } 1023 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "outDir": "./dist", 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "commonjs" /* Specify what module code is generated. */, 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 32 | "baseUrl": "./", // Specify the base directory to resolve non-relative module names. 33 | "paths": { 34 | "@/*": ["src/*"] // Map '@' to the 'src' directory. 35 | }, 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 42 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 43 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 44 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 45 | "resolveJsonModule": true /* Enable importing .json files. */, 46 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 47 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 48 | 49 | /* JavaScript Support */ 50 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 51 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 52 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 53 | 54 | /* Emit */ 55 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 56 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 57 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 58 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 59 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "noEmit": true, /* Disable emitting files from a compilation. */ 64 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 65 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 66 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 67 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 68 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 69 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 70 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 71 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 72 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 73 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 74 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 75 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 76 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 77 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 78 | 79 | /* Interop Constraints */ 80 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 81 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 82 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 83 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 84 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 85 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 86 | 87 | /* Type Checking */ 88 | "strict": true /* Enable all strict type-checking options. */, 89 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 90 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 91 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 92 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 93 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 94 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 95 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 96 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 97 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 98 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 99 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 100 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 101 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 102 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 103 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 104 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 105 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 106 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 107 | 108 | /* Completeness */ 109 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 110 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 111 | } 112 | } 113 | --------------------------------------------------------------------------------