├── README.md ├── assets ├── Snipaste_2025-05-18_21-37-06.png ├── en_readme.md └── pump.png ├── install.sh └── src ├── .gitignore ├── bot ├── arb_bot.js └── mintx_handler.js ├── common ├── constants.js └── utils.js ├── config.js ├── doc └── how-to-use-contract.md ├── idl ├── guard.json └── idl.json ├── main.js ├── package-lock.json ├── package.json ├── pool_fetcher ├── fetcher.js ├── meteora_amm_fetcher.js ├── meteora_dlmm_fetcher.js ├── pump_fetcher.js ├── raydium_amm_fetcher.js ├── raydium_clmm_fetcher.js └── raydium_cpmm_fetcher.js ├── pool_finder ├── meteora_amm_finder.js ├── meteora_finder.js ├── pool_finder.js ├── pump_finder.js └── raydium_finder.js └── tools └── warp_sol.js /README.md: -------------------------------------------------------------------------------- 1 | [English](https://github.com/touyi/solana-onchain-arbitrage-bot/blob/main/assets/en_readme.md) 2 | 3 | # 🌟star 破 388 开源套利合约 4 | # solana-onchain-arbitrage-bot 5 | 6 | 支持智能路由,计算最优套利路线 7 | solana onchain arbitrage bot, supports intelligent routing and calculates optimal arbitrage routes 8 | 9 | # 📝基本介绍 10 | 11 | 本仓库是基于合约智能路由的套利机器人,分为客户端和合约端,本仓库是客户端,主要功能: 12 | 13 | * 获取需要套利的交易池 14 | * 自动配对交易池,并且组建交易提交 15 | 16 | 对于合约端(合约地址:DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z) 17 | 18 | * 接收客户端提交的交易池 19 | * 自动计算最优套利路径以及最优输入(如有),并发起套利操作,否则交易返回失败 20 | 21 | 目前已经支持的交易池,后续会持续新增 22 | 23 | * PumpSwap 24 | * Raydium AMM 25 | * Raydium CPMM 26 | * Raydium CLMM 27 | * Meteora DLMM 28 | * Meteora Dynamic Pools 29 | 30 | > **使用本套利机器人对于成功套利的交易,收取利润的10%作为手续费,如果套利失败,或者无利润,本合约不收取任何手续费。** 31 | > 例如套利 输入0.5 sol,输出0.6 sol,利润就是0.1sol,手续费为0.1 * 10% = 0.01 sol 32 | 33 | # 🚀快速开始 34 | 35 | ## 安装环境 36 | 37 | > 目前只支持一键linux系统安装 38 | 39 | 根目录执行: 40 | 41 | ``` 42 | bash install.sh 43 | ``` 44 | 45 | ## 基本配置 46 | 47 | solana-onchain-arbitrage-bot/src/config.js 48 | 49 | config.js 中的以下配置必须填写,screctKey和screctKeyBase58填写一个即可 50 | ### 密钥 51 | ``` 52 | "base": { 53 | // rpc 请求url 54 | "rpcUrl": "https://api.mainnet-beta.solana.com", 55 | // 钱包私钥,32个数字 screctKey和screctKeyBase58 2个填一个即可 56 | "screctKey": [], 57 | // 钱包私钥base58编码字符串 screctKey和screctKeyBase58 2个填一个即可 58 | "screctKeyBase58": "", 59 | }, 60 | ``` 61 | ### 设置监听的mint 62 | 在配置中添加需要监听的mint,会自动拉取mint相关的pump raydium Meteora的pool,用于套利 63 | ```json 64 | // 需要监听套利的mint列表,自己任意修改, 65 | // 可以直接去爬gmgn的热榜数据:https://gmgn.ai/?chain=sol&1ren=0&1fr=1&1bu=0&1di=0&0fr=0 66 | "mintList": [ 67 | "B5boCskXEFb1RJJ9EqJNV3gt5fjhk85DeD7BgbLcpump", 68 | ... 69 | ], 70 | ``` 71 | 72 | ## 转换wsol 73 | 74 | dex一般都是直接使用wsol来交易,所以在启动交易的时候需要先在账户中转换好等量的wsol,使用仓库中提供的脚本进行转换: 75 | 在src/tools目录下执行:`node warp_sol.js --amount=1000000000` 76 | 其中amount是lamport单位的sol数量 77 | 当需要关闭wsol退回sol是执行`node warp_sol.js --amount=1000000000 --close` 78 | > 需要保证转换的数量需要大于config.js 配置中的maxInputAmount 配置字段,默认配置`1000000000`即1WSOL 79 | 80 | 81 | 82 | 83 | ## 启动运行 84 | 85 | 在src目录下执行: 86 | ```bash 87 | node main.js 88 | ``` 89 | 启动后看到如下日志输出: 90 | ```js 91 | 🚀 ~ swap::e: SendTransactionError: Simulation failed. 92 | Catch the `SendTransactionError` and call `getLogs()` on it for full details. 93 | at Connection.sendEncodedTransaction (/home/touyi/sol/solana-onchain-arbitrage-bot/src/node_modules/@solana/web3.js/lib/index.cjs.js:8206:13) 94 | at process.processTicksAndRejections (node:internal/process/task_queues:105:5) 95 | at async Connection.sendRawTransaction (/home/touyi/sol/solana-onchain-arbitrage-bot/src/node_modules/@solana/web3.js/lib/index.cjs.js:8171:20) 96 | at async file:///home/touyi/sol/solana-onchain-arbitrage-bot/src/bot/arb_bot.js:237:43 { 97 | signature: '', 98 | transactionMessage: 'Transaction simulation failed: Error processing Instruction 2: custom program error: 0x1772', 99 | transactionLogs: [ 100 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 101 | 'Program ComputeBudget111111111111111111111111111111 success', 102 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 103 | 'Program ComputeBudget111111111111111111111111111111 success', 104 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi invoke [1]', 105 | 'Program log: Instruction: ArbProcess32Account', 106 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z invoke [2]', 107 | 'Program log: Instruction: ArbProcess32Account', 108 | 'Program log: max_in 1000000000, min_profit 100, market_type [0, 0], market_flag [0, 0]', 109 | 'Program log: sol_log_compute_units programs/arb_touyi/src/processor.rs:550', 110 | 'Program consumption: 257040 units remaining', 111 | 'Program log: sol_log_compute_units programs/arb_touyi/src/processor.rs:20', 112 | 'Program consumption: 250388 units remaining', 113 | 'Program log: iy:0 dx:0 oy:0', 114 | 'Program log: iy:0 dx:0 oy:0', 115 | 'Program log: AnchorError thrown in programs/arb_touyi/src/processor.rs:48. Error Code: NoProfit. Error Number: 6002. Error Message: Not Profit.', 116 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z consumed 25565 of 268555 compute units', 117 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z failed: custom program error: 0x1772', 118 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi consumed 56710 of 299700 compute units', 119 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi failed: custom program error: 0x1772' 120 | ] 121 | } 122 | ``` 123 | 其中`Error Message: Not Profit.`,表示已经开始正常运行,这里之所以有`Not Profit`报错是因为在仿真交易的时候就已经发现没有利润可套,于是不会实际发送这个交易,只有仿真成功的时候才会实际发送交易。 124 | 如果期望所有交易都上链,可以配置bot: `"skipPreflight": false,`,配置后所有交易都会上链,但是注意,上链后即使交易失败,也会损失gas 125 | 126 | > **注意**:套利的用户中间代币账户(例如trump)如果不存在,机器人会自动创建,无需手动创建;如果存在,请确保该账户余额为空 127 | 128 | # 🌟套利原理 129 | 130 | 对于任意两个交易池,如果存在价格差,则可能通过套利来获取利润,假设: 131 | * 在交易池1中使用a个SOL买入b个代币X 132 | * 在交易池2中使用b个代币X卖出得到c个SOL 133 | * 可能的利润:p = c - a 134 | 135 | 136 | 如果p大于0,则可以进行套利,否则无法套利 137 | 138 | 对于多个交易池的套利,需要在交易池中提前计算出最优套利路线,然后按照最优路线进行套利。 139 | 140 | 限制于合约算力,目前只支持一跳最优路径计算,即:SOL->X->SOL, 合约会对输入的所以交易池两两配对,计算出最优套利的配对路径,然后按照最优路径进行套利。如果所有路径都无法套利,则不会进行套利。并且合约会报错,使得交易失败 141 | 142 | 143 | # 📅如何信任? 144 | 145 | 因为合约代码并不开源,所以让人担心的问题就是: 146 | * 我的私钥会不会被上传? 147 | * 你会不会在合约里面把我的代币全部转走? 148 | 149 | 为了取得信任,我们从以下几个方面来保证不会出现上述情况: 150 | 151 | **对于第一个问题**:客户端代码完全开源,你可以看到客户端代码中,私钥只用于本地签名交易,不会上传发送给任何第三方,也不会被合约读取到。 152 | 153 | **对于第二个问题**:套利合约代码并不开源,所以在不加防护的情况下合约拥有者确实可以做到这一点,因此我们开源了一份[防损失合约](https://github.com/touyi/guard_contract)代码仓库,其原理如下: 154 | 155 | 在套利合约当中,通过客户端代码我们可以看到输入套利合约的用户token账户只包含 156 | * sol账户(signer默认携带) 157 | * wsol账户(userTokenBase) 158 | * 套利的中间代币账户(tokenPair0UserTokenAccountX) 159 | 160 | 在套利合约当中,只可能做到转移这三个账户的余额。 161 | 162 | **“套利的中间代币账户”** 因为需要保证余额为空,所以是没有的。 163 | 164 | 对于SOL和WSOL,在[防损失合约](https://github.com/touyi/guard_contract)中,会确保套利合约调用后WSOL+SOL的数量一定大于调用前的数量,否则合约失败,整个交易回滚。以此保证不会在套利合约中出现损失。具体实现参考[防损失合约](https://github.com/touyi/guard_contract)代码 165 | 166 | ## 防损失合约使用 167 | ### 使用公开已部署的防损失合约 168 | 合约地址:`3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi` 169 | 170 | 在配中配置合约idl路径,目前在本代码库中idl下的`guard_contract.json`有一份已部署的防损失合约的idl,可以直接将其配置到config.js中使用: 171 | ```json 172 | "base": { 173 | // rpc 请求url 174 | "rpcUrl": "", 175 | // 钱包私钥,32个数字 screctKey和screctKeyBase58 2个填一个即可 176 | "screctKey": [], 177 | // 钱包私钥base58编码字符串 screctKey和screctKeyBase58 2个填一个即可 178 | "screctKeyBase58": "", 179 | // 如果使用防损失合约提交,请填写防损失合约IDL文件路径 180 | "guardContractIDL": "../idl/guard.json", 181 | }, 182 | ``` 183 | ### 部署自己的防损失合约 184 | 部署参考:[防损失合约](https://github.com/touyi/guard_contract) 185 | 186 | 部署成功后配置和上面一样,idl路径需要是你自己部署的防损失合约的idl路径。 187 | 188 | 189 | # 💰高级用法(自定义合约调用) 190 | 191 | 面向具有一定编码能力人群,可以自己实现客户端,实现自己的交易组装和发送币策略并提交套利交易。 192 | 193 | ## 套利合约的输入定义: 194 | ### Account输入 195 | ```rust 196 | 197 | #[derive(Accounts)] 198 | pub struct CommonAccountsInfo32<'info> { 199 | /// CHECK: NONE 200 | pub user: Signer<'info>, // 钱包地址 201 | #[account(mut)] 202 | /// CHECK: NONE 203 | pub user_token_base: UncheckedAccount<'info>, // 用户的wsol账户 204 | #[account(mut)] 205 | /// CHECK: NONE 206 | pub token_base_mint: UncheckedAccount<'info>, // wsol的mint地址: SOL111111111111111111111111111111111111111112 207 | 208 | #[account(mut)] 209 | /// CHECK: NONE 210 | pub token_program: UncheckedAccount<'info>, // 固定:TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 211 | 212 | #[account(address = anchor_lang::solana_program::system_program::ID)] 213 | /// CHECK: NONE 214 | pub sys_program: UncheckedAccount<'info>, // 固定:11111111111111111111111111111111 215 | 216 | #[account(mut)] 217 | /// CHECK: NONE 218 | pub token_pair_0_user_token_account_x: Option>, // 用法套利中间代币账户 219 | #[account(mut)] 220 | /// CHECK: NONE 221 | pub token_pair_0_mint_x: Option>, // 中间代币mint地址 222 | 223 | #[account(mut, address = RECIPIENT_PUBKEY)] 224 | /// CHECK: NONE 225 | pub recipient: AccountInfo<'info>, // 合约手续费接收账户,固定:B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu 226 | 227 | // 以下是交易池账户,最多支持29个账户 228 | #[account(mut)] 229 | /// CHECK: NONE 230 | pub account_0: Option>, 231 | #[account(mut)] 232 | /// CHECK: NONE 233 | pub account_1: Option>, 234 | #[account(mut)] 235 | /// CHECK: NONE 236 | pub account_2: Option>, 237 | ...... 238 | ...... 239 | #[account(mut)] 240 | /// CHECK: NONE 241 | pub account_27: Option>, 242 | #[account(mut)] 243 | /// CHECK: NONE 244 | pub account_28: Option>, 245 | } 246 | 247 | ``` 248 | 包含两部分: 249 | 250 | * 基本账户(accounts) 251 | 252 | 这部分账户按照上述代码填写即可 253 | 254 | * 交易池账户 255 | 256 | 交易池账户是按照一定的规则填写,下面以pumpswap举例说明如何填写交易池账户: 257 | 258 | ![image](https://github.com/touyi/solana-onchain-arbitrage-bot/blob/main/assets/pump.png) 259 | 260 | 对于每个交易池,如果要调用cpi并完成swap操作,交易池是需要一些输入账户的,如上图可以考到sell指令需要19 + 1=20个账户(+1是pumpswap Program的账户),那么在套利合约的输入中,就需要需要预留20个账户(account_0 - account_19)用于合约填充交易池的输入账户。 261 | 262 | 实际在实现的时候其实可以发现这里很多账户都是一些通用账户,例如user这些,所以这些实际都是填充在基本账户里面,交易池账户只填充一些特殊于当前交易的池子账户,例如Pool等,具体不同的交易池可以参考`solana-onchain-arbitrage-bot/src/pool_fetcher/` 目录下的实现不同池子fetcher实现,例如pumpswap的fetcher实现: 263 | ```js 264 | async getFillaccounts() { 265 | let input_accounts = [] 266 | input_accounts.push(this.pool_program); 267 | input_accounts.push(this.pool_key); 268 | input_accounts.push(this.global_config); 269 | input_accounts.push(this.base_mint_pool); 270 | input_accounts.push(this.quote_mint_pool); 271 | input_accounts.push(this.protocol_receiver); 272 | input_accounts.push(this.protocol_receiver_account); 273 | input_accounts.push(ASSOCIATED_TOKEN_PROGRAM_ID); 274 | input_accounts.push(this.authorith); 275 | input_accounts.push(this.coin_creator_vault_ata); 276 | input_accounts.push(this.coin_creator_auth); 277 | 278 | return input_accounts; 279 | } 280 | ``` 281 | 对于每个池子,只需要按照这个顺序填充对应的accounts即可,多个交易池子按照顺序填充套利合约的`account_xxx` 即可。 282 | 283 | ### 参数输入 284 | 285 | 上部分说明了如何填充交易池账户,但是一个交易中多个池子都填充在`account_xxx`中,套利合约如何区分这些不同池子,以及某一段account账户是什么类型池子呢?这里就需要额外的参数信息输入来辅助了,目前指令入口定义的参数见下面: 286 | 287 | ```rust 288 | pub fn arb_process_32_account( 289 | ctx: Context, 290 | max_in: u64, 291 | min_profit: u64, 292 | market_type: Vec, 293 | market_flag: Vec 294 | ); 295 | ``` 296 | 包含4个: 297 | * max_in:最大用于购买的wsol数量, 单位lamports, 限制最大输入,实际输入通过套利路径最优计算得到 298 | * min_profit:最小利润, 单位lamports,如果交易最终利润小于该值,交易会报错 299 | * market_type:交易池类型的数组,用于描述交易池账户部分的`account_xx'按照顺序都有哪些交易池类型,目前不同值代表: 300 | * 0:meteora DLMM 301 | * 1:raydium AMM 302 | * 2:pumpswap 303 | * 3:raydium CLMM 304 | * 4:raydium CPMM 305 | * 5:meteora Dynamic Pools 306 | * market_flag:交易池处理信息:这个字段是一个u8数组,每个u8代表一个交易池的处理信息,目前只使用了最高位,即`(1 << 7) & flag`,这个为1,表示对应位置的交易池是**翻转池**,**翻转池**的定义见下面。 307 | 308 | ### 翻转池 309 | 310 | 翻转池的使用是为了方便统一抽象,对于所有的交易池,池中都是一个交易对,即2个交易token,例如对于pump的这个交易对5wNu5QhdpRGrL37ffcd6TMMqZugQgxwafgz477rShtHy,在交易池的pool信息中一般都会记录这两个token的mint,我们按照记录的先后顺序,对第一个token叫做Token_X,第二个叫做Token_Y,如下: 311 | 312 | ![image](https://github.com/touyi/solana-onchain-arbitrage-bot/blob/main/assets/Snipaste_2025-05-18_21-37-06.png) 313 | 314 | Token_X是`Ce2gx9KGXJ6C9Mp5b5x1sn9Mg87JwEbrQby4Zqo3pump` 315 | 316 | Token_Y是`So11111111111111111111111111111111111111112` 317 | 318 | 因为交易中我们目前只支持WSOL的套利,所以需要保证每个交易池的Token_Y是WSOL,如果不是,我们就需要对这个交易池进行翻转(客户端构造accounts的时候不翻转,只是额外传入market_flag表示这个池子的Token_Y不是WSOL,会在合约中处理) 319 | 320 | 如果有更多疑问或bug反馈欢迎加入交流群讨论:https://t.me/+t3Gexbnw0rs5NWQ1 321 | 322 | # 📌后续计划 323 | * rust重构(doing) 324 | * jito 发送 325 | * Kamino 326 | * 高热币监控 327 | 328 | # 📚交流群 329 | 疑问或bug反馈请加交流群 330 | https://t.me/+t3Gexbnw0rs5NWQ1 331 | 332 | # 📜代码分析 333 | 334 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/touyi/solana-onchain-arbitrage-bot) 335 | 336 | 337 | 338 | -------------------------------------------------------------------------------- /assets/Snipaste_2025-05-18_21-37-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touyi/solana-onchain-arbitrage-bot/e8e2022ea2feee44eaabed0c1404cdec9d7aebfc/assets/Snipaste_2025-05-18_21-37-06.png -------------------------------------------------------------------------------- /assets/en_readme.md: -------------------------------------------------------------------------------- 1 | # 🌟 Star Break 388 Open Source Arbitrage Contract 2 | # Solana On-Chain Arbitrage Bot 3 | 4 | Supports intelligent routing and calculates optimal arbitrage routes. This is a Solana on-chain arbitrage bot that supports intelligent routing and calculates optimal arbitrage routes. 5 | 6 | ## 📝 Basic Introduction 7 | 8 | This repository contains an arbitrage bot based on contract intelligent routing, which is divided into a client and a contract. This repository represents the client, with the following main functions: 9 | 10 | - Retrieve trading pools for arbitrage. 11 | - Automatically pair trading pools and assemble and submit trades. 12 | 13 | Regarding the contract (contract address: DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z): 14 | 15 | - Receive trading pools submitted by the client. 16 | - Automatically calculate the optimal arbitrage path and the optimal input (if any), and initiate the arbitrage operation. Otherwise, the transaction will fail. 17 | 18 | The currently supported trading pools will be continuously expanded in the future: 19 | 20 | - PumpSwap 21 | - Raydium AMM 22 | - Raydium CPMM 23 | - Raydium CLMM 24 | - Meteora DLMM 25 | - Meteora Dynamic Pools 26 | 27 | > **For successful arbitrage trades using this arbitrage bot, a 10% fee of the profit will be charged. If the arbitrage fails or there is no profit, the contract will not charge any fees.** For example, if you input 0.5 SOL and output 0.6 SOL, the profit is 0.1 SOL, and the fee is 0.1 * 10% = 0.01 SOL. 28 | 29 | ## 🚀 Quick Start 30 | 31 | ### Install the Environment 32 | 33 | > Currently, only one-click installation on Linux systems is supported. 34 | 35 | Execute the following command in the root directory: 36 | 37 | ```bash 38 | bash install.sh 39 | ``` 40 | 41 | ### Basic Configuration 42 | 43 | The following configurations in `solana-onchain-arbitrage-bot/src/config.js` must be filled in. You only need to fill in either `screctKey` or `screctKeyBase58`. 44 | 45 | ```javascript 46 | "base": { 47 | // RPC request URL 48 | "rpcUrl": "https://api.mainnet-beta.solana.com", 49 | // Wallet private key, an array of 32 numbers. Fill in either screctKey or screctKeyBase58. 50 | "screctKey": [], 51 | // Wallet private key in Base58-encoded string. Fill in either screctKey or screctKeyBase58. 52 | "screctKeyBase58": "", 53 | } 54 | ``` 55 | 56 | ### Convert SOL to Wrapped SOL (WSOL) 57 | 58 | DEXs generally use WSOL for trading. Therefore, you need to convert an equivalent amount of SOL to WSOL in your account before starting the trading. Use the script provided in the repository to perform the conversion: 59 | 60 | Execute the following command in the `src/tools` directory: `node warp_sol.js --amount=1000000000` 61 | 62 | Here, `amount` is the amount of SOL in lamports. 63 | 64 | To convert WSOL back to SOL, execute `node warp_sol.js --amount=1000000000 --close` 65 | 66 | > Ensure that the converted amount is greater than the `maxInputAmount` field configured in `config.js`. The default configuration of `1000000000` represents 1 WSOL. 67 | 68 | ### Start the Bot 69 | 70 | Execute the following command in the `src` directory: 71 | 72 | ```bash 73 | node main.js 74 | ``` 75 | 76 | After starting, you will see the following log output: 77 | 78 | ```javascript 79 | 🚀 ~ swap::e: SendTransactionError: Simulation failed. 80 | Catch the `SendTransactionError` and call `getLogs()` on it for full details. 81 | at Connection.sendEncodedTransaction (/home/touyi/sol/solana-onchain-arbitrage-bot/src/node_modules/@solana/web3.js/lib/index.cjs.js:8206:13) 82 | at process.processTicksAndRejections (node:internal/process/task_queues:105:5) 83 | at async Connection.sendRawTransaction (/home/touyi/sol/solana-onchain-arbitrage-bot/src/node_modules/@solana/web3.js/lib/index.cjs.js:8171:20) 84 | at async file:///home/touyi/sol/solana-onchain-arbitrage-bot/src/bot/arb_bot.js:237:43 { 85 | signature: '', 86 | transactionMessage: 'Transaction simulation failed: Error processing Instruction 2: custom program error: 0x1772', 87 | transactionLogs: [ 88 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 89 | 'Program ComputeBudget111111111111111111111111111111 success', 90 | 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 91 | 'Program ComputeBudget111111111111111111111111111111 success', 92 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi invoke [1]', 93 | 'Program log: Instruction: ArbProcess32Account', 94 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z invoke [2]', 95 | 'Program log: Instruction: ArbProcess32Account', 96 | 'Program log: max_in 1000000000, min_profit 100, market_type [0, 0], market_flag [0, 0]', 97 | 'Program log: sol_log_compute_units programs/arb_touyi/src/processor.rs:550', 98 | 'Program consumption: 257040 units remaining', 99 | 'Program log: sol_log_compute_units programs/arb_touyi/src/processor.rs:20', 100 | 'Program consumption: 250388 units remaining', 101 | 'Program log: iy:0 dx:0 oy:0', 102 | 'Program log: iy:0 dx:0 oy:0', 103 | 'Program log: AnchorError thrown in programs/arb_touyi/src/processor.rs:48. Error Code: NoProfit. Error Number: 6002. Error Message: Not Profit.', 104 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z consumed 25565 of 268555 compute units', 105 | 'Program DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z failed: custom program error: 0x1772', 106 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi consumed 56710 of 299700 compute units', 107 | 'Program 3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi failed: custom program error: 0x1772' 108 | ] 109 | } 110 | ``` 111 | 112 | The message `Error Message: Not Profit.` indicates that the bot has started running normally. The `Not Profit` error occurs because the simulation has already determined that there is no profit to be made, so the transaction will not be actually sent. Only when the simulation succeeds will the transaction be sent. 113 | 114 | If you want all transactions to be broadcasted to the blockchain, you can configure `bot: "skipPreflight": false,`. After this configuration, all transactions will be broadcasted. However, note that even if a transaction fails, you will still incur gas fees. 115 | 116 | > **Note**: If the user's intermediate token account (e.g., trump) for arbitrage does not exist, the bot will automatically create it. If it exists, please ensure that the account balance is zero. 117 | 118 | ## 🌟 Arbitrage Principle 119 | 120 | For any two trading pools, if there is a price difference, it may be possible to profit from arbitrage. Assume that: 121 | 122 | - In trading pool 1, you use `a` SOL to buy `b` tokens of token X. 123 | - In trading pool 2, you sell `b` tokens of token X to get `c` SOL. 124 | - The potential profit: `p = c - a` 125 | 126 | If `p` is greater than 0, arbitrage is possible; otherwise, it is not. 127 | 128 | For arbitrage involving multiple trading pools, the optimal arbitrage route needs to be calculated in advance, and then the arbitrage is carried out according to the optimal route. 129 | 130 | Due to the contract's computational power limitations, currently only one-hop optimal path calculation is supported, i.e., SOL -> X -> SOL. The contract will pair all input trading pools in pairs, calculate the optimal arbitrage path, and then conduct arbitrage according to the optimal path. If no profitable path is found, no arbitrage will be carried out, and the contract will report an error, causing the transaction to fail. 131 | 132 | ## 📅 How to Build Trust? 133 | 134 | Since the contract code is not open-source, concerns may arise, such as: 135 | 136 | - Will my private key be uploaded? 137 | - Can the contract owner transfer all my tokens? 138 | 139 | To address these concerns, we take the following measures to ensure the safety of your assets: 140 | 141 | **Regarding the first concern**: The client code is completely open-source. You can see that in the client code, the private key is only used for local transaction signing and will not be uploaded to any third party or read by the contract. 142 | 143 | **Regarding the second concern**: The arbitrage contract code is not open-source. Without proper protection, the contract owner could potentially transfer your tokens. Therefore, we have open-sourced a [Guard Contract](https://github.com/touyi/guard_contract) repository. The principle is as follows: 144 | 145 | In the arbitrage contract, according to the client code, the user's token accounts input to the arbitrage contract only include: 146 | 147 | - SOL account (automatically included as the signer) 148 | - WSOL account (`userTokenBase`) 149 | - Intermediate token account for arbitrage (`tokenPair0UserTokenAccountX`) 150 | 151 | In the arbitrage contract, only the balances of these three accounts can be transferred. 152 | 153 | The "intermediate token account for arbitrage" should have a zero balance, so there is no risk of loss from this account. 154 | 155 | For SOL and WSOL, the [Guard Contract](https://github.com/touyi/guard_contract) ensures that the total amount of WSOL + SOL after the arbitrage contract is called is greater than the amount before the call. Otherwise, the contract will fail, and the entire transaction will be reverted. This mechanism guarantees that you will not suffer losses in the arbitrage contract. For the specific implementation, please refer to the [Guard Contract](https://github.com/touyi/guard_contract) code. 156 | 157 | ### Using the Guard Contract 158 | 159 | #### Using the Publicly Deployed Guard Contract 160 | 161 | Contract address: `3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi` 162 | 163 | Configure the contract IDL path in the configuration. Currently, there is an IDL file for the publicly deployed guard contract in the `idl` directory of this code repository, named `guard_contract.json`. You can directly configure it in `config.js` as follows: 164 | 165 | ```json 166 | "base": { 167 | // RPC request URL 168 | "rpcUrl": "", 169 | // Wallet private key, an array of 32 numbers. Fill in either screctKey or screctKeyBase58. 170 | "screctKey": [], 171 | // Wallet private key in Base58-encoded string. Fill in either screctKey or screctKeyBase58. 172 | "screctKeyBase58": "", 173 | // If using the guard contract for submission, fill in the IDL file path of the guard contract. 174 | "guardContractIDL": "../idl/guard.json", 175 | } 176 | ``` 177 | 178 | #### Deploying Your Own Guard Contract 179 | 180 | For deployment instructions, refer to the [Guard Contract](https://github.com/touyi/guard_contract) repository. 181 | 182 | After successful deployment, the configuration is the same as above. Make sure to specify the IDL path of your own deployed guard contract. 183 | 184 | ## 💰 Advanced Usage (Custom Contract Calls) 185 | 186 | This section is intended for users with some coding skills. You can implement your own client, assemble your own trades, and define your own token sending strategies to submit arbitrage transactions. 187 | 188 | ### Input Definition of the Arbitrage Contract 189 | 190 | #### Account Input 191 | 192 | ```rust 193 | #[derive(Accounts)] 194 | pub struct CommonAccountsInfo32<'info> { 195 | /// CHECK: NONE 196 | pub user: Signer<'info>, // Wallet address 197 | #[account(mut)] 198 | /// CHECK: NONE 199 | pub user_token_base: UncheckedAccount<'info>, // User's WSOL account 200 | #[account(mut)] 201 | /// CHECK: NONE 202 | pub token_base_mint: UncheckedAccount<'info>, // Mint address of WSOL: SOL111111111111111111111111111111111111111112 203 | 204 | #[account(mut)] 205 | /// CHECK: NONE 206 | pub token_program: UncheckedAccount<'info>, // Fixed: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA 207 | 208 | #[account(address = anchor_lang::solana_program::system_program::ID)] 209 | /// CHECK: NONE 210 | pub sys_program: UncheckedAccount<'info>, // Fixed: 11111111111111111111111111111111 211 | 212 | #[account(mut)] 213 | /// CHECK: NONE 214 | pub token_pair_0_user_token_account_x: Option>, // Intermediate token account for arbitrage 215 | #[account(mut)] 216 | /// CHECK: NONE 217 | pub token_pair_0_mint_x: Option>, // Mint address of the intermediate token 218 | 219 | #[account(mut, address = RECIPIENT_PUBKEY)] 220 | /// CHECK: NONE 221 | pub recipient: AccountInfo<'info>, // Contract fee recipient account, fixed: B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu 222 | 223 | // The following are trading pool accounts, supporting a maximum of 29 accounts 224 | #[account(mut)] 225 | /// CHECK: NONE 226 | pub account_0: Option>, 227 | #[account(mut)] 228 | /// CHECK: NONE 229 | pub account_1: Option>, 230 | #[account(mut)] 231 | /// CHECK: NONE 232 | pub account_2: Option>, 233 | ...... 234 | ...... 235 | #[account(mut)] 236 | /// CHECK: NONE 237 | pub account_27: Option>, 238 | #[account(mut)] 239 | /// CHECK: NONE 240 | pub account_28: Option>, 241 | } 242 | ``` 243 | 244 | The input consists of two parts: 245 | 246 | - **Basic Accounts (`accounts`)**: Fill in these accounts according to the above code. 247 | - **Trading Pool Accounts**: These accounts are filled in according to certain rules. Here is an example of how to fill in the trading pool accounts using PumpSwap: 248 | 249 | ![image](https://github.com/touyi/solana-onchain-arbitrage-bot/blob/main/assets/pump.png) 250 | 251 | For each trading pool, to call the CPI and complete a swap operation, the trading pool requires some input accounts. As shown in the figure above, the sell instruction requires 19 + 1 = 20 accounts (the additional account is the PumpSwap program account). Therefore, in the input of the arbitrage contract, 20 accounts (`account_0` - `account_19`) need to be reserved for the contract to fill in the input accounts of the trading pool. 252 | 253 | In practice, you will find that many of these accounts are common accounts, such as the `user` account. These common accounts are filled in the basic accounts, while the trading pool accounts only need to fill in the accounts specific to the current trading pool, such as the `Pool` account. For specific implementations of different trading pools, refer to the fetcher implementations in the `solana-onchain-arbitrage-bot/src/pool_fetcher/` directory. For example, the fetcher implementation for PumpSwap is as follows: 254 | 255 | ```javascript 256 | async getFillaccounts() { 257 | let input_accounts = []; 258 | input_accounts.push(this.pool_program); 259 | input_accounts.push(this.pool_key); 260 | input_accounts.push(this.global_config); 261 | input_accounts.push(this.base_mint_pool); 262 | input_accounts.push(this.quote_mint_pool); 263 | input_accounts.push(this.protocol_receiver); 264 | input_accounts.push(this.protocol_receiver_account); 265 | input_accounts.push(ASSOCIATED_TOKEN_PROGRAM_ID); 266 | input_accounts.push(this.authorith); 267 | input_accounts.push(this.coin_creator_vault_ata); 268 | input_accounts.push(this.coin_creator_auth); 269 | 270 | return input_accounts; 271 | } 272 | ``` 273 | 274 | For each trading pool, simply fill in the corresponding accounts in this order. Multiple trading pools should be filled in the `account_xxx` fields of the arbitrage contract in sequence. 275 | 276 | #### Parameter Input 277 | 278 | The previous section explained how to fill in the trading pool accounts. However, when multiple trading pools are filled in the `account_xxx` fields, how does the arbitrage contract distinguish between these different pools and determine the type of each account segment? This is where additional parameter information comes in. The currently defined parameters for the instruction entry are as follows: 279 | 280 | ```rust 281 | pub fn arb_process_32_account( 282 | ctx: Context, 283 | max_in: u64, 284 | min_profit: u64, 285 | market_type: Vec, 286 | market_flag: Vec 287 | ); 288 | ``` 289 | 290 | There are four parameters: 291 | 292 | - `max_in`: The maximum amount of WSOL to be used for purchasing, in lamports. This parameter limits the maximum input, and the actual input is determined through optimal calculation of the arbitrage path. 293 | - `min_profit`: The minimum profit, in lamports. If the final profit of the transaction is less than this value, the transaction will fail. 294 | - `market_type`: An array representing the types of trading pools. It describes the types of trading pools corresponding to the `account_xx` fields in the trading pool account section. Currently, different values represent the following: 295 | - 0: Meteora DLMM 296 | - 1: Raydium AMM 297 | - 2: PumpSwap 298 | - 3: Raydium CLMM 299 | - 4: Raydium CPMM 300 | - 5: Meteora Dynamic Pools 301 | - `market_flag`: Information about trading pool processing. This field is a `u8` array, where each `u8` represents the processing information of a trading pool. Currently, only the highest bit is used, i.e., `(1 << 7) & flag`. If this bit is set to 1, it indicates that the corresponding trading pool is a **flipped pool**. The definition of a flipped pool is explained below. 302 | 303 | #### Flipped Pools 304 | 305 | The concept of flipped pools is introduced for convenient and unified abstraction. For all trading pools, each pool contains a trading pair of two tokens. For example, in the PumpSwap trading pair `5wNu5QhdpRGrL37ffcd6TMMqZugQgxwafgz477rShtHy`, the pool information usually records the mints of these two tokens. We refer to the first token as `Token_X` and the second as `Token_Y`, as shown below: 306 | 307 | ![image](https://github.com/touyi/solana-onchain-arbitrage-bot/blob/main/assets/Snipaste_2025-05-18_21-37-06.png) 308 | 309 | `Token_X` is `Ce2gx9KGXJ6C9Mp5b5x1sn9Mg87JwEbrQby4Zqo3pump` 310 | 311 | `Token_Y` is `So11111111111111111111111111111111111111112` 312 | 313 | Since our current arbitrage support is limited to WSOL, we need to ensure that `Token_Y` in each trading pool is WSOL. If not, we need to flip the trading pool (the client does not flip the accounts during construction but simply passes an additional `market_flag` to indicate that `Token_Y` in this pool is not WSOL, and the contract will handle the flipping). 314 | 315 | If you have more questions or need to report bugs, please join our discussion group: https://t.me/+t3Gexbnw0rs5NWQ1 316 | 317 | ## 📌 Future Plans 318 | - refactor by rust(doing) 319 | - Support for Jito transactions 320 | - Integration with Kamino 321 | - Monitoring of high-volume tokens 322 | 323 | ## 📚 Discussion Group 324 | 325 | For questions or bug reports, please join our discussion group: 326 | https://t.me/+t3Gexbnw0rs5NWQ1 327 | 328 | ## 📜 Code Analysis 329 | 330 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/touyi/solana-onchain-arbitrage-bot) 331 | -------------------------------------------------------------------------------- /assets/pump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touyi/solana-onchain-arbitrage-bot/e8e2022ea2feee44eaabed0c1404cdec9d7aebfc/assets/pump.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | cd `dirname $0` 4 | log_info() { 5 | printf "[INFO] %s\n" "$1" 6 | } 7 | 8 | log_error() { 9 | printf "[ERROR] %s\n" "$1" >&2 10 | } 11 | install_nvm_and_node() { 12 | if [ -s "$HOME/.nvm/nvm.sh" ]; then 13 | log_info "NVM is already installed." 14 | else 15 | log_info "Installing NVM..." 16 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash 17 | fi 18 | 19 | export NVM_DIR="$HOME/.nvm" 20 | # Immediately source nvm and bash_completion for the current session 21 | if [ -s "$NVM_DIR/nvm.sh" ]; then 22 | . "$NVM_DIR/nvm.sh" 23 | else 24 | log_error "nvm not found. Ensure it is installed correctly." 25 | fi 26 | 27 | if [ -s "$NVM_DIR/bash_completion" ]; then 28 | . "$NVM_DIR/bash_completion" 29 | fi 30 | 31 | if command -v node >/dev/null 2>&1; then 32 | local current_node 33 | current_node=$(node --version) 34 | local latest_node 35 | latest_node=$(nvm version-remote node) 36 | if [ "$current_node" = "$latest_node" ]; then 37 | log_info "Latest Node.js ($current_node) is already installed." 38 | else 39 | log_info "Updating Node.js: Installed ($current_node), Latest ($latest_node)." 40 | nvm install node 41 | nvm alias default node 42 | nvm use default 43 | fi 44 | else 45 | log_info "Installing Node.js via NVM..." 46 | nvm install node 47 | nvm alias default node 48 | nvm use default 49 | fi 50 | 51 | echo "" 52 | } 53 | 54 | 55 | install_nvm_and_node 56 | 57 | cd bot/ && npm i -y -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pool_finder/config/meteora/ 3 | pool_finder/config/pump/ 4 | pool_finder/config/raydium/ 5 | pool_finder/config/meteora_amm/ 6 | *account_cache.json -------------------------------------------------------------------------------- /src/bot/arb_bot.js: -------------------------------------------------------------------------------- 1 | 2 | import * as utils from "../common/utils.js"; 3 | import * as constants from "../common/constants.js"; 4 | import { MintXHandler } from "./mintx_handler.js"; 5 | import { 6 | Connection, 7 | PublicKey, 8 | Transaction, 9 | ComputeBudgetProgram, 10 | TransactionMessage, 11 | VersionedTransaction, 12 | SystemProgram, 13 | Keypair, 14 | } from "@solana/web3.js" 15 | import { Program, AnchorProvider, Wallet } from '@coral-xyz/anchor'; 16 | import fs from "fs"; 17 | import { 18 | ASSOCIATED_TOKEN_PROGRAM_ID, 19 | TOKEN_PROGRAM_ID, 20 | } from "@solana/spl-token"; 21 | import BN from "bn.js"; 22 | 23 | 24 | const ACCOUNT_SIZE = 29; 25 | 26 | export class ArbBot { 27 | constructor(config, connection, poolFinderList) { 28 | this.config = config; 29 | this.pendingMintX = config.mintList; 30 | this.runningMintX = [] 31 | this.commonAltAccounts = [] 32 | this.poolFinderList = poolFinderList 33 | this.userKeypair = utils.createKeyPairWithConfig(config); 34 | console.log("userKeypair:", this.userKeypair.publicKey.toString()) 35 | this.connection = connection; 36 | this.maxRunningMintX = config.mintList.length; 37 | 38 | this.maxIOConcurrent = config.bot.maxIOConcurrent; 39 | this.skipPreflight = config.bot.skipPreflight; 40 | this.mintXExpirationTime = 1000 * config.bot.mintXExpirationTime; // 20 minutes 41 | this.maxSendRate = config.bot.maxSendRate; // x tx/s 42 | this.maxAmountIn = config.bot.maxInputAmount; 43 | this.minProfit = config.bot.minProfit; 44 | this.guardContractIDL = config.base.guardContractIDL; 45 | 46 | const provider = new AnchorProvider( 47 | this.connection, 48 | new Wallet(this.userKeypair), 49 | ) 50 | 51 | const readJsonFileSync = (filePath) => { 52 | const absolutePath = new URL(filePath, import.meta.url).pathname; 53 | const fileContent = fs.readFileSync(absolutePath, 'utf8'); 54 | return JSON.parse(fileContent); 55 | } 56 | 57 | 58 | if (this.guardContractIDL && fs.existsSync(this.guardContractIDL)) { 59 | const idl = readJsonFileSync(this.guardContractIDL); 60 | this.program = new Program( 61 | idl, 62 | provider); 63 | console.log("guardContractIDL:", this.guardContractIDL) 64 | } else { 65 | const idl = readJsonFileSync("../idl/idl.json"); 66 | this.program = new Program( 67 | idl, 68 | provider); 69 | } 70 | } 71 | 72 | 73 | async processMintX(mintX) { 74 | console.log("processMintX:", mintX) 75 | try { 76 | let mintXData = { 77 | "USDT": [], 78 | "USDC": [], 79 | "SOL": [], 80 | }; 81 | for (let index = 0; index < this.runningMintX.length; index++) { 82 | if (this.runningMintX[index].getMintX().toString() === mintX.toString()) { 83 | console.log("processMintX: already running:", mintX) 84 | // 删除尝试重新获取池 85 | this.runningMintX = [ 86 | ...this.runningMintX.slice(0, index), 87 | ...this.runningMintX.slice(index + 1), 88 | ] 89 | } 90 | } 91 | const getFirstThreeSupportPools = (poolInfos, size) => { 92 | if (!poolInfos) { 93 | return []; 94 | } 95 | return poolInfos.filter(poolInfo => utils.IsSupportPool(poolInfo.type)).slice(0, size); 96 | } 97 | 98 | for (let finder of this.poolFinderList) { 99 | let data = await finder.getPoolKey(mintX); 100 | let size = 3; 101 | mintXData.USDT = mintXData.USDT.concat(getFirstThreeSupportPools(data.USDT, size)); 102 | mintXData.USDC = mintXData.USDC.concat(getFirstThreeSupportPools(data.USDC, size)); 103 | mintXData.SOL = mintXData.SOL.concat(getFirstThreeSupportPools(data.SOL, size)); 104 | } 105 | if (mintXData.SOL.length < 2) { 106 | console.log("processMintX: length no support :", mintX) 107 | return; 108 | } 109 | 110 | const handler = new MintXHandler(mintX, mintXData, this.connection, true); 111 | await handler.init(); 112 | this.runningMintX.push(handler); 113 | this.runningMintX = [...this.runningMintX.slice(-this.maxRunningMintX)] 114 | console.log("processMintX Success:", mintX) 115 | } catch (e) { 116 | console.log("processMintX: ", e); 117 | } 118 | } 119 | 120 | 121 | async processPendingMintXMain() { 122 | while (true) { 123 | if (this.pendingMintX.length > 0) { 124 | const newMintXList = [...this.pendingMintX]; 125 | this.pendingMintX = []; 126 | for (let i = 0; i < newMintXList.length; i++) { 127 | await this.processMintX(newMintXList[i]); 128 | } 129 | } else { 130 | await new Promise((resolve) => setTimeout(resolve, 1000)); 131 | } 132 | } 133 | } 134 | 135 | async transactionConstruct(handler, blockhash) { 136 | let markets_accounts = [] 137 | let input_alt_accounts = [] 138 | let reverse_flag_list = [] 139 | let type_index_list = [] 140 | await handler.fillAccounts(markets_accounts, 141 | input_alt_accounts, 142 | reverse_flag_list, 143 | type_index_list, 144 | "SOL", 145 | ACCOUNT_SIZE, 146 | true); 147 | 148 | let input_accounts = { 149 | user: this.userKeypair.publicKey, 150 | userTokenBase: PublicKey.findProgramAddressSync([ 151 | this.userKeypair.publicKey.toBuffer(), 152 | TOKEN_PROGRAM_ID.toBuffer(), 153 | constants.WSOL.toBuffer(), 154 | ], ASSOCIATED_TOKEN_PROGRAM_ID)[0], 155 | tokenBaseMint: constants.WSOL, 156 | tokenProgram: TOKEN_PROGRAM_ID, 157 | sysProgram: SystemProgram.programId, 158 | tokenPair0UserTokenAccountX: PublicKey.findProgramAddressSync([ 159 | this.userKeypair.publicKey.toBuffer(), 160 | TOKEN_PROGRAM_ID.toBuffer(), 161 | handler.getMintX().toBuffer(), 162 | ], ASSOCIATED_TOKEN_PROGRAM_ID)[0], 163 | tokenPair0MintX: handler.getMintX(), 164 | recipient: constants.RECIPIENT, 165 | arb_program: constants.ARB_PROGRAM, 166 | } 167 | 168 | for (let i = 0; i < markets_accounts.length; i++) { 169 | input_accounts[`account${i}`] = markets_accounts[i]; 170 | } 171 | 172 | for (let i = markets_accounts.length; i < ACCOUNT_SIZE; i++) { 173 | input_accounts[`account${i}`] = null; 174 | } 175 | 176 | const arbIx = await this.program.methods 177 | .arbProcess32Account( 178 | new BN(this.maxAmountIn), 179 | new BN(this.minProfit), 180 | utils.serializeVecU8(type_index_list), 181 | utils.serializeVecU8(reverse_flag_list) 182 | ) 183 | .accounts(input_accounts).instruction(); 184 | 185 | let arbTx = null; 186 | { 187 | const arbInstructions = [ 188 | ComputeBudgetProgram.setComputeUnitLimit({ units: 300000 }), 189 | ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10 }), 190 | arbIx 191 | ]; 192 | const arbMessage = new TransactionMessage({ 193 | payerKey: this.userKeypair.publicKey, 194 | recentBlockhash: blockhash, 195 | instructions: arbInstructions, 196 | }) 197 | arbTx = new VersionedTransaction(arbMessage.compileToV0Message(this.commonAltAccounts)); 198 | arbTx.sign([this.userKeypair]); 199 | } 200 | 201 | return arbTx.serialize() 202 | } 203 | 204 | 205 | // 构造交易并发送 206 | async transactionConstructorMain() { 207 | let blockhash = null; 208 | let blockhashExpire = 0; 209 | const getLatestBlockhash = async () => { 210 | if (Date.now() > blockhashExpire) { 211 | let block_info = await this.connection.getLatestBlockhash("finalized"); 212 | blockhash = block_info.blockhash; 213 | blockhashExpire = Date.now() + 1000 * 12; 214 | } 215 | return blockhash; 216 | } 217 | // 限制发送速率 218 | let lastSendTime = 0; 219 | const sendLimitWait = async () => { 220 | while (Date.now() - lastSendTime < 1000 / this.maxSendRate) { 221 | await new Promise((resolve) => setTimeout(resolve, 0)); 222 | } 223 | lastSendTime = Date.now(); 224 | } 225 | 226 | while (true) { 227 | for (let i = 0; i < this.runningMintX.length; i++) { 228 | const handler = this.runningMintX[i]; 229 | while (this.maxIOConcurrent <= 0) { 230 | await new Promise((resolve) => setTimeout(resolve, 50)); 231 | } 232 | this.maxIOConcurrent -= 1; 233 | this.transactionConstruct(handler, await getLatestBlockhash()).then(async (arbTransaction) => { 234 | try { 235 | await sendLimitWait(); 236 | const signature = await this.connection.sendRawTransaction(arbTransaction, { 237 | skipPreflight: this.skipPreflight, 238 | preflightCommitment: "processed", 239 | maxRetries: 5, 240 | }) 241 | console.log("🚀 ~ swap::signature:", signature); 242 | } catch (e) { 243 | console.log("🚀 ~ swap::e:", e); 244 | } 245 | this.maxIOConcurrent += 1; 246 | }).catch((e) => { 247 | console.log("🚀 ~ swap::e:", e); 248 | this.maxIOConcurrent += 1; 249 | }) 250 | } 251 | // 检测是否过期 252 | if (this.runningMintX.length > 0) { 253 | if(this.runningMintX[0].getCreateTime() + this.mintXExpirationTime < Date.now()) { 254 | const refreshMintX = this.runningMintX[0].getMintX(); 255 | this.runningMintX = [ 256 | ...this.runningMintX.slice(1), 257 | ]; 258 | console.log("runningMintX outdate:", refreshMintX.toString()); 259 | await this.addMintX(refreshMintX.toString()); 260 | } 261 | } 262 | // 尝试让出线程 263 | await new Promise((resolve) => setTimeout(resolve, 0)); 264 | } 265 | } 266 | 267 | async addMintX(mintX) { 268 | this.pendingMintX.push(mintX); 269 | } 270 | 271 | async init() { 272 | for (let alt of this.config.address_lookup_tables) { 273 | const altTable = (await this.connection.getAddressLookupTable(new PublicKey(alt))).value 274 | this.commonAltAccounts.push(altTable) 275 | } 276 | } 277 | 278 | 279 | async run() { 280 | 281 | await this.init(); 282 | let promises = []; 283 | promises.push(utils.GuardForeverRun(async () => { 284 | await this.processPendingMintXMain(); 285 | })); 286 | 287 | promises.push(utils.GuardForeverRun(async () => { 288 | await this.transactionConstructorMain(); 289 | })); 290 | 291 | for (let promise of promises) { 292 | await promise; 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /src/bot/mintx_handler.js: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | } from "@solana/web3.js"; 4 | import { PumpSwapFetcher } from "../pool_fetcher/pump_fetcher.js"; 5 | import { MeteoraDLMMFetcher } from "../pool_fetcher/meteora_dlmm_fetcher.js"; 6 | import { RaydiumAMMFetcher } from "../pool_fetcher/raydium_amm_fetcher.js"; 7 | import { RaydiumCLMMFetcher } from "../pool_fetcher/raydium_clmm_fetcher.js"; 8 | import { RaydiumCPMMFetcher } from "../pool_fetcher/raydium_cpmm_fetcher.js"; 9 | import { MeteoraAmmFetcher } from "../pool_fetcher/meteora_amm_fetcher.js"; 10 | import * as constants from "../common/constants.js"; 11 | import * as utils from "../common/utils.js"; 12 | /* 13 | MintXData: 14 | 15 | { 16 | "USDT": [ 17 | { 18 | "type": "raydium_amm", 19 | "pool_key": "xxx", 20 | "meta": {} 21 | } 22 | ], 23 | "SOL": [ 24 | { 25 | "type": "raydium_amm", 26 | "pool_key": "xxx", 27 | "meta": {} 28 | } 29 | ], 30 | "USDC": [ 31 | { 32 | "type": "raydium_amm", 33 | "pool_key": "xxx", 34 | "meta": {} 35 | } 36 | ] 37 | } 38 | 39 | 40 | */ 41 | 42 | export class MintXHandler { 43 | constructor(mintX, mintXData, connection, disableAlt = false) { 44 | if (!(mintX instanceof PublicKey)) { 45 | this.mintX = new PublicKey(mintX); 46 | } else { 47 | this.mintX = mintX; 48 | } 49 | 50 | if (typeof mintXData === "string") { 51 | this.mintXData = JSON.parse(mintXData); 52 | } else { 53 | this.mintXData = mintXData; 54 | } 55 | this.disableAlt = disableAlt; 56 | this.connection = connection; 57 | this.fetcher_list_map = {}; 58 | this.fetcher_list_alt_account_map = {} 59 | this.fetcher_list_offset_map = {} 60 | this.createTime = Date.now(); 61 | } 62 | 63 | getCreateTime() { 64 | return this.createTime; 65 | } 66 | 67 | getMintX() { 68 | return this.mintX; 69 | } 70 | 71 | async createFetcherlist(pool_meta_list) { 72 | /* 73 | [ 74 | { 75 | "type": "raydium_amm", 76 | "pool_key": "xxx", 77 | "meta": {} 78 | } 79 | ] 80 | */ 81 | let pool_list = []; 82 | let alt_account_list = []; 83 | for (const pool_meta of pool_meta_list) { 84 | if (pool_meta.type === constants.POOLType.kPumpSwap) { 85 | const pool_key = new PublicKey(pool_meta.pool_key); 86 | const fetcher = new PumpSwapFetcher(pool_key, this.connection); 87 | await fetcher.fetch(); 88 | pool_list.push(fetcher); 89 | } else if (pool_meta.type === constants.POOLType.kRaydiumAMM) { 90 | const pool_key = new PublicKey(pool_meta.pool_key); 91 | const fetcher = new RaydiumAMMFetcher(pool_key, this.connection); 92 | await fetcher.fetch(); 93 | pool_list.push(fetcher); 94 | } else if (pool_meta.type === constants.POOLType.kMeteoraDLMM) { 95 | const pool_key = new PublicKey(pool_meta.pool_key); 96 | const fetcher = new MeteoraDLMMFetcher(pool_key, this.connection); 97 | await fetcher.fetch(); 98 | pool_list.push(fetcher); 99 | } else if (pool_meta.type === constants.POOLType.kRaydiumCLMM) { 100 | const pool_key = new PublicKey(pool_meta.pool_key); 101 | const fetcher = new RaydiumCLMMFetcher(pool_key, this.connection); 102 | await fetcher.fetch(); 103 | pool_list.push(fetcher); 104 | } else if (pool_meta.type === constants.POOLType.kRaydiumCPMM) { 105 | const pool_key = new PublicKey(pool_meta.pool_key); 106 | const fetcher = new RaydiumCPMMFetcher(pool_key, this.connection); 107 | await fetcher.fetch(); 108 | pool_list.push(fetcher); 109 | } else if (pool_meta.type === constants.POOLType.kMeteoraAMM) { 110 | const pool_key = new PublicKey(pool_meta.pool_key); 111 | const fetcher = new MeteoraAmmFetcher(pool_key, this.connection); 112 | await fetcher.fetch(); 113 | pool_list.push(fetcher); 114 | } else { 115 | throw new Error("Unknown pool type"); 116 | } 117 | if (!this.disableAlt) { 118 | try { 119 | let lookup_table = (await this.connection.getAddressLookupTable(new PublicKey(pool_meta.meta["alt_key"]))).value 120 | alt_account_list.push(lookup_table); 121 | } catch (e) { 122 | console.error(e); 123 | alt_account_list.push(null); 124 | } 125 | } else { 126 | alt_account_list.push(null); 127 | } 128 | 129 | } 130 | return {pool_list, alt_account_list}; 131 | } 132 | 133 | async init() { 134 | if ("SOL" in this.mintXData) { 135 | const { pool_list, alt_account_list } = await this.createFetcherlist(this.mintXData["SOL"]); 136 | this.fetcher_list_map["SOL"] = pool_list; 137 | this.fetcher_list_alt_account_map["SOL"] = alt_account_list; 138 | this.fetcher_list_offset_map["SOL"] = 0; 139 | } 140 | if ("USDC" in this.mintXData) { 141 | const { pool_list, alt_account_list } = await this.createFetcherlist(this.mintXData["USDC"]); 142 | this.fetcher_list_map["USDC"] = pool_list; 143 | this.fetcher_list_alt_account_map["USDC"] = alt_account_list; 144 | this.fetcher_list_offset_map["USDC"] = 0; 145 | } 146 | if ("USDT" in this.mintXData) { 147 | const { pool_list, alt_account_list } = await this.createFetcherlist(this.mintXData["USDT"]); 148 | this.fetcher_list_map["USDT"] = pool_list; 149 | this.fetcher_list_alt_account_map["USDT"] = alt_account_list; 150 | this.fetcher_list_offset_map["USDT"] = 0; 151 | } 152 | } 153 | 154 | containBaseTokenType(base_token_type) { 155 | return base_token_type in this.fetcher_list_map && this.fetcher_list_map[base_token_type].length > 1; 156 | } 157 | 158 | async fillAccounts(input_accounts, 159 | input_alt_accounts, 160 | reverse_flag_list, 161 | type_index_list, 162 | base_token_type, 163 | max_fill_size, 164 | randomSelect = false) { 165 | const baseTokenType2Mint = { 166 | "SOL": constants.WSOL, 167 | "USDC": constants.USDC, 168 | "USDT": constants.USDT 169 | } 170 | if (base_token_type in this.fetcher_list_map) { 171 | const max_len = this.fetcher_list_map[base_token_type].length; 172 | const offset = this.fetcher_list_offset_map[base_token_type] 173 | this.fetcher_list_offset_map[base_token_type] += 1; 174 | this.fetcher_list_offset_map[base_token_type] %= max_len; 175 | let selectIndices = Array.from({ length: max_len }, (_, i) => i); 176 | if (randomSelect) { 177 | selectIndices = utils.getRandomElements(selectIndices, max_len); 178 | } 179 | let fill_pool_size = 0; 180 | for (let index = 0; index < max_len; index += 1) { 181 | let selectIndex = selectIndices[index]; 182 | const fetcher = this.fetcher_list_map[base_token_type][(offset + selectIndex) % max_len] 183 | const alt_account = this.fetcher_list_alt_account_map[base_token_type][(offset + selectIndex) % max_len] 184 | fetcher.incrmentFetch(); 185 | let extend_accounts = await fetcher.getFillaccounts(); 186 | if (input_accounts.length + extend_accounts.length > max_fill_size) { 187 | if (fill_pool_size <= 1) { 188 | console.error("fill_pool_size <= 1", fill_pool_size); 189 | } 190 | break; 191 | } 192 | input_accounts.push(...extend_accounts); 193 | if (alt_account) { 194 | input_alt_accounts.push(alt_account); 195 | } 196 | type_index_list.push(fetcher.typeIndex()); 197 | if (fetcher.mintY().equals(baseTokenType2Mint[base_token_type])) { 198 | reverse_flag_list.push(0x00); 199 | } else { 200 | reverse_flag_list.push(0x80); 201 | } 202 | fill_pool_size += 1 203 | } 204 | } 205 | } 206 | } -------------------------------------------------------------------------------- /src/common/constants.js: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | export const POOLType = { 3 | kPumpSwap: "pumpswap", 4 | kMeteoraDLMM: "meteora_dlmm", 5 | kMeteoraAMM: "meteora_amm", 6 | kRaydiumAMM: "raydium_amm", 7 | kRaydiumCLMM: "raydium_clmm", 8 | kRaydiumCPMM: "raydium_cpmm", 9 | } 10 | 11 | export const WSOL = new PublicKey("So11111111111111111111111111111111111111112"); 12 | export const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); 13 | export const USDT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); 14 | 15 | 16 | export const RECIPIENT = new PublicKey("B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu"); 17 | 18 | export const ARB_PROGRAM = new PublicKey("DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z"); 19 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | import * as constants from "./constants.js"; 2 | import { 3 | createAssociatedTokenAccountIdempotentInstruction, 4 | getAssociatedTokenAddressSync 5 | } from "@solana/spl-token"; 6 | import { Connection, 7 | Keypair, 8 | PublicKey, 9 | sendAndConfirmTransaction, 10 | Transaction } from "@solana/web3.js"; 11 | import fs from "fs"; 12 | import bs58 from "bs58"; 13 | 14 | // 监听 uncaughtException 事件 15 | process.on('uncaughtException', (error) => { 16 | console.error('未捕获的异常:', error); 17 | console.error('完整堆栈信息:', error.stack); 18 | }); 19 | 20 | export function IsSupportPool(type) { 21 | if (type == constants.POOLType.kPumpSwap) { 22 | return true; 23 | } 24 | 25 | if (type == constants.POOLType.kRaydiumAMM) { 26 | return true; 27 | } 28 | 29 | if (type == constants.POOLType.kRaydiumCLMM) { 30 | return true; 31 | } 32 | 33 | if (type == constants.POOLType.kMeteoraDLMM) { 34 | return true; 35 | } 36 | 37 | if (type == constants.POOLType.kRaydiumCPMM) { 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | export async function GuardForeverRun(callback, delay = 100) { 45 | while (true) { 46 | try { 47 | await callback(); 48 | } catch (error) { 49 | console.error("GuardForeverRun Fail , retry", error); 50 | await new Promise(resolve => setTimeout(resolve, delay)); 51 | } 52 | } 53 | } 54 | 55 | 56 | let account_cache = new Set(); 57 | (() => { 58 | // load cache 59 | if (fs.existsSync("./account_cache.json")) { 60 | const data = fs.readFileSync("./account_cache.json", "utf8"); 61 | const cache = JSON.parse(data); 62 | account_cache = new Set(cache); 63 | } 64 | })(); 65 | export async function createATATokenAccount(mintx, conn, user, use_cache = true) { 66 | if (!(mintx instanceof PublicKey)) { 67 | mintx = new PublicKey(mintx) 68 | } 69 | const toAccount = getAssociatedTokenAddressSync( 70 | mintx, 71 | user.publicKey, 72 | ); 73 | 74 | if (use_cache && account_cache.has(toAccount.toString())) { 75 | return; 76 | } 77 | 78 | const accountInfo = await conn.getAccountInfo(toAccount); 79 | if (accountInfo) { 80 | account_cache.add(toAccount.toString()) 81 | return; 82 | } 83 | account_cache.add(toAccount.toString()) 84 | 85 | const ix = createAssociatedTokenAccountIdempotentInstruction( 86 | user.publicKey, 87 | toAccount, 88 | user.publicKey, 89 | mintx); 90 | const instructions = [ix]; 91 | let { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash("finalized"); 92 | const tx = new Transaction({ blockhash, lastValidBlockHeight, feePayer: user.publicKey, }).add(...instructions); 93 | const swapTxHash = await sendAndConfirmTransaction(conn, tx, [user]) 94 | console.log(swapTxHash) 95 | // save cache 96 | fs.writeFileSync("./account_cache.json", JSON.stringify(Array.from(account_cache))); 97 | } 98 | 99 | export function getYAwaysBaseMint(x_info, y_info, x_is_base_mint) { 100 | if (x_is_base_mint) { 101 | return [y_info, x_info]; 102 | } 103 | return [x_info, y_info]; 104 | } 105 | 106 | 107 | export function getBaseMintNameByAddress(base_mint) { 108 | if (base_mint == constants.WSOL.toString()) { 109 | return "SOL"; 110 | } 111 | 112 | if (base_mint == constants.USDT.toString()) { 113 | return "USDT" 114 | } 115 | 116 | if (base_mint == constants.USDC.toString()) { 117 | return "USDC" 118 | } 119 | 120 | return undefined; 121 | } 122 | 123 | 124 | export function getRandomElements(arr, count) { 125 | // 复制原数组,避免修改原数组 126 | const shuffled = [...arr]; 127 | const len = shuffled.length; 128 | // Fisher-Yates 洗牌算法 129 | for (let i = len - 1; i > 0; i--) { 130 | const j = Math.floor(Math.random() * (i + 1)); 131 | [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; 132 | } 133 | // 取前 count 个元素 134 | return shuffled.slice(0, count); 135 | } 136 | 137 | export function serializeVecU8(list) { 138 | return Buffer.from(new Uint8Array([...list])) 139 | } 140 | 141 | export function createKeyPairWithConfig(config) { 142 | if (config.base.screctKey.length > 0) { 143 | return Keypair.fromSecretKey(Uint8Array.from(config.base.screctKey)); 144 | } else { 145 | // base58 编码的密钥转换为Keypair 146 | return Keypair.fromSecretKey(bs58.decode(config.base.screctKeyBase58)); 147 | } 148 | } 149 | 150 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const globalConfig = { 2 | "base": { 3 | // rpc 请求url 4 | "rpcUrl": "https://api.mainnet-beta.solana.com", 5 | // 钱包私钥,32个数字 screctKey和screctKeyBase58 2个填一个即可 6 | "screctKey": [], 7 | // 钱包私钥base58编码字符串 screctKey和screctKeyBase58 2个填一个即可 8 | "screctKeyBase58": "", 9 | // 如果使用防损失合约提交,请填写防损失合约IDL文件路径 10 | "guardContractIDL": "", 11 | }, 12 | // 套利机器人配置 13 | "bot": { 14 | // 1 wsol, 最大用于购买的wsol数量, 单位lamports, 限制最大输入,实际输入通过套利路径最优计算得到 15 | "maxInputAmount": 1000000000, 16 | // 最小利润, 单位lamports,如果交易最终利润小于该值,交易会报错 17 | "minProfit": 500, 18 | // 最大交易发送数率,每秒发送的最大交易数,用于控制交易发送的速度 19 | "maxSendRate": 5, 20 | // 单进程异步并发数量,用于控制异步构造交易的并发数量,主要是异步IO,在js中始终无法超过单线程 21 | "maxIOConcurrent": 2, 22 | // 进程并发数量,用于控制进程的并发数量,会启动多个进程 23 | // "maxProcessConcurrent": 2, 24 | 25 | // 是否跳过仿真,跳过仿真会直接发送交易 26 | "skipPreflight": false, 27 | // mint 重新获取pool时间间隔,单位秒 28 | "mintXExpirationTime": 60 * 20, 29 | }, 30 | // 需要监听套利的mint列表,自己任意修改, 31 | // 可以直接去爬gmgn的热榜数据:https://gmgn.ai/?chain=sol&1ren=0&1fr=1&1bu=0&1di=0&0fr=0 32 | "mintList": [ 33 | "FrYz8JgpmxHFjrd8Lbzr3V8tVT37CKswSxm2yd4qbonk", 34 | "BY5zKtZW1cd7GJUc7AoqSPkvfeBrUEV4pAL3QabPPUMP", 35 | "H7xJEp3eCEYozScbZKZoL7kSAXnJV7VEUGkksx6dpump", 36 | "CYwvrqeCgMY8VhXjXUcHrYLhJWkkXzstY52hrkMypump", 37 | "7s6TBuZmLfvyqPh8MTJqQSeyxgF6P62QFNn237Lmpump", 38 | "CEDFLgJUPHMGsAEJUTsZdrVrrAfU7ythx8YrSSzapump", 39 | "B5boCskXEFb1RJJ9EqJNV3gt5fjhk85DeD7BgbLcpump", 40 | "GuKMr2mAFh4CFM4Qo2LkU6MKS2fmReTGcmu8GSudgos9", 41 | "DpySBBrUSyRoSSovFjaoxb9MityQJ9ZYbK9yPWxapump", 42 | "9AmXAJUk2HKSDAynfhGoJMw22rGNN12oAC2xcjzmpump", 43 | "A8YHuvQBMAxXoZAZE72FyC8B7jKHo8RJyByXRRffpump", 44 | "EA6YJYjtaygTdwZsPLZKCFPijDHJtkbgZy4xoJEHpump", 45 | "9Jc3SotKAograpx8Gw9Rxp6FgwEVvgFZn4me7bSKwjpy", 46 | "71QXMcNExFJMvGZ88dF5Q6v5f4c36BsdvTrun61mpump", 47 | "44uYCygv1NrJa8BR9wzmajuV4QHkTCtPEbxpzu7rbonk", 48 | "cdwmqcpjcqzrvVsKYANCdGkdvGG8H5rpeKVT9YZpump", 49 | "CfVs3waH2Z9TM397qSkaipTDhA9wWgtt8UchZKfwkYiu", 50 | "H1pRLTGu9Q3Kv6ziV9DCvTxNQA2vAVocYuGb41wZ1FBk", 51 | "2NhoUWfCbM8V63aCpqdTk4tpf6AmP1Rq11zJvEWHpump", 52 | "HcMHyLYQCDBFPYw8HSSVXc3jBdtezb3ADFdTY5Cfpump", 53 | "8tjDP7xhYBQ8jarFRxGmAghzGXEd3JNW5JATMmFjbonk", 54 | "3L9KUULbfR8ut1WrQ7FqVQrRVHHhVeFCXxM95fFFpump", 55 | "Dz9mQ9NzkBcCsuGPFJ3r1bS4wgqKMHBPiVuniW8Mbonk", 56 | "8gJAaxa5YkrDpXRF6qw1fHW5nxM8sCUkQ9WfXasgpump", 57 | "AsiNybFogE3LaUpKtmLyK6TWNcfg67QKmAoHvKkMpump", 58 | "EWst48ttynfMc3qKcw64TwRrKZQBH57R8JMtFUpHpump", 59 | "DWGxNqh3oQYfZostoCXXdMRaB67WCDYckUjYvsMopump", 60 | "5MyG7PPYjTHR6PLpFatuRUBUfM4Lp4Lw8bXByDRDpump", 61 | "GQeJJeQQHcZoaHBEEPMdJNUhwDiJNsXUaANiYA4vpump", 62 | "9wpLm21ab8ZMVJWH3pHeqgqNJqWos73G8qDRfaEwtray", 63 | "3fXqh7VDYgK5MCfjRRQkTz2rinm8JgeLtYe2EGjcC7M3", 64 | "2j9ZhqyA7THxBgkzkmFGUngSLumuw2R9pL18jBF1pump", 65 | "23cB5pvtxCL1Kj7cSrqAnz35suoTJbCCcryPVUhfAHsa", 66 | "9RGR9gxTpNNNEY6dq3FghuidY3TFEKAXkTQy6BedsBTc", 67 | "HtTYHz1Kf3rrQo6AqDLmss7gq5WrkWAaXn3tupUZbonk", 68 | "4XBoVFNwM4ZsWFGs7mRLeNeKLqRQukHtKy7rewL2vFYa", 69 | "8KeiNNWuxJ71iLRnBn1VKf6K23EM1AbX4k159zQkbonk", 70 | "AQQg4MDZUNhportScwoSLKZyg2d59uJNiw7Bp58fbonk", 71 | "Cr2mM4szbt8286XMn7iTpY5A8S17LbGAu1UyodkyEwn4", 72 | "8Ma3ZxjVmEeNbARkZCnE3bZXVPn73LvBghY9VFKjpump", 73 | "J27UYHX5oeaG1YbUGQc8BmJySXDjNWChdGB2Pi2TMDAq", 74 | "CgZTsf3rNnXsy3YkXmRr988p1Lrv9FpqBpLPWrAbmoon", 75 | "4ucDUtsj75XehbjBE9qEb8t51gucbZj2ALBVNT5pump", 76 | "5UUH9RTDiSpq6HKS6bp4NdU9PNJpXRXuiw6ShBTBhgH2", 77 | "7tPPYTBKrFLKKnoCwijrsfjAYadyp7GpAmSPUbVwbonk", 78 | "2WtqdJZEkCWcfNLU9Z2JUDiNLsdLecrNhFjyNeXkpump", 79 | "ENxHDG5mKZtLbTS3Km9LqXJpc9mu5MwgJm5akM3pump", 80 | "BJrEzqwLgH5YDVWmTkMfFK4MAvzBj5wn4UWzmFVXpump", 81 | "9wK8yN6iz1ie5kEJkvZCTxyN1x5sTdNfx8yeMY8Ebonk", 82 | "6a18sNMEuUG6jSgPNdkwuYc8vRHvziYaQ52foNtkk35g", 83 | "Hbqv4n33tidePXDKrDA7NGt3Jux1trSyxVpsDaWGpump", 84 | "HUWzPAUYLgAMWbP8MiQctEUDjqgp5dzQDVw61xpA1Xdc", 85 | "C9g8XAzPxZKf9CmjzTRHBNV9CMapEPqeFx5zsP84axxC", 86 | "Edc2bNSGody17f56QLDrGHxrH51CJvVXAJyER5vJpump", 87 | "ENfpbQUM5xAnNP8ecyEQGFJ6KwbuPjMwv7ZjR29cDuAb", 88 | "HJ2n2a3YK1LTBCRbS932cTtmXw4puhgG8Jb2WcpEpump", 89 | "Ai3eKAWjzKMV8wRwd41nVP83yqfbAVJykhvJVPxspump", 90 | "9sgZAUEyUsifAjrzsD74cCeV3dMzdeEswAJAHxoWpump", 91 | "8ncucXv6U6epZKHPbgaEBcEK399TpHGKCquSt4RnmX4f", 92 | "D9mendaps8MaMHtLz2w8Duum3FfamPh2yWX5owKZpump", 93 | "D9rQuzkDfQk8Bnv1hcozM1bV8CzdhTzKNVbD9uMfpump", 94 | "bd8eZhS9a2jEtzUcVPcgwU7LXWNhTNSwPRJ6QAoDray", 95 | "AtortPA9SVbkKmdzu5zg4jxgkR4howvPshorA9jYbonk", 96 | "38PgzpJYu2HkiYvV8qePFakB8tuobPdGm2FFEn7Dpump", 97 | "8yxD7uSEyEKpJqaSiunworBFzirAsRXKNjD2X1mdbonk", 98 | "7Tx8qTXSakpfaSFjdztPGQ9n2uyT1eUkYz7gYxxopump", 99 | "86iokc2n1RvznYCxuaiuyXwjrpev7Xz2cHJ8s6GDr2tx", 100 | "DEFn3kkkLrYczTZDTeuFQ24tPmMuep1dTuiK46QMu6BX", 101 | "DzN1qkcRdsxQRFb9yVvz73Fnu6SbD3f5oJgArVdx7Nzc", 102 | "EKAPifSWFLvUixDKE1JUpB6W4fU6yXS8gqsc2nevmoon", 103 | "97PVGU2DzFqsAWaYU17ZBqGvQFmkqtdMywYBNPAfy8vy", 104 | "GNxYv32buvqPS7M2GuXHxSxSN8weEkQiRuD575LXRYvu", 105 | "5aqodm2qSK4anKPRGvArGDnF7ko7bm1sQdC49sZrpump", 106 | "4SbneNwM14L7LnFUFCPdBPrSxRoGnxvHjhRBc1dNbonk", 107 | "FaNghUMLbz7LA8crTEQ7XLK9VSCt9MmyjajdYqVspump", 108 | "5evN2exivZXJfLaA1KhHfiJKWfwH8znqyH36w1SFz89Y", 109 | "DX3v7Agdh7R9rWFJsmnNiB7wCtjneBJng9jM9Yk8qZrJ", 110 | "3qVpCnqdaJtARzE2dYuCy5pm8X2NgF5hx9q9GosPpump", 111 | ], 112 | // address_lookup_tables, 113 | "address_lookup_tables": [ 114 | // 这个alt提供了一些交易池的公共地址(例如Program,authority等),以减少交易的体积, 也可以替换为其他的,支持多个 115 | "8xXRCZ18hs6dpwkgLMasgRoZStPRPJi2Dg6z54kSiWTE", 116 | ], 117 | 118 | } -------------------------------------------------------------------------------- /src/doc/how-to-use-contract.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touyi/solana-onchain-arbitrage-bot/e8e2022ea2feee44eaabed0c1404cdec9d7aebfc/src/doc/how-to-use-contract.md -------------------------------------------------------------------------------- /src/idl/guard.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "3SmBMUQe5QUpLc7wMrm97CRs3kXBSFtMZvPw8CDwZvUi", 3 | "metadata": { 4 | "name": "guard", 5 | "version": "0.1.0", 6 | "spec": "0.1.0", 7 | "description": "Created with Anchor" 8 | }, 9 | "instructions": [ 10 | { 11 | "name": "arb_process_32_account", 12 | "discriminator": [ 13 | 198, 14 | 43, 15 | 70, 16 | 199, 17 | 55, 18 | 193, 19 | 203, 20 | 81 21 | ], 22 | "accounts": [ 23 | { 24 | "name": "user", 25 | "signer": true 26 | }, 27 | { 28 | "name": "user_token_base", 29 | "writable": true 30 | }, 31 | { 32 | "name": "token_base_mint", 33 | "writable": true 34 | }, 35 | { 36 | "name": "token_program", 37 | "writable": true 38 | }, 39 | { 40 | "name": "sys_program", 41 | "address": "11111111111111111111111111111111" 42 | }, 43 | { 44 | "name": "token_pair_0_user_token_account_x", 45 | "writable": true, 46 | "optional": true 47 | }, 48 | { 49 | "name": "token_pair_0_mint_x", 50 | "writable": true, 51 | "optional": true 52 | }, 53 | { 54 | "name": "recipient", 55 | "writable": true, 56 | "address": "B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu" 57 | }, 58 | { 59 | "name": "account_0", 60 | "writable": true, 61 | "optional": true 62 | }, 63 | { 64 | "name": "account_1", 65 | "writable": true, 66 | "optional": true 67 | }, 68 | { 69 | "name": "account_2", 70 | "writable": true, 71 | "optional": true 72 | }, 73 | { 74 | "name": "account_3", 75 | "writable": true, 76 | "optional": true 77 | }, 78 | { 79 | "name": "account_4", 80 | "writable": true, 81 | "optional": true 82 | }, 83 | { 84 | "name": "account_5", 85 | "writable": true, 86 | "optional": true 87 | }, 88 | { 89 | "name": "account_6", 90 | "writable": true, 91 | "optional": true 92 | }, 93 | { 94 | "name": "account_7", 95 | "writable": true, 96 | "optional": true 97 | }, 98 | { 99 | "name": "account_8", 100 | "writable": true, 101 | "optional": true 102 | }, 103 | { 104 | "name": "account_9", 105 | "writable": true, 106 | "optional": true 107 | }, 108 | { 109 | "name": "account_10", 110 | "writable": true, 111 | "optional": true 112 | }, 113 | { 114 | "name": "account_11", 115 | "writable": true, 116 | "optional": true 117 | }, 118 | { 119 | "name": "account_12", 120 | "writable": true, 121 | "optional": true 122 | }, 123 | { 124 | "name": "account_13", 125 | "writable": true, 126 | "optional": true 127 | }, 128 | { 129 | "name": "account_14", 130 | "writable": true, 131 | "optional": true 132 | }, 133 | { 134 | "name": "account_15", 135 | "writable": true, 136 | "optional": true 137 | }, 138 | { 139 | "name": "account_16", 140 | "writable": true, 141 | "optional": true 142 | }, 143 | { 144 | "name": "account_17", 145 | "writable": true, 146 | "optional": true 147 | }, 148 | { 149 | "name": "account_18", 150 | "writable": true, 151 | "optional": true 152 | }, 153 | { 154 | "name": "account_19", 155 | "writable": true, 156 | "optional": true 157 | }, 158 | { 159 | "name": "account_20", 160 | "writable": true, 161 | "optional": true 162 | }, 163 | { 164 | "name": "account_21", 165 | "writable": true, 166 | "optional": true 167 | }, 168 | { 169 | "name": "account_22", 170 | "writable": true, 171 | "optional": true 172 | }, 173 | { 174 | "name": "account_23", 175 | "writable": true, 176 | "optional": true 177 | }, 178 | { 179 | "name": "account_24", 180 | "writable": true, 181 | "optional": true 182 | }, 183 | { 184 | "name": "account_25", 185 | "writable": true, 186 | "optional": true 187 | }, 188 | { 189 | "name": "account_26", 190 | "writable": true, 191 | "optional": true 192 | }, 193 | { 194 | "name": "account_27", 195 | "writable": true, 196 | "optional": true 197 | }, 198 | { 199 | "name": "account_28", 200 | "writable": true, 201 | "optional": true 202 | }, 203 | { 204 | "name": "arb_program", 205 | "writable": true, 206 | "address": "DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z" 207 | } 208 | ], 209 | "args": [ 210 | { 211 | "name": "max_in", 212 | "type": "u64" 213 | }, 214 | { 215 | "name": "min_profit", 216 | "type": "u64" 217 | }, 218 | { 219 | "name": "market_type", 220 | "type": "bytes" 221 | }, 222 | { 223 | "name": "market_flag", 224 | "type": "bytes" 225 | } 226 | ] 227 | }, 228 | { 229 | "name": "arb_process_64_account", 230 | "discriminator": [ 231 | 39, 232 | 254, 233 | 194, 234 | 218, 235 | 233, 236 | 22, 237 | 71, 238 | 203 239 | ], 240 | "accounts": [ 241 | { 242 | "name": "user", 243 | "signer": true 244 | }, 245 | { 246 | "name": "user_token_base", 247 | "writable": true 248 | }, 249 | { 250 | "name": "token_base_mint", 251 | "writable": true 252 | }, 253 | { 254 | "name": "token_program", 255 | "writable": true 256 | }, 257 | { 258 | "name": "sys_program", 259 | "address": "11111111111111111111111111111111" 260 | }, 261 | { 262 | "name": "token_pair_0_user_token_account_x", 263 | "writable": true, 264 | "optional": true 265 | }, 266 | { 267 | "name": "token_pair_0_mint_x", 268 | "writable": true, 269 | "optional": true 270 | }, 271 | { 272 | "name": "recipient", 273 | "writable": true, 274 | "address": "B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu" 275 | }, 276 | { 277 | "name": "account_0", 278 | "writable": true, 279 | "optional": true 280 | }, 281 | { 282 | "name": "account_1", 283 | "writable": true, 284 | "optional": true 285 | }, 286 | { 287 | "name": "account_2", 288 | "writable": true, 289 | "optional": true 290 | }, 291 | { 292 | "name": "account_3", 293 | "writable": true, 294 | "optional": true 295 | }, 296 | { 297 | "name": "account_4", 298 | "writable": true, 299 | "optional": true 300 | }, 301 | { 302 | "name": "account_5", 303 | "writable": true, 304 | "optional": true 305 | }, 306 | { 307 | "name": "account_6", 308 | "writable": true, 309 | "optional": true 310 | }, 311 | { 312 | "name": "account_7", 313 | "writable": true, 314 | "optional": true 315 | }, 316 | { 317 | "name": "account_8", 318 | "writable": true, 319 | "optional": true 320 | }, 321 | { 322 | "name": "account_9", 323 | "writable": true, 324 | "optional": true 325 | }, 326 | { 327 | "name": "account_10", 328 | "writable": true, 329 | "optional": true 330 | }, 331 | { 332 | "name": "account_11", 333 | "writable": true, 334 | "optional": true 335 | }, 336 | { 337 | "name": "account_12", 338 | "writable": true, 339 | "optional": true 340 | }, 341 | { 342 | "name": "account_13", 343 | "writable": true, 344 | "optional": true 345 | }, 346 | { 347 | "name": "account_14", 348 | "writable": true, 349 | "optional": true 350 | }, 351 | { 352 | "name": "account_15", 353 | "writable": true, 354 | "optional": true 355 | }, 356 | { 357 | "name": "account_16", 358 | "writable": true, 359 | "optional": true 360 | }, 361 | { 362 | "name": "account_17", 363 | "writable": true, 364 | "optional": true 365 | }, 366 | { 367 | "name": "account_18", 368 | "writable": true, 369 | "optional": true 370 | }, 371 | { 372 | "name": "account_19", 373 | "writable": true, 374 | "optional": true 375 | }, 376 | { 377 | "name": "account_20", 378 | "writable": true, 379 | "optional": true 380 | }, 381 | { 382 | "name": "account_21", 383 | "writable": true, 384 | "optional": true 385 | }, 386 | { 387 | "name": "account_22", 388 | "writable": true, 389 | "optional": true 390 | }, 391 | { 392 | "name": "account_23", 393 | "writable": true, 394 | "optional": true 395 | }, 396 | { 397 | "name": "account_24", 398 | "writable": true, 399 | "optional": true 400 | }, 401 | { 402 | "name": "account_25", 403 | "writable": true, 404 | "optional": true 405 | }, 406 | { 407 | "name": "account_26", 408 | "writable": true, 409 | "optional": true 410 | }, 411 | { 412 | "name": "account_27", 413 | "writable": true, 414 | "optional": true 415 | }, 416 | { 417 | "name": "account_28", 418 | "writable": true, 419 | "optional": true 420 | }, 421 | { 422 | "name": "account_29", 423 | "writable": true, 424 | "optional": true 425 | }, 426 | { 427 | "name": "account_30", 428 | "writable": true, 429 | "optional": true 430 | }, 431 | { 432 | "name": "account_31", 433 | "writable": true, 434 | "optional": true 435 | }, 436 | { 437 | "name": "account_32", 438 | "writable": true, 439 | "optional": true 440 | }, 441 | { 442 | "name": "account_33", 443 | "writable": true, 444 | "optional": true 445 | }, 446 | { 447 | "name": "account_34", 448 | "writable": true, 449 | "optional": true 450 | }, 451 | { 452 | "name": "account_35", 453 | "writable": true, 454 | "optional": true 455 | }, 456 | { 457 | "name": "account_36", 458 | "writable": true, 459 | "optional": true 460 | }, 461 | { 462 | "name": "account_37", 463 | "writable": true, 464 | "optional": true 465 | }, 466 | { 467 | "name": "account_38", 468 | "writable": true, 469 | "optional": true 470 | }, 471 | { 472 | "name": "account_39", 473 | "writable": true, 474 | "optional": true 475 | }, 476 | { 477 | "name": "account_40", 478 | "writable": true, 479 | "optional": true 480 | }, 481 | { 482 | "name": "account_41", 483 | "writable": true, 484 | "optional": true 485 | }, 486 | { 487 | "name": "account_42", 488 | "writable": true, 489 | "optional": true 490 | }, 491 | { 492 | "name": "account_43", 493 | "writable": true, 494 | "optional": true 495 | }, 496 | { 497 | "name": "account_44", 498 | "writable": true, 499 | "optional": true 500 | }, 501 | { 502 | "name": "account_45", 503 | "writable": true, 504 | "optional": true 505 | }, 506 | { 507 | "name": "account_46", 508 | "writable": true, 509 | "optional": true 510 | }, 511 | { 512 | "name": "account_47", 513 | "writable": true, 514 | "optional": true 515 | }, 516 | { 517 | "name": "account_48", 518 | "writable": true, 519 | "optional": true 520 | }, 521 | { 522 | "name": "account_49", 523 | "writable": true, 524 | "optional": true 525 | }, 526 | { 527 | "name": "account_50", 528 | "writable": true, 529 | "optional": true 530 | }, 531 | { 532 | "name": "account_51", 533 | "writable": true, 534 | "optional": true 535 | }, 536 | { 537 | "name": "account_52", 538 | "writable": true, 539 | "optional": true 540 | }, 541 | { 542 | "name": "account_53", 543 | "writable": true, 544 | "optional": true 545 | }, 546 | { 547 | "name": "account_54", 548 | "writable": true, 549 | "optional": true 550 | }, 551 | { 552 | "name": "arb_program", 553 | "writable": true, 554 | "address": "DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z" 555 | } 556 | ], 557 | "args": [ 558 | { 559 | "name": "max_in", 560 | "type": "u64" 561 | }, 562 | { 563 | "name": "min_profit", 564 | "type": "u64" 565 | }, 566 | { 567 | "name": "market_type", 568 | "type": "bytes" 569 | }, 570 | { 571 | "name": "market_flag", 572 | "type": "bytes" 573 | } 574 | ] 575 | } 576 | ], 577 | "errors": [ 578 | { 579 | "code": 6000, 580 | "name": "InvalidBaseAccount", 581 | "msg": "parse base amount error" 582 | }, 583 | { 584 | "code": 6001, 585 | "name": "NoProfit", 586 | "msg": "Not Profit" 587 | } 588 | ] 589 | } -------------------------------------------------------------------------------- /src/idl/idl.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": "DxeQQ7PQ94j26ism5ivTqNHAkteFNmgRpqYx7XQFqs9Z", 3 | "metadata": { 4 | "name": "arb_touyi", 5 | "version": "0.1.0", 6 | "spec": "0.1.0", 7 | "description": "Created with Anchor" 8 | }, 9 | "instructions": [ 10 | { 11 | "name": "arb_process_32_account", 12 | "discriminator": [ 13 | 198, 14 | 43, 15 | 70, 16 | 199, 17 | 55, 18 | 193, 19 | 203, 20 | 81 21 | ], 22 | "accounts": [ 23 | { 24 | "name": "user", 25 | "signer": true 26 | }, 27 | { 28 | "name": "user_token_base", 29 | "writable": true 30 | }, 31 | { 32 | "name": "token_base_mint", 33 | "writable": true 34 | }, 35 | { 36 | "name": "token_program", 37 | "writable": true 38 | }, 39 | { 40 | "name": "sys_program", 41 | "address": "11111111111111111111111111111111" 42 | }, 43 | { 44 | "name": "token_pair_0_user_token_account_x", 45 | "writable": true, 46 | "optional": true 47 | }, 48 | { 49 | "name": "token_pair_0_mint_x", 50 | "writable": true, 51 | "optional": true 52 | }, 53 | { 54 | "name": "recipient", 55 | "writable": true, 56 | "address": "B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu" 57 | }, 58 | { 59 | "name": "associated_token_program", 60 | "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 61 | }, 62 | { 63 | "name": "account_0", 64 | "writable": true, 65 | "optional": true 66 | }, 67 | { 68 | "name": "account_1", 69 | "writable": true, 70 | "optional": true 71 | }, 72 | { 73 | "name": "account_2", 74 | "writable": true, 75 | "optional": true 76 | }, 77 | { 78 | "name": "account_3", 79 | "writable": true, 80 | "optional": true 81 | }, 82 | { 83 | "name": "account_4", 84 | "writable": true, 85 | "optional": true 86 | }, 87 | { 88 | "name": "account_5", 89 | "writable": true, 90 | "optional": true 91 | }, 92 | { 93 | "name": "account_6", 94 | "writable": true, 95 | "optional": true 96 | }, 97 | { 98 | "name": "account_7", 99 | "writable": true, 100 | "optional": true 101 | }, 102 | { 103 | "name": "account_8", 104 | "writable": true, 105 | "optional": true 106 | }, 107 | { 108 | "name": "account_9", 109 | "writable": true, 110 | "optional": true 111 | }, 112 | { 113 | "name": "account_10", 114 | "writable": true, 115 | "optional": true 116 | }, 117 | { 118 | "name": "account_11", 119 | "writable": true, 120 | "optional": true 121 | }, 122 | { 123 | "name": "account_12", 124 | "writable": true, 125 | "optional": true 126 | }, 127 | { 128 | "name": "account_13", 129 | "writable": true, 130 | "optional": true 131 | }, 132 | { 133 | "name": "account_14", 134 | "writable": true, 135 | "optional": true 136 | }, 137 | { 138 | "name": "account_15", 139 | "writable": true, 140 | "optional": true 141 | }, 142 | { 143 | "name": "account_16", 144 | "writable": true, 145 | "optional": true 146 | }, 147 | { 148 | "name": "account_17", 149 | "writable": true, 150 | "optional": true 151 | }, 152 | { 153 | "name": "account_18", 154 | "writable": true, 155 | "optional": true 156 | }, 157 | { 158 | "name": "account_19", 159 | "writable": true, 160 | "optional": true 161 | }, 162 | { 163 | "name": "account_20", 164 | "writable": true, 165 | "optional": true 166 | }, 167 | { 168 | "name": "account_21", 169 | "writable": true, 170 | "optional": true 171 | }, 172 | { 173 | "name": "account_22", 174 | "writable": true, 175 | "optional": true 176 | }, 177 | { 178 | "name": "account_23", 179 | "writable": true, 180 | "optional": true 181 | }, 182 | { 183 | "name": "account_24", 184 | "writable": true, 185 | "optional": true 186 | }, 187 | { 188 | "name": "account_25", 189 | "writable": true, 190 | "optional": true 191 | }, 192 | { 193 | "name": "account_26", 194 | "writable": true, 195 | "optional": true 196 | }, 197 | { 198 | "name": "account_27", 199 | "writable": true, 200 | "optional": true 201 | }, 202 | { 203 | "name": "account_28", 204 | "writable": true, 205 | "optional": true 206 | } 207 | ], 208 | "args": [ 209 | { 210 | "name": "max_in", 211 | "type": "u64" 212 | }, 213 | { 214 | "name": "min_profit", 215 | "type": "u64" 216 | }, 217 | { 218 | "name": "market_type", 219 | "type": "bytes" 220 | }, 221 | { 222 | "name": "market_flag", 223 | "type": "bytes" 224 | } 225 | ] 226 | }, 227 | { 228 | "name": "arb_process_64_account", 229 | "discriminator": [ 230 | 39, 231 | 254, 232 | 194, 233 | 218, 234 | 233, 235 | 22, 236 | 71, 237 | 203 238 | ], 239 | "accounts": [ 240 | { 241 | "name": "user", 242 | "signer": true 243 | }, 244 | { 245 | "name": "user_token_base", 246 | "writable": true 247 | }, 248 | { 249 | "name": "token_base_mint", 250 | "writable": true 251 | }, 252 | { 253 | "name": "token_program", 254 | "writable": true 255 | }, 256 | { 257 | "name": "sys_program", 258 | "address": "11111111111111111111111111111111" 259 | }, 260 | { 261 | "name": "token_pair_0_user_token_account_x", 262 | "writable": true, 263 | "optional": true 264 | }, 265 | { 266 | "name": "token_pair_0_mint_x", 267 | "writable": true, 268 | "optional": true 269 | }, 270 | { 271 | "name": "recipient", 272 | "writable": true, 273 | "address": "B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu" 274 | }, 275 | { 276 | "name": "associated_token_program", 277 | "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 278 | }, 279 | { 280 | "name": "account_0", 281 | "writable": true, 282 | "optional": true 283 | }, 284 | { 285 | "name": "account_1", 286 | "writable": true, 287 | "optional": true 288 | }, 289 | { 290 | "name": "account_2", 291 | "writable": true, 292 | "optional": true 293 | }, 294 | { 295 | "name": "account_3", 296 | "writable": true, 297 | "optional": true 298 | }, 299 | { 300 | "name": "account_4", 301 | "writable": true, 302 | "optional": true 303 | }, 304 | { 305 | "name": "account_5", 306 | "writable": true, 307 | "optional": true 308 | }, 309 | { 310 | "name": "account_6", 311 | "writable": true, 312 | "optional": true 313 | }, 314 | { 315 | "name": "account_7", 316 | "writable": true, 317 | "optional": true 318 | }, 319 | { 320 | "name": "account_8", 321 | "writable": true, 322 | "optional": true 323 | }, 324 | { 325 | "name": "account_9", 326 | "writable": true, 327 | "optional": true 328 | }, 329 | { 330 | "name": "account_10", 331 | "writable": true, 332 | "optional": true 333 | }, 334 | { 335 | "name": "account_11", 336 | "writable": true, 337 | "optional": true 338 | }, 339 | { 340 | "name": "account_12", 341 | "writable": true, 342 | "optional": true 343 | }, 344 | { 345 | "name": "account_13", 346 | "writable": true, 347 | "optional": true 348 | }, 349 | { 350 | "name": "account_14", 351 | "writable": true, 352 | "optional": true 353 | }, 354 | { 355 | "name": "account_15", 356 | "writable": true, 357 | "optional": true 358 | }, 359 | { 360 | "name": "account_16", 361 | "writable": true, 362 | "optional": true 363 | }, 364 | { 365 | "name": "account_17", 366 | "writable": true, 367 | "optional": true 368 | }, 369 | { 370 | "name": "account_18", 371 | "writable": true, 372 | "optional": true 373 | }, 374 | { 375 | "name": "account_19", 376 | "writable": true, 377 | "optional": true 378 | }, 379 | { 380 | "name": "account_20", 381 | "writable": true, 382 | "optional": true 383 | }, 384 | { 385 | "name": "account_21", 386 | "writable": true, 387 | "optional": true 388 | }, 389 | { 390 | "name": "account_22", 391 | "writable": true, 392 | "optional": true 393 | }, 394 | { 395 | "name": "account_23", 396 | "writable": true, 397 | "optional": true 398 | }, 399 | { 400 | "name": "account_24", 401 | "writable": true, 402 | "optional": true 403 | }, 404 | { 405 | "name": "account_25", 406 | "writable": true, 407 | "optional": true 408 | }, 409 | { 410 | "name": "account_26", 411 | "writable": true, 412 | "optional": true 413 | }, 414 | { 415 | "name": "account_27", 416 | "writable": true, 417 | "optional": true 418 | }, 419 | { 420 | "name": "account_28", 421 | "writable": true, 422 | "optional": true 423 | }, 424 | { 425 | "name": "account_29", 426 | "writable": true, 427 | "optional": true 428 | }, 429 | { 430 | "name": "account_30", 431 | "writable": true, 432 | "optional": true 433 | }, 434 | { 435 | "name": "account_31", 436 | "writable": true, 437 | "optional": true 438 | }, 439 | { 440 | "name": "account_32", 441 | "writable": true, 442 | "optional": true 443 | }, 444 | { 445 | "name": "account_33", 446 | "writable": true, 447 | "optional": true 448 | }, 449 | { 450 | "name": "account_34", 451 | "writable": true, 452 | "optional": true 453 | }, 454 | { 455 | "name": "account_35", 456 | "writable": true, 457 | "optional": true 458 | }, 459 | { 460 | "name": "account_36", 461 | "writable": true, 462 | "optional": true 463 | }, 464 | { 465 | "name": "account_37", 466 | "writable": true, 467 | "optional": true 468 | }, 469 | { 470 | "name": "account_38", 471 | "writable": true, 472 | "optional": true 473 | }, 474 | { 475 | "name": "account_39", 476 | "writable": true, 477 | "optional": true 478 | }, 479 | { 480 | "name": "account_40", 481 | "writable": true, 482 | "optional": true 483 | }, 484 | { 485 | "name": "account_41", 486 | "writable": true, 487 | "optional": true 488 | }, 489 | { 490 | "name": "account_42", 491 | "writable": true, 492 | "optional": true 493 | }, 494 | { 495 | "name": "account_43", 496 | "writable": true, 497 | "optional": true 498 | }, 499 | { 500 | "name": "account_44", 501 | "writable": true, 502 | "optional": true 503 | }, 504 | { 505 | "name": "account_45", 506 | "writable": true, 507 | "optional": true 508 | }, 509 | { 510 | "name": "account_46", 511 | "writable": true, 512 | "optional": true 513 | }, 514 | { 515 | "name": "account_47", 516 | "writable": true, 517 | "optional": true 518 | }, 519 | { 520 | "name": "account_48", 521 | "writable": true, 522 | "optional": true 523 | }, 524 | { 525 | "name": "account_49", 526 | "writable": true, 527 | "optional": true 528 | }, 529 | { 530 | "name": "account_50", 531 | "writable": true, 532 | "optional": true 533 | }, 534 | { 535 | "name": "account_51", 536 | "writable": true, 537 | "optional": true 538 | }, 539 | { 540 | "name": "account_52", 541 | "writable": true, 542 | "optional": true 543 | }, 544 | { 545 | "name": "account_53", 546 | "writable": true, 547 | "optional": true 548 | }, 549 | { 550 | "name": "account_54", 551 | "writable": true, 552 | "optional": true 553 | } 554 | ], 555 | "args": [ 556 | { 557 | "name": "max_in", 558 | "type": "u64" 559 | }, 560 | { 561 | "name": "min_profit", 562 | "type": "u64" 563 | }, 564 | { 565 | "name": "market_type", 566 | "type": "bytes" 567 | }, 568 | { 569 | "name": "market_flag", 570 | "type": "bytes" 571 | } 572 | ] 573 | }, 574 | { 575 | "name": "test_raydium_clmm", 576 | "discriminator": [ 577 | 61, 578 | 119, 579 | 73, 580 | 17, 581 | 40, 582 | 8, 583 | 169, 584 | 176 585 | ], 586 | "accounts": [ 587 | { 588 | "name": "user", 589 | "signer": true 590 | }, 591 | { 592 | "name": "user_token_base", 593 | "writable": true 594 | }, 595 | { 596 | "name": "token_base_mint", 597 | "writable": true 598 | }, 599 | { 600 | "name": "token_program", 601 | "writable": true 602 | }, 603 | { 604 | "name": "sys_program", 605 | "address": "11111111111111111111111111111111" 606 | }, 607 | { 608 | "name": "token_pair_0_user_token_account_x", 609 | "writable": true, 610 | "optional": true 611 | }, 612 | { 613 | "name": "token_pair_0_mint_x", 614 | "writable": true, 615 | "optional": true 616 | }, 617 | { 618 | "name": "recipient", 619 | "writable": true, 620 | "address": "B2kcKQCZUWvK59w9V9n7oDiFwqrh5FowymgpsKZV5NHu" 621 | }, 622 | { 623 | "name": "associated_token_program", 624 | "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" 625 | }, 626 | { 627 | "name": "account_0", 628 | "writable": true, 629 | "optional": true 630 | }, 631 | { 632 | "name": "account_1", 633 | "writable": true, 634 | "optional": true 635 | }, 636 | { 637 | "name": "account_2", 638 | "writable": true, 639 | "optional": true 640 | }, 641 | { 642 | "name": "account_3", 643 | "writable": true, 644 | "optional": true 645 | }, 646 | { 647 | "name": "account_4", 648 | "writable": true, 649 | "optional": true 650 | }, 651 | { 652 | "name": "account_5", 653 | "writable": true, 654 | "optional": true 655 | }, 656 | { 657 | "name": "account_6", 658 | "writable": true, 659 | "optional": true 660 | }, 661 | { 662 | "name": "account_7", 663 | "writable": true, 664 | "optional": true 665 | }, 666 | { 667 | "name": "account_8", 668 | "writable": true, 669 | "optional": true 670 | }, 671 | { 672 | "name": "account_9", 673 | "writable": true, 674 | "optional": true 675 | }, 676 | { 677 | "name": "account_10", 678 | "writable": true, 679 | "optional": true 680 | }, 681 | { 682 | "name": "account_11", 683 | "writable": true, 684 | "optional": true 685 | }, 686 | { 687 | "name": "account_12", 688 | "writable": true, 689 | "optional": true 690 | }, 691 | { 692 | "name": "account_13", 693 | "writable": true, 694 | "optional": true 695 | }, 696 | { 697 | "name": "account_14", 698 | "writable": true, 699 | "optional": true 700 | }, 701 | { 702 | "name": "account_15", 703 | "writable": true, 704 | "optional": true 705 | }, 706 | { 707 | "name": "account_16", 708 | "writable": true, 709 | "optional": true 710 | }, 711 | { 712 | "name": "account_17", 713 | "writable": true, 714 | "optional": true 715 | }, 716 | { 717 | "name": "account_18", 718 | "writable": true, 719 | "optional": true 720 | }, 721 | { 722 | "name": "account_19", 723 | "writable": true, 724 | "optional": true 725 | }, 726 | { 727 | "name": "account_20", 728 | "writable": true, 729 | "optional": true 730 | }, 731 | { 732 | "name": "account_21", 733 | "writable": true, 734 | "optional": true 735 | }, 736 | { 737 | "name": "account_22", 738 | "writable": true, 739 | "optional": true 740 | }, 741 | { 742 | "name": "account_23", 743 | "writable": true, 744 | "optional": true 745 | }, 746 | { 747 | "name": "account_24", 748 | "writable": true, 749 | "optional": true 750 | }, 751 | { 752 | "name": "account_25", 753 | "writable": true, 754 | "optional": true 755 | }, 756 | { 757 | "name": "account_26", 758 | "writable": true, 759 | "optional": true 760 | }, 761 | { 762 | "name": "account_27", 763 | "writable": true, 764 | "optional": true 765 | }, 766 | { 767 | "name": "account_28", 768 | "writable": true, 769 | "optional": true 770 | } 771 | ], 772 | "args": [ 773 | { 774 | "name": "amount_in", 775 | "type": "u64" 776 | }, 777 | { 778 | "name": "market_type", 779 | "type": "u8" 780 | }, 781 | { 782 | "name": "reverse", 783 | "type": "bool" 784 | }, 785 | { 786 | "name": "y2x", 787 | "type": "bool" 788 | }, 789 | { 790 | "name": "use_limit", 791 | "type": "bool" 792 | }, 793 | { 794 | "name": "always_fail", 795 | "type": "bool" 796 | } 797 | ] 798 | } 799 | ], 800 | "errors": [ 801 | { 802 | "code": 6000, 803 | "name": "InvalidTokenAccount", 804 | "msg": "mint error" 805 | }, 806 | { 807 | "code": 6001, 808 | "name": "InvalidBaseAccount", 809 | "msg": "parse base amount error" 810 | }, 811 | { 812 | "code": 6002, 813 | "name": "NoProfit", 814 | "msg": "Not Profit" 815 | }, 816 | { 817 | "code": 6003, 818 | "name": "FakeProfit", 819 | "msg": "Fake Profit" 820 | }, 821 | { 822 | "code": 6004, 823 | "name": "NoSupportMarket", 824 | "msg": "no support market" 825 | } 826 | ] 827 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { globalConfig } from "./config.js"; 2 | import { ArbBot } from "./bot/arb_bot.js"; 3 | import { MeteoraAMMPoolKeyFinder } from "./pool_finder/meteora_amm_finder.js"; 4 | import { MeteoraPoolKeyFinder } from "./pool_finder/meteora_finder.js"; 5 | import { RaydiumPoolKeyFinder } from "./pool_finder/raydium_finder.js"; 6 | import { PumpPoolKeyFinder } from "./pool_finder/pump_finder.js"; 7 | import { 8 | Connection, 9 | } from "@solana/web3.js"; 10 | 11 | 12 | (async () => { 13 | const connection = new Connection(globalConfig.base.rpcUrl) 14 | const poolFinderList = [ 15 | new MeteoraAMMPoolKeyFinder(), 16 | new MeteoraPoolKeyFinder(), 17 | new RaydiumPoolKeyFinder(), 18 | new PumpPoolKeyFinder(), 19 | ]; 20 | 21 | const bot = new ArbBot(globalConfig, connection, poolFinderList); 22 | await bot.run() 23 | })(); -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arbitrage-bot", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "touyi", 9 | "license": "ISC", 10 | "description": "onchain arbitrage bot, supports intelligent routing and calculates optimal arbitrage routes", 11 | "dependencies": { 12 | "@coral-xyz/anchor": "^0.30.1", 13 | "@meteora-ag/dlmm": "1.4.2", 14 | "@raydium-io/raydium-sdk-v2": "0.1.118-alpha", 15 | "@solana/spl-token": "^0.4.13", 16 | "@solana/web3.js": "^1.91.6", 17 | "@types/bn.js": "^5.1.6", 18 | "axios": "^1.8.4", 19 | "bs58": "^6.0.0", 20 | "crypto-js": "^4.2.0", 21 | "http-proxy-agent": "^7.0.2", 22 | "https-proxy-agent": "^7.0.6", 23 | "ioredis": "^5.6.0", 24 | "prompt-sync": "^4.2.0", 25 | "typescript": "^5.8.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pool_fetcher/fetcher.js: -------------------------------------------------------------------------------- 1 | export class Fetcher { 2 | async incrmentFetch() {} 3 | async fetch(force = false) {} 4 | async getFillaccounts() { 5 | throw new Error("Not implemented getFillaccounts"); 6 | } 7 | 8 | mintY() { 9 | throw new Error("Not implemented mintY"); 10 | } 11 | 12 | poolKey() { 13 | throw new Error("Not implemented poolKey"); 14 | } 15 | 16 | typeIndex() { 17 | throw new Error("Not implemented typeIndex"); 18 | } 19 | } -------------------------------------------------------------------------------- /src/pool_fetcher/meteora_amm_fetcher.js: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js' 2 | import { Fetcher } from './fetcher.js' 3 | 4 | export class MeteoraAmmFetcher extends Fetcher { 5 | constructor(pool_key, connection) { 6 | super(); 7 | if (!(pool_key instanceof PublicKey)) { 8 | this.pool_key = new PublicKey(pool_key) 9 | } else { 10 | this.pool_key = pool_key 11 | } 12 | this.connection = connection 13 | this.programId = new PublicKey("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB"); 14 | this.vaultProgram = new PublicKey("24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi"); 15 | this.tokenXMint = null; 16 | this.tokenYMint = null; 17 | 18 | this.xVault = null; 19 | this.yVault = null; 20 | this.xTokenVault = null; 21 | this.yTokenVault = null; 22 | this.xVaultLpMint = null; 23 | this.yVaultLpMint = null; 24 | this.xVaultLp = null; 25 | this.yVaultLp = null; 26 | this.protocolTokenFeeX = null; 27 | this.protocolTokenFeeY = null; 28 | } 29 | 30 | async incrmentFetch() { 31 | 32 | } 33 | 34 | async fetch(force = false) { 35 | if (this.tokenXMint == null || force) { 36 | const poolState = await this.connection.getAccountInfo(this.pool_key); 37 | this.tokenXMint = new PublicKey(poolState.data.subarray(8 + 32, 8 + 32 + 32)); 38 | this.tokenYMint = new PublicKey(poolState.data.subarray(8 + 32 + 32, 8 + 32 + 32 + 32)); 39 | 40 | this.xVault = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32)); 41 | this.yVault = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32)); 42 | 43 | const xVaultAcc = await this.connection.getAccountInfo(this.xVault); 44 | const yVaultAcc = await this.connection.getAccountInfo(this.yVault); 45 | 46 | this.xTokenVault = new PublicKey(xVaultAcc.data.subarray(19, 19 + 32)); 47 | this.yTokenVault = new PublicKey(yVaultAcc.data.subarray(19, 19 + 32)); 48 | 49 | this.xVaultLpMint = new PublicKey(xVaultAcc.data.subarray(19 + 32 * 3, 19 + 32 * 3 + 32)); 50 | this.yVaultLpMint = new PublicKey(yVaultAcc.data.subarray(19 + 32 * 3, 19 + 32 * 3 + 32)); 51 | 52 | this.xVaultLp = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32)); 53 | this.yVaultLp = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); 54 | 55 | this.protocolTokenFeeX = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 2, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 2 + 32)); 56 | this.protocolTokenFeeY = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 2 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 2 + 32 + 32)); 57 | 58 | } 59 | } 60 | 61 | mintY() { 62 | return this.tokenYMint; 63 | } 64 | 65 | poolKey() { 66 | return this.pool_key; 67 | } 68 | 69 | typeIndex() { 70 | return 5; 71 | } 72 | 73 | async getFillaccounts() { 74 | let input_accounts = []; 75 | input_accounts.push(this.programId); 76 | input_accounts.push(this.pool_key); 77 | input_accounts.push(this.xVault); 78 | input_accounts.push(this.yVault); 79 | input_accounts.push(this.xTokenVault); 80 | input_accounts.push(this.yTokenVault); 81 | input_accounts.push(this.xVaultLpMint); 82 | input_accounts.push(this.yVaultLpMint); 83 | input_accounts.push(this.xVaultLp); 84 | input_accounts.push(this.yVaultLp); 85 | input_accounts.push(this.protocolTokenFeeX); 86 | input_accounts.push(this.protocolTokenFeeY); 87 | input_accounts.push(this.vaultProgram); 88 | return input_accounts; 89 | } 90 | 91 | } 92 | 93 | // const connection = new Connection(RichConfig.apiUrl); 94 | // const fetcher = new MeteoraAmmFetcher("B1AdQ85N2mJ2xtMg9bgThhsPoA6T3M26rt4TChWSiPpr", connection); 95 | // await fetcher.fetch(); 96 | 97 | // const input_accounts = await fetcher.getFillaccounts(); 98 | // console.log(input_accounts.map((item) => item.toBase58())); 99 | 100 | -------------------------------------------------------------------------------- /src/pool_fetcher/meteora_dlmm_fetcher.js: -------------------------------------------------------------------------------- 1 | import dlmm from "@meteora-ag/dlmm"; 2 | import { 3 | PublicKey, 4 | } from "@solana/web3.js"; 5 | import { Fetcher } from "./fetcher.js"; 6 | import { BN } from "bn.js"; 7 | 8 | const DLMM = dlmm.default; 9 | export class MeteoraDLMMFetcher extends Fetcher { 10 | constructor(pool_key, connection) { 11 | super(); 12 | if (!(pool_key instanceof PublicKey)) { 13 | this.pool_key = new PublicKey(pool_key) 14 | } else { 15 | this.pool_key = pool_key 16 | } 17 | this.connection = connection 18 | this.dlmmPool = null 19 | this.activeBins = null 20 | this.fetch_cd = 35000; 21 | this.last_fetch_time = 0; 22 | } 23 | 24 | async incrmentFetch() { 25 | if (Date.now() - this.last_fetch_time > this.fetch_cd) { 26 | await this.fetch(true); 27 | } 28 | } 29 | 30 | async fetch(force = false) { 31 | if (this.dlmmPool == null || force) { 32 | this.dlmmPool = await DLMM.create(this.connection, this.pool_key); 33 | } 34 | 35 | const activ_id = dlmm.binIdToBinArrayIndex(new BN(this.dlmmPool.lbPair.activeId)); 36 | const res = await dlmm.deriveBinArray(this.dlmmPool.pubkey, new BN(activ_id - 1), this.dlmmPool.program.programId); 37 | const res2 = await dlmm.deriveBinArray(this.dlmmPool.pubkey, new BN(activ_id), this.dlmmPool.program.programId); 38 | const res3 = await dlmm.deriveBinArray(this.dlmmPool.pubkey, new BN(activ_id + 1), this.dlmmPool.program.programId); 39 | this.activeBins = [res[0], res2[0], res3[0]]; 40 | this.last_fetch_time = Date.now(); 41 | } 42 | 43 | mintY() { 44 | return this.dlmmPool.lbPair.tokenYMint; 45 | } 46 | 47 | poolKey() { 48 | return this.pool_key; 49 | } 50 | typeIndex() { 51 | return 0; 52 | } 53 | 54 | async getFillaccounts() { 55 | let input_accounts = []; 56 | input_accounts.push(this.pool_key); 57 | input_accounts.push(this.dlmmPool.binArrayBitmapExtension ? this.dlmmPool.binArrayBitmapExtension.publicKey : null); 58 | input_accounts.push(this.dlmmPool.lbPair.reserveX); 59 | input_accounts.push(this.dlmmPool.lbPair.reserveY); 60 | input_accounts.push(this.dlmmPool.lbPair.oracle); 61 | input_accounts.push(new PublicKey("D1ZN9Wj1fRSUQfCjhvnu1hqDMT7hzjzBBpi12nVniYD6")); 62 | input_accounts.push(new PublicKey("LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo")); 63 | for (let i = 0; i < this.activeBins.length; i++) { 64 | input_accounts.push(this.activeBins[i]); 65 | } 66 | 67 | return input_accounts; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pool_fetcher/pump_fetcher.js: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | PublicKey, 4 | Keypair, 5 | ComputeBudgetProgram, 6 | TransactionMessage, 7 | VersionedTransaction, 8 | } from "@solana/web3.js"; 9 | 10 | import { 11 | ASSOCIATED_TOKEN_PROGRAM_ID, 12 | TOKEN_PROGRAM_ID 13 | } from "@solana/spl-token"; 14 | import { Fetcher } from "./fetcher.js"; 15 | 16 | export class PumpSwapFetcher extends Fetcher { 17 | constructor(pool_key, connection) { 18 | super(); 19 | if (!(pool_key instanceof PublicKey)) { 20 | this.pool_key = new PublicKey(pool_key) 21 | } else { 22 | this.pool_key = pool_key 23 | } 24 | this.connection = connection; 25 | this.base_mint = null; 26 | this.quote_mint = null; 27 | this.base_mint_pool = null; 28 | this.quote_mint_pool = null; 29 | this.global_config = new PublicKey("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw") 30 | this.protocol_receiver = new PublicKey("JCRGumoE9Qi5BBgULTgdgTLjSgkCMSbF62ZZfGs84JeU") 31 | this.authorith = new PublicKey("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR") 32 | this.pool_program = new PublicKey("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA") 33 | this.protocol_receiver_account = null; 34 | 35 | this.coin_creator_vault_ata = null; 36 | this.coin_creator_auth = null; 37 | } 38 | 39 | async incrmentFetch() { 40 | 41 | } 42 | 43 | typeIndex() { 44 | return 2; 45 | } 46 | 47 | mintY() { 48 | return this.quote_mint; 49 | } 50 | 51 | poolKey() { 52 | return this.pool_key; 53 | } 54 | 55 | async fetch(force = false) { 56 | if (this.base_mint == null || force) { 57 | const account = await this.connection.getAccountInfo(this.pool_key) 58 | this.base_mint = new PublicKey(account.data.subarray(8 + 3 + 32, 8 + 3 + 32 + 32)) 59 | this.quote_mint = new PublicKey(account.data.subarray(8 + 3 + 32 + 32, 8 + 3 + 32 + 32 + 32)) 60 | this.base_mint_pool = new PublicKey(account.data.subarray(8 + 3 + 32 + 32 + 32 + 32, 8 + 3 + 32 + 32 + 32 + 32 + 32)) 61 | this.quote_mint_pool = new PublicKey(account.data.subarray(8 + 3 + 32 + 32 + 32 + 32 + 32, 8 + 3 + 32 + 32 + 32 + 32 + 32 + 32)) 62 | 63 | 64 | const creator = new PublicKey(account.data.subarray(8 + 3 + 32 + 32 + 32 + 32 + 32 + 32 + 8, 8 + 3 + 32 + 32 + 32 + 32 + 32 + 32 + 8 + 32)) 65 | 66 | this.protocol_receiver_account = PublicKey.findProgramAddressSync([ 67 | this.protocol_receiver.toBuffer(), 68 | TOKEN_PROGRAM_ID.toBuffer(), 69 | this.quote_mint.toBuffer(), 70 | ], ASSOCIATED_TOKEN_PROGRAM_ID)[0] 71 | 72 | this.coin_creator_auth = PublicKey.findProgramAddressSync([ 73 | "creator_vault", 74 | creator.toBuffer(), 75 | ], this.pool_program)[0]; 76 | 77 | this.coin_creator_vault_ata = PublicKey.findProgramAddressSync([ 78 | this.coin_creator_auth.toBuffer(), 79 | TOKEN_PROGRAM_ID.toBuffer(), 80 | this.quote_mint.toBuffer(), 81 | ], ASSOCIATED_TOKEN_PROGRAM_ID)[0] 82 | } 83 | 84 | } 85 | 86 | 87 | async getFillaccounts() { 88 | let input_accounts = [] 89 | input_accounts.push(this.pool_program); 90 | input_accounts.push(this.pool_key); 91 | input_accounts.push(this.global_config); 92 | input_accounts.push(this.base_mint_pool); 93 | input_accounts.push(this.quote_mint_pool); 94 | input_accounts.push(this.protocol_receiver); 95 | input_accounts.push(this.protocol_receiver_account); 96 | input_accounts.push(ASSOCIATED_TOKEN_PROGRAM_ID); 97 | input_accounts.push(this.authorith); 98 | input_accounts.push(this.coin_creator_vault_ata); 99 | input_accounts.push(this.coin_creator_auth); 100 | 101 | return input_accounts; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/pool_fetcher/raydium_amm_fetcher.js: -------------------------------------------------------------------------------- 1 | import { Connection, PublicKey } from '@solana/web3.js' 2 | import { jsonInfo2PoolKeys, Raydium, AMM_V4, AMM_STABLE, DEVNET_PROGRAM_ID} from '@raydium-io/raydium-sdk-v2' 3 | import { Fetcher } from './fetcher.js' 4 | const VALID_PROGRAM_ID = new Set([ 5 | AMM_V4.toBase58(), 6 | AMM_STABLE.toBase58(), 7 | DEVNET_PROGRAM_ID.AmmV4.toBase58(), 8 | DEVNET_PROGRAM_ID.AmmStable.toBase58(), 9 | ]) 10 | 11 | export class RaydiumAMMFetcher extends Fetcher { 12 | constructor(pool_key, connection) { 13 | super(); 14 | if (!(pool_key instanceof PublicKey)) { 15 | this.pool_key = new PublicKey(pool_key) 16 | } else { 17 | this.pool_key = pool_key 18 | } 19 | this.connection = connection 20 | this.raydium = null 21 | this.poolKeys = null 22 | } 23 | 24 | async incrmentFetch() { 25 | 26 | } 27 | 28 | async fetch(force = false) { 29 | if (this.raydium == null || force) { 30 | 31 | 32 | this.raydium = await Raydium.load({ 33 | connection: this.connection, 34 | disableLoadToken: false 35 | }); 36 | 37 | // const isValidAmm = (id) => VALID_PROGRAM_ID.has(id) 38 | // const isValidAmmPool = async (id) => { 39 | // const poolInfos = await this.raydium.api.fetchPoolById({ ids: id }); 40 | // const poolInfo = poolInfos[0]; 41 | // if (!isValidAmm(poolInfo.programId)) 42 | // throw new Error('target pool is not AMM pool') 43 | // } 44 | 45 | // await isValidAmmPool(this.pool_key); 46 | this.poolKeys = await this.raydium.liquidity.getAmmPoolKeys(this.pool_key) 47 | this.poolKeys = jsonInfo2PoolKeys(this.poolKeys) 48 | } 49 | } 50 | 51 | mintY() { 52 | return this.poolKeys.mintB.address; 53 | } 54 | 55 | poolKey() { 56 | return this.pool_key; 57 | } 58 | 59 | typeIndex() { 60 | return 1; 61 | } 62 | 63 | async getFillaccounts() { 64 | let input_accounts = []; 65 | input_accounts.push(this.poolKeys.programId); // 0 66 | input_accounts.push(this.poolKeys.id); // 1 67 | input_accounts.push(this.poolKeys.authority); // 2 68 | input_accounts.push(this.poolKeys.vault.A); // 3 69 | input_accounts.push(this.poolKeys.vault.B); // 4 70 | 71 | return input_accounts; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/pool_fetcher/raydium_clmm_fetcher.js: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | PublicKey, 4 | Keypair, 5 | ComputeBudgetProgram, 6 | TransactionMessage, 7 | VersionedTransaction, 8 | } from "@solana/web3.js"; 9 | import { BN } from "bn.js"; 10 | import { 11 | ASSOCIATED_TOKEN_PROGRAM_ID, 12 | TOKEN_PROGRAM_ID 13 | } from "@solana/spl-token"; 14 | import { Fetcher } from "./fetcher.js"; 15 | 16 | const TICK_ARRAY_SIZE = 60; 17 | const POOL_TICK_ARRAY_BITMAP_SEED = Buffer.from("pool_tick_array_bitmap_extension", "utf8"); 18 | const TICK_ARRAY_SEED = Buffer.from("tick_array", "utf8"); 19 | 20 | export class RaydiumCLMMFetcher extends Fetcher { 21 | constructor(pool_key, connection) { 22 | super(); 23 | if (!(pool_key instanceof PublicKey)) { 24 | this.pool_key = new PublicKey(pool_key) 25 | } else { 26 | this.pool_key = pool_key 27 | } 28 | this.connection = connection; 29 | this.clmmProgram = new PublicKey("CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"); 30 | this.tokenProgram2022 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); 31 | this.memoProgram = new PublicKey("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); 32 | this.ammConfig = null; 33 | 34 | this.tokenXMintAccount = null; 35 | this.tokenYMintAccount = null; 36 | this.tokenXMint = null; 37 | this.tokenYMint = null; 38 | this.observationState = null; 39 | 40 | this.exBitMapAccount = null; 41 | this.tickArrays = null; 42 | 43 | this.fetch_cd = 30000; 44 | this.last_fetch_time = 0; 45 | } 46 | 47 | async incrmentFetch() { 48 | if (Date.now() - this.last_fetch_time > this.fetch_cd) { 49 | await this.fetch(true); 50 | this.last_fetch_time = Date.now(); 51 | } 52 | } 53 | 54 | typeIndex() { 55 | return 3; 56 | } 57 | 58 | mintY() { 59 | return this.tokenYMint; 60 | } 61 | 62 | poolKey() { 63 | return this.pool_key; 64 | } 65 | 66 | async fetch(force = false) { 67 | if (this.ammConfig == null || force) { 68 | const account = await this.connection.getAccountInfo(this.pool_key) 69 | 70 | this.ammConfig = new PublicKey(account.data.subarray(9, 9 + 32)) 71 | this.tokenXMint = new PublicKey(account.data.subarray(9 + 32 + 32, 9 + 32 + 32 + 32)) 72 | this.tokenYMint = new PublicKey(account.data.subarray(9 + 32 + 32 + 32, 9 + 32 + 32 + 32 + 32)) 73 | this.tokenXMintAccount = new PublicKey(account.data.subarray(9 + 32 + 32 + 32 + 32, 9 + 32 + 32 + 32 + 32 + 32)) 74 | this.tokenYMintAccount = new PublicKey(account.data.subarray(9 + 32 + 32 + 32 + 32 + 32, 9 + 32 + 32 + 32 + 32 + 32 + 32)) 75 | this.observationState = new PublicKey(account.data.subarray(9 + 32 + 32 + 32 + 32 + 32 + 32, 9 + 32 + 32 + 32 + 32 + 32 + 32 + 32)) 76 | 77 | const observationStateOffset = 9 + 32 + 32 + 32 + 32 + 32 + 32 + 32; 78 | 79 | let currentTickArrayStartIndex = null; 80 | let tickSpacing = null; 81 | let tickCurrent = null; 82 | { 83 | const tickSpacingDataView = new DataView(new Uint8Array(account.data.subarray(observationStateOffset + 2, observationStateOffset + 2 + 2)).buffer) 84 | tickSpacing = tickSpacingDataView.getUint16(0, true); 85 | 86 | const tickCurrentDataView = new DataView(new Uint8Array(account.data.subarray(observationStateOffset + 2 + 2 + 32, observationStateOffset + 2 + 2 + 32 + 4)).buffer) 87 | tickCurrent = tickCurrentDataView.getInt32(0, true); 88 | 89 | const ticksInArray = this.tickCount(tickSpacing); 90 | 91 | let startIndex = tickCurrent / ticksInArray; 92 | if (tickCurrent < 0 && tickCurrent % ticksInArray != 0) { 93 | startIndex = Math.ceil(startIndex) - 1; 94 | } else { 95 | startIndex = Math.floor(startIndex); 96 | } 97 | currentTickArrayStartIndex = startIndex * ticksInArray; 98 | } 99 | 100 | 101 | // bitmap 102 | let tickArrayBitmap = []; 103 | let positiveTickArrayBitmap = []; 104 | let negativeTickArrayBitmap = []; 105 | { 106 | const bitmapData = new Uint8Array(account.data.subarray(observationStateOffset + 164 + 507, observationStateOffset + 164 + 507 + 16 * 8)) 107 | for (let i = 0; i < 16 * 8; i+=8) { 108 | tickArrayBitmap.push(new BN(bitmapData.slice(i, i + 8), "le")) 109 | } 110 | this.exBitMapAccount = PublicKey.findProgramAddressSync([ 111 | POOL_TICK_ARRAY_BITMAP_SEED, 112 | this.pool_key.toBuffer(), 113 | ], this.clmmProgram)[0] 114 | 115 | const exBitMapAccountData = await this.connection.getAccountInfo(this.exBitMapAccount) 116 | new PublicKey(exBitMapAccountData.data.subarray(8, 8 + 32)).toString() 117 | 118 | const positiveTickUint8Array = new Uint8Array(exBitMapAccountData.data.subarray(8 + 32, 8 + 32 + (14 * 8 * 8))) 119 | const negativeTickUint8Array = new Uint8Array(exBitMapAccountData.data.subarray(8 + 32 + (14 * 8 * 8), 8 + 32 + (14 * 8 * 8) * 2)) 120 | for (let i = 0; i < 14; i++) { 121 | let positive = [] 122 | let negative = [] 123 | for (let j = 0; j < 8; j++) { 124 | const pb = new BN(positiveTickUint8Array.slice((i * 8 + j) * 8, (i * 8 + j + 1) * 8), "le"); 125 | const nb = new BN(negativeTickUint8Array.slice((i * 8 + j) * 8, (i * 8 + j + 1) * 8), "le"); 126 | positive.push(pb) 127 | negative.push(nb) 128 | } 129 | positiveTickArrayBitmap.push(positive) 130 | negativeTickArrayBitmap.push(negative) 131 | } 132 | } 133 | 134 | // search 135 | let startIndexArray = null; 136 | { 137 | const tickArrayOffset = Math.floor(currentTickArrayStartIndex / (tickSpacing * TICK_ARRAY_SIZE)); 138 | startIndexArray = [ 139 | ...this.searchLowBit( 140 | tickArrayBitmap, 141 | positiveTickArrayBitmap, 142 | negativeTickArrayBitmap, 143 | tickArrayOffset - 1, 144 | tickSpacing, 145 | 1 146 | ), 147 | ...this.searchHightBit( 148 | tickArrayBitmap, 149 | positiveTickArrayBitmap, 150 | negativeTickArrayBitmap, 151 | tickArrayOffset, 152 | tickSpacing, 153 | 2 154 | ) 155 | ] 156 | } 157 | // tickArrays 158 | this.tickArrays = []; 159 | { 160 | for (let i = 0; i < startIndexArray.length; i++) { 161 | const startIndex = startIndexArray[i]; 162 | this.tickArrays.push(PublicKey.findProgramAddressSync([ 163 | TICK_ARRAY_SEED, 164 | this.pool_key.toBuffer(), 165 | this.i32ToBytes(startIndex) 166 | ], this.clmmProgram)[0]); 167 | } 168 | } 169 | } 170 | 171 | } 172 | 173 | i32ToBytes(num) { 174 | const arr = new ArrayBuffer(4); 175 | const view = new DataView(arr); 176 | view.setInt32(0, num, false); 177 | return new Uint8Array(arr); 178 | } 179 | 180 | searchLowBit( 181 | tickArrayBitmap, 182 | positiveTickArrayBitmap, 183 | negativeTickArrayBitmap, 184 | currentTickArrayBitStartIndex, 185 | tickSpacing, 186 | expectedCount, 187 | ) { 188 | const tickArrayBitmaps = [ 189 | ...[...negativeTickArrayBitmap].reverse(), 190 | tickArrayBitmap.slice(0, 8), 191 | tickArrayBitmap.slice(8, 16), 192 | ...positiveTickArrayBitmap, 193 | ].map((bns) => { 194 | let b = new BN(0); 195 | for (let i = 0; i < bns.length; i++) { 196 | b = b.add(bns[i].shln(64 * i)); 197 | } 198 | return b; 199 | }); 200 | let result = []; 201 | while (currentTickArrayBitStartIndex >= -7680) { 202 | const arrayIndex = Math.floor((currentTickArrayBitStartIndex + 7680) / 512); 203 | const searchIndex = (currentTickArrayBitStartIndex + 7680) % 512; 204 | 205 | if (tickArrayBitmaps[arrayIndex].testn(searchIndex)) result.push(currentTickArrayBitStartIndex); 206 | 207 | currentTickArrayBitStartIndex--; 208 | if (result.length === expectedCount) break; 209 | } 210 | const tickCount = this.tickCount(tickSpacing); 211 | return result.map((i) => i * tickCount); 212 | } 213 | 214 | searchHightBit( 215 | tickArrayBitmap, 216 | positiveTickArrayBitmap, 217 | negativeTickArrayBitmap, 218 | currentTickArrayBitStartIndex, 219 | tickSpacing, 220 | expectedCount, 221 | ) { 222 | const tickArrayBitmaps = [ 223 | ...[...negativeTickArrayBitmap].reverse(), 224 | tickArrayBitmap.slice(0, 8), 225 | tickArrayBitmap.slice(8, 16), 226 | ...positiveTickArrayBitmap, 227 | ].map((bns) => { 228 | let b = new BN(0); 229 | for (let i = 0; i < bns.length; i++) { 230 | b = b.add(bns[i].shln(64 * i)); 231 | } 232 | return b; 233 | }); 234 | const result = []; 235 | while (currentTickArrayBitStartIndex < 7680) { 236 | const arrayIndex = Math.floor((currentTickArrayBitStartIndex + 7680) / 512); 237 | const searchIndex = (currentTickArrayBitStartIndex + 7680) % 512; 238 | 239 | if (tickArrayBitmaps[arrayIndex].testn(searchIndex)) result.push(currentTickArrayBitStartIndex); 240 | 241 | currentTickArrayBitStartIndex++; 242 | if (result.length === expectedCount) break; 243 | } 244 | 245 | const tickCount = this.tickCount(tickSpacing); 246 | return result.map((i) => i * tickCount); 247 | } 248 | 249 | tickCount(tickSpacing) { 250 | return tickSpacing * TICK_ARRAY_SIZE 251 | } 252 | 253 | 254 | async getFillaccounts() { 255 | let input_accounts = [] 256 | input_accounts.push(this.clmmProgram); 257 | input_accounts.push(this.ammConfig); 258 | input_accounts.push(this.pool_key); 259 | input_accounts.push(this.tokenXMintAccount); 260 | input_accounts.push(this.tokenYMintAccount); 261 | input_accounts.push(this.observationState); 262 | input_accounts.push(this.tokenProgram2022) 263 | input_accounts.push(this.memoProgram) 264 | input_accounts.push(this.exBitMapAccount) 265 | 266 | if (this.tickArrays.length > 3) { 267 | throw new Error("tickArrays length > 3") 268 | } 269 | for (let i = 0; i < this.tickArrays.length; i++) { 270 | input_accounts.push(this.tickArrays[i]) 271 | } 272 | for (let i = 0; i < 3 - this.tickArrays.length; i++) { 273 | input_accounts.push(null) 274 | } 275 | 276 | return input_accounts; 277 | } 278 | } 279 | 280 | // const c = new Connection("https://mainnet.helius-rpc.com/?api-key=e4829446-181d-47e1-a466-b099184296c7") 281 | // // const ray = new RaydiumCLMMFetcher("GQsPr4RJk9AZkkfWHud7v4MtotcxhaYzZHdsPCg9vNvW", c) 282 | // const ray = new RaydiumCLMMFetcher("CebffaLQemzZzFqi9P7gPpZTXMsZQxkcpkTMfEMu9Hqg", c) 283 | // await ray.fetch() 284 | // const fillaccounts = await ray.getFillaccounts() 285 | // fillaccounts.forEach((fillaccount) => { 286 | // if (fillaccount == null) { 287 | // console.log("fillaccount: null") 288 | // return 289 | // } 290 | // console.log("fillaccount:", fillaccount.toString()) 291 | // }) -------------------------------------------------------------------------------- /src/pool_fetcher/raydium_cpmm_fetcher.js: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | PublicKey, 4 | Keypair, 5 | ComputeBudgetProgram, 6 | TransactionMessage, 7 | VersionedTransaction, 8 | } from "@solana/web3.js"; 9 | import { Fetcher } from "./fetcher.js"; 10 | const OBSERVATION_SEED = Buffer.from("observation", "utf8"); 11 | 12 | export class RaydiumCPMMFetcher extends Fetcher { 13 | constructor(pool_key, connection) { 14 | super(); 15 | if (!(pool_key instanceof PublicKey)) { 16 | this.pool_key = new PublicKey(pool_key) 17 | } else { 18 | this.pool_key = pool_key 19 | } 20 | this.connection = connection; 21 | 22 | this.programId = new PublicKey("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"); 23 | this.authority = new PublicKey("GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL"); 24 | this.ammConfig = null; 25 | this.tokenXVault = null; 26 | this.tokenYVault = null; 27 | this.tokenXProgram = null; 28 | this.tokenYProgram = null; 29 | this.mintTokenX = null; 30 | this.mintTokenY = null; 31 | this.observation = null; 32 | } 33 | 34 | async incrmentFetch() { 35 | 36 | } 37 | 38 | typeIndex() { 39 | return 4; 40 | } 41 | 42 | mintY() { 43 | return this.mintTokenY; 44 | } 45 | 46 | poolKey() { 47 | return this.pool_key; 48 | } 49 | 50 | async fetch(force = false) { 51 | if (this.ammConfig == null || force) { 52 | const poolState = await this.connection.getAccountInfo(this.pool_key); 53 | this.ammConfig = new PublicKey(poolState.data.subarray(8, 8 + 32)); 54 | this.tokenXVault = new PublicKey(poolState.data.subarray(8 + 32 + 32, 8 + 32 + 32 + 32)); 55 | this.tokenYVault = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32)); 56 | this.mintTokenX = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32)); 57 | this.mintTokenY = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); 58 | this.tokenXProgram = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); 59 | this.tokenYProgram = new PublicKey(poolState.data.subarray(8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 32, 8 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 32 + 32)); 60 | this.observation = PublicKey.findProgramAddressSync([OBSERVATION_SEED, this.pool_key.toBuffer()], this.programId)[0]; 61 | } 62 | 63 | } 64 | 65 | 66 | async getFillaccounts() { 67 | let input_accounts = [] 68 | input_accounts.push(this.programId); 69 | input_accounts.push(this.authority); 70 | input_accounts.push(this.ammConfig); 71 | input_accounts.push(this.pool_key); 72 | input_accounts.push(this.tokenXVault); 73 | input_accounts.push(this.tokenYVault); 74 | input_accounts.push(this.tokenXProgram); 75 | input_accounts.push(this.tokenYProgram); 76 | input_accounts.push(this.observation); 77 | return input_accounts; 78 | } 79 | } -------------------------------------------------------------------------------- /src/pool_finder/meteora_amm_finder.js: -------------------------------------------------------------------------------- 1 | import { PoolKeyFinder, createNewMintXData, IsBaseMint} from "./pool_finder.js"; 2 | import * as utils from "../common/utils.js" 3 | import * as constants from "../common/constants.js" 4 | import path from "path"; 5 | 6 | export class MeteoraAMMPoolKeyFinder extends PoolKeyFinder { 7 | constructor() { 8 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 9 | super(path.join(__dirname, "config/meteora_amm/")); 10 | } 11 | 12 | updateConfigByHttpData(mintX, data) { 13 | this.config[mintX] = [createNewMintXData(), data.expiration_time]; 14 | data.data.forEach(poolInfo => { 15 | if (!this.IsSufficientLiquidity(poolInfo.trading_volume)) { 16 | return; 17 | } 18 | 19 | const [xMintName, yMintName] = poolInfo.pool_name.split("-"); 20 | const x_type = IsBaseMint(xMintName); 21 | const y_type = IsBaseMint(yMintName); 22 | if ((x_type && y_type) || (!x_type && !y_type)) { 23 | return; 24 | } 25 | 26 | let [x_info, y_info] = utils.getYAwaysBaseMint({ address: poolInfo.pool_token_mints[0], symbol: xMintName}, { address: poolInfo.pool_token_mints[1], symbol: yMintName}, x_type); 27 | const base_mint_name = y_info.symbol == "WSOL" ? "SOL": y_info.symbol; 28 | this.config[x_info.address][0][base_mint_name].push({ 29 | "type": constants.POOLType.kMeteoraAMM, 30 | "pool_key": poolInfo.pool_address, 31 | "meta": {} 32 | }); 33 | }); 34 | } 35 | 36 | getSearchSOLPoolUrl(mintX) { 37 | return `https://www.meteora.ag/amm/pools/search?page=0&size=100&filter=${mintX}&pool_type=dynamic&sort_key=volume&order_by=desc` 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pool_finder/meteora_finder.js: -------------------------------------------------------------------------------- 1 | import { PoolKeyFinder, createNewMintXData, IsBaseMint} from "./pool_finder.js"; 2 | import * as utils from "../common/utils.js" 3 | import * as constants from "../common/constants.js" 4 | 5 | import path from "path"; 6 | 7 | export class MeteoraPoolKeyFinder extends PoolKeyFinder { 8 | constructor() { 9 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 10 | super(path.join(__dirname, "config/meteora/")); 11 | } 12 | 13 | updateConfigByHttpData(mintX, data) { 14 | this.config[mintX] = [createNewMintXData(), data.expiration_time]; 15 | data.groups.forEach(element => { 16 | element.pairs.forEach(poolInfo => { 17 | // 24h liquidity, USDT. 18 | if (!this.IsSufficientLiquidity(poolInfo.trade_volume_24h)) { 19 | return; 20 | } 21 | 22 | const [xMintName, yMintName] = poolInfo.name.split("-"); 23 | const x_type = IsBaseMint(xMintName); 24 | const y_type = IsBaseMint(yMintName); 25 | 26 | // both base mint or both not base mint 27 | if ((x_type && y_type) || (!x_type && !y_type)) { 28 | return; 29 | } 30 | 31 | let [x_info, y_info] = utils.getYAwaysBaseMint({ address: poolInfo.mint_x, symbol: xMintName}, { address: poolInfo.mint_y, symbol: yMintName}, x_type); 32 | 33 | const base_mint_name = y_info.symbol == "WSOL" ? "SOL": y_info.symbol; 34 | this.config[x_info.address][0][base_mint_name].push({ 35 | "type": constants.POOLType.kMeteoraDLMM, 36 | "pool_key": poolInfo.address, 37 | "meta": {} 38 | }); 39 | }); 40 | }); 41 | } 42 | 43 | getSearchSOLPoolUrl(mintX) { 44 | // only dlmm now!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!11111 45 | return `https://www.meteora.ag/clmm-api/pair/all_by_groups?page=0&limit=100&unknown=true&search_term=${mintX}&sort_key=volume&order_by=desc` 46 | } 47 | } -------------------------------------------------------------------------------- /src/pool_finder/pool_finder.js: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "mint_x": "abc", 4 | "USDT" : [ 5 | { 6 | "type": "raydium_amm", 7 | "pool_key": "xxx", 8 | "subkeys": ["aaa", "bbb"], 9 | "meta": {} 10 | } 11 | ], 12 | "SOL": [ 13 | { 14 | "type": "raydium_amm", 15 | "pool_key": "xxx", 16 | "subkeys": ["aaa", "bbb"], 17 | "meta": {} 18 | } 19 | ], 20 | "USDC":[] 21 | } 22 | */ 23 | 24 | import fs from "fs"; 25 | import path from "path"; 26 | import https from "https"; 27 | import * as constants from "../common/constants.js"; 28 | 29 | let BaseMint = ["WSOL", "SOL", "USDC", "USDT", constants.WSOL.toString(), constants.USDC.toString(), constants.USDT.toString()]; 30 | export function IsBaseMint(symbol) { 31 | return BaseMint.includes(symbol); 32 | } 33 | 34 | export function createNewMintXData() { 35 | return { 36 | "USDT": [], 37 | "USDC": [], 38 | "SOL": [], 39 | }; 40 | } 41 | 42 | export class PoolKeyFinder { 43 | // mintX -> { USDT: {}, USDC: {}, SOL: {}} 44 | config = {}; 45 | 46 | constructor(configCachePath) { 47 | this.configCachePath = configCachePath; 48 | 49 | try { 50 | this.loadLocalCache(); 51 | } catch(e) { 52 | console.log(e); 53 | } 54 | } 55 | 56 | IsSufficientLiquidity(liquidity) { 57 | // 24h, USDT. 58 | return liquidity > 10000 59 | } 60 | 61 | loadLocalCache() { 62 | if (!fs.existsSync(this.configCachePath)) { 63 | fs.mkdirSync(this.configCachePath, { recursive: true }); 64 | } 65 | 66 | fs.readdirSync(this.configCachePath).forEach(subFileName => { 67 | const subPath = path.join(this.configCachePath, subFileName); 68 | this.updateConfigByHttpData(subFileName.split(".")[0], JSON.parse(fs.readFileSync(subPath))); 69 | }); 70 | } 71 | 72 | async getPoolKey(mintX, tryUpdate = true) { // if config has mintX and not outdated. 73 | // console.log(this.config); 74 | if (this.config[mintX] && (!tryUpdate || this.config[mintX][1] > Date.now())) { 75 | return this.config[mintX][0]; 76 | } 77 | await this.tryUpdateMintXInfo(mintX); 78 | if (this.config[mintX] && this.config[mintX][1] > Date.now()) { 79 | return this.config[mintX][0]; 80 | } 81 | 82 | return createNewMintXData(); 83 | } 84 | 85 | 86 | async tryUpdateMintXInfo(mintX, callback) { 87 | return new Promise((resolve, reject) => { 88 | https.get(this.getSearchSOLPoolUrl(mintX), (res) => { 89 | let data = ""; 90 | res.on('data', (d) => { 91 | data += d; 92 | }); 93 | res.on("end", () => { 94 | try { 95 | data = JSON.parse(data); 96 | if (Array.isArray(data)) { 97 | data = { "data": data }; 98 | } 99 | // expiration_time 1800000ms ---> half hour. 100 | data.expiration_time = Date.now() + 1800000; 101 | fs.writeFileSync(this.configCachePath + mintX + ".json", JSON.stringify(data, null, 4)); 102 | this.updateConfigByHttpData(mintX, data); 103 | } catch(e) { 104 | console.log(e); 105 | } 106 | resolve(); 107 | }); 108 | 109 | res.on("error", (e) => { resolve(); }); 110 | }); 111 | }) 112 | } 113 | 114 | // interface 115 | updateConfigByHttpData(mintX, data) {} 116 | getSearchSOLPoolUrl(mintX) {} 117 | } 118 | 119 | // save cache. 120 | -------------------------------------------------------------------------------- /src/pool_finder/pump_finder.js: -------------------------------------------------------------------------------- 1 | import * as utils from "../common/utils.js" 2 | import * as constants from "../common/constants.js" 3 | import { PoolKeyFinder, createNewMintXData, IsBaseMint } from "./pool_finder.js"; 4 | import path from "path"; 5 | 6 | const getYAwaysBaseMint = (x_info, y_info, x_is_base_mint) => { 7 | if (x_is_base_mint) { 8 | return [y_info, x_info]; 9 | } 10 | 11 | return [x_info, y_info]; 12 | } 13 | 14 | export class PumpPoolKeyFinder extends PoolKeyFinder { 15 | constructor() { 16 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 17 | super(path.join(__dirname, "config/pump/")); 18 | } 19 | 20 | updateConfigByHttpData(mintX, data) { 21 | this.config[mintX] = [createNewMintXData(), data.expiration_time]; 22 | if (!data || !data.data) { 23 | return; 24 | } 25 | data.data.forEach(poolInfo => { 26 | // 24h liquidity, USDT. 27 | if (!this.IsSufficientLiquidity(Number(poolInfo.liquidityUSD))) { 28 | return; 29 | } 30 | 31 | const x_type = IsBaseMint(poolInfo.baseMint); 32 | const y_type = IsBaseMint(poolInfo.quoteMint); 33 | 34 | // both base mint or both not base mint 35 | if ((x_type && y_type) || (!x_type && !y_type)) { 36 | return; 37 | } 38 | 39 | let [x_info, y_info] = getYAwaysBaseMint({ address: poolInfo.baseMint}, { address: poolInfo.quoteMint}, x_type); 40 | const base_mint_name = utils.getBaseMintNameByAddress(y_info.address); 41 | this.config[x_info.address][0][base_mint_name].push({ 42 | "type": constants.POOLType.kPumpSwap, 43 | "pool_key": poolInfo.address, 44 | "meta": {} 45 | }); 46 | }); 47 | } 48 | 49 | getSearchSOLPoolUrl(mintX) { 50 | return `https://swap-api.pump.fun/v1/pools/pair?mintA=${constants.WSOL.toString()}&mintB=${mintX}`; 51 | } 52 | } -------------------------------------------------------------------------------- /src/pool_finder/raydium_finder.js: -------------------------------------------------------------------------------- 1 | import { PoolKeyFinder , IsBaseMint, createNewMintXData} from "./pool_finder.js"; 2 | import * as utils from "../common/utils.js" 3 | import * as constants from "../common/constants.js" 4 | 5 | import path from "path"; 6 | 7 | 8 | const getPoolTypeByProgramId = (programId)=>{ 9 | if (programId === "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8") { 10 | return constants.POOLType.kRaydiumAMM; 11 | } 12 | 13 | if (programId === "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK") { 14 | return constants.POOLType.kRaydiumCLMM; 15 | } 16 | 17 | if (programId === "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C") { 18 | return constants.POOLType.kRaydiumCPMM; 19 | } 20 | 21 | return "other"; 22 | } 23 | 24 | export class RaydiumPoolKeyFinder extends PoolKeyFinder { 25 | constructor() { 26 | const __dirname = path.dirname(new URL(import.meta.url).pathname); 27 | super(path.join(__dirname, "config/raydium/")); 28 | } 29 | 30 | updateConfigByHttpData(mintX, data) { 31 | this.config[mintX] = [createNewMintXData(), data.expiration_time]; 32 | // console.log(" RaydiumPoolKeyFinder load data ", data.data.data); 33 | data.data.data.forEach(poolInfo => { 34 | // 24h liquidity, USDT. 35 | if (!this.IsSufficientLiquidity(poolInfo.day.volume)) { 36 | return; 37 | } 38 | 39 | const x_type = IsBaseMint(poolInfo.mintA.symbol); 40 | const y_type = IsBaseMint(poolInfo.mintB.symbol); 41 | 42 | // both base mint or both not base mint 43 | if ((x_type && y_type) || (!x_type && !y_type)) { 44 | return; 45 | } 46 | 47 | let [x_info, y_info] = utils.getYAwaysBaseMint(poolInfo.mintA, poolInfo.mintB, x_type); 48 | 49 | const base_mint_name = y_info.symbol == "WSOL" ? "SOL": y_info.symbol; 50 | this.config[x_info.address][0][base_mint_name].push({ 51 | "type": getPoolTypeByProgramId(poolInfo.programId), 52 | "pool_key": poolInfo.id, 53 | "meta": {} 54 | }); 55 | }); 56 | } 57 | 58 | getSearchSOLPoolUrl(mintX) { 59 | return `https://api-v3.raydium.io/pools/info/mint?mint1=${mintX}&mint2=${constants.WSOL.toString()}&poolType=all&poolSortField=volume24h&sortType=desc&pageSize=20&page=1` 60 | } 61 | } -------------------------------------------------------------------------------- /src/tools/warp_sol.js: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | Keypair, 4 | PublicKey, 5 | SystemProgram, 6 | Transaction, 7 | sendAndConfirmTransaction 8 | } from '@solana/web3.js'; 9 | import { 10 | createAssociatedTokenAccountInstruction, 11 | getAssociatedTokenAddressSync, 12 | createSyncNativeInstruction, 13 | createCloseAccountInstruction 14 | } from '@solana/spl-token'; 15 | import { globalConfig } from "../config.js"; 16 | import fs from 'fs'; 17 | import * as utils from "../common/utils.js"; 18 | import * as constants from "../common/constants.js"; 19 | 20 | // 连接到Solana网络 21 | const connection = new Connection(globalConfig.base.rpcUrl) 22 | 23 | const wallet = utils.createKeyPairWithConfig(globalConfig); 24 | console.log(`wallet: ${wallet.publicKey.toString()}`); 25 | 26 | const convertSolToWsol = async (amount) => { 27 | // 获取WSOL的关联令牌账户地址 28 | const associatedTokenAccount = getAssociatedTokenAddressSync( 29 | constants.WSOL, 30 | wallet.publicKey 31 | ); 32 | 33 | // 创建交易对象 34 | const transaction = new Transaction(); 35 | 36 | // 如果关联账户不存在,则创建它 37 | const accountInfo = await connection.getAccountInfo(associatedTokenAccount); 38 | if (!accountInfo) { 39 | transaction.add( 40 | createAssociatedTokenAccountInstruction( 41 | wallet.publicKey, 42 | associatedTokenAccount, 43 | wallet.publicKey, 44 | constants.WSOL 45 | ) 46 | ); 47 | } 48 | 49 | // 添加将SOL转换为WSOL的指令 50 | transaction.add( 51 | SystemProgram.transfer({ 52 | fromPubkey: wallet.publicKey, 53 | toPubkey: associatedTokenAccount, 54 | lamports: amount 55 | }), 56 | createSyncNativeInstruction(associatedTokenAccount) 57 | ); 58 | 59 | // 发送并确认交易 60 | const signature = await sendAndConfirmTransaction( 61 | connection, 62 | transaction, 63 | [wallet] 64 | ); 65 | 66 | console.log('SOL to WSOL Transaction signature:', signature); 67 | }; 68 | 69 | 70 | 71 | const convertWsolToSol = async () => { 72 | // 获取WSOL的关联令牌账户地址 73 | const associatedTokenAccount = getAssociatedTokenAddressSync( 74 | constants.WSOL, 75 | wallet.publicKey 76 | ); 77 | 78 | // 创建交易对象 79 | const transaction = new Transaction(); 80 | 81 | // 添加关闭关联令牌账户的指令,将WSOL转换回SOL 82 | transaction.add( 83 | createCloseAccountInstruction( 84 | associatedTokenAccount, 85 | wallet.publicKey, 86 | wallet.publicKey, 87 | [] 88 | ) 89 | ); 90 | 91 | // 发送并确认交易 92 | const signature = await sendAndConfirmTransaction( 93 | connection, 94 | transaction, 95 | [wallet] 96 | ); 97 | 98 | console.log('WSOL to SOL Transaction signature:', signature); 99 | }; 100 | 101 | (async () => { 102 | let amount; 103 | let wrapSol = true; 104 | for (let i = 2; i < process.argv.length; i++) { 105 | const arg = process.argv[i]; 106 | if (arg.startsWith('--amount=')) { 107 | const value = arg.split('=')[1]; 108 | amount = parseFloat(value); 109 | if (isNaN(amount)) { 110 | console.error('错误:amount 参数必须是有效的数字。'); 111 | showUsage(); 112 | process.exit(1); 113 | } 114 | } else if (arg === '--close') { 115 | wrapSol = false; 116 | } else { 117 | console.error(`错误:未知参数 ${arg}。`); 118 | showUsage(); 119 | process.exit(1); 120 | } 121 | } 122 | 123 | // 检查是否提供了必要参数 124 | if (amount === undefined) { 125 | console.error('错误:必须提供 amount 参数。'); 126 | showUsage(); 127 | process.exit(1); 128 | } else { 129 | // console.log(`amount: ${amount}, wrapSol: ${wrapSol}`); 130 | if (wrapSol) { 131 | await convertSolToWsol(amount); 132 | } else { 133 | await convertWsolToSol(); 134 | } 135 | } 136 | 137 | // 使用提示函数 138 | function showUsage() { 139 | console.log('将sol转换为wsol\n使用方法: node warp_sol.js --amount= [--close]'); 140 | console.log(' --amount=<数字> 要转换的数量,必须是数字。'); 141 | console.log(' --close 关闭wsol,返还所有sol。'); 142 | } 143 | })(); 144 | --------------------------------------------------------------------------------