├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .vscode └── launch.json ├── Components ├── DefaultBox.js ├── EscNotification.js ├── IntroTitle.js └── TabNotification.js ├── LICENSE ├── README.md ├── gif1.gif ├── gif2.gif ├── package-lock.json ├── package.json ├── src ├── bot.js ├── exit.js ├── generator.js ├── index.js ├── setup.js ├── ui.js └── utils.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SOLANA_WALLET_PRIVATE_KEY=hgq847chjjjJUPITERiiiISaaaAWESOMEaaANDiiiIwwWANNAbbbBErrrRICHh 2 | DEFAULT_RPC= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "prettier"], 7 | "plugins": ["prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": 2022, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "prettier/prettier": "error", 14 | "endOfLine": "auto" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # temp files 107 | temp/* 108 | /config.json 109 | 110 | # useless stuff 111 | .DS_Store -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/bot.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Components/DefaultBox.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box } = require("ink"); 3 | 4 | const importJsx = require("import-jsx"); 5 | const IntroTitle = importJsx("./IntroTitle"); 6 | 7 | const DefaultBox = ({ children }) => { 8 | return ( 9 | 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | module.exports = DefaultBox; 23 | -------------------------------------------------------------------------------- /Components/EscNotification.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | 4 | const EscNotification = () => { 5 | return ( 6 | 7 | 8 | Use ESC key if U wanna exit 9 | 10 | 11 | ); 12 | }; 13 | 14 | module.exports = EscNotification; 15 | -------------------------------------------------------------------------------- /Components/IntroTitle.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const BigText = require("ink-big-text"); 3 | const Gradient = require("ink-gradient"); 4 | 5 | const IntroTitle = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | module.exports = IntroTitle; 14 | -------------------------------------------------------------------------------- /Components/TabNotification.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | 4 | const EscNotification = ({ skip = false }) => { 5 | if (skip) 6 | return ( 7 | 8 | 9 | Press TAB to use DEFAULT from .env 10 | 11 | 12 | ); 13 | 14 | return ( 15 | 16 | 17 | Press TAB wen done 18 | 19 | 20 | ); 21 | }; 22 | 23 | module.exports = EscNotification; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pawel Mioduszewski 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 | # solana-jupiter-bot 2 | 3 | > CAUTION! Use at Your own risk! I take no responsibility for your transactions! 4 | 5 | > ⚠️ EPILEPSY WARNING - CLI UI is constantly refreshed and may be disruptive for sensitive ppl 6 | 7 | # WTF is this? 8 | 9 | It's a trading bot that can trade on Solana blockchain by utilizing Jupiter Agregator SDK. 10 | There are two parts: 11 | 12 | - config wizard 13 | - trading bot 14 | 15 | With Config wizard You can easly setup your trading strategy. 16 | 17 | ### CLI UI 18 | 19 | 📊 Bot have CLI UI which helps You monitor your trading strategy. 20 | 21 | CLI UI have current simulated profit chart and latency chart. Latency chart shows You the time taken to computate routes with Jupiter SDK. 22 | 23 | All trades are stored in trades history and will be shown in the table. Table is limited to 5 entries, but history stores all trades. 24 | 25 | 💡 UI elements can be hidden or shown using hotkeys (read below). 26 | 27 | > THIS README IS NOT COMPLETED YET. 28 | 29 | ![](https://github.com/pmioduszewski/solana-jupiter-bot/blob/main/gif1.gif) 30 | ![](https://github.com/pmioduszewski/solana-jupiter-bot/blob/main/gif2.gif) 31 | 32 | ## Install 33 | 34 | ```bash 35 | $ git clone https://github.com/pmioduszewski/solana-jupiter-bot && cd solana-jupiter-bot 36 | $ yarn 37 | ``` 38 | 39 | Set Your wallet private key in `.env` file 40 | 41 | ```js 42 | SOLANA_WALLET_PRIVATE_KEY = 43 | hgq847chjjjJUPITERiiiISaaaAWESOMEaaANDiiiIwwWANNAbbbBErrrRICHh; 44 | ``` 45 | 46 | \*[optionally] set default RPC (it can be also set in wizard) 47 | 48 | ```js 49 | SOLANA_WALLET_PRIVATE_KEY=hgq847chjjjJUPITERiiiISaaaAWESOMEaaANDiiiIwwWANNAbbbBErrrRICHh 50 | DEFAULT_RPC=https://my-super-lazy-rpc.gov 51 | ``` 52 | 53 | ## USAGE 54 | 55 | ``` 56 | $ solana-jupiter-bot: 57 | 58 | Usage 59 | $ yarn start 60 | This will open Config Wizard and start bot 61 | 62 | $ yarn trade 63 | Start Bot and Trade with latest config 64 | ``` 65 | 66 | Have fun! 67 | 68 | ## Hotkeys 69 | 70 | While bot is running You can use some hotkeys that will change behaviour of bot or UI 71 | 72 | `[H]` - show/hide Help 73 | 74 | `[CTRL] + [C]` - obviously it will kill the bot 75 | 76 | `[E]` - force execution with current setup & profit 77 | 78 | `[R]` - revert back last swap 79 | 80 | `[L]` - show/hide latency chart (of Jupiter `computeRoutes()`) 81 | 82 | `[P]` - show/hide profit chart 83 | 84 | `[T]` - show/hide trade history table \*_table isn't working yet_ 85 | 86 | `[S]` - simulation mode switch (enable/disable trading) 87 | -------------------------------------------------------------------------------- /gif1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmioduszewski/solana-jupiter-bot/e07d9c358f31591c0e04ce0e80b5a6660469e23a/gif1.gif -------------------------------------------------------------------------------- /gif2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmioduszewski/solana-jupiter-bot/e07d9c358f31591c0e04ce0e80b5a6660469e23a/gif2.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-jupiter-bot", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/pmioduszewski/solana-jupiter-bot.git" 8 | }, 9 | "bin": "./src/index.js", 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "scripts": { 14 | "config": "node ./src/cli.js", 15 | "start": "node ./src/index.js && node ./src/bot.js", 16 | "trade": "node ./src/bot.js", 17 | "test": "xo && ava" 18 | }, 19 | "files": [ 20 | "./src/index.js", 21 | "./src/generator.js" 22 | ], 23 | "dependencies": { 24 | "@jup-ag/core": "^1.0.0-beta.27", 25 | "@solana/web3.js": "^1.44.0", 26 | "asciichart": "^1.5.25", 27 | "axios": "^0.27.2", 28 | "bs58": "^5.0.0", 29 | "cliui": "^7.0.4", 30 | "import-jsx": "^4.0.1", 31 | "ink": "^3.2.0", 32 | "ink-big-text": "^1.2.0", 33 | "ink-gradient": "^2.0.0", 34 | "ink-select-input": "^4.2.1", 35 | "ink-spinner": "^4.0.3", 36 | "ink-text-input": "^4.0.3", 37 | "keypress": "^0.2.1", 38 | "meow": "^9.0.0", 39 | "moment": "^2.29.3", 40 | "ora-classic": "^5.4.2", 41 | "react": "^17.0.2", 42 | "strip-ansi": "^7.0.1" 43 | }, 44 | "ava": { 45 | "require": [ 46 | "@babel/register" 47 | ] 48 | }, 49 | "babel": { 50 | "presets": [ 51 | "@babel/preset-env", 52 | "@babel/preset-react" 53 | ] 54 | }, 55 | "xo": { 56 | "extends": "xo-react", 57 | "rules": { 58 | "react/prop-types": "off" 59 | } 60 | }, 61 | "devDependencies": { 62 | "@ava/babel": "^2.0.0", 63 | "@babel/preset-env": "^7.18.2", 64 | "@babel/preset-react": "^7.17.12", 65 | "@babel/register": "^7.17.7", 66 | "chalk": "^4.1.2", 67 | "eslint-config-xo-react": "^0.27.0", 68 | "eslint-plugin-react": "^7.30.0", 69 | "eslint-plugin-react-hooks": "^4.5.0", 70 | "ink-testing-library": "^2.1.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | require("dotenv").config(); 3 | 4 | const { PublicKey } = require("@solana/web3.js"); 5 | 6 | const fs = require("fs"); 7 | const { setup } = require("./setup"); 8 | 9 | const { calculateProfit, toDecimal, toNumber } = require("./utils"); 10 | const { handleExit } = require("./exit"); 11 | const keypress = require("keypress"); 12 | const ora = require("ora-classic"); 13 | const { clearInterval } = require("timers"); 14 | const printToConsole = require("./ui"); 15 | 16 | // read config.json file 17 | const configSpinner = ora({ 18 | text: "Loading config...", 19 | discardStdin: false, 20 | }).start(); 21 | const config = JSON.parse(fs.readFileSync("./config.json")); 22 | configSpinner.succeed("Config loaded!"); 23 | 24 | // cache 25 | const cache = { 26 | startTime: new Date(), 27 | firstSwap: true, 28 | firstSwapInQueue: false, 29 | queue: {}, 30 | queueThrottle: 1, 31 | sideBuy: true, 32 | iteration: 0, 33 | iterationPerMinute: { 34 | start: performance.now(), 35 | value: 0, 36 | counter: 0, 37 | }, 38 | initialBalance: { 39 | tokenA: 0, 40 | tokenB: 0, 41 | }, 42 | 43 | currentBalance: { 44 | tokenA: 0, 45 | tokenB: 0, 46 | }, 47 | currentProfit: { 48 | tokenA: 0, 49 | tokenB: 0, 50 | }, 51 | lastBalance: { 52 | tokenA: 0, 53 | tokenB: 0, 54 | }, 55 | profit: { 56 | tokenA: 0, 57 | tokenB: 0, 58 | }, 59 | maxProfitSpotted: { 60 | buy: 0, 61 | sell: 0, 62 | }, 63 | tradeCounter: { 64 | buy: { success: 0, fail: 0 }, 65 | sell: { success: 0, fail: 0 }, 66 | }, 67 | ui: { 68 | defaultColor: config.ui.defaultColor, 69 | showPerformanceOfRouteCompChart: false, 70 | showProfitChart: true, 71 | showTradeHistory: true, 72 | hideRpc: false, 73 | showHelp: true, 74 | }, 75 | chart: { 76 | spottedMax: { 77 | buy: new Array(120).fill(0), 78 | sell: new Array(120).fill(0), 79 | }, 80 | performanceOfRouteComp: new Array(120).fill(0), 81 | }, 82 | hotkeys: { 83 | e: false, 84 | r: false, 85 | }, 86 | tradingEnabled: config.tradingEnabled, 87 | swappingRightNow: false, 88 | tradingMode: config.tradingMode, 89 | tradeHistory: new Array(), 90 | performanceOfTxStart: 0, 91 | availableRoutes: { 92 | buy: 0, 93 | sell: 0, 94 | }, 95 | }; 96 | 97 | const swap = async (jupiter, route) => { 98 | try { 99 | const performanceOfTxStart = performance.now(); 100 | cache.performanceOfTxStart = performanceOfTxStart; 101 | 102 | const { execute } = await jupiter.exchange({ 103 | routeInfo: route, 104 | }); 105 | const result = await execute(); 106 | 107 | const performanceOfTx = performance.now() - performanceOfTxStart; 108 | 109 | return [result, performanceOfTx]; 110 | } catch (error) { 111 | console.log("Swap error: ", error); 112 | } 113 | }; 114 | 115 | const failedSwapHandler = (tx, tradeEntry, route) => { 116 | const msg = tx.error.message; 117 | 118 | // update counter 119 | cache.tradeCounter[cache.sideBuy ? "buy" : "sell"].fail++; 120 | 121 | // update trade history 122 | config.storeFailedTxInHistory; 123 | 124 | // update trade history 125 | let tempHistory = cache.tradeHistory || []; 126 | tempHistory.push(tradeEntry); 127 | cache.tradeHistory = tempHistory; 128 | 129 | // add AMM to blockedAMMs 130 | const marketInfos = JSON.parse(JSON.stringify(route.marketInfos, null, 2)); 131 | // TODO: add AMM to blockedAMMs if there is error called "Unknown" 132 | // for (const market of marketInfos) { 133 | // if (msg.toLowerCase().includes("unknown")) 134 | // cache.blockedAMMs[market.amm.id] = msg; 135 | // } 136 | }; 137 | 138 | const successSwapHandler = (tx, tradeEntry, tokenA, tokenB) => { 139 | // update counter 140 | cache.tradeCounter[cache.sideBuy ? "buy" : "sell"].success++; 141 | 142 | // update balance 143 | if (cache.sideBuy) { 144 | cache.lastBalance.tokenA = cache.currentBalance.tokenA; 145 | cache.currentBalance.tokenA = 0; 146 | cache.currentBalance.tokenB = tx.outputAmount; 147 | } else { 148 | cache.lastBalance.tokenB = cache.currentBalance.tokenB; 149 | cache.currentBalance.tokenB = 0; 150 | cache.currentBalance.tokenA = tx.outputAmount; 151 | } 152 | 153 | if (cache.firstSwap) { 154 | cache.lastBalance.tokenB = tx.outputAmount; 155 | cache.initialBalance.tokenB = tx.outputAmount; 156 | } 157 | 158 | // update profit 159 | if (cache.sideBuy) { 160 | cache.currentProfit.tokenA = 0; 161 | cache.currentProfit.tokenB = calculateProfit( 162 | cache.initialBalance.tokenB, 163 | cache.currentBalance.tokenB 164 | ); 165 | } else { 166 | cache.currentProfit.tokenB = 0; 167 | cache.currentProfit.tokenA = calculateProfit( 168 | cache.initialBalance.tokenA, 169 | cache.currentBalance.tokenA 170 | ); 171 | } 172 | 173 | // update trade history 174 | let tempHistory = cache.tradeHistory || []; 175 | 176 | tradeEntry.inAmount = toDecimal( 177 | tx.inputAmount, 178 | cache.sideBuy ? tokenA.decimals : tokenB.decimals 179 | ); 180 | tradeEntry.outAmount = toDecimal( 181 | tx.outputAmount, 182 | cache.sideBuy ? tokenB.decimals : tokenA.decimals 183 | ); 184 | 185 | tradeEntry.profit = calculateProfit( 186 | cache.lastBalance[cache.sideBuy ? "tokenB" : "tokenA"], 187 | tx.outputAmount 188 | ); 189 | 190 | tempHistory.push(tradeEntry); 191 | cache.tradeHistory = tempHistory; 192 | 193 | // first swap done 194 | if (cache.firstSwap) { 195 | cache.firstSwap = false; 196 | cache.firstSwapInQueue = false; 197 | } 198 | }; 199 | 200 | const pingpongMode = async (jupiter, tokenA, tokenB) => { 201 | cache.iteration++; 202 | const date = new Date(); 203 | const i = cache.iteration; 204 | cache.queue[i] = -1; 205 | if (cache.firstSwap) cache.firstSwapInQueue = true; 206 | try { 207 | // calculate & update iteration per minute 208 | const iterationTimer = 209 | (performance.now() - cache.iterationPerMinute.start) / 1000; 210 | 211 | if (iterationTimer >= 60) { 212 | cache.iterationPerMinute.value = Number( 213 | cache.iterationPerMinute.counter.toFixed() 214 | ); 215 | cache.iterationPerMinute.start = performance.now(); 216 | cache.iterationPerMinute.counter = 0; 217 | } else cache.iterationPerMinute.counter++; 218 | 219 | // Calculate amount that will be used for trade 220 | const amountToTrade = cache.firstSwap 221 | ? cache.initialBalance.tokenA 222 | : cache.currentBalance[cache.sideBuy ? "tokenA" : "tokenB"]; 223 | const baseAmount = cache.lastBalance[cache.sideBuy ? "tokenB" : "tokenA"]; 224 | 225 | // default slippage 226 | const slippage = 1; 227 | 228 | // set input / output token 229 | const inputToken = cache.sideBuy ? tokenA : tokenB; 230 | const outputToken = cache.sideBuy ? tokenB : tokenA; 231 | 232 | // check current routes 233 | const performanceOfRouteCompStart = performance.now(); 234 | const routes = await jupiter.computeRoutes({ 235 | inputMint: new PublicKey(inputToken.address), 236 | outputMint: new PublicKey(outputToken.address), 237 | inputAmount: amountToTrade, 238 | slippage, 239 | forceFeech: true, 240 | }); 241 | 242 | // count available routes 243 | cache.availableRoutes[cache.sideBuy ? "buy" : "sell"] = 244 | routes.routesInfos.length; 245 | 246 | // update status as OK 247 | cache.queue[i] = 0; 248 | 249 | const performanceOfRouteComp = 250 | performance.now() - performanceOfRouteCompStart; 251 | 252 | // choose first route 253 | const route = await routes.routesInfos[0]; 254 | 255 | // update slippage with "profit or kill" slippage 256 | const profitOrKillSlippage = cache.firstSwap 257 | ? route.outAmountWithSlippage 258 | : cache.lastBalance[cache.sideBuy ? "tokenB" : "tokenA"]; 259 | 260 | route.outAmountWithSlippage = profitOrKillSlippage; 261 | 262 | // calculate profitability 263 | 264 | let simulatedProfit = cache.firstSwap 265 | ? 0 266 | : calculateProfit(baseAmount, await route.outAmount); 267 | 268 | // store max profit spotted 269 | if ( 270 | simulatedProfit > cache.maxProfitSpotted[cache.sideBuy ? "buy" : "sell"] 271 | ) { 272 | cache.maxProfitSpotted[cache.sideBuy ? "buy" : "sell"] = simulatedProfit; 273 | } 274 | 275 | printToConsole({ 276 | date, 277 | i, 278 | performanceOfRouteComp, 279 | inputToken, 280 | outputToken, 281 | tokenA, 282 | tokenB, 283 | route, 284 | simulatedProfit, 285 | cache, 286 | config, 287 | }); 288 | 289 | // check profitability and execute tx 290 | let tx, performanceOfTx; 291 | if ( 292 | !cache.swappingRightNow && 293 | (cache.firstSwap || 294 | cache.hotkeys.e || 295 | cache.hotkeys.r || 296 | simulatedProfit >= config.minPercProfit) 297 | ) { 298 | // hotkeys 299 | if (cache.hotkeys.e) { 300 | console.log("[E] PRESSED - EXECUTION FORCED BY USER!"); 301 | cache.hotkeys.e = false; 302 | } 303 | if (cache.hotkeys.r) { 304 | console.log("[R] PRESSED - REVERT BACK SWAP!"); 305 | } 306 | 307 | if (cache.tradingEnabled || cache.hotkeys.r) { 308 | cache.swappingRightNow = true; 309 | // store trade to the history 310 | let tradeEntry = { 311 | date: date.toLocaleString(), 312 | buy: cache.sideBuy, 313 | inputToken: inputToken.symbol, 314 | outputToken: outputToken.symbol, 315 | inAmount: toDecimal(route.inAmount, inputToken.decimals), 316 | expectedOutAmount: toDecimal(route.outAmount, outputToken.decimals), 317 | expectedProfit: simulatedProfit, 318 | }; 319 | 320 | // start refreshing status 321 | const printTxStatus = setInterval(() => { 322 | if (cache.swappingRightNow) { 323 | printToConsole({ 324 | date, 325 | i, 326 | performanceOfRouteComp, 327 | inputToken, 328 | outputToken, 329 | tokenA, 330 | tokenB, 331 | route, 332 | simulatedProfit, 333 | cache, 334 | config, 335 | }); 336 | } 337 | }, 500); 338 | 339 | [tx, performanceOfTx] = await swap(jupiter, route); 340 | 341 | // stop refreshing status 342 | clearInterval(printTxStatus); 343 | 344 | const profit = cache.firstSwap 345 | ? 0 346 | : calculateProfit( 347 | cache.currentBalance[cache.sideBuy ? "tokenB" : "tokenA"], 348 | tx.outputAmount 349 | ); 350 | 351 | tradeEntry = { 352 | ...tradeEntry, 353 | outAmount: tx.outputAmount || 0, 354 | profit, 355 | performanceOfTx, 356 | error: tx.error?.message || null, 357 | }; 358 | 359 | // handle TX results 360 | if (tx.error) failedSwapHandler(tx, tradeEntry, route); 361 | else { 362 | if (cache.hotkeys.r) { 363 | console.log("[R] - REVERT BACK SWAP - SUCCESS!"); 364 | cache.tradingEnabled = false; 365 | console.log("TRADING DISABLED!"); 366 | cache.hotkeys.r = false; 367 | } 368 | successSwapHandler(tx, tradeEntry, tokenA, tokenB); 369 | } 370 | } 371 | } 372 | 373 | if (tx) { 374 | if (!tx.error) { 375 | // change side 376 | cache.sideBuy = !cache.sideBuy; 377 | } 378 | cache.swappingRightNow = false; 379 | } 380 | 381 | printToConsole({ 382 | date, 383 | i, 384 | performanceOfRouteComp, 385 | inputToken, 386 | outputToken, 387 | tokenA, 388 | tokenB, 389 | route, 390 | simulatedProfit, 391 | cache, 392 | config, 393 | }); 394 | } catch (error) { 395 | cache.queue[i] = 1; 396 | console.log(error); 397 | } finally { 398 | delete cache.queue[i]; 399 | } 400 | }; 401 | 402 | const watcher = async (jupiter, tokenA, tokenB) => { 403 | if (!cache.swappingRightNow) { 404 | if (cache.firstSwap && Object.keys(cache.queue).length === 0) { 405 | const firstSwapSpinner = ora({ 406 | text: "Executing first swap...", 407 | discardStdin: false, 408 | }).start(); 409 | await pingpongMode(jupiter, tokenA, tokenB); 410 | if (cache.firstSwap) firstSwapSpinner.fail("First swap failed!"); 411 | else firstSwapSpinner.stop(); 412 | } else if ( 413 | !cache.firstSwap && 414 | !cache.firstSwapInQueue && 415 | Object.keys(cache.queue).length < cache.queueThrottle && 416 | cache.tradingMode === "pingpong" 417 | ) { 418 | await pingpongMode(jupiter, tokenA, tokenB); 419 | } 420 | } 421 | }; 422 | 423 | const run = async () => { 424 | try { 425 | const setupSpinner = ora({ 426 | text: "Setting up...", 427 | discardStdin: false, 428 | }).start(); 429 | const { jupiter, tokenA, tokenB, blockedAMMs } = await setup(config); 430 | setupSpinner.succeed("Setup done!"); 431 | 432 | // load blocked AMMs to cache 433 | cache.blockedAMMs = blockedAMMs; 434 | 435 | // set initial & last balance for tokenA 436 | cache.initialBalance.tokenA = toNumber(config.tradeSize, tokenA.decimals); 437 | cache.currentBalance.tokenA = cache.initialBalance.tokenA; 438 | cache.lastBalance.tokenA = cache.initialBalance.tokenA; 439 | 440 | setInterval(() => watcher(jupiter, tokenA, tokenB), config.minInterval); 441 | 442 | // hotkeys 443 | keypress(process.stdin); 444 | 445 | process.stdin.on("keypress", function (ch, key) { 446 | // console.log('got "keypress"', key); 447 | if (key && key.ctrl && key.name == "c") { 448 | cache.tradingEnabled = false; // stop all trades 449 | console.log("[CTRL] + [C] PRESS AGAIN TO EXIT!"); 450 | process.stdin.pause(); 451 | process.stdin.setRawMode(false); 452 | process.stdin.resume(); 453 | } 454 | 455 | // [E] - forced execution 456 | if (key && key.name === "e") { 457 | cache.hotkeys.e = true; 458 | } 459 | 460 | // [R] - revert back swap 461 | if (key && key.name === "r") { 462 | cache.hotkeys.r = true; 463 | } 464 | 465 | // [P] - switch profit chart visibility 466 | if (key && key.name === "p") { 467 | cache.ui.showProfitChart = !cache.ui.showProfitChart; 468 | } 469 | 470 | // [L] - switch performance chart visibility 471 | if (key && key.name === "l") { 472 | cache.ui.showPerformanceOfRouteCompChart = 473 | !cache.ui.showPerformanceOfRouteCompChart; 474 | } 475 | 476 | // [H] - switch trade history visibility 477 | if (key && key.name === "t") { 478 | cache.ui.showTradeHistory = !cache.ui.showTradeHistory; 479 | } 480 | 481 | // [I] - incognito mode (hide RPC) 482 | if (key && key.name === "i") { 483 | cache.ui.hideRpc = !cache.ui.hideRpc; 484 | } 485 | 486 | // [H] - switch help visibility 487 | if (key && key.name === "h") { 488 | cache.ui.showHelp = !cache.ui.showHelp; 489 | } 490 | 491 | // [S] - simulation mode switch 492 | if (key && key.name === "s") { 493 | cache.tradingEnabled = !cache.tradingEnabled; 494 | } 495 | }); 496 | 497 | process.stdin.setRawMode(true); 498 | process.stdin.resume(); 499 | } catch (error) { 500 | console.log(error); 501 | } finally { 502 | handleExit(config, cache); 503 | } 504 | }; 505 | 506 | run(); 507 | -------------------------------------------------------------------------------- /src/exit.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | exports.handleExit = (config, cache) => { 4 | try { 5 | // write cache to file 6 | fs.writeFileSync("./temp/cache.json", JSON.stringify(cache, null, 2)); 7 | 8 | // write blockedAMMs to file 9 | fs.writeFileSync( 10 | "./temp/blockedAMMs.json", 11 | JSON.stringify(cache.blockedAMMs, null, 2) 12 | ); 13 | 14 | // write trade history to file 15 | fs.writeFileSync( 16 | "./temp/tradeHistory.json", 17 | JSON.stringify(cache.tradeHistory, null, 2) 18 | ); 19 | 20 | // write config to file 21 | delete cache.tradeHistory; 22 | delete cache.blockedAMMs; 23 | fs.writeFileSync("./config.json", JSON.stringify(config, null, 2)); 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/generator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("dotenv").config(); 4 | const React = require("react"); 5 | const { Text, Box, useApp, useInput, Newline, useStdin } = require("ink"); 6 | const { useState, useEffect } = require("react"); 7 | const { default: SelectInput } = require("ink-select-input"); 8 | const fs = require("fs"); 9 | 10 | // import components 11 | const importJsx = require("import-jsx"); 12 | const { default: axios } = require("axios"); 13 | const { TOKEN_LIST_URL } = require("@jup-ag/core"); 14 | const { default: Spinner } = require("ink-spinner"); 15 | const { default: TextInput } = require("ink-text-input"); 16 | const BigText = require("ink-big-text"); 17 | const Gradient = require("ink-gradient"); 18 | const DefaultBox = importJsx("../Components/DefaultBox"); 19 | 20 | const EscNotification = importJsx("../Components/EscNotification"); 21 | const TabNotification = importJsx("../Components/TabNotification"); 22 | 23 | const networks = [ 24 | { label: "mainnet-beta", value: "mainnet-beta" }, 25 | { label: "testnet", value: "testnet" }, 26 | { label: "devnet", value: "devnet" }, 27 | ]; 28 | 29 | const tradingModes = [ 30 | { label: "pingpong", value: "pingpong" }, 31 | { label: "arbitrage (coming soon)", value: "arbitrage" }, 32 | ]; 33 | 34 | const App = (props) => { 35 | const [network, setNetwork] = useState(props.network || ""); 36 | const [rpcURL, setRpcURL] = useState(props.rpc); 37 | const [rpc, setRpc] = useState([]); 38 | const [isRpcsSet, setIsRpcsSet] = useState(false); 39 | const [tradingMode, setTradingMode] = useState(""); 40 | const [tokens, setTokens] = useState([]); 41 | const [tokenA, setTokenA] = useState({}); 42 | const [tokenB, setTokenB] = useState({}); 43 | const [tokenAisSet, setTokenAisSet] = useState(false); 44 | const [tokenBisSet, setTokenBisSet] = useState(false); 45 | const [tradingEnabled, setTradingEnabled] = useState(undefined); 46 | const [tradeSize, setTradeSize] = useState(""); 47 | const [minPercProfit, setMinPercProfit] = useState("1"); 48 | const [isMinPercProfitSet, setIsMinPercProfitSet] = useState(false); 49 | const [minInterval, setMinInterval] = useState("30"); 50 | const [storeFailedTxInHistory, setStoreFailedTxInHistory] = useState(true); 51 | const [readyToStart, setReadyToStart] = useState(false); 52 | 53 | const { exit } = useApp(); 54 | 55 | useInput((input, key) => { 56 | if (key.escape) exit(); 57 | if (key.tab && !isRpcsSet) { 58 | setRpc([process.env.DEFAULT_RPC]); 59 | setIsRpcsSet(true); 60 | } 61 | if (readyToStart && key.return) { 62 | const config = { 63 | tokenA, 64 | tokenB, 65 | tradingMode, 66 | tradeSize: parseFloat(tradeSize), 67 | network, 68 | rpc, 69 | minPercProfit: parseFloat(minPercProfit), 70 | minInterval: parseInt(minInterval), 71 | tradingEnabled, 72 | storeFailedTxInHistory, 73 | ui: { 74 | defaultColor: "cyan", 75 | }, 76 | }; 77 | 78 | // save config to config.json file 79 | fs.writeFileSync("./config.json", JSON.stringify(config, null, 2)); 80 | 81 | // save tokenst to tokens.json file 82 | fs.writeFileSync("./temp/tokens.json", JSON.stringify(tokens, null, 2)); 83 | exit(); 84 | } 85 | }); 86 | 87 | const { setRawMode } = useStdin(); 88 | 89 | useEffect(() => { 90 | setRawMode(true); 91 | 92 | return () => { 93 | setRawMode(false); 94 | }; 95 | }); 96 | 97 | useEffect(() => { 98 | network != "" && 99 | axios.get(TOKEN_LIST_URL[network]).then((res) => setTokens(res.data)); 100 | }, [network]); 101 | 102 | if (!network) 103 | return ( 104 | 105 | 106 | Select Solana Network: 107 | 108 | setNetwork(item.value)} 111 | /> 112 | 113 | 114 | ); 115 | 116 | if (!isRpcsSet) 117 | return ( 118 | 119 | 120 | Paste Solana RPC: 121 | 122 | setRpcURL(url)} 124 | value={rpcURL || ""} 125 | placeholder={ 126 | process.env.DEFAULT_RPC 127 | ? process.env.DEFAULT_RPC 128 | : "https://my-super-expensive-quick-af-rpc.com" 129 | } 130 | onSubmit={(url) => { 131 | setRpc([...rpc, url]); 132 | setRpcURL(""); 133 | }} 134 | /> 135 | 136 | {rpc.map((url, index) => ( 137 | 142 | {index === 0 ? "MAIN" : `${index + 1}. `}{" "} 143 | {url} 144 | 145 | ))} 146 | 147 | {process.env.DEFAULT_RPC && rpc.length === 0 && ( 148 | 149 | )} 150 | {rpc.length > 0 && } 151 | 152 | 153 | 154 | 155 | ); 156 | 157 | if (!tradingMode) 158 | return ( 159 | 160 | 161 | Choose Trading Mode: 162 | 163 | setTradingMode(item.value)} 166 | /> 167 | 168 | 169 | ); 170 | 171 | if (!readyToStart) { 172 | return ( 173 | 174 | Config 175 | 176 | Network: {network} 177 | 178 | 179 | Trading Mode: {tradingMode} 180 | 181 | {tokens?.length > 0 ? ( 182 | <> 183 | 184 | Available Tokens:{" "} 185 | {tokens?.length || 0} 186 | 187 | 188 | {/* SET TOKEN A */} 189 | 190 | Token A:{" "} 191 | {!tokenAisSet ? ( 192 | <> 193 | setTokenA({ symbol: value })} 196 | placeholder="type token symbol & use arrow keys to select hint" 197 | focus={!tokenAisSet} 198 | onSubmit={() => setTokenAisSet(true)} 199 | /> 200 | Case Sensitive! 201 | 202 | ) : ( 203 | {tokenA?.symbol} 204 | )} 205 | 206 | 207 | {tokenA?.symbol?.length > 1 && !tokenA?.address && ( 208 | ({ label: t.symbol, value: t.address })) 211 | .filter((t) => t.label.includes(tokenA.symbol))} 212 | limit={4} 213 | onSelect={(s) => setTokenA({ ...tokenA, address: s.value })} 214 | /> 215 | )} 216 | {tokenA?.address && ( 217 | 218 | Token A Address:{" "} 219 | {tokenA?.address} 220 | 221 | )} 222 | 223 | {/* SET TOKEN B */} 224 | 225 | Token B:{" "} 226 | {!tokenBisSet ? ( 227 | <> 228 | setTokenB({ symbol: value })} 231 | placeholder="type token symbol & use arrow keys to select hint" 232 | focus={ 233 | tokenB?.address ? false : tokenA?.address ? true : false 234 | } 235 | onSubmit={() => setTokenBisSet(true)} 236 | /> 237 | Case Sensitive! 238 | 239 | ) : ( 240 | {tokenB?.symbol} 241 | )} 242 | 243 | {tokenB?.symbol?.length > 1 && 244 | tokenA?.address && 245 | !tokenB?.address && ( 246 | ({ label: t.symbol, value: t.address })) 249 | .filter((t) => t.label.includes(tokenB.symbol))} 250 | limit={4} 251 | onSelect={(s) => setTokenB({ ...tokenB, address: s.value })} 252 | /> 253 | )} 254 | {tokenB?.address && ( 255 | 256 | Token B Address:{" "} 257 | {tokenB?.address} 258 | 259 | )} 260 | 261 | ) : ( 262 | 263 | Available Tokens: 264 | 265 | )} 266 | 267 | 268 | {tokenAisSet && tokenBisSet && ( 269 | 270 | Allow Trading: 271 | {tradingEnabled === undefined ? ( 272 | setTradingEnabled(item.value)} 278 | itemComponent={(item) => ( 279 | 280 | {item.label} 281 | 282 | )} 283 | onHighlight={(item) => ( 284 | 285 | {tradingEnabled == item.value ? ">" : " "} 286 | {item.label} 287 | 288 | )} 289 | /> 290 | ) : ( 291 | {tradingEnabled ? "true" : "false"} 292 | )} 293 | 294 | )} 295 | 296 | {tokenAisSet && tokenBisSet && tradingEnabled !== undefined && ( 297 | <> 298 | 299 | Min. % Profit: 300 | {!isMinPercProfitSet ? ( 301 | setMinPercProfit(value)} 304 | placeholder="example 0.10" 305 | onSubmit={() => setIsMinPercProfitSet(true)} 306 | /> 307 | ) : ( 308 | {minPercProfit} 309 | )} 310 | 311 | 312 | {isMinPercProfitSet && ( 313 | 314 | Trade Size: 315 | setTradeSize(value)} 318 | placeholder="example 0.10" 319 | onSubmit={() => setReadyToStart(true)} 320 | /> 321 | 322 | )} 323 | 324 | )} 325 | 326 | 327 | 328 | ); 329 | } 330 | 331 | if (readyToStart) { 332 | return ( 333 | 334 | 335 | 336 | 337 | 338 | 339 | ); 340 | } 341 | 342 | return ; 343 | }; 344 | 345 | module.exports = App; 346 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | const React = require("react"); 4 | const importJsx = require("import-jsx"); 5 | const { render } = require("ink"); 6 | const meow = require("meow"); 7 | 8 | const generator = importJsx("./generator"); 9 | 10 | const cli = meow(` 11 | Usage 12 | $ solana-jupiter-bot 13 | 14 | Options 15 | --name Your name 16 | 17 | Examples 18 | $ solana-jupiter-bot --name=Jane 19 | Hello, Jane 20 | `); 21 | 22 | console.clear(); 23 | 24 | render(React.createElement(generator, cli.flags)).waitUntilExit(); 25 | -------------------------------------------------------------------------------- /src/setup.js: -------------------------------------------------------------------------------- 1 | const { Jupiter } = require("@jup-ag/core"); 2 | const { Connection, Keypair } = require("@solana/web3.js"); 3 | const bs58 = require("bs58"); 4 | const fs = require("fs"); 5 | 6 | const setup = async (config) => { 7 | try { 8 | // read tokens.json file 9 | const tokens = JSON.parse(fs.readFileSync("./temp/tokens.json")); 10 | 11 | // find tokens full Object 12 | const tokenA = tokens.find((t) => t.address === config.tokenA.address); 13 | const tokenB = tokens.find((t) => t.address === config.tokenB.address); 14 | 15 | // check wallet 16 | if (!process.env.SOLANA_WALLET_PRIVATE_KEY) 17 | console.log("Wallet is not set") && process.exit(1); 18 | 19 | const wallet = Keypair.fromSecretKey( 20 | bs58.decode(process.env.SOLANA_WALLET_PRIVATE_KEY) 21 | ); 22 | 23 | // connect to RPC 24 | const connection = new Connection(config.rpc[0]); 25 | 26 | const jupiter = await Jupiter.load({ 27 | connection, 28 | cluster: config.network, 29 | user: wallet, 30 | }); 31 | 32 | // read blocked AMMs from blockedAMMs.json 33 | const blockedAMMs = {}; 34 | if (fs.existsSync("./blockedAMMs.json")) { 35 | const blockedAMMs = JSON.parse(fs.readFileSync("./blockedAMMs.json")); 36 | } 37 | 38 | return { jupiter, tokenA, tokenB, blockedAMMs }; 39 | } catch (error) { 40 | console.log(error); 41 | } 42 | }; 43 | exports.setup = setup; 44 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | const ui = require("cliui")({ width: 140 }); 2 | const chart = require("asciichart"); 3 | const moment = require("moment"); 4 | const chalk = require("chalk"); 5 | const { toDecimal } = require("./utils"); 6 | 7 | function printToConsole({ 8 | date, 9 | i, 10 | performanceOfRouteComp, 11 | inputToken, 12 | outputToken, 13 | tokenA, 14 | tokenB, 15 | route, 16 | simulatedProfit, 17 | cache, 18 | config, 19 | }) { 20 | try { 21 | // update max profitability spotted chart 22 | if (cache.ui.showProfitChart) { 23 | let spottetMaxTemp = 24 | cache.chart.spottedMax[cache.sideBuy ? "buy" : "sell"]; 25 | spottetMaxTemp.shift(); 26 | spottetMaxTemp.push( 27 | simulatedProfit === Infinity 28 | ? 0 29 | : parseFloat(simulatedProfit.toFixed(2)) 30 | ); 31 | cache.chart.spottedMax.buy = spottetMaxTemp; 32 | } 33 | 34 | // update performance chart 35 | if (cache.ui.showPerformanceOfRouteCompChart) { 36 | let performanceTemp = cache.chart.performanceOfRouteComp; 37 | performanceTemp.shift(); 38 | performanceTemp.push(parseInt(performanceOfRouteComp.toFixed())); 39 | cache.chart.performanceOfRouteComp = performanceTemp; 40 | } 41 | 42 | // check swap status 43 | let swapStatus; 44 | if (cache.swappingRightNow) { 45 | swapStatus = performance.now() - cache.performanceOfTxStart; 46 | } 47 | 48 | // refresh console before print 49 | console.clear(); 50 | ui.resetOutput(); 51 | 52 | // show HOTKEYS HELP 53 | if (cache.ui.showHelp) { 54 | ui.div( 55 | chalk.gray("[H] - show/hide help"), 56 | chalk.gray("[CTRL]+[C] - exit"), 57 | chalk.gray("[I] - incognito RPC") 58 | ); 59 | ui.div( 60 | chalk.gray("[L] - show/hide latency chart"), 61 | chalk.gray("[P] - show/hide profit chart"), 62 | chalk.gray("[T] - show/hide trade history") 63 | ); 64 | ui.div( 65 | chalk.gray("[E] - force execution"), 66 | chalk.gray("[R] - revert back swap"), 67 | chalk.gray("[S] - simulation mode switch") 68 | ); 69 | ui.div(" "); 70 | } 71 | 72 | ui.div( 73 | { 74 | text: `TIMESTAMP: ${chalk[cache.ui.defaultColor]( 75 | date.toLocaleString() 76 | )}`, 77 | }, 78 | { 79 | text: `I: ${ 80 | i % 2 === 0 81 | ? chalk[cache.ui.defaultColor].bold(i) 82 | : chalk[cache.ui.defaultColor](i) 83 | } | ${chalk.bold[cache.ui.defaultColor]( 84 | cache.iterationPerMinute.value 85 | )} i/min`, 86 | }, 87 | { 88 | text: `RPC: ${chalk[cache.ui.defaultColor]( 89 | cache.ui.hideRpc 90 | ? `${config.rpc[0].slice(0, 5)}...${config.rpc[0].slice(-5)}` 91 | : config.rpc[0] 92 | )}`, 93 | } 94 | ); 95 | 96 | ui.div( 97 | { 98 | text: `STARTED: ${chalk[cache.ui.defaultColor]( 99 | moment(cache.startTime).fromNow() 100 | )}`, 101 | }, 102 | { 103 | text: `LOOKUP (ROUTE): ${chalk.bold[cache.ui.defaultColor]( 104 | performanceOfRouteComp.toFixed() 105 | )} ms`, 106 | }, 107 | { 108 | text: `MIN INTERVAL: ${chalk[cache.ui.defaultColor]( 109 | config.minInterval 110 | )} ms QUEUE: ${chalk[cache.ui.defaultColor]( 111 | Object.keys(cache.queue).length 112 | )}/${chalk[cache.ui.defaultColor](cache.queueThrottle)}`, 113 | } 114 | ); 115 | 116 | ui.div( 117 | " ", 118 | " ", 119 | Object.values(cache.queue) 120 | .map( 121 | (v) => `${chalk[v === 0 ? "green" : v < 0 ? "yellow" : "red"]("●")}` 122 | ) 123 | .join(" ") 124 | ); 125 | 126 | if (cache.ui.showPerformanceOfRouteCompChart) 127 | ui.div( 128 | chart.plot(cache.chart.performanceOfRouteComp, { 129 | padding: " ".repeat(10), 130 | height: 5, 131 | }) 132 | ); 133 | 134 | ui.div(""); 135 | ui.div(chalk.gray("-".repeat(140))); 136 | 137 | ui.div( 138 | `${ 139 | cache.tradingEnabled 140 | ? "TRADING" 141 | : chalk.bold.magentaBright("SIMULATION") 142 | }: ${chalk.bold[cache.ui.defaultColor]( 143 | inputToken.symbol 144 | )} -> ${chalk.bold[cache.ui.defaultColor](outputToken.symbol)}`, 145 | `ROUTES: ${chalk.bold.yellowBright( 146 | cache.availableRoutes[cache.sideBuy ? "buy" : "sell"] 147 | )}`, 148 | { 149 | text: cache.swappingRightNow 150 | ? chalk.bold[ 151 | swapStatus < 45000 152 | ? "greenBright" 153 | : swapStatus < 60000 154 | ? "yellowBright" 155 | : "redBright" 156 | ](`SWAPPING ... ${swapStatus.toFixed()} ms`) 157 | : " ", 158 | } 159 | ); 160 | ui.div(""); 161 | 162 | ui.div("BUY", "SELL", " ", " "); 163 | 164 | ui.div( 165 | { 166 | text: `SUCCESS : ${chalk.bold.green(cache.tradeCounter.buy.success)}`, 167 | }, 168 | { 169 | text: `SUCCESS: ${chalk.bold.green(cache.tradeCounter.sell.success)}`, 170 | }, 171 | { 172 | text: " ", 173 | }, 174 | { 175 | text: " ", 176 | } 177 | ); 178 | ui.div( 179 | { 180 | text: `FAIL: ${chalk.bold.red(cache.tradeCounter.buy.fail)}`, 181 | }, 182 | { 183 | text: `FAIL: ${chalk.bold.red(cache.tradeCounter.sell.fail)}`, 184 | }, 185 | { 186 | text: " ", 187 | }, 188 | { 189 | text: " ", 190 | } 191 | ); 192 | ui.div(""); 193 | 194 | ui.div( 195 | { 196 | text: `IN: ${chalk.yellowBright( 197 | toDecimal(route.inAmount, inputToken.decimals) 198 | )} ${chalk[cache.ui.defaultColor](inputToken.symbol)}`, 199 | }, 200 | { 201 | text: `PROFIT: ${chalk[simulatedProfit > 0 ? "greenBright" : "red"]( 202 | simulatedProfit.toFixed(2) 203 | )} %`, 204 | }, 205 | { 206 | text: `OUT: ${chalk[simulatedProfit > 0 ? "greenBright" : "red"]( 207 | toDecimal(route.outAmount, outputToken.decimals) 208 | )} ${chalk[cache.ui.defaultColor](outputToken.symbol)}`, 209 | }, 210 | { 211 | text: `NOMINAL SIZE: ${chalk[cache.ui.defaultColor]( 212 | `${config.tradeSize} ${inputToken.symbol}` 213 | )}`, 214 | }, 215 | { 216 | text: `SLIPPAGE: ${chalk.magentaBright( 217 | toDecimal(route.outAmountWithSlippage, outputToken.decimals) 218 | )}`, 219 | } 220 | ); 221 | 222 | ui.div(" "); 223 | 224 | ui.div("CURRENT BALANCE", "LAST BALANCE", "INIT BALANCE", "PROFIT", " "); 225 | 226 | ui.div( 227 | `${chalk[cache.currentBalance.tokenA > 0 ? "yellowBright" : "gray"]( 228 | toDecimal(cache.currentBalance.tokenA, tokenA.decimals) 229 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 230 | 231 | `${chalk[cache.lastBalance.tokenA > 0 ? "yellowBright" : "gray"]( 232 | toDecimal(cache.lastBalance.tokenA, tokenA.decimals) 233 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 234 | 235 | `${chalk[cache.initialBalance.tokenA > 0 ? "yellowBright" : "gray"]( 236 | toDecimal(cache.initialBalance.tokenA, tokenA.decimals) 237 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 238 | 239 | `${chalk[cache.currentProfit.tokenA > 0 ? "greenBright" : "redBright"]( 240 | cache.currentProfit.tokenA.toFixed(2) 241 | )} %`, 242 | " " 243 | ); 244 | 245 | ui.div( 246 | `${chalk[cache.currentBalance.tokenB > 0 ? "yellowBright" : "gray"]( 247 | toDecimal(cache.currentBalance.tokenB, tokenB.decimals) 248 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 249 | 250 | `${chalk[cache.lastBalance.tokenB > 0 ? "yellowBright" : "gray"]( 251 | toDecimal(cache.lastBalance.tokenB, tokenB.decimals) 252 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 253 | 254 | `${chalk[cache.initialBalance.tokenB > 0 ? "yellowBright" : "gray"]( 255 | toDecimal(cache.initialBalance.tokenB, tokenB.decimals) 256 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 257 | 258 | `${chalk[cache.currentProfit.tokenB > 0 ? "greenBright" : "redBright"]( 259 | cache.currentProfit.tokenB.toFixed(2) 260 | )} %`, 261 | " " 262 | ); 263 | 264 | ui.div(chalk.gray("-".repeat(140))); 265 | ui.div(""); 266 | 267 | if (cache.ui.showProfitChart) { 268 | ui.div( 269 | chart.plot(cache.chart.spottedMax[cache.sideBuy ? "buy" : "sell"], { 270 | padding: " ".repeat(10), 271 | height: 4, 272 | colors: [simulatedProfit > 0 ? chart.lightgreen : chart.lightred], 273 | }) 274 | ); 275 | 276 | ui.div(""); 277 | } 278 | 279 | ui.div( 280 | { 281 | text: `MAX (BUY): ${chalk[cache.ui.defaultColor]( 282 | cache.maxProfitSpotted.buy.toFixed(2) 283 | )} %`, 284 | }, 285 | { 286 | text: `MAX (SELL): ${chalk[cache.ui.defaultColor]( 287 | cache.maxProfitSpotted.sell.toFixed(2) 288 | )} %`, 289 | }, 290 | { text: " " } 291 | ); 292 | 293 | ui.div(""); 294 | ui.div(chalk.gray("-".repeat(140))); 295 | ui.div(""); 296 | 297 | if (cache.ui.showTradeHistory) { 298 | ui.div( 299 | { text: `TIMESTAMP` }, 300 | { text: `SIDE` }, 301 | { text: `IN` }, 302 | { text: `OUT` }, 303 | { text: `PROFIT` }, 304 | { text: `EXP. OUT` }, 305 | { text: `EXP. PROFIT` }, 306 | { text: `ERROR` } 307 | ); 308 | 309 | ui.div(" "); 310 | 311 | if (cache?.tradeHistory?.length > 0) { 312 | const tableData = [...cache.tradeHistory].slice(-5); 313 | tableData.map((entry, i) => 314 | ui.div( 315 | { text: `${entry.date}`, border: true }, 316 | { text: `${entry.buy ? "BUY" : "SELL"}`, border: true }, 317 | { text: `${entry.inAmount} ${entry.inputToken}`, border: true }, 318 | { text: `${entry.outAmount} ${entry.outputToken}`, border: true }, 319 | { 320 | text: `${entry.profit > 0 ? entry.profit.toFixed(2) : "-"} %`, 321 | border: true, 322 | }, 323 | { 324 | text: `${entry.expectedOutAmount} ${entry.inputToken}`, 325 | border: true, 326 | }, 327 | { 328 | text: `${entry.expectedProfit.toFixed(2)} %`, 329 | border: true, 330 | }, 331 | { 332 | text: `${entry.error ? chalk.bold.redBright(entry.error) : "-"}`, 333 | border: true, 334 | } 335 | ) 336 | ); 337 | } 338 | } 339 | ui.div(""); 340 | 341 | // print UI 342 | console.log(ui.toString()); 343 | 344 | delete swapStatus; 345 | } catch (error) { 346 | console.log(error); 347 | } 348 | } 349 | 350 | module.exports = printToConsole; 351 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const calculateProfit = (oldVal, newVal) => ((newVal - oldVal) / oldVal) * 100; 2 | 3 | const toDecimal = (number, decimals) => 4 | parseFloat(number / 10 ** decimals).toFixed(decimals); 5 | 6 | const toNumber = (number, decimals) => number * 10 ** decimals; 7 | 8 | module.exports = { 9 | calculateProfit, 10 | toDecimal, 11 | toNumber, 12 | }; 13 | --------------------------------------------------------------------------------