├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── custom_onchain.js ├── jito_utils.js ├── jupgrid.js ├── server.js ├── settings.js ├── tradelog.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | package-lock.json 4 | pnpm-lock.yaml 5 | *.txt 6 | userSettings.json 7 | userPriceLayers.json 8 | .vscode/ 9 | .idea/ 10 | *.json 11 | !package.json 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Spuddya7x 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Jupgrid: Decentralized Grid Trading Bot Version 0.5.2 Beta 3 | 4 | 5 | ![GitHub last commit](https://img.shields.io/github/last-commit/ARBProtocol/jupgrid) ![GitHub issues](https://img.shields.io/github/issues/ARBProtocol/jupgrid) ![GitHub number of milestones](https://img.shields.io/github/milestones/all/ARBProtocol/jupgrid) ![GitHub stars](https://img.shields.io/github/stars/ARBProtocol/jupgrid?style=social) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/arbprotocol?style=social)](https://twitter.com/arbprotocol) 7 | 8 | JupGrid is a cutting-edge, fully decentralized cryptocurrency grid trading bot designed to operate on the Jupiter Limit Order Book. It runs locally on your machine, offering a secure and personal way to automate a grid trading bot. This bot places 1 buy and 1 sell order at a time, meaning you can be more capital-efficient! 9 | 10 | Use of this bot/script is at your own risk. Use of this bot/script can lead to loss of funds, so please exercise proper due diligence and DYOR before continuing. 11 | 12 | ## Table of Contents 13 | 14 | - [Features](#features-) 15 | - [Installation](#installation-) 16 | - [Usage](#usage-) 17 | - [Configuration](#configuration-) 18 | - [Contributing](#contributing-) 19 | - [License](#license-) 20 | 21 | ## Features ✨ 22 | 23 | - **Fully Decentralized Trading:** Operates on the Jupiter Limit Order Book, ensuring full control over your trading data and strategy. 24 | [Jupiter Limit Order Book](https://jup.ag/limit/SOL-USDC) 25 | - **Local Operation:** Runs on your own machine or a VPS, providing an additional layer of security and privacy. 26 | - **Simple Grid Strategy:** Places one buy order and one sell order based on user-defined parameters, optimizing for market conditions, whilst being capital efficient. 27 | - **Easy Setup:** Comes with a straightforward installation and setup process, including auto-creation of necessary user files. 28 | - **User Prompted Parameters:** Dynamically prompts the user for trading parameters, allowing for flexible and responsive trading setups. 29 | 30 | ## Installation 🔧 31 | 32 | Download the source code by cloning it: 33 | 34 | ```bash 35 | git clone https://github.com/ARBProtocol/jupgrid 36 | npm install 37 | ``` 38 | 39 | ## Usage 🚀 40 | 41 | 1. **Initial Setup:** Run Jupgrid for the first time to create the necessary user configuration files: 42 | 43 | ```bash 44 | node . 45 | ``` 46 | 47 | This will generate a `.env` file where you will fill in your secure data. 48 | 49 | 2. **Configuration:** Open the `.env` file in a text editor and input your Phantom wallet Private Key, and the URL to your RPC. 50 | 51 | 3. **Encryption:** Start Jupgrid with `node .` again. This time you will be prompted to enter a password to locally encrypt your private key and RPC connection. 52 | 53 | 4. **Start JupGrid!** Start JupGrid a 3rd time with `node .` and this time you will be prompted to enter the password you entered previously. You will then be show the start-up prompts, which allow you to modify the following parameters: 54 | 55 | - Token A: 56 | - Token B: 57 | - Infinity Target Value: (Maximum $ value of Token B you want to hold - Dont set this higher than your TokenA+B value!) 58 | - Spread (% difference from current market price to orders): 59 | - Stop Loss ($ value for BOTH Token A and Token B - If your wallet hits this value, the script will stop for safety) 60 | - Delay (This is used to stop you getting rate-limited by Jupiter API. JupGrid is a "slow" bot, and thus doesnt need information every second). 61 | 62 | Jupgrid will then place one buy and one sell order based on the parameters you have set. 63 | 64 | ## Configuration ⚙️ 65 | 66 | The `.env` file will need to contain your Phantom Wallet Private Key and URL to your RPC connection. Ensure you fill it out before running the bot for the second time: 67 | 68 | - `RPC_URL`=YourRPCURLHere 69 | - `PRIVATE_KEY`=YourPrivateKeyHere 70 | 71 | Once these are encrypted, they are no longer human-readable. Please ensure you have other copies of this information saved elsewhere. 72 | 73 | There will also be a `userSettings.json` file created. This will contain data on the parameters you set during setup. 74 | 75 | ## Contributing 🤝 76 | 77 | We welcome contributions from everyone! To contribute: 78 | 79 | 1. Fork the project 80 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 81 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 82 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 83 | 5. Create a new Pull Request 84 | 6. ❤️ 85 | 86 | #### Follow us on Twitter: [@arbsolana](https://twitter.com/arbprotocol) for more updates. 87 | 88 | ## License 📄 89 | 90 | This project is licensed under the MIT License - see the `LICENSE` file for details. 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jup-grid", 3 | "version": "0.5.2", 4 | "description": "JupGrid", 5 | "type": "module", 6 | "main": "src/jupgrid.js", 7 | "author": { 8 | "name": "SpuddyA7X", 9 | "handle": "@SpuddyA7X" 10 | }, 11 | "devDependencies": { 12 | "standard": "^17.1.0" 13 | }, 14 | "scripts": { 15 | "fmt": "standard --fix", 16 | "lint": "standard" 17 | }, 18 | "dependencies": { 19 | "@jup-ag/api": "^6.0.23", 20 | "@jup-ag/core": "4.0.0-beta.21", 21 | "@jup-ag/limit-order-sdk": "^0.1.10", 22 | "@project-serum/anchor": "^0.26.0", 23 | "@solana/spl-token": "^0.4.6", 24 | "@solana/web3.js": "^1.92.3", 25 | "axios": "^1.7.2", 26 | "asciichart": "^1.5.25", 27 | "bs58": "^5.0.0", 28 | "chalk": "^5.3.0", 29 | "cross-fetch": "^4.0.0", 30 | "crypto": "^1.0.1", 31 | "dotenv": "^16.4.5", 32 | "express": "^4.19.2", 33 | "ora": "^8.0.1", 34 | "prompt-sync": "^4.2.0", 35 | "readline": "^1.3.0", 36 | "winston": "^3.13.0", 37 | "ws": "^8.17.0" 38 | } 39 | } -------------------------------------------------------------------------------- /src/custom_onchain.js: -------------------------------------------------------------------------------- 1 | import { PublicKey, TransactionInstruction } from "@solana/web3.js"; 2 | 3 | const PROGRAM_ID = new PublicKey( 4 | "HARBRqBp3GL6BzN5CoSFnKVQMpGah4mkBCDFLxigGARB" 5 | ); 6 | 7 | export function arbgate(accounts) { 8 | const keys = [ 9 | { pubkey: accounts.signer, isSigner: false, isWritable: true }, 10 | { pubkey: accounts.toCheck, isSigner: false, isWritable: false } 11 | ]; 12 | const identifier = Buffer.from([230, 144, 187, 66, 156, 221, 77, 41]); 13 | const data = identifier; 14 | const ix = new TransactionInstruction({ keys, PROGRAM_ID, data }); 15 | return ix; 16 | } 17 | -------------------------------------------------------------------------------- /src/jito_utils.js: -------------------------------------------------------------------------------- 1 | //This file handles the control of JITO Bundles. Wrapping, getting tip and managing TXs 2 | import bs58 from 'bs58'; 3 | import Websocket from 'ws'; 4 | import ora from 'ora'; 5 | import { 6 | Keypair, 7 | PublicKey, 8 | SystemProgram, 9 | TransactionMessage, 10 | VersionedTransaction 11 | } from '@solana/web3.js'; 12 | 13 | import { 14 | checkOpenOrders, 15 | cancelOrder, 16 | createTx, 17 | balanceCheck, 18 | getBalance, 19 | selectedAddressA, 20 | selectedAddressB, 21 | selectedTokenA, 22 | selectedTokenB, 23 | payer, 24 | connection, 25 | infinityBuyInputLamports, 26 | infinityBuyOutputLamports, 27 | infinitySellInputLamports, 28 | infinitySellOutputLamports, 29 | checkArray, 30 | maxJitoTip 31 | } from './jupgrid.js'; 32 | 33 | import { delay } from './utils.js'; 34 | 35 | export { 36 | encodeTransactionToBase58, 37 | jitoTipCheck, 38 | jitoController, 39 | jitoCancelOrder, 40 | jitoSetInfinity, 41 | jitoRebalance, 42 | handleJitoBundle, 43 | sendJitoBundle 44 | }; 45 | 46 | const [MIN_WAIT, MAX_WAIT] = [5e2, 5e3]; 47 | const JitoBlockEngine = "https://mainnet.block-engine.jito.wtf/api/v1/bundles"; 48 | const TIP_ACCOUNTS = [ 49 | "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", 50 | "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", 51 | "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", 52 | "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", 53 | "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", 54 | "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", 55 | "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", 56 | "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT" 57 | ]; 58 | const getRandomTipAccount = () => 59 | TIP_ACCOUNTS[Math.floor(Math.random() * TIP_ACCOUNTS.length)]; 60 | 61 | let { 62 | jitoRetry = 0, 63 | } = {}; 64 | 65 | function encodeTransactionToBase58(transaction) { 66 | // Function to encode a transaction to base58 67 | const encodedTransaction = bs58.encode(transaction.serialize()); 68 | return encodedTransaction; 69 | } 70 | 71 | async function jitoTipCheck() { 72 | const JitoTipWS = 'ws://bundles-api-rest.jito.wtf/api/v1/bundles/tip_stream'; 73 | const tipws = new Websocket(JitoTipWS); 74 | let resolveMessagePromise; 75 | let rejectMessagePromise; 76 | 77 | // Create a promise that resolves with the first message received 78 | const messagePromise = new Promise((resolve, reject) => { 79 | resolveMessagePromise = resolve; 80 | rejectMessagePromise = reject; 81 | }); 82 | 83 | // Open WebSocket connection 84 | tipws.on('open', function open() { 85 | }); 86 | 87 | // Handle messages 88 | tipws.on('message', function incoming(data) { 89 | var enc = new TextDecoder("utf-8"); 90 | const str = enc.decode(data); // Convert Buffer to string 91 | 92 | try { 93 | const json = JSON.parse(str); // Parse string to JSON 94 | const emaPercentile50th = json[0].ema_landed_tips_50th_percentile; // Access the 50th percentile property 95 | console.log(`50th Percentile: ${emaPercentile50th.toFixed(9)}`); 96 | if (emaPercentile50th !== null) { 97 | resolveMessagePromise(emaPercentile50th); 98 | } else { 99 | rejectMessagePromise(new Error('50th percentile is null')); 100 | } 101 | } catch (err) { 102 | rejectMessagePromise(err); 103 | } 104 | }); 105 | 106 | // Handle errors 107 | tipws.on('error', function error(err) { 108 | console.error('WebSocket error:', err); 109 | rejectMessagePromise(err); 110 | }); 111 | 112 | try { 113 | // Wait for the first message or a timeout 114 | const emaPercentile50th = await Promise.race([ 115 | messagePromise, 116 | new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 21000)) 117 | ]); 118 | 119 | tipws.close(); // Close WebSocket connection 120 | return emaPercentile50th; 121 | } catch (err) { 122 | console.error(err); 123 | tipws.close(); // Close WebSocket connection 124 | return 0.00005; // Return a default of 0.00005 if the request fails 125 | } 126 | } 127 | 128 | async function jitoController(task) { 129 | let result = "unknown"; 130 | // Initial operation 131 | switch (task) { 132 | case "cancel": 133 | result = await jitoCancelOrder(task); 134 | break; 135 | case "infinity": 136 | result = await jitoSetInfinity(task); 137 | break; 138 | case "rebalance": 139 | result = await jitoRebalance(task); 140 | break; 141 | default: 142 | // unintended code 143 | console.log("Unknown Error state. Exiting..."); 144 | process.exit(0); 145 | } 146 | jitoRetry = 1; 147 | // Retry loop 148 | while (jitoRetry < 20) { 149 | switch (result) { 150 | case "succeed": 151 | console.log("Operation Succeeded\n"); 152 | 153 | jitoRetry = 21; 154 | break; 155 | case "cancelFail": 156 | console.log("Retrying Cancel Orders..."); 157 | jitoRetry++; 158 | result = await jitoCancelOrder(task); 159 | break; 160 | case "infinityFail": 161 | console.log("Retrying Infinity Orders..."); 162 | jitoRetry++; 163 | result = await jitoSetInfinity(task); 164 | break; 165 | case "rebalanceFail": 166 | console.log("Retrying Rebalance Orders..."); 167 | jitoRetry++; 168 | result = await jitoRebalance(task); 169 | break; 170 | default: 171 | console.log("Unknown Error state. Exiting..."); 172 | process.exit(0); 173 | } 174 | } 175 | } 176 | 177 | async function jitoCancelOrder(task) { 178 | await checkOpenOrders(); 179 | if (checkArray.length === 0) { 180 | console.log("No orders found to cancel."); 181 | return "succeed"; 182 | } else { 183 | console.log("Cancelling Orders"); 184 | const transaction1 = await cancelOrder(checkArray, payer); 185 | if (transaction1 === "skip") { 186 | console.log("Skipping Cancel..."); 187 | return "succeed"; 188 | } 189 | const result = await handleJitoBundle(task, transaction1); 190 | return result; 191 | } 192 | } 193 | 194 | async function jitoSetInfinity(task) { 195 | // cancel any existing, place 2 new 196 | const base1 = Keypair.generate(); 197 | const base2 = Keypair.generate(); 198 | 199 | await checkOpenOrders(); 200 | 201 | if (checkArray.length === 0) { 202 | console.log("No orders found to cancel."); 203 | const order1 = await createTx( 204 | infinityBuyInputLamports, 205 | infinityBuyOutputLamports, 206 | selectedAddressA, 207 | selectedAddressB, 208 | base1 209 | ); 210 | const order2 = await createTx( 211 | infinitySellInputLamports, 212 | infinitySellOutputLamports, 213 | selectedAddressB, 214 | selectedAddressA, 215 | base2 216 | ); 217 | const transaction1 = order1.transaction; 218 | const transaction2 = order2.transaction; 219 | const transactions = [transaction1, transaction2]; 220 | const result = await handleJitoBundle(task, ...transactions); 221 | return result; 222 | } else { 223 | console.log("Found Orders to Cancel"); 224 | //Triple check for open orders 225 | await checkOpenOrders(); 226 | const transaction1 = await cancelOrder(checkArray, payer); 227 | const order1 = await createTx( 228 | infinityBuyInputLamports, 229 | infinityBuyOutputLamports, 230 | selectedAddressA, 231 | selectedAddressB, 232 | base1 233 | ); 234 | const order2 = await createTx( 235 | infinitySellInputLamports, 236 | infinitySellOutputLamports, 237 | selectedAddressB, 238 | selectedAddressA, 239 | base2 240 | ); 241 | const transaction2 = order1.transaction; 242 | const transaction3 = order2.transaction; 243 | const transactions = [transaction1, transaction2, transaction3]; 244 | const result = await handleJitoBundle(task, ...transactions); 245 | return result; 246 | } 247 | } 248 | 249 | async function jitoRebalance(task) { 250 | const transaction1 = await balanceCheck(); 251 | if (transaction1 === "skip") { 252 | console.log("Skipping Rebalance..."); 253 | return "succeed"; 254 | } 255 | const result = await handleJitoBundle(task, transaction1); 256 | return result; 257 | } 258 | 259 | async function handleJitoBundle(task, ...transactions) { 260 | let tipValueInSol; 261 | try { 262 | tipValueInSol = await jitoTipCheck(); 263 | } catch (err) { 264 | console.error(err); 265 | tipValueInSol = 0.00005; // Replace 0 with your default value 266 | } 267 | if (tipValueInSol > maxJitoTip) { 268 | tipValueInSol = maxJitoTip; 269 | } 270 | const tipValueInLamports = tipValueInSol * 1_000_000_000; 271 | const roundedTipValueInLamports = Math.round(tipValueInLamports); 272 | 273 | // Limit to 9 digits 274 | const limitedTipValueInLamports = Math.floor( 275 | Number(roundedTipValueInLamports.toFixed(9)) * 1.1 //+10% of tip to edge out competition 276 | ); 277 | try { 278 | const tipAccount = new PublicKey(getRandomTipAccount()); 279 | const instructionsSub = []; 280 | const tipIxn = SystemProgram.transfer({ 281 | fromPubkey: payer.publicKey, 282 | toPubkey: tipAccount, 283 | lamports: limitedTipValueInLamports 284 | }); 285 | // console.log("Tries: ",retries); 286 | console.log( 287 | `Jito Fee: ${limitedTipValueInLamports / Math.pow(10, 9)} SOL` 288 | ); 289 | instructionsSub.push(tipIxn); 290 | const resp = await connection.getLatestBlockhash("confirmed"); 291 | 292 | const messageSub = new TransactionMessage({ 293 | payerKey: payer.publicKey, 294 | recentBlockhash: resp.blockhash, 295 | instructions: instructionsSub 296 | }).compileToV0Message(); 297 | 298 | const txSub = new VersionedTransaction(messageSub); 299 | txSub.sign([payer]); 300 | const bundletoSend = [...transactions, txSub]; 301 | 302 | // Ensure that bundletoSend is not empty 303 | if (bundletoSend.length === 0) { 304 | throw new Error("Bundle is empty."); 305 | } 306 | 307 | // Call sendJitoBundle with the correct bundleToSend 308 | const result = await sendJitoBundle(task, bundletoSend); 309 | return result; 310 | } catch (error) { 311 | console.error("\nBundle Construction Error: ", error); 312 | } 313 | } 314 | 315 | async function sendJitoBundle(task, bundletoSend) { 316 | const encodedBundle = bundletoSend.map(encodeTransactionToBase58); 317 | 318 | const { balanceA: preJitoA, balanceB: preJitoB } = await getBalance( 319 | payer, 320 | selectedAddressA, 321 | selectedAddressB, 322 | selectedTokenA, 323 | selectedTokenB 324 | ); 325 | await checkOpenOrders(); 326 | const preBundleOrders = checkArray; 327 | 328 | const data = { 329 | jsonrpc: "2.0", 330 | id: 1, 331 | method: "sendBundle", 332 | params: [encodedBundle] 333 | }; 334 | 335 | let response; 336 | const maxRetries = 5; 337 | for (let i = 0; i <= maxRetries; i++) { 338 | try { 339 | response = await fetch(JitoBlockEngine, { 340 | method: "POST", 341 | headers: { 342 | "Content-Type": "application/json" 343 | }, 344 | body: JSON.stringify(data) 345 | }); 346 | 347 | if (response.ok) break; 348 | 349 | if (response.status === 429) { 350 | const waitTime = Math.min(MIN_WAIT * Math.pow(2, i), MAX_WAIT); 351 | const jitter = Math.random() * 0.3 * waitTime; 352 | await new Promise((resolve) => 353 | setTimeout(resolve, waitTime + jitter) 354 | ); 355 | } else { 356 | throw new Error("Unexpected error"); 357 | } 358 | } catch (error) { 359 | if (i === maxRetries) { 360 | console.error("Max retries exceeded"); 361 | program.exit(0); 362 | } 363 | } 364 | } 365 | const responseText = await response.text(); 366 | const responseData = JSON.parse(responseText); 367 | 368 | const result = responseData.result; 369 | const url = `https://explorer.jito.wtf/bundle/${result}`; 370 | console.log(`\nResult ID: ${url}`); 371 | 372 | console.log("Checking for 30 seconds..."); 373 | let jitoChecks = 1; 374 | const maxChecks = 30; 375 | let spinner; 376 | let bundleLanded = false; 377 | while (jitoChecks <= maxChecks) { 378 | spinner = ora( 379 | `Checking Jito Bundle Status... ${jitoChecks}/${maxChecks}` 380 | ).start(); 381 | console.log("\nTask: ", task); 382 | try { 383 | // Wait 1 second before each balance check to avoid error 429 384 | await delay(1000); // Adding delay here 385 | const { balanceA: postJitoA, balanceB: postJitoB } = await getBalance( 386 | payer, 387 | selectedAddressA, 388 | selectedAddressB, 389 | selectedTokenA, 390 | selectedTokenB 391 | ); 392 | if (postJitoA !== preJitoA || postJitoB !== preJitoB) { 393 | bundleLanded = true; 394 | spinner.stop(); 395 | console.log( 396 | "\nBundle Landed, waiting for orders to finalize..." 397 | ); 398 | if (task !== "rebalance") { 399 | let bundleChecks = 1; 400 | while (bundleChecks <= 30) { 401 | let postBundleOrders; 402 | await checkOpenOrders(); 403 | postBundleOrders = checkArray; 404 | if (postBundleOrders !== preBundleOrders) { 405 | console.log( 406 | "\nBundle Landed, Orders Updated, Skipping Timer" 407 | ); 408 | await delay(1000); 409 | jitoChecks = 31; 410 | break; 411 | } else { 412 | console.log( 413 | `Checking Orders for ${bundleChecks} of 30 seconds` 414 | ); 415 | await delay(1000); 416 | bundleChecks++; 417 | } 418 | } 419 | } 420 | jitoChecks = 31; 421 | break; 422 | } 423 | jitoChecks++; 424 | } catch (error) { 425 | console.error("Error in balance check:", error); 426 | } 427 | spinner.stop(); 428 | } 429 | 430 | if (spinner) { 431 | spinner.stop(); 432 | } 433 | console.log("Waiting for 5 seconds - This is for testing...") 434 | await delay(5000); 435 | await checkOpenOrders(); 436 | switch (task) { 437 | case "cancel": 438 | if (checkArray.length > 0) { 439 | console.log("Cancelling Orders Failed, Retrying..."); 440 | return "cancelFail"; 441 | } else { 442 | console.log("Orders Cancelled Successfully"); 443 | return "succeed"; 444 | } 445 | case "infinity": 446 | if (checkArray.length !== 2) { 447 | console.log("Placing Infinity Orders Failed, Retrying..."); 448 | return "infinityFail"; 449 | } else { 450 | console.log("Infinity Orders Placed Successfully"); 451 | return "succeed"; 452 | } 453 | case "rebalance": 454 | if (bundleLanded) { 455 | console.log("Rebalancing Tokens Successful"); 456 | return "succeed"; 457 | } else { 458 | console.log("Rebalancing Tokens Failed, Retrying..."); 459 | return "rebalanceFail"; 460 | } 461 | default: 462 | console.log("Unknown state, retrying..."); 463 | return "unknown"; 464 | } 465 | } -------------------------------------------------------------------------------- /src/jupgrid.js: -------------------------------------------------------------------------------- 1 | // #region imports 2 | import axios from 'axios'; 3 | import chalk from 'chalk'; 4 | import fetch from 'cross-fetch'; 5 | import * as fs from 'fs'; 6 | 7 | import { 8 | LimitOrderProvider, 9 | ownerFilter 10 | } from '@jup-ag/limit-order-sdk'; 11 | import * as solanaWeb3 from '@solana/web3.js'; 12 | import { 13 | Connection, 14 | VersionedTransaction 15 | } from '@solana/web3.js'; 16 | 17 | import { 18 | envload, 19 | loaduserSettings, 20 | saveuserSettings 21 | } from './settings.js'; 22 | import { 23 | delay, 24 | downloadTokensList, 25 | getTokenAccounts, 26 | getTokens, 27 | questionAsync, 28 | rl 29 | } from './utils.js'; 30 | import { 31 | jitoController, 32 | } from './jito_utils.js'; 33 | import asciichart from 'asciichart' 34 | // #endregion 35 | 36 | // #region exports 37 | export { 38 | initialize, 39 | checkOpenOrders, 40 | cancelOrder, 41 | createTx, 42 | balanceCheck, 43 | getBalance, 44 | connection, 45 | payer, 46 | selectedAddressA, 47 | selectedAddressB, 48 | selectedTokenA, 49 | selectedTokenB, 50 | infinityBuyInputLamports, 51 | infinityBuyOutputLamports, 52 | infinitySellInputLamports, 53 | infinitySellOutputLamports, 54 | checkArray, 55 | maxJitoTip 56 | }; 57 | // #endregion 58 | 59 | // #region constants 60 | // use fs to to read version from package.json 61 | const packageInfo = JSON.parse(fs.readFileSync("package.json", "utf8")); 62 | 63 | let currentVersion = packageInfo.version; 64 | let configVersion = currentVersion; 65 | 66 | const [payer, rpcUrl] = envload(); 67 | 68 | const connection = new Connection(rpcUrl, "processed", { 69 | confirmTransactionInitialTimeout: 5000 70 | }); 71 | const limitOrder = new LimitOrderProvider(connection); 72 | 73 | let shutDown = false; 74 | 75 | const walletAddress = payer.publicKey.toString(); 76 | const displayAddress = `${walletAddress.slice(0, 4)}...${walletAddress.slice(-4)}`; 77 | 78 | const quoteurl = "https://quote-api.jup.ag/v6/quote"; 79 | 80 | 81 | const USDC_MINT_ADDRESS = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; 82 | const SOL_MINT_ADDRESS = "So11111111111111111111111111111111111111112"; 83 | // #endregion 84 | 85 | 86 | // #region properties 87 | let { 88 | validTokenA = null, 89 | validTokenB = null, 90 | selectedTokenA = null, 91 | selectedTokenB = null, 92 | selectedAddressA = null, 93 | selectedAddressB = null, 94 | selectedDecimalsA = null, 95 | selectedDecimalsB = null, 96 | validSpread = null, 97 | stopLossUSD = null, 98 | infinityTarget = null, 99 | loaded = false, 100 | openOrders = [], 101 | checkArray = [], 102 | tokens = [], 103 | newPrice = null, 104 | startPrice = null, 105 | spread = null, 106 | spreadbps = null, 107 | initBalanceA = 0, 108 | initUsdBalanceA = 0, 109 | initBalanceB = 0, 110 | initUsdBalanceB = 0, 111 | currBalanceA = 0, 112 | currBalanceB = 0, 113 | currUSDBalanceA = 0, 114 | currUSDBalanceB = 0, 115 | initUsdTotalBalance = 0, 116 | currUsdTotalBalance = 0, 117 | tokenRebalanceValue = null, 118 | tokenARebalanceValue = 0, 119 | tokenBRebalanceValue = 0, 120 | startTime = new Date(), 121 | monitorDelay = null, 122 | adjustmentA = 0, 123 | adjustmentB = 0, 124 | stopLoss = false, 125 | maxJitoTip = null, 126 | infinityBuyInputLamports, 127 | infinityBuyOutputLamports, 128 | infinitySellInputLamports, 129 | infinitySellOutputLamports, 130 | counter = 0, 131 | askForRebalance = true, 132 | rebalanceCounter = 0, 133 | newPriceBUp = null, 134 | newPriceBDown = null, 135 | lastKnownPrice = null, 136 | currentTracker = null, 137 | sellPrice = null, 138 | buyPrice = null, 139 | iteration = 0, 140 | userSettings = { 141 | selectedTokenA: null, 142 | selectedTokenB: null, 143 | tradeSize: null, 144 | spread: null, 145 | rebalanceAllowed: null, 146 | rebalancePercentage: null, 147 | rebalanceSlippageBPS: null, 148 | monitorDelay: null, 149 | stopLossUSD: null, 150 | infinityTarget: null, 151 | infinityMode: null 152 | } 153 | } = {}; 154 | // #endregion 155 | 156 | //Util Functions 157 | function formatElapsedTime(startTime) { 158 | const currentTime = new Date(); 159 | const elapsedTime = currentTime - startTime; // Difference in milliseconds 160 | 161 | let totalSeconds = Math.floor(elapsedTime / 1000); 162 | let hours = Math.floor(totalSeconds / 3600); 163 | totalSeconds %= 3600; 164 | let minutes = Math.floor(totalSeconds / 60); 165 | let seconds = totalSeconds % 60; 166 | 167 | // Padding with '0' if necessary 168 | hours = String(hours).padStart(2, "0"); 169 | minutes = String(minutes).padStart(2, "0"); 170 | seconds = String(seconds).padStart(2, "0"); 171 | 172 | console.log(`\u{23F1} Run time: ${hours}:${minutes}:${seconds}`); 173 | } 174 | 175 | async function fetchPrice(tokenAddress) { 176 | const response = await axios.get(`https://price.jup.ag/v6/price?ids=${tokenAddress}`); 177 | const price = response.data.data[tokenAddress].price; 178 | return parseFloat(price); 179 | } 180 | 181 | async function updateUSDVal(mintAddress, balance, decimals) { 182 | try { 183 | let price = await fetchPrice(mintAddress); 184 | let balanceLamports = Math.floor(balance * Math.pow(10, decimals)); 185 | const usdBalance = balanceLamports * price; 186 | const usdBalanceLamports =usdBalance / Math.pow(10, decimals); 187 | return usdBalanceLamports; 188 | } catch (error) { 189 | // Error is not critical. 190 | // Reuse the previous balances and try another update again next cycle. 191 | } 192 | } 193 | 194 | async function fetchNewUSDValues() { 195 | const tempUSDBalanceA = await updateUSDVal( 196 | selectedAddressA, 197 | currBalanceA, 198 | selectedDecimalsA 199 | ); 200 | const tempUSDBalanceB = await updateUSDVal( 201 | selectedAddressB, 202 | currBalanceB, 203 | selectedDecimalsB 204 | ); 205 | 206 | return { 207 | newUSDBalanceA: tempUSDBalanceA ?? currUSDBalanceA, 208 | newUSDBalanceB: tempUSDBalanceB ?? currUSDBalanceB, 209 | }; 210 | } 211 | 212 | function calculateProfitOrLoss(currUsdTotalBalance, initUsdTotalBalance) { 213 | const profitOrLoss = currUsdTotalBalance - initUsdTotalBalance; 214 | const percentageChange = (profitOrLoss / initUsdTotalBalance) * 100; 215 | return { profitOrLoss, percentageChange }; 216 | } 217 | 218 | function displayProfitOrLoss(profitOrLoss, percentageChange) { 219 | if (profitOrLoss > 0) { 220 | console.log( 221 | `Profit : ${chalk.green(`+$${profitOrLoss.toFixed(2)} (+${percentageChange.toFixed(2)}%)`)}` 222 | ); 223 | } else if (profitOrLoss < 0) { 224 | console.log( 225 | `Loss : ${chalk.red(`-$${Math.abs(profitOrLoss).toFixed(2)} (-${Math.abs(percentageChange).toFixed(2)}%)`)}` 226 | ); 227 | } else { 228 | console.log(`Difference : $${profitOrLoss.toFixed(2)} (0.00%)`); // Neutral 229 | } 230 | } 231 | 232 | async function updatePrice() { 233 | let retries = 0; 234 | const maxRetries = 5; 235 | while (retries < maxRetries) { 236 | try { 237 | let newPrice = await fetchPrice(selectedAddressB); 238 | if(newPrice !== undefined) { 239 | lastKnownPrice = newPrice; 240 | return newPrice; 241 | } 242 | } catch (error) { 243 | console.error(`Fetch price failed. Attempt ${retries + 1} of ${maxRetries}`); 244 | } 245 | retries++; 246 | } 247 | 248 | if(lastKnownPrice !== null) { 249 | return lastKnownPrice; 250 | } else { 251 | throw new Error("Unable to fetch price and no last known price available"); 252 | } 253 | } 254 | 255 | async function formatTokenPrice(price) { 256 | let multiplier = 1; 257 | let quantity = ""; 258 | 259 | if (price >= 1) { 260 | // For prices above $1, no adjustment needed 261 | return { multiplier, quantity }; 262 | } else { 263 | // Adjust for prices below $1 264 | if (price <= 0.00000001) { 265 | multiplier = 100000000; 266 | quantity = "per 100,000,000"; 267 | } else if (price <= 0.0000001) { 268 | multiplier = 10000000; 269 | quantity = "per 10,000,000"; 270 | } else if (price <= 0.000001) { 271 | multiplier = 1000000; 272 | quantity = "per 1,000,000"; 273 | } else if (price <= 0.00001) { 274 | multiplier = 100000; 275 | quantity = "per 100,000"; 276 | } else if (price <= 0.0001) { 277 | multiplier = 10000; 278 | quantity = "per 10,000"; 279 | } else if (price <= 0.001) { 280 | multiplier = 1000; 281 | quantity = "per 1,000"; 282 | } else if (price <= 0.99) { 283 | multiplier = 100; 284 | quantity = "per 100"; 285 | } else if (price >= 1) { 286 | multiplier = 1; // No change needed, but included for clarity 287 | quantity = ""; // No additional quantity description needed 288 | } 289 | return { multiplier, quantity }; 290 | } 291 | } 292 | 293 | async function getBalance( 294 | payer, 295 | selectedAddressA, 296 | selectedAddressB, 297 | selectedTokenA, 298 | selectedTokenB 299 | ) { 300 | async function getSOLBalanceAndUSDC() { 301 | const lamports = await connection.getBalance(payer.publicKey); 302 | const solBalance = lamports / solanaWeb3.LAMPORTS_PER_SOL; 303 | if (solBalance === 0) { 304 | console.log(`You do not have any SOL, please check and try again.`); 305 | process.exit(0); 306 | } 307 | let usdBalance = 0; 308 | if (selectedTokenA === "SOL" || selectedTokenB === "SOL") { 309 | try { 310 | const queryParams = { 311 | inputMint: SOL_MINT_ADDRESS, 312 | outputMint: USDC_MINT_ADDRESS, 313 | amount: lamports, // Amount in lamports 314 | slippageBps: 0 315 | }; 316 | const response = await axios.get(quoteurl, { 317 | params: queryParams 318 | }); 319 | usdBalance = response.data.outAmount / Math.pow(10, 6) || 0; 320 | tokenRebalanceValue = 321 | response.data.outAmount / (lamports / Math.pow(10, 3)); 322 | } catch (error) { 323 | console.error("Error fetching USDC equivalent for SOL:", error); 324 | } 325 | } 326 | return { balance: solBalance, usdBalance, tokenRebalanceValue }; 327 | } 328 | 329 | async function getTokenAndUSDCBalance(mintAddress, decimals) { 330 | if ( 331 | !mintAddress || 332 | mintAddress === "So11111111111111111111111111111111111111112" 333 | ) { 334 | return getSOLBalanceAndUSDC(); 335 | } 336 | 337 | const tokenAccounts = await getTokenAccounts( 338 | connection, 339 | payer.publicKey, 340 | mintAddress 341 | ); 342 | if (tokenAccounts.value.length > 0) { 343 | const balance = 344 | tokenAccounts.value[0].account.data.parsed.info.tokenAmount 345 | .uiAmount; 346 | let usdBalance = 0; 347 | if (balance === 0) { 348 | console.log( 349 | `You do not have a balance for ${mintAddress}, please check and try again.` 350 | ); 351 | process.exit(0); 352 | } 353 | if (mintAddress !== USDC_MINT_ADDRESS) { 354 | const queryParams = { 355 | inputMint: mintAddress, 356 | outputMint: USDC_MINT_ADDRESS, 357 | amount: Math.floor(balance * Math.pow(10, decimals)), 358 | slippageBps: 0 359 | }; 360 | 361 | try { 362 | const response = await axios.get(quoteurl, { 363 | params: queryParams 364 | }); 365 | // Save USD Balance and adjust down for Lamports 366 | usdBalance = response.data.outAmount / Math.pow(10, 6); 367 | tokenRebalanceValue = 368 | response.data.outAmount / (balance * Math.pow(10, 6)); 369 | } catch (error) { 370 | console.error("Error fetching USDC equivalent:", error); 371 | usdBalance = 1; 372 | } 373 | } else { 374 | usdBalance = balance; // If the token is USDC, its balance is its USD equivalent 375 | if (usdBalance === 0) { 376 | console.log( 377 | `You do not have any USDC, please check and try again.` 378 | ); 379 | process.exit(0); 380 | } 381 | tokenRebalanceValue = 1; 382 | } 383 | 384 | return { balance, usdBalance, tokenRebalanceValue }; 385 | } else { 386 | return { balance: 0, usdBalance: 0, tokenRebalanceValue: null }; 387 | } 388 | } 389 | 390 | const resultA = await getTokenAndUSDCBalance( 391 | selectedAddressA, 392 | selectedDecimalsA 393 | ); 394 | const resultB = await getTokenAndUSDCBalance( 395 | selectedAddressB, 396 | selectedDecimalsB 397 | ); 398 | 399 | if (resultA.balance === 0 || resultB.balance === 0) { 400 | console.log( 401 | "Please ensure you have a balance in both tokens to continue." 402 | ); 403 | process.exit(0); 404 | } 405 | 406 | return { 407 | balanceA: resultA.balance, 408 | usdBalanceA: resultA.usdBalance, 409 | tokenARebalanceValue: resultA.tokenRebalanceValue, 410 | balanceB: resultB.balance, 411 | usdBalanceB: resultB.usdBalance, 412 | tokenBRebalanceValue: resultB.tokenRebalanceValue 413 | }; 414 | } 415 | 416 | //Initialize functions 417 | async function loadQuestion() { 418 | try { 419 | await downloadTokensList(); 420 | console.log("Updated Token List\n"); 421 | console.log(`Connected Wallet: ${displayAddress}\n`); 422 | 423 | if (!fs.existsSync("userSettings.json")) { 424 | console.log("No user data found. Starting with fresh inputs."); 425 | initialize(); 426 | } else { 427 | const askForLoadSettings = () => { 428 | rl.question( 429 | "Do you wish to load your saved settings? (Y/N): ", 430 | function (responseQ) { 431 | responseQ = responseQ.toUpperCase(); // Case insensitivity 432 | 433 | if (responseQ === "Y") { 434 | try { 435 | // Show user data 436 | const userSettings = loaduserSettings(); 437 | // Check if the saved version matches the current version 438 | if (userSettings.configVersion !== currentVersion) { 439 | console.log(`Version mismatch detected. Your settings version: ${userSettings.configVersion}, current version: ${currentVersion}.`); 440 | // Here you can choose to automatically initialize with fresh settings 441 | // or prompt the user for an action (e.g., update settings, discard, etc.) 442 | console.log("Changing to blank settings, please continue.\n"); 443 | initialize(); // Example action: re-initialize with fresh settings 444 | return; 445 | } 446 | console.log("User data loaded successfully."); 447 | console.log( 448 | `\nPrevious JupGrid Settings: 449 | Version: ${userSettings.configVersion} 450 | Token A: ${chalk.cyan(userSettings.selectedTokenA)} 451 | Token B: ${chalk.magenta(userSettings.selectedTokenB)} 452 | Token B Target Value: ${userSettings.infinityTarget} 453 | Spread: ${userSettings.spread}% 454 | Stop Loss: ${userSettings.stopLossUSD} 455 | Maximum Jito Tip: ${userSettings.maxJitoTip} SOL 456 | Monitoring delay: ${userSettings.monitorDelay}ms\n` 457 | ); 458 | // Prompt for confirmation to use these settings 459 | rl.question( 460 | "Proceed with these settings? (Y/N): ", 461 | function (confirmResponse) { 462 | confirmResponse = 463 | confirmResponse.toUpperCase(); 464 | if (confirmResponse === "Y") { 465 | // Apply loaded settings 466 | ({ 467 | currentVersion, 468 | selectedTokenA, 469 | selectedAddressA, 470 | selectedDecimalsA, 471 | selectedTokenB, 472 | selectedAddressB, 473 | selectedDecimalsB, 474 | spread, 475 | monitorDelay, 476 | stopLossUSD, 477 | maxJitoTip, 478 | infinityTarget 479 | } = userSettings); 480 | console.log( 481 | "Settings applied successfully!" 482 | ); 483 | initialize(); 484 | } else if (confirmResponse === "N") { 485 | console.log( 486 | "Discarding saved settings, please continue." 487 | ); 488 | initialize(); // Start initialization with blank settings 489 | } else { 490 | console.log( 491 | "Invalid response. Please type 'Y' or 'N'." 492 | ); 493 | askForLoadSettings(); // Re-ask the original question 494 | } 495 | } 496 | ); 497 | } catch (error) { 498 | console.error( 499 | `Failed to load settings: ${error}` 500 | ); 501 | initialize(); // Proceed with initialization in case of error 502 | } 503 | } else if (responseQ === "N") { 504 | console.log("Starting with blank settings."); 505 | initialize(); 506 | } else { 507 | console.log( 508 | "Invalid response. Please type 'Y' or 'N'." 509 | ); 510 | askForLoadSettings(); // Re-ask if the response is not Y/N 511 | } 512 | } 513 | ); 514 | }; 515 | 516 | askForLoadSettings(); // Start the question loop 517 | } 518 | } catch (error) { 519 | console.error("Error:", error); 520 | } 521 | } 522 | 523 | async function initialize() { 524 | tokens = await getTokens(); 525 | 526 | if (selectedTokenA != null) { 527 | validTokenA = true; 528 | } 529 | 530 | if (selectedTokenB != null) { 531 | validTokenB = true; 532 | } 533 | 534 | if (spread != null) { 535 | validSpread = true; 536 | } 537 | 538 | let validMonitorDelay = false; 539 | if (monitorDelay >= 1000) { 540 | validMonitorDelay = true; 541 | } 542 | 543 | let validStopLossUSD = false; 544 | if (stopLossUSD != null) { 545 | validStopLossUSD = true; 546 | } 547 | 548 | let validJitoMaxTip = false; 549 | if (maxJitoTip != null) { 550 | validJitoMaxTip = true; 551 | } 552 | 553 | let validInfinityTarget = false; 554 | if (infinityTarget != null) { 555 | validInfinityTarget = true; 556 | } 557 | 558 | if (userSettings.selectedTokenA) { 559 | const tokenAExists = tokens.some( 560 | (token) => token.symbol === userSettings.selectedTokenA 561 | ); 562 | if (!tokenAExists) { 563 | console.log( 564 | `Token ${userSettings.selectedTokenA} from user data not found in the updated token list. Please re-enter.` 565 | ); 566 | userSettings.selectedTokenA = null; // Reset selected token A 567 | userSettings.selectedAddressA = null; // Reset selected address 568 | userSettings.selectedDecimalsA = null; // Reset selected token decimals 569 | } else { 570 | validTokenA = true; 571 | } 572 | } 573 | 574 | while (!validTokenA) { 575 | console.log("\nDuring this Beta stage, we are only allowing USDC as Token A. Is that ok?"); 576 | // Simulate the user entered 'USDC' as their answer 577 | let answer = 'USDC'; 578 | 579 | const token = tokens.find((t) => t.symbol === answer); 580 | if (token) { 581 | console.log(`Selected Token: ${token.symbol} 582 | Token Address: ${token.address} 583 | Token Decimals: ${token.decimals}`); 584 | const confirmAnswer = await questionAsync( 585 | `Is this the correct token? (Y/N): ` 586 | ); 587 | if ( 588 | confirmAnswer.toLowerCase() === "y" || 589 | confirmAnswer.toLowerCase() === "yes" 590 | ) { 591 | validTokenA = true; 592 | selectedTokenA = token.symbol; 593 | selectedAddressA = token.address; 594 | selectedDecimalsA = token.decimals; 595 | } 596 | } else { 597 | console.log(`Token ${answer} not found. Please Try Again.`); 598 | } 599 | } 600 | 601 | if (userSettings.selectedTokenB) { 602 | const tokenBExists = tokens.some( 603 | (token) => token.symbol === userSettings.selectedTokenB 604 | ); 605 | if (!tokenBExists) { 606 | console.log( 607 | `Token ${userSettings.selectedTokenB} from user data not found in the updated token list. Please re-enter.` 608 | ); 609 | userSettings.selectedTokenB = null; // Reset selected token B 610 | userSettings.selectedAddressB = null; // Reset selected address 611 | userSettings.selectedDecimalsB = null; // Reset selected token decimals 612 | } else { 613 | validTokenB = true; 614 | } 615 | } 616 | 617 | while (!validTokenB) { 618 | const answer = await questionAsync( 619 | `\nPlease Enter The Second Token Symbol (B) (Case Sensitive): ` 620 | ); 621 | const token = tokens.find((t) => t.symbol === answer); 622 | if (token) { 623 | console.log(`Selected Token: ${token.symbol} 624 | Token Address: ${token.address} 625 | Token Decimals: ${token.decimals}`); 626 | const confirmAnswer = await questionAsync( 627 | `Is this the correct token? (Y/N): ` 628 | ); 629 | if ( 630 | confirmAnswer.toLowerCase() === "y" || 631 | confirmAnswer.toLowerCase() === "yes" 632 | ) { 633 | validTokenB = true; 634 | selectedTokenB = token.symbol; 635 | selectedAddressB = token.address; 636 | selectedDecimalsB = token.decimals; 637 | } 638 | } else { 639 | console.log(`Token ${answer} not found. Please Try Again.`); 640 | } 641 | } 642 | 643 | // If infinity target value is not valid, prompt the user 644 | while (!validInfinityTarget) { 645 | const infinityTargetInput = await questionAsync( 646 | `\nPlease Enter the Token B Target Value (in USD): ` 647 | ); 648 | infinityTarget = Math.floor(parseFloat(infinityTargetInput)); 649 | if ( 650 | !isNaN(infinityTarget) && 651 | Number.isInteger(infinityTarget) && 652 | infinityTarget > userSettings.stopLossUSD 653 | ) { 654 | userSettings.infinityTarget = infinityTarget; 655 | validInfinityTarget = true; 656 | } else { 657 | console.log( 658 | "Invalid Token B Target value. Please enter a valid integer that is larger than the stop loss value." 659 | ); 660 | } 661 | } 662 | 663 | // Ask user for spread % 664 | // Check if spread percentage is valid 665 | if (userSettings.spread) { 666 | validSpread = !isNaN(parseFloat(userSettings.spread)); 667 | if (!validSpread) { 668 | console.log( 669 | "Invalid spread percentage found in user data. Please re-enter." 670 | ); 671 | userSettings.spread = null; // Reset spread percentage 672 | } else validSpread = true; 673 | } 674 | 675 | // If spread percentage is not valid, prompt the user 676 | while (!validSpread) { 677 | const spreadInput = await questionAsync( 678 | `\nWhat % Spread Difference Between Market and Orders? 679 | Recommend >0.3% to cover Jupiter Fees, but 1% or greater for best performance:` 680 | ); 681 | spread = parseFloat(spreadInput); 682 | if (!isNaN(spread)) { 683 | userSettings.spread = spread; 684 | validSpread = true; 685 | } else { 686 | console.log( 687 | "Invalid spread percentage. Please enter a valid number (No % Symbol)." 688 | ); 689 | } 690 | } 691 | 692 | if (userSettings.stopLossUSD) { 693 | validStopLossUSD = !isNaN(parseFloat(userSettings.stopLossUSD)); 694 | if (!validStopLossUSD) { 695 | console.log( 696 | "Invalid stop loss value found in user data. Please re-enter." 697 | ); 698 | userSettings.stopLossUSD = null; // Reset stop loss value 699 | } else validStopLossUSD = true; 700 | } 701 | 702 | // If stop loss value is not valid, prompt the user 703 | while (!validStopLossUSD) { 704 | const stopLossUSDInput = await questionAsync( 705 | `\nPlease Enter the Stop Loss Value in USD: 706 | (Enter 0 for no stoploss) ` 707 | ); 708 | stopLossUSD = parseFloat(stopLossUSDInput); 709 | if (!isNaN(stopLossUSD)) { 710 | userSettings.stopLossUSD = stopLossUSD; 711 | validStopLossUSD = true; 712 | } else { 713 | console.log( 714 | "Invalid stop loss value. Please enter a valid number." 715 | ); 716 | } 717 | } 718 | 719 | while (!validJitoMaxTip) { 720 | const maxJitoTipQuestion = await questionAsync( 721 | `\nEnter the maximum Jito tip in SOL 722 | This is the maximum tip you are willing to pay for a Jito order, 723 | However, we use a dynamic tip based on the last 30 minute average tip. 724 | (Default 0.0002 SOL, Minimum 0.00001): ` 725 | ); 726 | // Check if input is empty and set default value 727 | if (maxJitoTipQuestion.trim() === '') { 728 | maxJitoTip = 0.0002; 729 | validJitoMaxTip = true; 730 | } else { 731 | const parsedMaxJitoTip = parseFloat(maxJitoTipQuestion.trim()); 732 | if (!isNaN(parsedMaxJitoTip) && parsedMaxJitoTip >= 0.00001) { 733 | maxJitoTip = parsedMaxJitoTip; 734 | validJitoMaxTip = true; 735 | } else { 736 | console.log( 737 | "Invalid Jito tip. Please enter a valid number greater than or equal to 0.00001." 738 | ); 739 | } 740 | } 741 | 742 | } 743 | 744 | while (!validMonitorDelay) { 745 | const monitorDelayQuestion = await questionAsync( 746 | `\nEnter the delay between price checks in milliseconds. 747 | (minimum 100ms, recommended/default > 5000ms): ` 748 | ); 749 | // Check if input is empty and set default value 750 | if (monitorDelayQuestion.trim() === '') { 751 | monitorDelay = 5000; 752 | validMonitorDelay = true; 753 | } else { 754 | const parsedMonitorDelay = parseInt(monitorDelayQuestion.trim()); 755 | if (!isNaN(parsedMonitorDelay) && parsedMonitorDelay >= 100) { 756 | monitorDelay = parsedMonitorDelay; 757 | validMonitorDelay = true; 758 | } else { 759 | console.log( 760 | "Invalid monitor delay. Please enter a valid number greater than or equal to 1000." 761 | ); 762 | } 763 | } 764 | } 765 | 766 | spreadbps = spread * 100; 767 | //rl.close(); // Close the readline interface after question loops are done. 768 | 769 | saveuserSettings( 770 | configVersion, 771 | selectedTokenA, 772 | selectedAddressA, 773 | selectedDecimalsA, 774 | selectedTokenB, 775 | selectedAddressB, 776 | selectedDecimalsB, 777 | spread, 778 | monitorDelay, 779 | stopLossUSD, 780 | maxJitoTip, 781 | infinityTarget 782 | ); 783 | // First Price check during init 784 | console.log("Getting Latest Price Data..."); 785 | newPrice = await fetchPrice(selectedAddressB); 786 | startPrice = newPrice; 787 | 788 | console.clear(); 789 | console.log(`Starting JupGrid v${packageInfo.version}; 790 | Your Token Selection for A - Symbol: ${chalk.cyan(selectedTokenA)}, Address: ${chalk.cyan(selectedAddressA)} 791 | Your Token Selection for B - Symbol: ${chalk.magenta(selectedTokenB)}, Address: ${chalk.magenta(selectedAddressB)}`); 792 | startInfinity(); 793 | } 794 | 795 | if (loaded === false) { 796 | loadQuestion(); 797 | } 798 | 799 | //Start Functions 800 | async function startInfinity() { 801 | console.log(`Checking for existing orders to cancel...`); 802 | await jitoController("cancel"); 803 | const initialBalances = await getBalance( 804 | payer, 805 | selectedAddressA, 806 | selectedAddressB, 807 | selectedTokenA, 808 | selectedTokenB 809 | ); 810 | initBalanceA = initialBalances.balanceA; 811 | initUsdBalanceA = initialBalances.usdBalanceA; 812 | initBalanceB = initialBalances.balanceB; 813 | initUsdBalanceB = initialBalances.usdBalanceB; 814 | initUsdTotalBalance = initUsdBalanceA + initUsdBalanceB; 815 | infinityGrid(); 816 | } 817 | 818 | //Jito Functions 819 | async function infinityGrid() { 820 | if (shutDown) return; 821 | 822 | // Increment trades counter 823 | counter++; 824 | 825 | // Cancel any existing orders 826 | await jitoController("cancel"); 827 | 828 | // Check to see if we need to rebalance 829 | await jitoController("rebalance"); 830 | askForRebalance = false; 831 | 832 | // Get the current balances 833 | const { balanceA, balanceB } = await getBalance(payer, selectedAddressA, selectedAddressB, selectedTokenA, selectedTokenB); 834 | let balanceALamports = balanceA * Math.pow(10, selectedDecimalsA); 835 | let balanceBLamports = balanceB * Math.pow(10, selectedDecimalsB); 836 | 837 | // Get the current market price 838 | const marketPrice = await fetchPrice(selectedAddressB); 839 | await delay(1000) 840 | const marketPrice2 = await fetchPrice(selectedAddressB); 841 | await delay(1000) 842 | const marketPrice3 = await fetchPrice(selectedAddressB); 843 | const averageMarketPrice = (marketPrice + marketPrice2 + marketPrice3) / 3; 844 | currUsdTotalBalance = balanceA + (balanceB * averageMarketPrice); 845 | console.log(`Current USD Total Balance: ${currUsdTotalBalance}`) 846 | 847 | // Emergency Stop Loss 848 | if (currUsdTotalBalance < stopLossUSD) { 849 | console.clear(); 850 | console.log(`\n\u{1F6A8} Emergency Stop Loss Triggered! - Exiting`); 851 | stopLoss = true; 852 | process.kill(process.pid, "SIGINT"); 853 | } 854 | // Calculate the new prices of tokenB when it's up and down by the spread% 855 | newPriceBUp = averageMarketPrice * (1 + (spreadbps * 1.3) / 10000); 856 | newPriceBDown = averageMarketPrice * (1 - spreadbps / 10000); 857 | 858 | // Calculate the current value of TokenB in USD 859 | const currentValueUSD = balanceBLamports / Math.pow(10, selectedDecimalsB) * averageMarketPrice; 860 | 861 | // Calculate the target value of TokenB in USD at the new prices 862 | const targetValueUSDUp = balanceBLamports / Math.pow(10, selectedDecimalsB) * newPriceBUp; 863 | const targetValueUSDDown = balanceBLamports / Math.pow(10, selectedDecimalsB) * newPriceBDown; 864 | 865 | // Calculate the initial lamports to sell and buy 866 | let lamportsToSellInitial = Math.floor((targetValueUSDUp - infinityTarget) / newPriceBUp * Math.pow(10, selectedDecimalsB)/0.998); 867 | let lamportsToBuyInitial = Math.floor((infinityTarget - targetValueUSDDown) / newPriceBDown * Math.pow(10, selectedDecimalsB)/0.998); 868 | 869 | // Adjust the lamports to buy based on the potential cancellation of the sell order 870 | let lamportsToBuy = lamportsToBuyInitial - lamportsToSellInitial; 871 | 872 | // lamportsToSell remains the same as lamportsToSellInitial 873 | let lamportsToSell = lamportsToSellInitial; 874 | 875 | // Calculate the expected USDC for the sell and buy 876 | const decimalDiff = selectedDecimalsB - selectedDecimalsA; 877 | const expectedUSDCForSell = (lamportsToSell * newPriceBUp) / Math.pow(10, selectedDecimalsB); 878 | const expectedUSDCForBuy = (lamportsToBuy * newPriceBDown) / Math.pow(10, selectedDecimalsB); 879 | const expectedUSDCForSellLamports = Math.floor((lamportsToSell * newPriceBUp) / Math.pow(10, decimalDiff)); 880 | const expectedUSDCForBuyLamports = Math.floor((lamportsToBuy * newPriceBDown) / Math.pow(10, decimalDiff)); 881 | 882 | // Derive the MarketUp and MarketDown prices from the lamports to buy/sell 883 | const derivedMarketPriceUp = expectedUSDCForSellLamports / lamportsToSell; 884 | const derivedMarketPriceDown = expectedUSDCForBuyLamports / lamportsToBuy; 885 | 886 | //Translate variables to be used for jitoController 887 | infinityBuyInputLamports = expectedUSDCForBuyLamports; 888 | infinityBuyOutputLamports = lamportsToBuy; 889 | infinitySellInputLamports = lamportsToSell; 890 | infinitySellOutputLamports = expectedUSDCForSellLamports; 891 | 892 | // Check if the balances are enough to place the orders (With a 5% buffer) 893 | if (infinitySellInputLamports > balanceBLamports * 1.05) { 894 | console.log("Token B Balance not enough to place Sell Order. Exiting."); 895 | process.kill(process.pid, "SIGINT"); 896 | } 897 | if (infinityBuyInputLamports > balanceALamports * 1.05) { 898 | console.log("Token A Balance not enough to place Buy Order. Exiting."); 899 | process.kill(process.pid, "SIGINT"); 900 | } 901 | // Log the values 902 | 903 | /* 904 | console.log(`TokenA Balance: ${balanceA}`); 905 | console.log(`TokenA Balance Lamports: ${balanceALamports}`); 906 | console.log(`TokenB Balance: ${balanceB}`); 907 | console.log(`TokenB Balance Lamports: ${balanceBLamports}`); 908 | console.log(`TokenB Balance USD: ${currentValueUSD}`); 909 | console.log(`Infinity Target: ${infinityTarget}`); 910 | console.log(`Market Price: ${marketPrice.toFixed(2)}`); 911 | console.log(`Market Price Up: ${newPriceBUp.toFixed(2)}`); 912 | console.log(`Derived Market Price Up: ${derivedMarketPriceUp.toFixed(2)}`); 913 | console.log(`Market Price Down: ${newPriceBDown.toFixed(2)}`); 914 | console.log(`Derived Market Price Down: ${derivedMarketPriceDown.toFixed(2)}`); 915 | console.log(`Target Value of TokenB in USD Up: ${targetValueUSDUp}`); 916 | console.log(`Target Value of TokenB in USD Down: ${targetValueUSDDown}`); 917 | console.log(`Lamports to Sell: ${lamportsToSell}`); 918 | console.log(`Expected USDC for Sell: ${expectedUSDCForSell}`); 919 | console.log(`USDC Lamports for Sell ${expectedUSDCForSellLamports}`); 920 | console.log(`Lamports to Buy: ${lamportsToBuy}`); 921 | console.log(`Expected USDC for Buy: ${expectedUSDCForBuy}`); 922 | console.log(`USDC Lamports for Buy ${expectedUSDCForBuyLamports}\n`); 923 | */ 924 | 925 | await jitoController("infinity"); 926 | console.log( 927 | "Pause for 5 seconds to allow orders to finalize on blockchain.", 928 | await delay(5000) 929 | ); 930 | monitor(); 931 | } 932 | 933 | async function createTx(inAmount, outAmount, inputMint, outputMint, base) { 934 | if (shutDown) return; 935 | 936 | const maxRetries = 5; 937 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 938 | 939 | let attempt = 0; 940 | while (attempt < maxRetries) { 941 | attempt++; 942 | try { 943 | const tokenAccounts = await getTokenAccounts( 944 | connection, 945 | payer.publicKey, 946 | new solanaWeb3.PublicKey( 947 | "9tzZzEHsKnwFL1A3DyFJwj36KnZj3gZ7g4srWp9YTEoh" 948 | ) 949 | ); 950 | if (tokenAccounts.value.length === 0) { 951 | console.log( 952 | "No ARB token accounts found. Please purchase at least 25k ARB and try again." 953 | ); 954 | process.exit(0); 955 | } 956 | 957 | const response = await fetch( 958 | "https://jup.ag/api/limit/v1/createOrder", 959 | { 960 | method: "POST", 961 | headers: { "Content-Type": "application/json" }, 962 | body: JSON.stringify({ 963 | owner: payer.publicKey.toString(), 964 | inAmount, 965 | outAmount, 966 | inputMint: inputMint.toString(), 967 | outputMint: outputMint.toString(), 968 | expiredAt: null, 969 | base: base.publicKey.toString(), 970 | referralAccount: 971 | "7WGULgEo4Veqj6sCvA3VNxGgBf3EXJd8sW2XniBda3bJ", 972 | referralName: "Jupiter GridBot" 973 | }) 974 | } 975 | ); 976 | 977 | if (!response.ok) { 978 | throw new Error( 979 | `Failed to create order: ${response.statusText}` 980 | ); 981 | } 982 | 983 | const responseData = await response.json(); 984 | const { tx: encodedTransaction } = responseData; 985 | 986 | // Deserialize the raw transaction 987 | const transactionBuf = Buffer.from(encodedTransaction, "base64"); 988 | const transaction = solanaWeb3.Transaction.from(transactionBuf); 989 | transaction.sign(payer, base); 990 | return { 991 | transaction, 992 | orderPubkey: responseData.orderPubkey 993 | }; 994 | 995 | // to be handled later 996 | // return { txid, orderPubkey: responseData.orderPubkey}; 997 | } catch (error) { 998 | await delay(2000); 999 | } 1000 | } 1001 | // If we get here, its proper broken... 1002 | throw new Error("Order Creation failed after maximum attempts."); 1003 | } 1004 | 1005 | async function cancelOrder(target = [], payer) { 1006 | const retryCount = 10; 1007 | for (let i = 0; i < retryCount; i++) { 1008 | target = await checkOpenOrders(); 1009 | if (target.length === 0) { 1010 | console.log("No orders to cancel."); 1011 | return "skip"; 1012 | } 1013 | console.log(target); 1014 | const requestData = { 1015 | owner: payer.publicKey.toString(), 1016 | feePayer: payer.publicKey.toString(), 1017 | orders: Array.from(target) 1018 | }; 1019 | try { 1020 | const response = await fetch("https://jup.ag/api/limit/v1/cancelOrders", { 1021 | method: "POST", 1022 | headers: { 1023 | "Content-Type": "application/json" 1024 | }, 1025 | body: JSON.stringify(requestData) 1026 | }); 1027 | 1028 | if (!response.ok) { 1029 | console.log("Bad Cancel Order Request"); 1030 | throw new Error(`HTTP error! Status: ${response.status}`); 1031 | } 1032 | 1033 | const responseData = await response.json(); 1034 | const transactionBase64 = responseData.tx; 1035 | const transactionBuf = Buffer.from(transactionBase64, "base64"); 1036 | const transaction = solanaWeb3.Transaction.from(transactionBuf); 1037 | 1038 | const { blockhash } = await connection.getLatestBlockhash(); 1039 | transaction.recentBlockhash = blockhash; 1040 | transaction.sign(payer); 1041 | return transaction; 1042 | } catch (error) { 1043 | await delay(2000); 1044 | if (i === retryCount - 1) throw error; // If last retry, throw error 1045 | console.log(`Attempt ${i + 1} failed. Retrying...`); 1046 | 1047 | target = await checkOpenOrders(); 1048 | } 1049 | } 1050 | } 1051 | 1052 | async function balanceCheck() { 1053 | console.log("Checking Portfolio, we will rebalance if necessary."); 1054 | const currentBalances = await getBalance( 1055 | payer, 1056 | selectedAddressA, 1057 | selectedAddressB, 1058 | selectedTokenA, 1059 | selectedTokenB 1060 | ); 1061 | 1062 | currBalanceA = currentBalances.balanceA; 1063 | currBalanceB = currentBalances.balanceB; 1064 | currUSDBalanceA = currentBalances.usdBalanceA; 1065 | currUSDBalanceB = currentBalances.usdBalanceB; 1066 | currUsdTotalBalance = currUSDBalanceA + currUSDBalanceB; 1067 | tokenARebalanceValue = currentBalances.tokenARebalanceValue; 1068 | tokenBRebalanceValue = currentBalances.tokenBRebalanceValue; 1069 | let currBalanceALamports = currBalanceA * Math.pow(10, selectedDecimalsA); 1070 | let currBalanceBLamports = currBalanceB * Math.pow(10, selectedDecimalsB); 1071 | if (currUsdTotalBalance < infinityTarget) { 1072 | console.log( 1073 | `Your total balance is not high enough for your Token B Target Value. Please either increase your wallet balance or reduce your target.` 1074 | ); 1075 | process.exit(0); 1076 | } 1077 | const targetUsdBalancePerToken = infinityTarget; 1078 | const percentageDifference = Math.abs( 1079 | (currUSDBalanceB - targetUsdBalancePerToken) / targetUsdBalancePerToken 1080 | ); 1081 | if (percentageDifference > 0.03) { 1082 | if (currUSDBalanceB < targetUsdBalancePerToken) { 1083 | const deficit = 1084 | (targetUsdBalancePerToken - currUSDBalanceB) * 1085 | Math.pow(10, selectedDecimalsA); 1086 | adjustmentA = Math.floor( 1087 | Math.abs((-1 * deficit) / tokenARebalanceValue) 1088 | ); 1089 | } else if (currUSDBalanceB > targetUsdBalancePerToken) { 1090 | const surplus = 1091 | (currUSDBalanceB - targetUsdBalancePerToken) * 1092 | Math.pow(10, selectedDecimalsB); 1093 | adjustmentB = Math.floor( 1094 | Math.abs(-1 * (surplus / tokenBRebalanceValue)) 1095 | ); 1096 | } 1097 | } else { 1098 | console.log("Token B $ value within 3% of target, skipping rebalance."); 1099 | return "skip"; 1100 | } 1101 | const rebalanceSlippageBPS = 200; 1102 | 1103 | const confirmTransaction = async () => { 1104 | if (!askForRebalance) { 1105 | return true; 1106 | } 1107 | const answer = await questionAsync('Do you want to proceed with this transaction? (Y/n) '); 1108 | if (answer.toUpperCase() === 'N') { 1109 | console.log('Transaction cancelled by user. Closing program.'); 1110 | process.exit(0); 1111 | } else { 1112 | askForRebalance = false; 1113 | return true; 1114 | } 1115 | }; 1116 | 1117 | if (adjustmentA > 0) { 1118 | if (adjustmentA > currBalanceALamports) { 1119 | console.log(adjustmentA); 1120 | console.log(currBalanceALamports); 1121 | console.log( 1122 | `You do not have enough ${selectedTokenA} to rebalance. There has been an error. 1123 | Attempting to swap ${chalk.cyan(adjustmentA / Math.pow(10, selectedDecimalsA))} ${chalk.cyan(selectedTokenA)} to ${chalk.magenta(selectedTokenB)}` 1124 | ); 1125 | process.exit(0); 1126 | } 1127 | console.log( 1128 | `Need to trade ${chalk.cyan(adjustmentA / Math.pow(10, selectedDecimalsA))} ${chalk.cyan(selectedTokenA)} to ${chalk.magenta(selectedTokenB)} to balance.` 1129 | ); 1130 | const userConfirmation = await confirmTransaction(); 1131 | if (userConfirmation) { 1132 | const rebalanceTx = await rebalanceTokens( 1133 | selectedAddressA, 1134 | selectedAddressB, 1135 | adjustmentA, 1136 | rebalanceSlippageBPS, 1137 | quoteurl 1138 | ); 1139 | return rebalanceTx; 1140 | } else { 1141 | console.log('Transaction cancelled by user.'); 1142 | return; 1143 | } 1144 | } else if (adjustmentB > 0) { 1145 | if (adjustmentB > currBalanceBLamports) { 1146 | console.log(adjustmentB); 1147 | console.log(currBalanceBLamports); 1148 | console.log( 1149 | `You do not have enough ${selectedTokenB} to rebalance. There has been an error. 1150 | Attempting to swap ${chalk.magenta(adjustmentB / Math.pow(10, selectedDecimalsB))} ${chalk.magenta(selectedTokenB)} to ${chalk.cyan(selectedTokenA)}` 1151 | ); 1152 | process.exit(0); 1153 | } 1154 | console.log( 1155 | `Need to trade ${chalk.magenta(adjustmentB / Math.pow(10, selectedDecimalsB))} ${chalk.magenta(selectedTokenB)} to ${chalk.cyan(selectedTokenA)} to balance.` 1156 | ); 1157 | const userConfirmation = await confirmTransaction(); 1158 | if (userConfirmation) { 1159 | const rebalanceTx = await rebalanceTokens( 1160 | selectedAddressB, 1161 | selectedAddressA, 1162 | adjustmentB, 1163 | rebalanceSlippageBPS, 1164 | quoteurl 1165 | ); 1166 | return rebalanceTx; 1167 | } else { 1168 | console.log('Transaction cancelled by user.'); 1169 | return; 1170 | } 1171 | } 1172 | } 1173 | 1174 | async function rebalanceTokens( 1175 | inputMint, 1176 | outputMint, 1177 | rebalanceValue, 1178 | rebalanceSlippageBPS, 1179 | quoteurl 1180 | ) { 1181 | if (shutDown) return; 1182 | const rebalanceLamports = Math.floor(rebalanceValue); 1183 | console.log(`Rebalancing Tokens ${chalk.cyan(selectedTokenA)} and ${chalk.magenta(selectedTokenB)}`); 1184 | 1185 | try { 1186 | // Fetch the quote 1187 | const quoteResponse = await axios.get( 1188 | `${quoteurl}?inputMint=${inputMint}&outputMint=${outputMint}&amount=${rebalanceLamports}&autoSlippage=true&maxAutoSlippageBps=200` //slippageBps=${rebalanceSlippageBPS} 1189 | ); 1190 | 1191 | const swapApiResponse = await fetch( 1192 | "https://quote-api.jup.ag/v6/swap", 1193 | { 1194 | method: "POST", 1195 | headers: { 1196 | "Content-Type": "application/json" 1197 | }, 1198 | body: JSON.stringify({ 1199 | quoteResponse: quoteResponse.data, 1200 | userPublicKey: payer.publicKey, 1201 | wrapAndUnwrapSol: true 1202 | }) 1203 | } 1204 | ); 1205 | 1206 | const { blockhash } = await connection.getLatestBlockhash(); 1207 | const swapData = await swapApiResponse.json(); 1208 | 1209 | if (!swapData || !swapData.swapTransaction) { 1210 | throw new Error("Swap transaction data not found."); 1211 | } 1212 | 1213 | // Deserialize the transaction correctly for a versioned message 1214 | const swapTransactionBuffer = Buffer.from( 1215 | swapData.swapTransaction, 1216 | "base64" 1217 | ); 1218 | const transaction = VersionedTransaction.deserialize( 1219 | swapTransactionBuffer 1220 | ); 1221 | 1222 | transaction.recentBlockhash = blockhash; 1223 | transaction.sign([payer]); 1224 | return transaction; 1225 | } catch (error) { 1226 | console.error("Error during the transaction:", error); 1227 | } 1228 | } 1229 | //Main Loop/Display Functions 1230 | async function monitor() { 1231 | if (shutDown) return; 1232 | const maxRetries = 20; 1233 | let retries = 0; 1234 | await updateMainDisplay(); 1235 | while (retries < maxRetries) { 1236 | try { 1237 | await checkOpenOrders(); 1238 | await handleOrders(checkArray); 1239 | break; // Break the loop if we've successfully handled the price monitoring 1240 | } catch (error) { 1241 | console.log(error); 1242 | console.error( 1243 | `Error: Connection or Token Data Error (Monitor Price) - (Attempt ${retries + 1} of ${maxRetries})` 1244 | ); 1245 | retries++; 1246 | 1247 | if (retries === maxRetries) { 1248 | console.error( 1249 | "Maximum number of retries reached. Unable to retrieve data." 1250 | ); 1251 | return null; 1252 | } 1253 | } 1254 | } 1255 | } 1256 | 1257 | async function updateMainDisplay() { 1258 | console.clear(); 1259 | console.log(`Jupgrid v${packageInfo.version}`); 1260 | console.log(`\u{267E} Infinity Mode`); 1261 | console.log(`\u{1F4B0} Wallet: ${displayAddress}`); 1262 | formatElapsedTime(startTime); 1263 | console.log(`-`); 1264 | console.log( 1265 | `\u{1F527} Settings: ${chalk.cyan(selectedTokenA)}/${chalk.magenta(selectedTokenB)}\n\u{1F3AF} ${selectedTokenB} Target Value: $${infinityTarget}\n\u{1F6A8} Stop Loss at $${stopLossUSD}\n\u{2B65} Spread: ${spread}%\n\u{1F55A} Monitor Delay: ${monitorDelay}ms` 1266 | ); 1267 | try { 1268 | const { newUSDBalanceA, newUSDBalanceB } = await fetchNewUSDValues(); 1269 | currUSDBalanceA = newUSDBalanceA; 1270 | currUSDBalanceB = newUSDBalanceB; 1271 | currUsdTotalBalance = currUSDBalanceA + currUSDBalanceB; // Recalculate total 1272 | newPrice = await updatePrice(selectedAddressB); 1273 | } catch (error) { 1274 | // Error is not critical. Reuse the previous balances and try another update again next cycle. 1275 | } 1276 | 1277 | if (currUsdTotalBalance < stopLossUSD) { 1278 | // Emergency Stop Loss 1279 | console.clear(); 1280 | console.log( 1281 | `\n\u{1F6A8} Emergency Stop Loss Triggered! - Cashing out and Exiting` 1282 | ); 1283 | stopLoss = true; 1284 | process.kill(process.pid, "SIGINT"); 1285 | } 1286 | 1287 | let {multiplier, quantity} = await formatTokenPrice(newPrice); 1288 | let adjustedNewPrice = newPrice * multiplier 1289 | let adjustedNewPriceBUp = newPriceBUp * multiplier 1290 | let adjustedNewPriceBDown = newPriceBDown * multiplier 1291 | if(iteration === 0) 1292 | { 1293 | currentTracker = new Array(50).fill(adjustedNewPrice); 1294 | sellPrice = new Array(50).fill(adjustedNewPriceBUp); 1295 | buyPrice = new Array(50).fill(adjustedNewPriceBDown); 1296 | } 1297 | 1298 | currentTracker.splice(0,0,(adjustedNewPrice).toString()) 1299 | currentTracker.pop(); 1300 | buyPrice.splice(0,0,(adjustedNewPriceBDown).toString()) 1301 | buyPrice.pop(); 1302 | sellPrice.splice(0,0,(adjustedNewPriceBUp).toString()) 1303 | sellPrice.pop(); 1304 | iteration++; 1305 | var config = { 1306 | height:20, 1307 | colors:[ 1308 | asciichart.blue, 1309 | asciichart.green, 1310 | asciichart.yellow, 1311 | ] 1312 | } 1313 | console.log(`- 1314 | Starting Balance : $${initUsdTotalBalance.toFixed(2)} 1315 | Current Balance : $${currUsdTotalBalance.toFixed(2)}`); 1316 | 1317 | const { profitOrLoss, percentageChange } = calculateProfitOrLoss(currUsdTotalBalance, initUsdTotalBalance); 1318 | displayProfitOrLoss(profitOrLoss, percentageChange); 1319 | 1320 | console.log(`Market Change %: ${(((newPrice - startPrice) / startPrice) * 100).toFixed(2)}% 1321 | Market Change USD: ${(newPrice - startPrice).toFixed(9)} 1322 | Performance Delta: ${(percentageChange - ((newPrice - startPrice) / startPrice) * 100).toFixed(2)}% 1323 | - 1324 | Latest Snapshot Balance ${chalk.cyan(selectedTokenA)}: ${chalk.cyan(currBalanceA.toFixed(5))} (Change: ${chalk.cyan((currBalanceA - initBalanceA).toFixed(5))}) - Worth: $${currUSDBalanceA.toFixed(2)} 1325 | Latest Snapshot Balance ${chalk.magenta(selectedTokenB)}: ${chalk.magenta(currBalanceB.toFixed(5))} (Change: ${chalk.magenta((currBalanceB - initBalanceB).toFixed(5))}) - Worth: $${currUSDBalanceB.toFixed(2)} 1326 | - 1327 | Starting Balance A - ${chalk.cyan(selectedTokenA)}: ${chalk.cyan(initBalanceA.toFixed(5))} 1328 | Starting Balance B - ${chalk.magenta(selectedTokenB)}: ${chalk.magenta(initBalanceB.toFixed(5))} 1329 | - 1330 | Trades: ${counter} 1331 | Rebalances: ${rebalanceCounter} 1332 | - 1333 | Sell Order Price: ${newPriceBUp.toFixed(9)} - Selling ${chalk.magenta(Math.abs(infinitySellInputLamports / Math.pow(10, selectedDecimalsB)))} ${chalk.magenta(selectedTokenB)} for ${chalk.cyan(Math.abs(infinitySellOutputLamports / Math.pow(10, selectedDecimalsA)))} ${chalk.cyan(selectedTokenA)} 1334 | Current Price ${quantity}:`); 1335 | console.log(asciichart.plot([currentTracker,buyPrice,sellPrice],config)); 1336 | console.log(`Buy Order Price: ${newPriceBDown.toFixed(9)} - Buying ${chalk.magenta(Math.abs(infinityBuyOutputLamports / Math.pow(10, selectedDecimalsB)))} ${chalk.magenta(selectedTokenB)} for ${chalk.cyan(Math.abs(infinityBuyInputLamports / Math.pow(10, selectedDecimalsA)))} ${chalk.cyan(selectedTokenA)}\n`); 1337 | } 1338 | 1339 | async function checkOpenOrders() { 1340 | openOrders = []; 1341 | checkArray = []; 1342 | 1343 | // Make the JSON request 1344 | openOrders = await limitOrder.getOrders([ 1345 | ownerFilter(payer.publicKey, "processed") 1346 | ]); 1347 | 1348 | // Create an array to hold publicKey values 1349 | checkArray = openOrders.map((order) => order.publicKey.toString()); 1350 | return checkArray; 1351 | } 1352 | 1353 | async function handleOrders(checkArray) { 1354 | if (checkArray.length !== 2) { 1355 | infinityGrid(); 1356 | } else { 1357 | console.log("2 open orders. Waiting for change."); 1358 | await delay(monitorDelay); 1359 | await monitor(); 1360 | } 1361 | } 1362 | 1363 | //End Function 1364 | process.on("SIGINT", () => { 1365 | console.log("\nCTRL+C detected! Performing cleanup..."); 1366 | shutDown = true; 1367 | (async () => { 1368 | await jitoController("cancel"); 1369 | process.exit(0); 1370 | })(); 1371 | }); 1372 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | class APIServer { 4 | constructor(tradelogger) { 5 | this.app = express(); 6 | this.tradelog = tradelogger; 7 | 8 | this.app.use(express.json()); 9 | 10 | this.app.get("/log", (req, res) => { 11 | res.send(this.tradelog.log); 12 | }); 13 | } 14 | 15 | start() { 16 | this.app.listen(3333, () => { 17 | console.log("API Server listening on port 3333"); 18 | }); 19 | } 20 | } 21 | 22 | export { APIServer }; 23 | -------------------------------------------------------------------------------- /src/settings.js: -------------------------------------------------------------------------------- 1 | import bs58 from 'bs58'; 2 | import dotenv from 'dotenv'; 3 | import fs from 'fs'; 4 | import promptSync from 'prompt-sync'; 5 | 6 | import { Keypair } from '@solana/web3.js'; 7 | 8 | import { initialize } from './jupgrid.js'; 9 | import * as utils from './utils.js'; 10 | 11 | const prompt = promptSync({ sigint: true }); 12 | 13 | function envload() { 14 | const envFilePath = ".env"; 15 | const defaultEnvContent = 16 | "RPC_URL=Your_RPC_Here\nPRIVATE_KEY=Your_Private_Key_Here"; 17 | const encflag = "love_from_the_jupgrid_devs_<3"; 18 | try { 19 | if (!fs.existsSync(envFilePath)) { 20 | fs.writeFileSync(envFilePath, defaultEnvContent, "utf8"); 21 | console.log( 22 | "\u{2714} .env file created. Please fill in your private information, and start JupGrid again." 23 | ); 24 | process.exit(0); 25 | } 26 | console.log("\u{2714} Env Loaded Successfully.\n"); 27 | } catch (error) { 28 | console.error( 29 | "\u{274C} An error occurred while checking or creating the .env file:", 30 | error 31 | ); 32 | process.exit(1); 33 | } 34 | dotenv.config(); 35 | if (!process.env.PRIVATE_KEY || !process.env.RPC_URL) { 36 | console.error( 37 | "\u{274C} Missing required environment variables in .env file. Please ensure PRIVATE_KEY and RPC_URL are set." 38 | ); 39 | process.exit(1); 40 | } 41 | while (1) { 42 | if (process.env.FLAG) { 43 | try { 44 | const password = prompt.hide( 45 | "\u{1F512} Enter your password to decrypt your private key (input hidden): " 46 | ); 47 | const cryptr = new utils.Encrypter(password); 48 | const decdflag = cryptr.decrypt(process.env.FLAG); 49 | if (decdflag !== encflag) { 50 | console.error( 51 | "\u{274C} Invalid password. Please ensure you are using the correct password." 52 | ); 53 | continue; 54 | } 55 | 56 | return [ 57 | Keypair.fromSecretKey( 58 | new Uint8Array( 59 | bs58.decode( 60 | cryptr.decrypt(process.env.PRIVATE_KEY) 61 | ) 62 | ) 63 | ), 64 | process.env.RPC_URL 65 | ]; 66 | } catch (error) { 67 | console.error( 68 | "\u{274C} Invalid password. Please ensure you are using the correct password." 69 | ); 70 | console.error("\u{274C} An error occurred:", error); 71 | continue; 72 | } 73 | } else { 74 | const pswd = prompt.hide( 75 | "\u{1F50F} Enter a password to encrypt your private key with (input hidden): " 76 | ); 77 | const cryptr = new utils.Encrypter(pswd); 78 | const encryptedKey = cryptr.encrypt(process.env.PRIVATE_KEY, pswd); 79 | const encryptedFlag = cryptr.encrypt(encflag, pswd); 80 | fs.writeFileSync( 81 | envFilePath, 82 | `RPC_URL=${process.env.RPC_URL}\n//Do NOT touch these two - you risk breaking encryption!\nPRIVATE_KEY=${encryptedKey}\nFLAG=${encryptedFlag}`, 83 | "utf8" 84 | ); 85 | console.log( 86 | "\u{1F512} Encrypted private key and flag saved to .env file. Please restart JupGrid to continue." 87 | ); 88 | process.exit(0); 89 | } 90 | } // end while 91 | } 92 | 93 | function saveuserSettings( 94 | configVersion, 95 | selectedTokenA, 96 | selectedAddressA, 97 | selectedDecimalsA, 98 | selectedTokenB, 99 | selectedAddressB, 100 | selectedDecimalsB, 101 | spread, 102 | monitorDelay, 103 | stopLossUSD, 104 | maxJitoTip, 105 | infinityTarget 106 | ) { 107 | try { 108 | fs.writeFileSync( 109 | "userSettings.json", 110 | JSON.stringify( 111 | { 112 | configVersion, 113 | selectedTokenA, 114 | selectedAddressA, 115 | selectedDecimalsA, 116 | selectedTokenB, 117 | selectedAddressB, 118 | selectedDecimalsB, 119 | spread, 120 | monitorDelay, 121 | stopLossUSD, 122 | maxJitoTip, 123 | infinityTarget 124 | }, 125 | null, 126 | 4 127 | ) 128 | ); 129 | console.log("\u{2714} User data saved successfully."); 130 | } catch (error) { 131 | console.error("Error saving user data:", error); 132 | } 133 | } 134 | 135 | function loaduserSettings() { 136 | try { 137 | const data = fs.readFileSync("userSettings.json"); 138 | const userSettings = JSON.parse(data); 139 | return userSettings; 140 | } catch (error) { 141 | console.log("No user data found. Starting with fresh inputs."); 142 | initialize(); 143 | } 144 | } 145 | 146 | export { envload, loaduserSettings, saveuserSettings }; -------------------------------------------------------------------------------- /src/tradelog.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | class TradeLogger { 4 | constructor() { 5 | this.sessionstart = Date.now(); 6 | this.filename = `logs/${this.sessionstart}.json`; 7 | this.initialized = false; 8 | } 9 | 10 | initLog() { 11 | // Read userSettings.json and create log object 12 | this.log = { 13 | settings: JSON.parse(fs.readFileSync("userSettings.json")), 14 | log: [] 15 | }; 16 | 17 | // Write initial file 18 | this.writeLog(); 19 | 20 | this.initialized = true; 21 | } 22 | 23 | readLog() { 24 | this.log = JSON.parse(fs.readFileSync(this.filename)); 25 | } 26 | 27 | writeLog() { 28 | fs.writeFileSync(this.filename, JSON.stringify(this.log, null, 4)); 29 | } 30 | 31 | logTrade(object) { 32 | // Validate object type 33 | if (object.type !== "placement" && object.type !== "closure") { 34 | console.error("Invalid object type"); 35 | return; 36 | } 37 | this.readLog(); 38 | this.log.log.push(object); 39 | this.writeLog(); 40 | } 41 | } 42 | 43 | export { TradeLogger }; 44 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import crypto from "crypto"; 3 | import fs from "fs"; 4 | import readline from "readline"; 5 | 6 | import solanaWeb3 from "@solana/web3.js"; 7 | 8 | function delay(ms) { 9 | return new Promise((resolve) => { 10 | setTimeout(resolve, ms); 11 | }); 12 | } 13 | 14 | const rl = readline.createInterface({ 15 | input: process.stdin, 16 | output: process.stdout 17 | }); 18 | 19 | function questionAsync(question) { 20 | return new Promise((resolve) => { 21 | rl.question(question, resolve); 22 | }); 23 | } 24 | 25 | async function downloadTokensList() { 26 | const response = await axios.get("https://token.jup.ag/strict"); 27 | const { data } = response; 28 | const tokens = data.map(({ symbol, address, decimals }) => ({ 29 | symbol, 30 | address, 31 | decimals 32 | })); 33 | fs.writeFileSync("tokens.txt", JSON.stringify(tokens)); 34 | return data; 35 | } 36 | 37 | async function getTokens() { 38 | if (!fs.existsSync("tokens.txt")) { 39 | await downloadTokensList(); 40 | } 41 | return JSON.parse(fs.readFileSync("tokens.txt")); 42 | } 43 | 44 | class Encrypter { 45 | constructor(encryptionKey) { 46 | this.algorithm = "aes-192-cbc"; 47 | this.key = crypto.scryptSync(encryptionKey, "salt", 24); 48 | } 49 | 50 | encrypt(clearText) { 51 | const iv = crypto.randomBytes(16); 52 | const cipher = crypto.createCipheriv(this.algorithm, this.key, iv); 53 | const encrypted = cipher.update(clearText, "utf8", "hex"); 54 | return [ 55 | encrypted + cipher.final("hex"), 56 | Buffer.from(iv).toString("hex") 57 | ].join("|"); 58 | } 59 | 60 | decrypt(encryptedText) { 61 | const [encrypted, iv] = encryptedText.split("|"); 62 | if (!iv) throw new Error("IV not found"); 63 | const decipher = crypto.createDecipheriv( 64 | this.algorithm, 65 | this.key, 66 | Buffer.from(iv, "hex") 67 | ); 68 | return ( 69 | decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8") 70 | ); 71 | } 72 | } 73 | 74 | async function getTokenAccounts(connection, address, tokenMintAddress) { 75 | return await connection.getParsedTokenAccountsByOwner(address, { 76 | mint: new solanaWeb3.PublicKey(tokenMintAddress) 77 | }); 78 | } 79 | 80 | export { 81 | delay, 82 | downloadTokensList, 83 | Encrypter, 84 | getTokenAccounts, 85 | getTokens, 86 | questionAsync, 87 | rl 88 | }; 89 | --------------------------------------------------------------------------------