├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gifs ├── bot.gif ├── history.gif └── wizard.gif ├── .github └── workflows │ └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── bot │ ├── cache.js │ ├── exit.js │ ├── index.js │ ├── setup.js │ ├── swap.js │ └── ui │ │ ├── index.js │ │ ├── intro.js │ │ ├── listenHotkeys.js │ │ └── printToConsole.js ├── constants │ └── index.js ├── index.js ├── utils │ ├── index.js │ └── transaction.js └── wizard │ ├── Components │ ├── EscNotification.js │ ├── Help.js │ ├── Layout.js │ ├── Main.js │ ├── Menu.js │ ├── Router.js │ ├── TabNotification.js │ └── WizardHeader.js │ ├── Pages │ ├── Advanced.js │ ├── Confirm.js │ ├── Network.js │ ├── Priority.js │ ├── Profit.js │ ├── Rpc.js │ ├── Slippage.js │ ├── Strategy.js │ ├── Tokens.js │ ├── TradingMax.js │ └── TradingSize.js │ ├── WizardContext.js │ ├── WizardProvider.js │ ├── index.js │ └── reducer.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 | DEBUG=false 2 | SKIP_INTRO=false 3 | UI_COLOR=cyan 4 | TRADING_ENABLED=true 5 | WRAP_UNWRAP_SOL=true 6 | NODE_ENV=production 7 | SOLANA_WALLET_PRIVATE_KEY=PRIVATE_KEY_HERE_PROTECT_THIS_AT_ALL_COSTS 8 | DEFAULT_RPC=https://change-this-url-in-env 9 | ALT_RPC_LIST=https://change-this-url-in-env.gov,https://change-alt-urls-in-env.com 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "prettier" 11 | ], 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": "latest" 17 | }, 18 | "rules": { 19 | "prettier/prettier": "error", 20 | "react/prop-types": "off" 21 | 22 | }, 23 | "plugins": [ 24 | "react", 25 | "prettier" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gifs/bot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARBProtocol/solana-jupiter-bot/d3e13c1e36af9ae6fa3a7e4b7300ce117736cf81/.gifs/bot.gif -------------------------------------------------------------------------------- /.gifs/history.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARBProtocol/solana-jupiter-bot/d3e13c1e36af9ae6fa3a7e4b7300ce117736cf81/.gifs/history.gif -------------------------------------------------------------------------------- /.gifs/wizard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARBProtocol/solana-jupiter-bot/d3e13c1e36af9ae6fa3a7e4b7300ce117736cf81/.gifs/wizard.gif -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '40 6 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | days-before-issue-stale: 30 25 | days-before-issue-close: 14 26 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.' 27 | close-issue-message: 'This issue was closed because it has been inactive for 14 days since being marked as stale.' 28 | days-before-pr-stale: -1 29 | days-before-pr-close: -1 30 | stale-issue-label: 'stale' 31 | -------------------------------------------------------------------------------- /.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 112 | 113 | # debug stuff 114 | *.cpuprofile 115 | *.heapprofile 116 | *.heapsnapshot 117 | 118 | # Qodana 119 | .idea 120 | qodana.yaml -------------------------------------------------------------------------------- /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 | > ⚠️ This bot can lead to loss of your funds, use at your own risk. Start with small amounts and protect your keys. 2 | 3 | > This README is not complete. Try asking the community on the [ARB Discord](https://discord.gg/Z8JJCuq4) if you have any questions. 4 | 5 | --- 6 | 7 | # Arb Jupiter Bot - JUPV4 8 | 9 | [![JupiterSDK](https://img.shields.io/badge/Jupiter%20SDK-v1-%2392EEF0.svg?logo=data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDY0IDY0IiB3aWR0aD0iNjQiIGhlaWdodD0iNjQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iUCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiLz48bGluZWFyR3JhZGllbnQgaWQ9ImcxIiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImcyIiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImczIiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9Imc0IiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9Imc1IiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9Imc2IiB4MT0iNTEuOCIgeTE9IjguMyIgeDI9IjkuNiIgeTI9IjYzLjQiIGhyZWY9IiNQIj48c3RvcCBzdG9wLWNvbG9yPSIjZjlhNDNhIi8+PHN0b3Agb2Zmc2V0PSIuMyIgc3RvcC1jb2xvcj0iI2ZmYzQ1ZSIvPjxzdG9wIG9mZnNldD0iLjUiIHN0b3AtY29sb3I9IiM5M2VlZWYiLz48c3RvcCBvZmZzZXQ9Ii44IiBzdG9wLWNvbG9yPSIjODFmNGZlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjM2JkYmZiIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHN0eWxlPi5he2ZpbGw6dXJsKCNnMSl9LmJ7ZmlsbDp1cmwoI2cyKX0uY3tmaWxsOnVybCgjZzMpfS5ke2ZpbGw6dXJsKCNnNCl9LmV7ZmlsbDp1cmwoI2c1KX0uZntmaWxsOnVybCgjZzYpfTwvc3R5bGU+PHBhdGggY2xhc3M9ImEiIGQ9Im0xOS4xIDU0YzQuMyAyLjUgNy43IDUuNyAxMC4xIDkuMy00LjYtMC41LTktMi0xMy00LjMtNC0yLjMtNy40LTUuNC0xMC4xLTkuMSA0LjMgMC4zIDguOCAxLjYgMTMgNC4xeiIvPjxwYXRoIGNsYXNzPSJiIiBkPSJtMzkuOCA2Mi42cS0zLjEgMC43LTYuMyAwLjhjLTIuNi00LjgtNi45LTkuMy0xMi41LTEyLjYtNS42LTMuMy0xMS43LTQuOC0xNy4yLTQuNnEtMS41LTIuOC0yLjQtNS45YzYuNC0yLjEgMTUuMy0wLjkgMjMuNSAzLjggOC4yIDQuOCAxMy42IDExLjkgMTQuOSAxOC41eiIvPjxwYXRoIGNsYXNzPSJjIiBkPSJtNDYgOS4yYy03LjctNC41LTE1LjUtNy4xLTIyLjUtOCA0LjItMS4yIDguNi0xLjUgMTIuOS0wLjkgNC40IDAuNSA4LjYgMS45IDEyLjMgNC4xIDMuOCAyLjIgNy4xIDUuMSA5LjcgOC42IDIuNyAzLjUgNC42IDcuNSA1LjYgMTEuOC00LjItNS43LTEwLjQtMTEuMi0xOC0xNS42eiIvPjxwYXRoIGNsYXNzPSJkIiBkPSJtNTUuOCA1My43cTAgMC4xLTAuMSAwLjEtMC42IDAuNy0xLjMgMS4zYy0wLjQtMi44LTEuNC01LjgtMy4xLTguNy0zLjYtNi43LTkuOS0xMi44LTE3LjctMTcuMy03LjgtNC42LTE2LjItNy0yMy44LTYuOS0zLjQgMC4xLTYuNCAwLjctOS4xIDEuN3EwLjItMC45IDAuNS0xLjggMCAwIDAtMC4xYzEuOS0zLjIgNi41LTUgMTIuNi00LjggNyAwLjEgMTUgMi42IDIyLjYgNy4xIDcuNiA0LjQgMTMuOCAxMC4xIDE3LjQgMTYuMSAzLjEgNS4yIDMuOSAxMC4xIDIgMTMuM3oiLz48cGF0aCBjbGFzcz0iZSIgZD0ibTQ4LjUgNTkuM3EtMS45IDEuMS00IDEuOS0wLjYtMi44LTEuOC01LjRjLTMtNi4yLTguNS0xMS45LTE1LjQtMTUuOS02LjktNC0xNC41LTYtMjEuNC01LjRxLTIuOCAwLjItNS42IDEtMC4zLTIuMi0wLjMtNC40YzYuMi00LjcgMTktMy43IDMwLjYgMy4xIDExLjcgNi43IDE5IDE3LjQgMTcuOSAyNS4xeiIvPjxwYXRoIGNsYXNzPSJmIiBkPSJtNjIuNCA0Mi4yYy0zLjEtOC41LTExLjEtMTcuNC0yMi41LTI0LTExLjMtNi42LTIzLTkuMS0zMS45LTcuNiAyLjEtMi41IDYuMi0zLjcgMTEuNi0zLjYgNyAwLjIgMTUgMi44IDIyLjcgNy4yIDcuNiA0LjUgMTMuOCAxMC4yIDE3LjUgMTYuMiAyLjcgNC41IDMuNiA4LjcgMi42IDExLjh6Ii8+PC9zdmc+)](https://docs.jup.ag/jupiter-core/jupiter-sdk/v1) 10 | [![](https://img.shields.io/badge/License-MIT-brightgreen)](#license) 11 | [![Twitter](https://img.shields.io/twitter/follow/ArbProtocol.svg?style=social&label=ArbProtocol)](https://twitter.com/ArbProtocol) 12 | [![Discord](https://img.shields.io/discord/985095351293845514?logo=discord&logoColor=white&style=flat-square)](https://discord.gg/wcxYzfKNaE) 13 | 14 | This bot is an open-source CLI tool that allows you to automate your crypto trading strategies on the Solana blockchain. The bot is currently written in JS and uses the Jupiter V4 SDK to find routes and execute trades. 15 | 16 | ## nav 17 | 18 | ### [features](#features) · [CLI UI](#cli-ui) · ⚡️[install](#install) · [quickstart](#quickstart) · [hotkeys](#hotkeys) · [contributing](#contributing) · [license](#license) · [risk](#risk-disclaimer) 19 | 20 | --- 21 | 22 | ## features 23 | 24 | - [x] mainnet network support 25 | - [x] all **Jupiter Aggregator** coins 26 | - [x] easy to use **Config Wizard** 27 | - [x] CLI UI 28 | - **Trading strategies** 29 | - [x] Arbitrage strategy 30 | - [x] PingPong strategy 31 | - **Slippage management** 32 | - [x] BPS slippage 33 | - **Profit management** 34 | - [x] min profit % _(target)_ 35 | - **Charts** 36 | - [x] latency chart 37 | - [x] simulated profit chart 38 | - **History & Statistics** 39 | - [x] history of trades (CLI table) 40 | - [x] statistics of all trades 41 | - **Advanced** 42 | 43 | - [x] custom UI colors 44 | - [x] min. interval beetween iterations _(to avoid 429)_ 45 | 46 | - [x] hotkeys 47 | - [x] lots of fun 48 | 49 | # CLI UI 50 | 51 | > ⚠️ EPILEPSY WARNING - CLI UI is constantly refreshed and may be disruptive for sensitive people 52 | 53 | 🔥 CLI UI helps you monitor your trading strategy with little impact on performance. 54 | 55 | CLI UI currently displays a simulated profit chart and latency chart. The latency chart shows you the time taken to compute routes with Jupiter SDK. 56 | 57 | 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. 58 | 59 | 💡 UI elements can be hidden or shown using [hotkeys](#hotkeys). 60 | 61 | ![](https://github.com/arbprotocol/solana-jupiter-bot/blob/main/.gifs/wizard.gif) 62 | 63 | ![](https://github.com/arbprotocol/solana-jupiter-bot/blob/main/.gifs/bot.gif) 64 | 65 | ![](https://github.com/arbprotocol/solana-jupiter-bot/blob/main/.gifs/history.gif) 66 | 67 | · [back to top](#nav) · 68 | 69 | # install 70 | 71 | > Please don't use `npm`, use `yarn` instead. 72 | 73 | ```bash 74 | $ git clone https://github.com/arbprotocol/solana-jupiter-bot && cd solana-jupiter-bot 75 | $ yarn 76 | ``` 77 | 78 | Set your wallet private key in the `.env` file 79 | 80 | ```js 81 | SOLANA_WALLET_PRIVATE_KEY=hgq847chjjjJUPITERiiiISaaaAWESOMEaaANDiiiIwwWANNAbbbBErrrRICHh 82 | ``` 83 | 84 | Set the default RPC 85 | **_ARB Protocol RPC is used by default_** 86 | 87 | ```js 88 | DEFAULT_RPC=https://my-super-lazy-rpc.gov 89 | ALT_RPC_LIST=https://change-this-url-in-env.gov,https://change-alt-urls-in-env.com 90 | ``` 91 | 92 | · [back to top](#nav) · 93 | 94 | # quickstart 95 | 96 | 1. Clone this repo 97 | 2. Install dependencies 98 | 3. Set your wallet private key in the `.env` file 99 | 4. Run `yarn start` to start the Config Wizard 100 | 5. Follow the steps in the Config Wizard 101 | 102 | ``` 103 | Usage: 104 | $ yarn start 105 | This will open Config Wizard and start the bot 106 | 107 | $ yarn trade 108 | Start Bot and Trade with latest config 109 | 110 | $ yarn wizard 111 | Start Config Wizard only 112 | ``` 113 | 114 | ### some tips 👀 115 | 116 | 🔨 The bot is a Tool, not a holy grail that will make you rich just by running it. If you don't know what you are doing, you WILL lose money. 117 | 118 | 👉 RPC / Network speed & good trading strategy is the key to success. You can speed up the bor but disabling AMMS not being used or too slow. 119 | 120 | 🙉 Not everything is so obvious. eg. a larger trade size can lead to smaller profits than a lower trade size. 121 | 122 | 🛑 If you frequently get 429 errors, you should increase the `minInterval` in the config (`Advanced` step). 123 | 124 | 🥵 You want to `arbitrage` USDC or USDT? YES! Guess what? E V E R Y O N E wants to arbitrage USDC or USDT. If you don't have access to a super fancy RPC, there is a good chance you will end up with lots of 'Slippage errors'. 125 | 126 | 🥶 `SLIPPAGE ERRORS` are not bot errors. They are part of the Solana blockchain. If you want to avoid them you have to go with the super fancy RPC or pick the less popular pairs/coins - just try to find out which ones hold profit opportunities. 127 | 128 | 🏓 `PingPong` strategy is really poweful on sideways markets. Search through charts the coins that are constantly moving up and down. 129 | 130 | 📡 If you can't run the bot, it's likely something wrong with the network, RPC, or config file. 131 | 132 | 😬 Don't like the intro? You can disable it in the .env file with `SKIP_INTRO=true` 133 | 134 | · [back to top](#nav) · 135 | 136 | # hotkeys 137 | 138 | While the bot is running, you can use some hotkeys that will change the behaviour of the bot or UI 139 | 140 | `[H]` - show/hide Help 141 | 142 | `[CTRL] + [C]` - obviously it will kill the bot 143 | 144 | `[I]` - incognito RPC _Hide RPC address - helpful when streaming / screenshotting_ 145 | 146 | `[E]` - force execution with current setup & profit _*(may result in loss - you bypass all rules)*_ 147 | 148 | `[R]` - force execution, stop the bot _*(may result in loss - you bypass all rules)*_ 149 | 150 | `[L]` - show/hide latency chart (of Jupiter `computeRoutes()`) 151 | 152 | `[P]` - show/hide profit chart 153 | 154 | `[T]` - show/hide trade history table \*_table isn't working yet_ 155 | 156 | `[S]` - simulation mode switch (enable/disable trading) 157 | 158 | · [back to top](#nav) · 159 | 160 | # Balance management and error handling 161 | 162 | Balances are checked when the bot loads and also as it is running to avoid it running when there is insufficient balance (to help prevent spamming NSF transactions). There is also a setting in the bot that will end the process when too many errors have stacked up. This acts as a fail-safe. It is set to 100 errors at the moment. If you want to adjust this setting it is located currently in the /src/bot/swap.js file line 78. 163 | 164 | if (cache.tradeCounter.errorcount>100){ 165 | console.log('Error Count is too high for swaps: '+cache.tradeCounter.errorcount); 166 | console.log('Ending to stop endless transactions failing'); 167 | process.exit(); 168 | } 169 | 170 | 171 | # Slippage management 172 | 173 | Advanced slippage handling has been added to the code. USE AT YOUR OWN RISK! To enable it you need to set adaptiveSlippage: 1 in the config.json to enable this feature. This will adjust the slippage for the route to be a percentage of the total less the required profit. It takes the simulated profit and removes the percentage required profit to create an adaptive slippage with some handling for the size of the profit detected. Related code area is shown below anv can be edited as needed. 174 | 175 | var slippagerevised = (100*(simulatedProfit-cache.config.minPercProfit+(slippage/100))).toFixed(3) 176 | 177 | if (slippagerevised>500) { 178 | // Make sure on really big numbers it is only 30% of the total 179 | slippagerevised = (0.3*slippagerevised).toFixed(3); 180 | } else { 181 | slippagerevised = (0.8*slippagerevised).toFixed(3); 182 | } 183 | 184 | 185 | ## BPS slippage 186 | 187 | Simple BPS slippage. The slippage is set in the Jupiter SDK. Make sure your percentage profit is a few points above the slippage. For instance 0.1% profit would need to have a slippage BPS below 10 to break even. Best to leave a little wiggle rooom for safety as low liquidity can cause your result to be a new netagive. Always test and check to make sure you are trading in a pair that can support the size you are swapping. 188 | 189 | 190 | 191 | ## Speed of Lookups 192 | 193 | One way you can speed up the lookup of the bot is to disable unused routes or ones that are too slow to be meaningful. Be careful here as you do not want to turn off the wrong ones. You can check with Birdeye and Jupiter front end to see the main pools and then do an analysis of the key ones for the pair you are swapping. Others can be turned off to gain some speed. Always test with a small amount and expect you will LOSE to begin with if you turn off so many you access only low liquidity pools. Edit at your own RISK. 194 | 195 | Edit the /src/bot/setup.js file to alter these settings: 196 | 197 | To turn one **off**, set the value to **true** 198 | 199 | ammsToExclude: { 200 | 'Aldrin': false, 201 | 'Crema': false, 202 | 'Cropper': true, 203 | 'Cykura': true, 204 | 'DeltaFi': false, 205 | 'GooseFX': true, 206 | 'Invariant': false, 207 | 'Lifinity': false, 208 | 'Lifinity V2': false, 209 | 'Marinade': false, 210 | 'Mercurial': false, 211 | 'Meteora': false, 212 | 'Raydium': false, 213 | 'Raydium CLMM': false, 214 | 'Saber': false, 215 | 'Serum': true, 216 | 'Orca': false, 217 | 'Step': false, 218 | 'Penguin': false, 219 | 'Saros': false, 220 | 'Stepn': true, 221 | 'Orca (Whirlpools)': false, 222 | 'Sencha': false, 223 | 'Saber (Decimals)': false, 224 | 'Dradex': true, 225 | 'Balansol': true, 226 | 'Openbook': false, 227 | 'Marco Polo': false, 228 | 'Oasis': false, 229 | 'BonkSwap': false, 230 | 'Phoenix': false, 231 | 'Symmetry': true, 232 | 'Unknown': true 233 | } 234 | 235 | 236 | ## Pro ARB trading configurations 237 | 238 | In some circumstances -- when speed is a key element to the success of ARB trading, enabling the setting `onlyDirectRoutes: true` may be an option to explore. This feature limits the available routes supplied via the JUP4 SDK to a subset of what is available by only returning direct routes. 239 | 240 | It is possible this can help make things route faster and convert more trades. It definitely does not guarantee the best route. You need to ensure the routes and pools are sufficient for the amount you are trading. So **always test** with a very small amount and **use at your own risk**. Setting it on the wrong pair can cause you to lose tokens. 241 | 242 | This setting can be found in the ./src/bot/index.js file as shown below: 243 | 244 | const routes = await jupiter.computeRoutes({ 245 | inputMint: new PublicKey(inputToken.address), 246 | outputMint: new PublicKey(outputToken.address), 247 | amount: amountInJSBI, 248 | slippageBps: slippage, 249 | feeBps: 0, 250 | forceFetch: true, 251 | **onlyDirectRoutes: false,** 252 | filterTopNResult: 2, 253 | enforceSingleTx: false, 254 | swapMode: 'ExactIn', 255 | }); 256 | 257 | 258 | · [back to top](#nav) · 259 | 260 | # contributing 261 | 262 | Arb Jupiter Bot is an open source project and contributions are welcome. 263 | 264 | · [back to top](#nav) · 265 | 266 | # license 267 | 268 | **MIT** yay! 🎉 269 | 270 | · [back to top](#nav) · 271 | 272 | # risk disclaimer 273 | 274 | This software is provided as is, without any warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors be liable for any claim, damages, or other liability. 275 | 276 | · [back to top](#nav) · 277 | 278 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solana-jupiter-bot", 3 | "version": "0.1.72-beta-jupv4", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ARBProtocol/solana-jupiter-bot.git" 8 | }, 9 | "bin": "./src/index.js", 10 | "engines": { 11 | "node": ">=16" 12 | }, 13 | "scripts": { 14 | "wizard": "node ./src/index.js", 15 | "start": "node --no-deprecation ./src/index.js && node --no-deprecation ./src/bot/index.js", 16 | "trade": "node --no-deprecation ./src/bot/index.js", 17 | "test": "xo && ava", 18 | "transaction": "node ./src/utils/transaction.js" 19 | }, 20 | "files": [ 21 | "./src/index.js", 22 | "./src/generator.js" 23 | ], 24 | "dependencies": { 25 | "@coral-xyz/anchor": "^0", 26 | "@jup-ag/core": "^4.0.0-beta.21", 27 | "@solana/web3.js": "^1.66.1", 28 | "asciichart": "^1.5.25", 29 | "axios": "^0.27.2", 30 | "bs58": "^5.0.0", 31 | "cliui": "^7.0.4", 32 | "express": "4.17.1", 33 | "gradient-string": "^2.0.1", 34 | "import-jsx": "^4.0.1", 35 | "ink": "^3.2.0", 36 | "ink-big-text": "^1.2.0", 37 | "ink-divider": "^3.0.0", 38 | "ink-gradient": "^2.0.0", 39 | "ink-select-input": "^4.2.1", 40 | "ink-spinner": "^4.0.3", 41 | "ink-text-input": "^4.0.3", 42 | "jsbi": "^4.3.0", 43 | "keypress": "^0.2.1", 44 | "meow": "^9.0.0", 45 | "moment": "^2.29.3", 46 | "open": "^8.4.0", 47 | "ora-classic": "^5.4.2", 48 | "promise-retry": "^2.0.1", 49 | "react": "^17.0.2", 50 | "strip-ansi": "^7.0.1" 51 | }, 52 | "ava": { 53 | "require": [ 54 | "@babel/register" 55 | ] 56 | }, 57 | "babel": { 58 | "presets": [ 59 | "@babel/preset-env", 60 | "@babel/preset-react" 61 | ] 62 | }, 63 | "xo": { 64 | "extends": "xo-react", 65 | "rules": { 66 | "react/prop-types": "off" 67 | } 68 | }, 69 | "devDependencies": { 70 | "@ava/babel": "^2.0.0", 71 | "@babel/preset-env": "^7.18.2", 72 | "@babel/preset-react": "^7.17.12", 73 | "@babel/register": "^7.17.7", 74 | "chalk": "^4.1.2", 75 | "eslint": "^8.19.0", 76 | "eslint-config-prettier": "^8.5.0", 77 | "eslint-config-xo-react": "^0.27.0", 78 | "eslint-plugin-prettier": "^4.2.1", 79 | "eslint-plugin-react": "^7.30.1", 80 | "eslint-plugin-react-hooks": "^4.5.0", 81 | "ink-testing-library": "^2.1.0", 82 | "prettier": "2.7.1", 83 | "typescript": "^4.6.4" 84 | }, 85 | "resolutions": { 86 | "@solana/buffer-layout": "4.0.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/bot/cache.js: -------------------------------------------------------------------------------- 1 | // global cache 2 | const cache = { 3 | startTime: new Date(), 4 | queue: {}, 5 | queueThrottle: 1, 6 | sideBuy: true, 7 | iteration: 0, 8 | walletpubkey: '', 9 | walletpubkeyfull: '', 10 | iterationPerMinute: { 11 | start: performance.now(), 12 | value: 0, 13 | counter: 0, 14 | }, 15 | initialBalance: { 16 | tokenA: 0, 17 | tokenB: 0, 18 | }, 19 | 20 | currentBalance: { 21 | tokenA: 0, 22 | tokenB: 0, 23 | }, 24 | currentProfit: { 25 | tokenA: 0, 26 | tokenB: 0, 27 | }, 28 | lastBalance: { 29 | tokenA: 0, 30 | tokenB: 0, 31 | }, 32 | profit: { 33 | tokenA: 0, 34 | tokenB: 0, 35 | }, 36 | maxProfitSpotted: { 37 | buy: 0, 38 | sell: 0, 39 | }, 40 | tradeCounter: { 41 | buy: { success: 0, fail: 0 }, 42 | sell: { success: 0, fail: 0 }, 43 | failedbalancecheck: 0, 44 | errorcount: 0, 45 | }, 46 | ui: { 47 | defaultColor: process.env.UI_COLOR ?? "cyan", 48 | showPerformanceOfRouteCompChart: false, 49 | showProfitChart: false, 50 | showTradeHistory: false, 51 | hideRpc: false, 52 | showHelp: false, 53 | allowClear: true, 54 | }, 55 | chart: { 56 | spottedMax: { 57 | buy: new Array(120).fill(0), 58 | sell: new Array(120).fill(0), 59 | }, 60 | performanceOfRouteComp: new Array(120).fill(0), 61 | }, 62 | hotkeys: { 63 | e: false, 64 | r: false, 65 | }, 66 | tradingEnabled: 67 | process.env.TRADING_ENABLED === undefined 68 | ? true 69 | : process.env.TRADING_ENABLED === "true", 70 | wrapUnwrapSOL: 71 | process.env.WRAP_UNWRAP_SOL === undefined 72 | ? true 73 | : process.env.WRAP_UNWRAP_SOL === "true", 74 | swappingRightNow: false, 75 | fetchingResultsFromSolscan: false, 76 | fetchingResultsFromSolscanStart: 0, 77 | tradeHistory: [], 78 | performanceOfTxStart: 0, 79 | availableRoutes: { 80 | buy: 0, 81 | sell: 0, 82 | }, 83 | isSetupDone: false, 84 | }; 85 | 86 | module.exports = cache; 87 | -------------------------------------------------------------------------------- /src/bot/exit.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const chalk = require("chalk"); 3 | const cache = require("./cache"); 4 | const logExit = (code = 0, error) => { 5 | code === 0 && console.log(chalk.black.bgMagentaBright.bold(error.message)); 6 | 7 | if (code === 1) { 8 | error?.message && 9 | console.log( 10 | chalk.black.bgRedBright.black("ERROR: " + chalk.bold(error.message)) 11 | ); 12 | error?.stack && console.log(chalk.redBright(error.stack)); 13 | 14 | if (cache.isSetupDone) { 15 | console.log( 16 | chalk.black.bgYellowBright( 17 | "Closing connections... ", 18 | chalk.bold("WAIT! ") 19 | ) 20 | ); 21 | console.log(chalk.yellowBright.bgBlack("Press [Ctrl]+[C] to force exit")); 22 | } 23 | } 24 | }; 25 | 26 | const handleExit = () => { 27 | try { 28 | console.log( 29 | chalk.black.bgMagentaBright( 30 | `\n Exit time: ${chalk.bold(new Date().toLocaleString())} ` 31 | ) 32 | ); 33 | 34 | // write cache to file 35 | try { 36 | fs.writeFileSync("./temp/cache.json", JSON.stringify(cache, null, 2)); 37 | console.log( 38 | chalk.black.bgGreenBright( 39 | ` > Cache saved to ${chalk.bold("./temp/cache.json")} ` 40 | ) 41 | ); 42 | } catch (error) { 43 | console.log( 44 | chalk.black.bgRedBright( 45 | ` X Error saving cache to ${chalk.bold("./temp/cache.json")} ` 46 | ) 47 | ); 48 | } 49 | 50 | // write trade history to file 51 | try { 52 | fs.writeFileSync( 53 | "./temp/tradeHistory.json", 54 | JSON.stringify(cache.tradeHistory, null, 2) 55 | ); 56 | console.log( 57 | chalk.black.bgGreenBright( 58 | ` > Trade history saved to ${chalk.bold("./temp/tradeHistory.json")} ` 59 | ) 60 | ); 61 | } catch (error) { 62 | console.log( 63 | chalk.black.bgRedBright( 64 | ` X Error saving trade history to ${chalk.bold( 65 | "./temp/tradeHistory.json" 66 | )} ` 67 | ) 68 | ); 69 | } 70 | console.log(chalk.black.bgMagentaBright.bold(" Exit Done! \n")); 71 | } catch (error) { 72 | console.log(error); 73 | } 74 | }; 75 | 76 | module.exports = { logExit, handleExit }; 77 | -------------------------------------------------------------------------------- /src/bot/index.js: -------------------------------------------------------------------------------- 1 | console.clear(); 2 | 3 | require("dotenv").config(); 4 | const { clearInterval } = require("timers"); 5 | const { PublicKey } = require("@solana/web3.js"); 6 | const JSBI = require('jsbi'); 7 | const { setTimeout } = require("timers/promises"); 8 | const { 9 | calculateProfit, 10 | toDecimal, 11 | toNumber, 12 | updateIterationsPerMin, 13 | checkRoutesResponse, 14 | checkArbReady, 15 | } = require("../utils"); 16 | const { handleExit, logExit } = require("./exit"); 17 | const cache = require("./cache"); 18 | const { setup, getInitialotherAmountThreshold, checkTokenABalance } = require("./setup"); 19 | const { printToConsole } = require("./ui/"); 20 | const { swap, failedSwapHandler, successSwapHandler } = require("./swap"); 21 | 22 | const waitabit = async (ms) => { 23 | const mySecondPromise = new Promise(function(resolve,reject){ 24 | console.log('construct a promise...') 25 | setTimeout(() => { 26 | reject(console.log('Error in promise')); 27 | },ms) 28 | }) 29 | } 30 | 31 | function getRandomAmt(runtime) { 32 | const min = Math.ceil((runtime*10000)*0.99); 33 | const max = Math.floor((runtime*10000)*1.01); 34 | return ((Math.floor(Math.random() * (max - min + 1)) + min)/10000); 35 | } 36 | 37 | const pingpongStrategy = async (jupiter, tokenA, tokenB) => { 38 | cache.iteration++; 39 | const date = new Date(); 40 | const i = cache.iteration; 41 | cache.queue[i] = -1; 42 | 43 | try { 44 | // calculate & update iterations per minute 45 | updateIterationsPerMin(cache); 46 | 47 | // Calculate amount that will be used for trade 48 | const amountToTrade = 49 | cache.config.tradeSize.strategy === "cumulative" 50 | ? cache.currentBalance[cache.sideBuy ? "tokenA" : "tokenB"] 51 | : cache.initialBalance[cache.sideBuy ? "tokenA" : "tokenB"]; 52 | 53 | const baseAmount = cache.lastBalance[cache.sideBuy ? "tokenB" : "tokenA"]; 54 | const slippage = typeof cache.config.slippage === "number" ? cache.config.slippage : 1; // 1BPS is 0.01% 55 | 56 | // set input / output token 57 | const inputToken = cache.sideBuy ? tokenA : tokenB; 58 | const outputToken = cache.sideBuy ? tokenB : tokenA; 59 | const tokdecimals = cache.sideBuy ? inputToken.decimals : outputToken.decimals; 60 | const amountInJSBI = JSBI.BigInt(amountToTrade); 61 | 62 | // check current routes via JUP4 SDK 63 | const performanceOfRouteCompStart = performance.now(); 64 | const routes = await jupiter.computeRoutes({ 65 | inputMint: new PublicKey(inputToken.address), 66 | outputMint: new PublicKey(outputToken.address), 67 | amount: amountInJSBI, 68 | slippageBps: slippage, 69 | forceFetch: true, 70 | onlyDirectRoutes: false, 71 | filterTopNResult: 2, 72 | }); 73 | 74 | checkRoutesResponse(routes); 75 | 76 | // count available routes 77 | cache.availableRoutes[cache.sideBuy ? "buy" : "sell"] = 78 | routes.routesInfos.length; 79 | 80 | // update status as OK 81 | cache.queue[i] = 0; 82 | 83 | const performanceOfRouteComp = performance.now() - performanceOfRouteCompStart; 84 | 85 | // choose first route 86 | const route = await routes.routesInfos[0]; 87 | 88 | // calculate profitability 89 | const simulatedProfit = calculateProfit(String(baseAmount), await JSBI.toNumber(route.outAmount)); 90 | 91 | // Alter slippage to be larger based on the profit if enabled in the config 92 | // set cache.config.adaptiveSlippage=1 to enable 93 | // Profit minus minimum profit 94 | // default to the set slippage 95 | var slippagerevised = slippage; 96 | 97 | if ((simulatedProfit > cache.config.minPercProfit) && cache.config.adaptiveSlippage == 1){ 98 | var slippagerevised = (100*(simulatedProfit-cache.config.minPercProfit+(slippage/100))).toFixed(3) 99 | 100 | if (slippagerevised>500) { 101 | // Make sure on really big numbers it is only 30% of the total 102 | slippagerevised = (0.3*slippagerevised).toFixed(3); 103 | } else { 104 | slippagerevised = (0.8*slippagerevised).toFixed(3); 105 | } 106 | 107 | //console.log("Setting slippage to "+slippagerevised); 108 | route.slippageBps = slippagerevised; 109 | } 110 | 111 | // store max profit spotted 112 | if ( 113 | simulatedProfit > cache.maxProfitSpotted[cache.sideBuy ? "buy" : "sell"] 114 | ) { 115 | cache.maxProfitSpotted[cache.sideBuy ? "buy" : "sell"] = simulatedProfit; 116 | } 117 | 118 | printToConsole({ 119 | date, 120 | i, 121 | performanceOfRouteComp, 122 | inputToken, 123 | outputToken, 124 | tokenA, 125 | tokenB, 126 | route, 127 | simulatedProfit, 128 | }); 129 | 130 | // check profitability and execute tx 131 | let tx, performanceOfTx; 132 | if ( 133 | !cache.swappingRightNow && 134 | (cache.hotkeys.e || 135 | cache.hotkeys.r || 136 | simulatedProfit >= cache.config.minPercProfit) 137 | ) { 138 | // hotkeys 139 | if (cache.hotkeys.e) { 140 | console.log("[E] PRESSED - EXECUTION FORCED BY USER!"); 141 | cache.hotkeys.e = false; 142 | } 143 | if (cache.hotkeys.r) { 144 | console.log("[R] PRESSED - REVERT BACK SWAP!"); 145 | route.otherAmountThreshold = 0; 146 | } 147 | 148 | if (cache.tradingEnabled || cache.hotkeys.r) { 149 | cache.swappingRightNow = true; 150 | // store trade to the history 151 | let tradeEntry = { 152 | date: date.toLocaleString(), 153 | buy: cache.sideBuy, 154 | inputToken: inputToken.symbol, 155 | outputToken: outputToken.symbol, 156 | inAmount: toDecimal(route.amount, inputToken.decimals), 157 | expectedOutAmount: toDecimal(route.outAmount, outputToken.decimals), 158 | expectedProfit: simulatedProfit, 159 | slippage: slippagerevised, 160 | }; 161 | 162 | // start refreshing status 163 | const printTxStatus = setInterval(() => { 164 | if (cache.swappingRightNow) { 165 | printToConsole({ 166 | date, 167 | i, 168 | performanceOfRouteComp, 169 | inputToken, 170 | outputToken, 171 | tokenA, 172 | tokenB, 173 | route, 174 | simulatedProfit, 175 | }); 176 | } 177 | }, 50); 178 | 179 | [tx, performanceOfTx] = await swap(jupiter, route); 180 | 181 | // stop refreshing status 182 | clearInterval(printTxStatus); 183 | 184 | const profit = calculateProfit( 185 | cache.currentBalance[cache.sideBuy ? "tokenB" : "tokenA"], 186 | tx.outputAmount 187 | ); 188 | 189 | tradeEntry = { 190 | ...tradeEntry, 191 | outAmount: tx.outputAmount || 0, 192 | profit, 193 | performanceOfTx, 194 | error: tx.error?.code === 6001 ? "Slippage Tolerance Exceeded" : tx.error?.message || null, 195 | }; 196 | 197 | var waittime = await waitabit(100); 198 | 199 | // handle TX results 200 | if (tx.error) { 201 | await failedSwapHandler(tradeEntry, inputToken, amountToTrade); 202 | } 203 | else { 204 | if (cache.hotkeys.r) { 205 | console.log("[R] - REVERT BACK SWAP - SUCCESS!"); 206 | cache.tradingEnabled = false; 207 | console.log("TRADING DISABLED!"); 208 | cache.hotkeys.r = false; 209 | } 210 | await successSwapHandler(tx, tradeEntry, tokenA, tokenB); 211 | } 212 | } 213 | } 214 | 215 | if (tx) { 216 | if (!tx.error) { 217 | // change side 218 | cache.sideBuy = !cache.sideBuy; 219 | } 220 | cache.swappingRightNow = false; 221 | } 222 | 223 | printToConsole({ 224 | date, 225 | i, 226 | performanceOfRouteComp, 227 | inputToken, 228 | outputToken, 229 | tokenA, 230 | tokenB, 231 | route, 232 | simulatedProfit, 233 | }); 234 | 235 | } catch (error) { 236 | cache.queue[i] = 1; 237 | console.log(error); 238 | } finally { 239 | delete cache.queue[i]; 240 | } 241 | }; 242 | 243 | const arbitrageStrategy = async (jupiter, tokenA) => { 244 | 245 | //console.log('ARB STRAT ACTIVE'); 246 | 247 | cache.iteration++; 248 | const date = new Date(); 249 | const i = cache.iteration; 250 | cache.queue[i] = -1; 251 | swapactionrun: try { 252 | // calculate & update iterations per minute 253 | updateIterationsPerMin(cache); 254 | 255 | // Calculate amount that will be used for trade 256 | const amountToTrade = 257 | cache.config.tradeSize.strategy === "cumulative" 258 | ? cache.currentBalance["tokenA"] 259 | : cache.initialBalance["tokenA"]; 260 | const baseAmount = amountToTrade; 261 | 262 | //BNI AMT to TRADE 263 | const amountInJSBI = JSBI.BigInt(amountToTrade); 264 | //console.log('Amount to trade:'+amountToTrade); 265 | 266 | // default slippage 267 | const slippage = typeof cache.config.slippage === "number" ? cache.config.slippage : 1; // 100 is 0.1% 268 | 269 | // set input / output token 270 | const inputToken = tokenA; 271 | const outputToken = tokenA; 272 | 273 | // check current routes 274 | const performanceOfRouteCompStart = performance.now(); 275 | const routes = await jupiter.computeRoutes({ 276 | inputMint: new PublicKey(inputToken.address), 277 | outputMint: new PublicKey(outputToken.address), 278 | amount: amountInJSBI, 279 | slippageBps: slippage, 280 | feeBps: 0, 281 | forceFetch: true, 282 | onlyDirectRoutes: false, 283 | filterTopNResult: 2, 284 | enforceSingleTx: false, 285 | swapMode: 'ExactIn', 286 | }); 287 | 288 | //console.log('Routes Lookup Run for '+ inputToken.address); 289 | checkRoutesResponse(routes); 290 | 291 | // count available routes 292 | cache.availableRoutes[cache.sideBuy ? "buy" : "sell"] = 293 | routes.routesInfos.length; 294 | 295 | // update status as OK 296 | cache.queue[i] = 0; 297 | 298 | const performanceOfRouteComp = performance.now() - performanceOfRouteCompStart; 299 | 300 | // choose first route 301 | const route = await routes.routesInfos[0]; 302 | 303 | // calculate profitability 304 | const simulatedProfit = calculateProfit(baseAmount, await JSBI.toNumber(route.outAmount)); 305 | const minPercProfitRnd = getRandomAmt(cache.config.minPercProfit); 306 | //console.log('mpp:'+minPercProfitRnd); 307 | 308 | var slippagerevised = slippage; 309 | 310 | if ((simulatedProfit > cache.config.minPercProfit) && cache.config.adaptiveSlippage === 1){ 311 | slippagerevised = (100*(simulatedProfit-minPercProfitRnd+(slippage/100))).toFixed(3) 312 | 313 | // Set adaptive slippage 314 | if (slippagerevised>500) { 315 | // Make sure on really big numbers it is only 30% of the total if > 50% 316 | slippagerevised = (0.30*slippagerevised).toFixed(3); 317 | } else { 318 | slippagerevised = (0.80*slippagerevised).toFixed(3); 319 | } 320 | //console.log("Setting slippage to "+slippagerevised); 321 | route.slippageBps = slippagerevised; 322 | } 323 | 324 | // store max profit spotted 325 | if (simulatedProfit > cache.maxProfitSpotted["buy"]) { 326 | cache.maxProfitSpotted["buy"] = simulatedProfit; 327 | } 328 | 329 | printToConsole({ 330 | date, 331 | i, 332 | performanceOfRouteComp, 333 | inputToken, 334 | outputToken, 335 | tokenA, 336 | tokenB: tokenA, 337 | route, 338 | simulatedProfit, 339 | }); 340 | 341 | // check profitability and execute tx 342 | let tx, performanceOfTx; 343 | if ( 344 | !cache.swappingRightNow && 345 | (cache.hotkeys.e || 346 | cache.hotkeys.r || 347 | simulatedProfit >= minPercProfitRnd) 348 | ) { 349 | // hotkeys 350 | if (cache.hotkeys.e) { 351 | console.log("[E] PRESSED - EXECUTION FORCED BY USER!"); 352 | cache.hotkeys.e = false; 353 | } 354 | if (cache.hotkeys.r) { 355 | console.log("[R] PRESSED - REVERT BACK SWAP!"); 356 | route.otherAmountThreshold = 0; 357 | } 358 | 359 | if (cache.tradingEnabled || cache.hotkeys.r) { 360 | cache.swappingRightNow = true; 361 | // store trade to the history 362 | console.log('swappingRightNow'); 363 | let tradeEntry = { 364 | date: date.toLocaleString(), 365 | buy: cache.sideBuy, 366 | inputToken: inputToken.symbol, 367 | outputToken: outputToken.symbol, 368 | inAmount: toDecimal(route.amount, inputToken.decimals), 369 | expectedOutAmount: toDecimal(route.outAmount, outputToken.decimals), 370 | expectedProfit: simulatedProfit, 371 | }; 372 | 373 | // start refreshing status 374 | const printTxStatus = setInterval(() => { 375 | if (cache.swappingRightNow) { 376 | printToConsole({ 377 | date, 378 | i, 379 | performanceOfRouteComp, 380 | inputToken, 381 | outputToken, 382 | tokenA, 383 | tokenB: tokenA, 384 | route, 385 | simulatedProfit, 386 | }); 387 | } 388 | }, 250); 389 | 390 | [tx, performanceOfTx] = await swap(jupiter, route); 391 | 392 | // stop refreshing status 393 | clearInterval(printTxStatus); 394 | 395 | // Calculate the profit of the trade 396 | const profit = calculateProfit(tradeEntry.inAmount, tx.outputAmount); 397 | 398 | tradeEntry = { 399 | ...tradeEntry, 400 | outAmount: tx.outputAmount || 0, 401 | profit, 402 | performanceOfTx, 403 | error: tx.error?.code === 6001 ? "Slippage Tolerance Exceeded" : tx.error?.message || null, 404 | slippage: slippagerevised, 405 | }; 406 | 407 | // handle TX results 408 | if (tx.error) { 409 | // Slippage tolerance exceeded 410 | await failedSwapHandler(tradeEntry, inputToken, amountToTrade); 411 | } else { 412 | if (cache.hotkeys.r) { 413 | console.log("[R] - REVERT BACK SWAP - SUCCESS!"); 414 | cache.tradingEnabled = false; 415 | console.log("TRADING DISABLED!"); 416 | cache.hotkeys.r = false; 417 | } 418 | await successSwapHandler(tx, tradeEntry, tokenA, tokenA); 419 | } 420 | } 421 | } 422 | 423 | if (tx) { 424 | cache.swappingRightNow = false; 425 | } 426 | 427 | printToConsole({ 428 | date, 429 | i, 430 | performanceOfRouteComp, 431 | inputToken, 432 | outputToken, 433 | tokenA, 434 | tokenB: tokenA, 435 | route, 436 | simulatedProfit, 437 | }); 438 | } catch (error) { 439 | cache.queue[i] = 1; 440 | throw error; 441 | } finally { 442 | delete cache.queue[i]; 443 | } 444 | }; 445 | 446 | const watcher = async (jupiter, tokenA, tokenB) => { 447 | if ( 448 | !cache.swappingRightNow && 449 | Object.keys(cache.queue).length < cache.queueThrottle 450 | ) { 451 | if (cache.config.tradingStrategy === "pingpong") { 452 | await pingpongStrategy(jupiter, tokenA, tokenB); 453 | } 454 | if (cache.config.tradingStrategy === "arbitrage") { 455 | await arbitrageStrategy(jupiter, tokenA); 456 | } 457 | } 458 | }; 459 | 460 | const run = async () => { 461 | try { 462 | // Are they ARB ready and part of the community? 463 | await checkArbReady(); 464 | 465 | // set everything up 466 | const { jupiter, tokenA, tokenB, wallet } = await setup(); 467 | 468 | // Set pubkey display 469 | const walpubkeyfull = wallet.publicKey.toString(); 470 | console.log(`Wallet Enabled: ${walpubkeyfull}`); 471 | cache.walletpubkeyfull = walpubkeyfull; 472 | cache.walletpubkey = walpubkeyfull.slice(0,5) + '...' + walpubkeyfull.slice(walpubkeyfull.length-3); 473 | //console.log(cache.walletpubkey); 474 | 475 | if (cache.config.tradingStrategy === "pingpong") { 476 | // set initial & current & last balance for tokenA 477 | console.log('Trade Size is:'+cache.config.tradeSize.value); 478 | 479 | cache.initialBalance.tokenA = toNumber( 480 | cache.config.tradeSize.value, 481 | tokenA.decimals 482 | ); 483 | cache.currentBalance.tokenA = cache.initialBalance.tokenA; 484 | cache.lastBalance.tokenA = cache.initialBalance.tokenA; 485 | 486 | // Double check the wallet has sufficient amount of tokenA 487 | var realbalanceTokenA = await checkTokenABalance(tokenA,cache.initialBalance.tokenA); 488 | 489 | // set initial & last balance for tokenB 490 | cache.initialBalance.tokenB = await getInitialotherAmountThreshold( 491 | jupiter, 492 | tokenA, 493 | tokenB, 494 | cache.initialBalance.tokenA 495 | ); 496 | cache.lastBalance.tokenB = cache.initialBalance.tokenB; 497 | } else if (cache.config.tradingStrategy === "arbitrage") { 498 | // set initial & current & last balance for tokenA 499 | //console.log('Trade Size is:'+cache.config.tradeSize.value+' decimals:'+tokenA.decimals); 500 | 501 | cache.initialBalance.tokenA = toNumber( 502 | cache.config.tradeSize.value, 503 | tokenA.decimals 504 | ); 505 | 506 | cache.currentBalance.tokenA = cache.initialBalance.tokenA; 507 | cache.lastBalance.tokenA = cache.initialBalance.tokenA; 508 | 509 | // Double check the wallet has sufficient amount of tokenA 510 | var realbalanceTokenA = await checkTokenABalance(tokenA,cache.initialBalance.tokenA); 511 | 512 | if (realbalanceTokenA watcher(jupiter, tokenA, tokenB), 520 | cache.config.minInterval 521 | ); 522 | } catch (error) { 523 | logExit(error); 524 | process.exitCode = 1; 525 | } 526 | }; 527 | 528 | run(); 529 | 530 | // handle exit 531 | process.on("exit", handleExit); 532 | -------------------------------------------------------------------------------- /src/bot/setup.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const chalk = require("chalk"); 3 | const ora = require("ora-classic"); 4 | const bs58 = require("bs58"); 5 | const { Jupiter } = require("@jup-ag/core"); 6 | const { Connection, Keypair, PublicKey, LAMPORTS_PER_SOL } = require("@solana/web3.js"); 7 | 8 | var JSBI = (require('jsbi')); 9 | var invariant = (require('tiny-invariant')); 10 | var _Decimal = (require('decimal.js')); 11 | var _Big = (require('big.js')); 12 | var toFormat = (require('toformat')); 13 | var anchor = require('@project-serum/anchor'); 14 | 15 | const { logExit } = require("./exit"); 16 | const { loadConfigFile, toDecimal } = require("../utils"); 17 | const { intro, listenHotkeys } = require("./ui"); 18 | const { setTimeout } = require("timers/promises"); 19 | const cache = require("./cache"); 20 | const wrapUnwrapSOL = cache.wrapUnwrapSOL; 21 | 22 | // Account balance code 23 | const balanceCheck = async (checkToken) => { 24 | let checkBalance = Number(0); 25 | let t = Number(0); 26 | 27 | const connection = new Connection(process.env.DEFAULT_RPC); 28 | wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_WALLET_PRIVATE_KEY)); 29 | 30 | if (wrapUnwrapSOL && checkToken.address === 'So11111111111111111111111111111111111111112') { 31 | // This is where Native balance is needing to be checked and not the Wrapped SOL ATA 32 | try { 33 | const balance = await connection.getBalance(wallet.publicKey); 34 | checkBalance = Number(balance); 35 | } catch (error) { 36 | console.error('Error fetching native SOL balance:', error); 37 | } 38 | } else { 39 | // Normal token so look up the ATA balance(s) 40 | try { 41 | let totalTokenBalance = BigInt(0); 42 | const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 43 | mint: new PublicKey(checkToken.address) 44 | }); 45 | 46 | tokenAccounts.value.forEach((accountInfo) => { 47 | const parsedInfo = accountInfo.account.data.parsed.info; 48 | totalTokenBalance += BigInt(parsedInfo.tokenAmount.amount); 49 | }); 50 | 51 | // Convert totalTokenBalance to a regular number 52 | checkBalance = Number(totalTokenBalance); 53 | 54 | } catch (error) { 55 | console.error('Error fetching token balance:', error); 56 | } 57 | } 58 | 59 | try { 60 | // Pass back the BN version to match 61 | let checkBalanceUi = toDecimal(checkBalance,checkToken.decimals); 62 | console.log(`Wallet balance for ${checkToken.symbol} is ${checkBalanceUi} (${checkBalance})`); 63 | } catch (error) { 64 | console.error('Silence is golden.. Or not...:', error); 65 | } 66 | 67 | if (checkBalance>Number(0)){ 68 | return checkBalance; 69 | } else { 70 | return(Number(0)); 71 | } 72 | }; 73 | 74 | // Handle Balance Errors Messaging 75 | const checkTokenABalance = async (tokenA, initialTradingBalance) => { 76 | try { 77 | // Check the balance of TokenA to make sure there is enough to trade with 78 | var realbalanceTokenA = await balanceCheck(tokenA); 79 | bal1 = toDecimal(realbalanceTokenA,tokenA.decimals); 80 | bal2 = toDecimal(initialTradingBalance,tokenA.decimals); 81 | 82 | if (realbalanceTokenA < initialTradingBalance) { 83 | throw new Error(`\x1b[93mThere is insufficient balance in your wallet of ${tokenA.symbol}\x1b[0m 84 | \nYou currently only have \x1b[93m${bal1}\x1b[0m ${tokenA.symbol}. 85 | \nTo run the bot you need \x1b[93m${bal2}\x1b[0m ${tokenA.symbol}. 86 | \nEither add more ${tokenA.symbol} to your wallet or lower the amount below ${bal1}.\n`); 87 | } 88 | return realbalanceTokenA; 89 | } catch (error) { 90 | // Handle errors gracefully 91 | console.error(`\n====================\n\n${error.message}\n====================\n`); 92 | // Return an appropriate error code or rethrow the error if necessary 93 | process.exit(1); // Exiting with a non-zero code to indicate failure 94 | } 95 | } 96 | 97 | const setup = async () => { 98 | let spinner, tokens, tokenA, tokenB, wallet; 99 | try { 100 | // listen for hotkeys 101 | listenHotkeys(); 102 | await intro(); 103 | 104 | // load config file and store it in cache 105 | cache.config = loadConfigFile({ showSpinner: false }); 106 | 107 | spinner = ora({ 108 | text: "Loading tokens...", 109 | discardStdin: false, 110 | color: "magenta", 111 | }).start(); 112 | 113 | try { 114 | tokens = JSON.parse(fs.readFileSync("./temp/tokens.json")); 115 | tokenA = tokens.find((t) => t.address === cache.config.tokenA.address); 116 | 117 | if (cache.config.tradingStrategy !== "arbitrage") 118 | tokenB = tokens.find((t) => t.address === cache.config.tokenB.address); 119 | } catch (error) { 120 | spinner.text = chalk.black.bgRedBright( 121 | `\n Loading tokens failed!\n Please run the Wizard to generate it using ${chalk.bold( 122 | "`yarn start`" 123 | )}\n` 124 | ); 125 | throw error; 126 | } 127 | 128 | try { 129 | spinner.text = "Checking wallet..."; 130 | if ( 131 | !process.env.SOLANA_WALLET_PRIVATE_KEY || 132 | (process.env.SOLANA_WALLET_PUBLIC_KEY && 133 | process.env.SOLANA_WALLET_PUBLIC_KEY?.length !== 88) 134 | ) { 135 | throw new Error("Wallet check failed!"); 136 | } else { 137 | wallet = Keypair.fromSecretKey( 138 | bs58.decode(process.env.SOLANA_WALLET_PRIVATE_KEY) 139 | ); 140 | } 141 | } catch (error) { 142 | spinner.text = chalk.black.bgRedBright( 143 | `\n Wallet check failed! \n Please make sure that ${chalk.bold( 144 | "SOLANA_WALLET_PRIVATE_KEY " 145 | )}\n inside ${chalk.bold(".env")} file is correct \n` 146 | ); 147 | logExit(1, error); 148 | process.exitCode = 1; 149 | throw error; 150 | } 151 | 152 | // Set up the RPC connection 153 | const connection = new Connection(cache.config.rpc[0]); 154 | 155 | spinner.text = "Loading the Jupiter V4 SDK and getting ready to trade..."; 156 | 157 | const jupiter = await Jupiter.load({ 158 | connection, 159 | cluster: cache.config.network, 160 | user: wallet, 161 | restrictIntermediateTokens: false, 162 | shouldLoadSerumOpenOrders: false, 163 | wrapUnwrapSOL: cache.wrapUnwrapSOL, 164 | ammsToExclude: { 165 | 'Aldrin': false, 166 | 'Crema': false, 167 | 'Cropper': true, 168 | 'Cykura': true, 169 | 'DeltaFi': false, 170 | 'GooseFX': true, 171 | 'Invariant': false, 172 | 'Lifinity': false, 173 | 'Lifinity V2': false, 174 | 'Marinade': false, 175 | 'Mercurial': false, 176 | 'Meteora': false, 177 | 'Raydium': false, 178 | 'Raydium CLMM': false, 179 | 'Saber': false, 180 | 'Serum': true, 181 | 'Orca': false, 182 | 'Step': false, 183 | 'Penguin': false, 184 | 'Saros': false, 185 | 'Stepn': true, 186 | 'Orca (Whirlpools)': false, 187 | 'Sencha': false, 188 | 'Saber (Decimals)': false, 189 | 'Dradex': true, 190 | 'Balansol': true, 191 | 'Openbook': false, 192 | 'Marco Polo': false, 193 | 'Oasis': false, 194 | 'BonkSwap': false, 195 | 'Phoenix': false, 196 | 'Symmetry': true, 197 | 'Unknown': true 198 | } 199 | }); 200 | cache.isSetupDone = true; 201 | spinner.succeed("Checking to ensure you are ARB ready...\n====================\n"); 202 | return { jupiter, tokenA, tokenB, wallet }; 203 | } catch (error) { 204 | if (spinner) 205 | spinner.fail( 206 | chalk.bold.redBright(`Setting up failed!\n ${spinner.text}`) 207 | ); 208 | logExit(1, error); 209 | process.exitCode = 1; 210 | } 211 | }; 212 | 213 | const getInitialotherAmountThreshold = async ( 214 | jupiter, 215 | inputToken, 216 | outputToken, 217 | amountToTrade 218 | ) => { 219 | let spinner; 220 | try { 221 | const tokenDecimals = cache.sideBuy ? inputToken.decimals : outputToken.decimals; 222 | const spinnerText = `Computing routes for the token with amountToTrade ${amountToTrade} with decimals ${tokenDecimals}`; 223 | 224 | spinner = ora({ 225 | text: spinnerText, 226 | discardStdin: false, 227 | color: "magenta", 228 | }).start(); 229 | 230 | //JSBI AMT to TRADE 231 | const amountInJSBI = JSBI.BigInt(amountToTrade); 232 | 233 | // compute routes for the first time 234 | const routes = await jupiter.computeRoutes({ 235 | inputMint: new PublicKey(inputToken.address), 236 | outputMint: new PublicKey(outputToken.address), 237 | amount: amountInJSBI, 238 | slippageBps: 0, 239 | forceFetch: true, 240 | onlyDirectRoutes: false, 241 | filterTopNResult: 1, 242 | }); 243 | 244 | if (routes?.routesInfos?.length > 0) spinner.succeed("Routes computed!"); 245 | else spinner.fail("No routes found. Something is wrong! Check tokens:"+inputToken.address+" "+outputToken.address); 246 | 247 | return routes.routesInfos[0].otherAmountThreshold; 248 | } catch (error) { 249 | if (spinner) 250 | spinner.fail(chalk.bold.redBright("Computing routes failed!\n")); 251 | logExit(1, error); 252 | process.exitCode = 1; 253 | } 254 | }; 255 | 256 | module.exports = { 257 | setup, 258 | getInitialotherAmountThreshold, 259 | balanceCheck, 260 | checkTokenABalance, 261 | }; 262 | -------------------------------------------------------------------------------- /src/bot/swap.js: -------------------------------------------------------------------------------- 1 | const { calculateProfit, toDecimal, storeItInTempAsJSON } = require("../utils"); 2 | const cache = require("./cache"); 3 | const { setTimeout } = require("timers/promises"); 4 | const { balanceCheck } = require("./setup"); 5 | const { checktrans } = require("../utils/transaction.js"); 6 | const promiseRetry = require("promise-retry"); 7 | 8 | const waitabit = async (ms) => { 9 | const mySecondPromise = new Promise(function(resolve,reject){ 10 | console.log('construct a promise...') 11 | setTimeout(() => { 12 | reject(console.log('Error in promise')); 13 | },ms) 14 | }) 15 | } 16 | 17 | const swap = async (jupiter, route) => { 18 | try { 19 | const performanceOfTxStart = performance.now(); 20 | cache.performanceOfTxStart = performanceOfTxStart; 21 | 22 | if (process.env.DEBUG) storeItInTempAsJSON("routeInfoBeforeSwap", route); 23 | 24 | // pull the trade priority 25 | const priority = typeof cache.config.priority === "number" ? cache.config.priority : 100; //100 BPS default if not set 26 | cache.priority = priority; 27 | 28 | const { execute } = await jupiter.exchange({ 29 | routeInfo: route, 30 | computeUnitPriceMicroLamports: priority, 31 | }); 32 | const result = await execute(); 33 | 34 | if (process.env.DEBUG) storeItInTempAsJSON("result", result); 35 | 36 | // Reset counter on success 37 | cache.tradeCounter.failedbalancecheck = 0; 38 | cache.tradeCounter.errorcount = 0; 39 | 40 | const performanceOfTx = performance.now() - performanceOfTxStart; 41 | 42 | return [result, performanceOfTx]; 43 | } catch (error) { 44 | console.log("Swap error: ", error); 45 | } 46 | }; 47 | exports.swap = swap; 48 | 49 | const failedSwapHandler = async(tradeEntry, inputToken, tradeAmount) => { 50 | // update trade counter 51 | cache.tradeCounter[cache.sideBuy ? "buy" : "sell"].fail++; 52 | 53 | // Update trade history if configured 54 | if (cache.config.storeFailedTxInHistory) { 55 | cache.tradeHistory.push(tradeEntry); 56 | } 57 | 58 | // Double check the balance 59 | const realbalanceToken = await balanceCheck(inputToken); 60 | 61 | // If balance is insufficient, handle it 62 | if (Number(realbalanceToken) < Number(tradeAmount)) { 63 | cache.tradeCounter.failedbalancecheck++; 64 | 65 | if (cache.tradeCounter.failedbalancecheck > 5) { 66 | console.log(`Balance Lookup is too low for token: ${realbalanceToken} < ${tradeAmount}`); 67 | console.log(`Failed For: ${cache.tradeCounter.failedbalancecheck} times`); 68 | process.exit(); 69 | } 70 | } 71 | 72 | // Increment error count and check if too high 73 | cache.tradeCounter.errorcount += 1; 74 | if (cache.tradeCounter.errorcount > 100) { 75 | console.log(`Error Count is too high for swaps: ${cache.tradeCounter.errorcount}`); 76 | console.log('Ending to stop endless transactions failing'); 77 | process.exit(); 78 | } 79 | 80 | }; 81 | exports.failedSwapHandler = failedSwapHandler; 82 | 83 | const successSwapHandler = async (tx, tradeEntry, tokenA, tokenB) => { 84 | if (process.env.DEBUG) storeItInTempAsJSON(`txResultFromSDK_${tx?.txid}`, tx); 85 | 86 | // update counter 87 | cache.tradeCounter[cache.sideBuy ? "buy" : "sell"].success++; 88 | 89 | if (cache.config.tradingStrategy === "pingpong") { 90 | // update balance 91 | if (cache.sideBuy) { 92 | cache.lastBalance.tokenA = cache.currentBalance.tokenA; 93 | cache.currentBalance.tokenA = 0; 94 | cache.currentBalance.tokenB = tx.outputAmount; 95 | } else { 96 | cache.lastBalance.tokenB = cache.currentBalance.tokenB; 97 | cache.currentBalance.tokenB = 0; 98 | cache.currentBalance.tokenA = tx.outputAmount; 99 | } 100 | 101 | // update profit 102 | if (cache.sideBuy) { 103 | cache.currentProfit.tokenA = 0; 104 | cache.currentProfit.tokenB = calculateProfit( 105 | String(cache.initialBalance.tokenB), 106 | String(cache.currentBalance.tokenB) 107 | ); 108 | } else { 109 | cache.currentProfit.tokenB = 0; 110 | cache.currentProfit.tokenA = calculateProfit( 111 | String(cache.initialBalance.tokenA), 112 | String(cache.currentBalance.tokenA) 113 | ); 114 | } 115 | 116 | // update trade history 117 | let tempHistory = cache.tradeHistory; 118 | 119 | tradeEntry.inAmount = toDecimal( 120 | tx.inputAmount, 121 | cache.sideBuy ? tokenA.decimals : tokenB.decimals 122 | ); 123 | tradeEntry.outAmount = toDecimal( 124 | tx.outputAmount, 125 | cache.sideBuy ? tokenB.decimals : tokenA.decimals 126 | ); 127 | 128 | tradeEntry.profit = calculateProfit( 129 | String(cache.lastBalance[cache.sideBuy ? "tokenB" : "tokenA"]), 130 | String(tx.outputAmount) 131 | ); 132 | tempHistory.push(tradeEntry); 133 | cache.tradeHistory = tempHistory; 134 | 135 | } 136 | if (cache.config.tradingStrategy === "arbitrage") { 137 | /** check real amounts because Jupiter SDK returns wrong amounts 138 | * when trading ARB TokenA <> TokenA (arbitrage) 139 | */ 140 | 141 | try { 142 | // BETA LOOKUP FOR RESULT VIA RPC 143 | var txresult = []; 144 | var err2 = -1; 145 | var rcount = 0; 146 | var retries = 30; 147 | 148 | const fetcher = async (retry) => { 149 | 150 | console.log('Looking for ARB trade result via RPC.'); 151 | rcount++; 152 | 153 | if (rcount>=retries){ 154 | // Exit max retries 155 | console.log(`Reached max attempts to fetch transaction. Assuming it did not complete.`); 156 | return -1; 157 | } 158 | 159 | // Get the results of the transaction from the RPC 160 | // Sometimes this takes time for it to post so retry logic is implemented 161 | [txresult, err2] = await checktrans(tx?.txid,cache.walletpubkeyfull); 162 | 163 | if (err2==0 && txresult) { 164 | if (txresult?.[tokenA.address]?.change>0) { 165 | 166 | // update balance 167 | cache.lastBalance.tokenA = cache.currentBalance.tokenA; 168 | cache.currentBalance.tokenA = (cache.currentBalance.tokenA+txresult?.[tokenA.address]?.change); 169 | 170 | // update profit 171 | cache.currentProfit.tokenA = calculateProfit( 172 | String(cache.initialBalance.tokenA), 173 | String(cache.currentBalance.tokenA) 174 | ); 175 | 176 | // update trade history 177 | let tempHistory = cache.tradeHistory; 178 | 179 | tradeEntry.inAmount = toDecimal( 180 | cache.lastBalance.tokenA, tokenA.decimals 181 | ); 182 | tradeEntry.outAmount = toDecimal( 183 | cache.currentBalance.tokenA, tokenA.decimals 184 | ); 185 | 186 | tradeEntry.profit = calculateProfit( 187 | String(cache.lastBalance.tokenA), 188 | String(cache.currentBalance.tokenA) 189 | ); 190 | tempHistory.push(tradeEntry); 191 | cache.tradeHistory = tempHistory; 192 | 193 | //console.log(`Tx result with output token, returning..`); 194 | return txresult; 195 | } else { 196 | retry(new Error("Transaction was not posted yet... Retrying...")); 197 | } 198 | } else if(err2==2){ 199 | // Transaction failed. Kill it and retry 200 | err.message = JSON.stringify(txresult); 201 | return -1; 202 | } else{ 203 | retry(new Error("Transaction was not posted yet. Retrying...")); 204 | } 205 | }; 206 | 207 | const lookresult = await promiseRetry(fetcher, { 208 | retries: retries, 209 | minTimeout: 1000, 210 | maxTimeout: 4000, 211 | randomize: true, 212 | }); 213 | 214 | if (lookresult==-1){ 215 | //console.log('Lookup Shows Failed Transaction.'); 216 | outputamt = 0; 217 | err.status=true; 218 | } else { 219 | // Track the output amount 220 | inputamt = txresult[tokenA.address].start; 221 | outputamt = txresult[tokenA.address].end; 222 | 223 | cache.currentProfit.tokenA = calculateProfit( 224 | cache.initialBalance.tokenA, 225 | cache.currentBalance.tokenA 226 | ); 227 | 228 | // update trade history 229 | let tempHistory = cache.tradeHistory; 230 | 231 | tradeEntry.inAmount = toDecimal(inputamt, tokenA.decimals); 232 | tradeEntry.outAmount = toDecimal(outputamt, tokenA.decimals); 233 | 234 | tradeEntry.profit = calculateProfit(tradeEntry.inAmount,tradeEntry.outAmount); 235 | tempHistory.push(tradeEntry); 236 | cache.tradeHistory = tempHistory; 237 | } 238 | 239 | } catch (error) { 240 | console.log("Fetch Result Error: ", error); 241 | } 242 | } 243 | }; 244 | exports.successSwapHandler = successSwapHandler; 245 | -------------------------------------------------------------------------------- /src/bot/ui/index.js: -------------------------------------------------------------------------------- 1 | const intro = require("./intro"); 2 | const printToConsole = require("./printToConsole"); 3 | const listenHotkeys = require("./listenHotkeys"); 4 | 5 | module.exports = { intro, printToConsole, listenHotkeys }; 6 | -------------------------------------------------------------------------------- /src/bot/ui/intro.js: -------------------------------------------------------------------------------- 1 | const ui = require("cliui")({ width: 140 }); 2 | const chalk = require("chalk"); 3 | const gradient = require("gradient-string"); 4 | 5 | const package = require("../../../package.json"); 6 | const { DISCORD_INVITE_URL } = require("../../constants"); 7 | 8 | const universeSize = 15; 9 | const color = "white"; 10 | const startWarp = 30; 11 | let colorsSet = [ 12 | "#cf4884", 13 | "#8832b3", 14 | "#b5b4fa", 15 | "#cdadff", 16 | "#6d29c5", 17 | "#4e21d9", 18 | "#481ede", 19 | ]; 20 | 21 | const random = (h = 100, l = 1) => Math.floor(Math.random() * (h - l + 1)) + l; 22 | 23 | async function intro() { 24 | try { 25 | const skipIntro = process.env.SKIP_INTRO === "true" || false; 26 | 27 | if (!skipIntro) { 28 | ui.div(" "); 29 | for (let i = 0; i < 200; i++) { 30 | const speed = i > 50 ? 100 - i : i; 31 | const a = colorsSet.shift(); 32 | colorsSet.push(a); 33 | const g = gradient(colorsSet); 34 | 35 | const char = 36 | i > startWarp 37 | ? i > 180 38 | ? g("/").repeat(random(i / 10, i / 10 - 2)) 39 | : "-".repeat(random(i / 10, i / 10 - 2)) 40 | : "•"; 41 | await new Promise((resolve) => setTimeout(resolve, speed)); 42 | 43 | console.clear(); 44 | ui.resetOutput(); 45 | 46 | for (let ii = 0; ii < universeSize; ii++) { 47 | ui.div({ 48 | text: `${chalk[color](char)}`, 49 | padding: [0, 0, 0, random()], 50 | }); 51 | } 52 | 53 | ui.div( 54 | { 55 | text: g(`ARB SOLANA BOT - ${package.version}`), 56 | width: 50, 57 | align: "center", 58 | padding: [1, 0, 1, 0], 59 | }, 60 | { 61 | text: `Discord: ${chalk.magenta(DISCORD_INVITE_URL)}\n ${chalk.gray( 62 | "- PRESS [D] TO OPEN -" 63 | )}`, 64 | width: 50, 65 | align: "center", 66 | padding: [1, 0, 1, 0], 67 | } 68 | ); 69 | 70 | for (let ii = 0; ii < universeSize; ii++) { 71 | ui.div({ 72 | text: `${chalk[color](char)}`, 73 | padding: [0, 0, 0, random()], 74 | }); 75 | } 76 | 77 | console.log(ui.toString()); 78 | } 79 | ui.div(""); 80 | console.clear(); 81 | } 82 | } catch (error) { 83 | console.log(error); 84 | } 85 | } 86 | 87 | module.exports = intro; 88 | -------------------------------------------------------------------------------- /src/bot/ui/listenHotkeys.js: -------------------------------------------------------------------------------- 1 | const keypress = require("keypress"); 2 | const open = require("open"); 3 | 4 | const { DISCORD_INVITE_URL } = require("../../constants"); 5 | const { logExit } = require("../exit"); 6 | const cache = require("../cache"); 7 | 8 | const listenHotkeys = () => { 9 | keypress(process.stdin); 10 | 11 | process.stdin.on("keypress", function (ch, key) { 12 | if (key && key.ctrl && key.name == "c") { 13 | cache.ui.allowClear = false; 14 | // eslint-disable-next-line no-undef 15 | if (global.botInterval) clearInterval(botInterval); 16 | logExit(0, { message: "[CTRL]+[C] exiting by user " }); 17 | process.exitCode = 0; 18 | process.stdin.setRawMode(false); 19 | process.exit(0); 20 | } 21 | 22 | // [E] - forced execution 23 | if (key && key.name === "e") { 24 | cache.hotkeys.e = true; 25 | } 26 | 27 | // [R] - revert back swap 28 | if (key && key.name === "r") { 29 | cache.hotkeys.r = true; 30 | } 31 | 32 | // [P] - switch profit chart visibility 33 | if (key && key.name === "p") { 34 | cache.ui.showProfitChart = !cache.ui.showProfitChart; 35 | } 36 | 37 | // [L] - switch performance chart visibility 38 | if (key && key.name === "l") { 39 | cache.ui.showPerformanceOfRouteCompChart = 40 | !cache.ui.showPerformanceOfRouteCompChart; 41 | } 42 | 43 | // [H] - switch trade history visibility 44 | if (key && key.name === "t") { 45 | cache.ui.showTradeHistory = !cache.ui.showTradeHistory; 46 | } 47 | 48 | // [I] - incognito mode (hide RPC) 49 | if (key && key.name === "i") { 50 | cache.ui.hideRpc = !cache.ui.hideRpc; 51 | } 52 | 53 | // [H] - switch help visibility 54 | if (key && key.name === "h") { 55 | cache.ui.showHelp = !cache.ui.showHelp; 56 | } 57 | 58 | // [S] - simulation mode switch 59 | if (key && key.name === "s") { 60 | cache.tradingEnabled = !cache.tradingEnabled; 61 | } 62 | 63 | // [D] - open discord invite link 64 | if (key && key.name === "d") { 65 | open(DISCORD_INVITE_URL); 66 | } 67 | }); 68 | 69 | process.stdin.setRawMode(true); 70 | process.stdin.resume(); 71 | }; 72 | 73 | module.exports = listenHotkeys; 74 | -------------------------------------------------------------------------------- /src/bot/ui/printToConsole.js: -------------------------------------------------------------------------------- 1 | const ui = require("cliui")({ width: 140 }); 2 | const chalk = require("chalk"); 3 | const moment = require("moment"); 4 | const chart = require("asciichart"); 5 | const JSBI = require('jsbi'); 6 | 7 | const { toDecimal } = require("../../utils"); 8 | const package = require("../../../package.json"); 9 | const cache = require("../cache"); 10 | 11 | function printToConsole({ 12 | date, 13 | i, 14 | performanceOfRouteComp, 15 | inputToken, 16 | outputToken, 17 | tokenA, 18 | tokenB, 19 | route, 20 | simulatedProfit, 21 | }) { 22 | try { 23 | if (cache.ui.allowClear) { 24 | // update max profitability spotted chart 25 | if (cache.ui.showProfitChart) { 26 | let spottetMaxTemp = 27 | cache.chart.spottedMax[cache.sideBuy ? "buy" : "sell"]; 28 | spottetMaxTemp.shift(); 29 | spottetMaxTemp.push( 30 | simulatedProfit === Infinity 31 | ? 0 32 | : parseFloat(simulatedProfit.toFixed(2)) 33 | ); 34 | cache.chart.spottedMax.buy = spottetMaxTemp; 35 | } 36 | 37 | // update performance chart 38 | if (cache.ui.showPerformanceOfRouteCompChart) { 39 | let performanceTemp = cache.chart.performanceOfRouteComp; 40 | performanceTemp.shift(); 41 | performanceTemp.push(parseInt(performanceOfRouteComp.toFixed())); 42 | cache.chart.performanceOfRouteComp = performanceTemp; 43 | } 44 | 45 | // check swap / fetch result status 46 | let statusMessage = " "; 47 | let statusPerformance; 48 | if (cache.swappingRightNow) { 49 | statusPerformance = performance.now() - cache.performanceOfTxStart; 50 | statusMessage = chalk.bold[ 51 | statusPerformance < 45000 52 | ? "greenBright" 53 | : statusPerformance < 60000 54 | ? "yellowBright" 55 | : "redBright" 56 | ](`SWAPPING ... ${(statusPerformance / 1000).toFixed(2)} s`); 57 | } else if (cache.fetchingResultsFromSolscan) { 58 | statusPerformance = 59 | performance.now() - cache.fetchingResultsFromSolscanStart; 60 | statusMessage = chalk.bold[ 61 | statusPerformance < 45000 62 | ? "greenBright" 63 | : statusPerformance < 90000 64 | ? "yellowBright" 65 | : "redBright" 66 | ](`FETCHING RESULT ... ${(statusPerformance / 1000).toFixed(2)} s`); 67 | } 68 | 69 | // refresh console before print 70 | console.clear(); 71 | ui.resetOutput(); 72 | 73 | // show HOTKEYS HELP 74 | if (cache.ui.showHelp) { 75 | ui.div( 76 | chalk.gray("[H] - show/hide help"), 77 | chalk.gray("[CTRL]+[C] - exit"), 78 | chalk.gray("[I] - incognito RPC") 79 | ); 80 | ui.div( 81 | chalk.gray("[L] - show/hide latency chart"), 82 | chalk.gray("[P] - show/hide profit chart"), 83 | chalk.gray("[T] - show/hide trade history") 84 | ); 85 | ui.div( 86 | chalk.gray("[E] - force execution"), 87 | chalk.gray("[R] - revert back swap"), 88 | chalk.gray("[S] - simulation mode switch") 89 | ); 90 | ui.div(" "); 91 | } 92 | 93 | ui.div( 94 | { 95 | text: `TIMESTAMP: ${chalk[cache.ui.defaultColor]( 96 | date.toLocaleString() 97 | )}`, 98 | }, 99 | { 100 | text: `I: ${ 101 | i % 2 === 0 102 | ? chalk[cache.ui.defaultColor].bold(i) 103 | : chalk[cache.ui.defaultColor](i) 104 | } | ${chalk.bold[cache.ui.defaultColor]( 105 | cache.iterationPerMinute.value 106 | )} i/min`, 107 | }, 108 | { 109 | text: `RPC: ${chalk[cache.ui.defaultColor]( 110 | cache.ui.hideRpc 111 | ? `${cache.config.rpc[0].slice( 112 | 0, 113 | 5 114 | )}...${cache.config.rpc[0].slice(-5)}` 115 | : cache.config.rpc[0] 116 | )}`, 117 | }, 118 | ); 119 | 120 | const performanceOfRouteCompColor = 121 | performanceOfRouteComp < 1000 ? cache.ui.defaultColor : "redBright"; 122 | 123 | ui.div( 124 | { 125 | text: `STARTED: ${chalk[cache.ui.defaultColor]( 126 | moment(cache.startTime).fromNow() 127 | )}`, 128 | }, 129 | { 130 | text: `LOOKUP (ROUTE): ${chalk.bold[performanceOfRouteCompColor]( 131 | performanceOfRouteComp.toFixed() 132 | )} ms`, 133 | }, 134 | { 135 | text: `MIN INTERVAL: ${chalk[cache.ui.defaultColor]( 136 | cache.config.minInterval 137 | )} ms QUEUE: ${chalk[cache.ui.defaultColor]( 138 | Object.keys(cache.queue).length 139 | )}/${chalk[cache.ui.defaultColor](cache.queueThrottle)}`, 140 | } 141 | ); 142 | 143 | ui.div( 144 | " ", 145 | " ", 146 | Object.values(cache.queue) 147 | .map( 148 | (v) => `${chalk[v === 0 ? "green" : v < 0 ? "yellow" : "red"]("●")}` 149 | ) 150 | .join(" ") 151 | ); 152 | 153 | if (cache.ui.showPerformanceOfRouteCompChart) 154 | ui.div( 155 | chart.plot(cache.chart.performanceOfRouteComp, { 156 | padding: " ".repeat(10), 157 | height: 5, 158 | }) 159 | ); 160 | 161 | // Show pubkey for identification of bot instance 162 | const pubkey = cache.ui.hideRpc ? 'hidden' : cache.walletpubkey; 163 | 164 | ui.div(`ARB PROTOCOL ${package.version} - (${pubkey})`); 165 | ui.div(chalk.gray("-".repeat(140))); 166 | 167 | ui.div( 168 | `${ 169 | cache.tradingEnabled 170 | ? "TRADING" 171 | : chalk.bold.magentaBright("SIMULATION") 172 | }: ${chalk.bold[cache.ui.defaultColor](inputToken.symbol)} ${ 173 | cache.config.tradingStrategy === "arbitrage" 174 | ? "" 175 | : `-> ${chalk.bold[cache.ui.defaultColor](outputToken.symbol)}` 176 | }`, 177 | `ROUTES: ${chalk.bold.yellowBright( 178 | cache.availableRoutes[cache.sideBuy ? "buy" : "sell"] 179 | )}`, 180 | `STRATEGY: ${chalk.bold[cache.ui.defaultColor]( 181 | cache.config.tradingStrategy 182 | )}`, 183 | { 184 | text: statusMessage, 185 | } 186 | ); 187 | ui.div(""); 188 | 189 | ui.div("BUY", "SELL", " ", " "); 190 | 191 | ui.div( 192 | { 193 | text: `SUCCESS : ${chalk.bold.green(cache.tradeCounter.buy.success)}`, 194 | }, 195 | { 196 | text: `SUCCESS: ${chalk.bold.green(cache.tradeCounter.sell.success)}`, 197 | }, 198 | { 199 | text: " ", 200 | }, 201 | { 202 | text: " ", 203 | } 204 | ); 205 | ui.div( 206 | { 207 | text: `FAIL: ${chalk.bold.red(cache.tradeCounter.buy.fail)}`, 208 | }, 209 | { 210 | text: `FAIL: ${chalk.bold.red(cache.tradeCounter.sell.fail)}`, 211 | }, 212 | { 213 | text: " ", 214 | }, 215 | { 216 | text: " ", 217 | } 218 | ); 219 | ui.div(""); 220 | 221 | ui.div( 222 | { 223 | text: `IN: ${chalk.yellowBright( 224 | toDecimal(String(route.inAmount), inputToken.decimals) 225 | )} ${chalk[cache.ui.defaultColor](inputToken.symbol)}`, 226 | }, 227 | { 228 | text: " ", 229 | }, 230 | { 231 | text: `SLIPPAGE: ${chalk.magentaBright( 232 | `${ 233 | cache.config.slippage + " BPS" 234 | }` 235 | )}`, 236 | }, 237 | { 238 | text: " ", 239 | }, 240 | { 241 | text: " ", 242 | } 243 | ); 244 | 245 | ui.div( 246 | { 247 | text: `OUT: ${chalk[simulatedProfit > 0 ? "greenBright" : "red"]( 248 | toDecimal(String(route.outAmount), outputToken.decimals) 249 | )} ${chalk[cache.ui.defaultColor](outputToken.symbol)}`, 250 | }, 251 | { 252 | text: " ", 253 | }, 254 | { 255 | text: `MIN. OUT: ${chalk.magentaBright( 256 | toDecimal(String(route.otherAmountThreshold), outputToken.decimals) 257 | )}`, 258 | }, 259 | { 260 | text: `W/UNWRAP SOL: ${chalk[cache.ui.defaultColor]( 261 | cache.wrapUnwrapSOL ? "on" : "off" 262 | )}`, 263 | }, 264 | { 265 | text: " ", 266 | } 267 | ); 268 | 269 | ui.div( 270 | { 271 | text: `PROFIT: ${chalk[simulatedProfit > 0 ? "greenBright" : "red"]( 272 | simulatedProfit.toFixed(2) 273 | )} % ${chalk.gray(`(${cache?.config?.minPercProfit})`)}`, 274 | }, 275 | { 276 | text: " ", 277 | }, 278 | { 279 | text: " ", 280 | }, 281 | { 282 | text: " ", 283 | }, 284 | { 285 | text: " ", 286 | } 287 | ); 288 | 289 | ui.div(" "); 290 | 291 | ui.div("CURRENT BALANCE", "LAST BALANCE", "INIT BALANCE", "PROFIT", " "); 292 | 293 | ui.div( 294 | `${chalk[JSBI.GT(cache.currentBalance.tokenA, 0) ? "yellowBright" : "gray"]( 295 | toDecimal(cache.currentBalance.tokenA, tokenA.decimals) 296 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 297 | 298 | `${chalk[JSBI.GT(cache.lastBalance.tokenA, 0) ? "yellowBright" : "gray"]( 299 | toDecimal(cache.lastBalance.tokenA, tokenA.decimals) 300 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 301 | 302 | `${chalk[JSBI.GT(cache.initialBalance.tokenA,0) ? "yellowBright" : "gray"]( 303 | toDecimal(cache.initialBalance.tokenA, tokenA.decimals) 304 | )} ${chalk[cache.ui.defaultColor](tokenA.symbol)}`, 305 | 306 | `${chalk[JSBI.GT(cache.currentProfit.tokenA,0) ? "greenBright" : "redBright"]( 307 | cache.currentProfit.tokenA.toFixed(2) 308 | )} %`, 309 | " " 310 | ); 311 | 312 | ui.div( 313 | `${chalk[JSBI.GT(cache.currentBalance.tokenB,0) ? "yellowBright" : "gray"]( 314 | toDecimal(String(cache.currentBalance.tokenB), tokenB.decimals) 315 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 316 | 317 | `${chalk[JSBI.GT(cache.lastBalance.tokenB,0) ? "yellowBright" : "gray"]( 318 | toDecimal(String(cache.lastBalance.tokenB), tokenB.decimals) 319 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 320 | 321 | `${chalk[JSBI.GT(cache.initialBalance.tokenB,0) ? "yellowBright" : "gray"]( 322 | toDecimal(String(cache.initialBalance.tokenB), tokenB.decimals) 323 | )} ${chalk[cache.ui.defaultColor](tokenB.symbol)}`, 324 | 325 | `${chalk[JSBI.GT(cache.currentProfit.tokenB,0) ? "greenBright" : "redBright"]( 326 | cache.currentProfit.tokenB.toFixed(2) 327 | )} %`, 328 | " " 329 | ); 330 | 331 | ui.div(chalk.gray("-".repeat(140))); 332 | ui.div(""); 333 | 334 | if (cache.ui.showProfitChart) { 335 | ui.div( 336 | chart.plot(cache.chart.spottedMax[cache.sideBuy ? "buy" : "sell"], { 337 | padding: " ".repeat(10), 338 | height: 4, 339 | colors: [simulatedProfit > 0 ? chart.lightgreen : chart.lightred], 340 | }) 341 | ); 342 | 343 | ui.div(""); 344 | } 345 | 346 | ui.div( 347 | { 348 | text: `MAX (BUY): ${chalk[cache.ui.defaultColor]( 349 | cache.maxProfitSpotted.buy.toFixed(2) 350 | )} %`, 351 | }, 352 | { 353 | text: `MAX (SELL): ${chalk[cache.ui.defaultColor]( 354 | cache.maxProfitSpotted.sell.toFixed(2) 355 | )} %`, 356 | }, 357 | { 358 | text: `ADAPTIVE SLIPPAGE: ${chalk[cache.ui.defaultColor]( 359 | (cache.config.adaptiveSlippage==1) ? 'ON' : 'OFF' 360 | )}`, 361 | }, 362 | ); 363 | 364 | ui.div(""); 365 | ui.div(chalk.gray("-".repeat(140))); 366 | ui.div(""); 367 | 368 | if (cache.ui.showTradeHistory) { 369 | ui.div( 370 | { text: `TIMESTAMP` }, 371 | { text: `SIDE` }, 372 | { text: `IN` }, 373 | { text: `OUT` }, 374 | { text: `PROFIT` }, 375 | { text: `EXP. OUT` }, 376 | { text: `EXP. PROFIT` }, 377 | { text: `ERROR` } 378 | ); 379 | 380 | ui.div(" "); 381 | 382 | if (cache?.tradeHistory?.length > 0) { 383 | const tableData = [...cache.tradeHistory].slice(-5); 384 | tableData.map((entry) => 385 | ui.div( 386 | { text: `${entry.date}`, border: true }, 387 | { text: `${entry.buy ? "BUY" : "SELL"}`, border: true }, 388 | { text: `${entry.inAmount} ${entry.inputToken}`, border: true }, 389 | { text: `${entry.outAmount} ${entry.outputToken}`, border: true }, 390 | { 391 | text: `${ 392 | chalk[ 393 | entry.profit > 0 394 | ? "greenBright" 395 | : entry.profit < 0 396 | ? "redBright" 397 | : "cyanBright" 398 | ](isNaN(entry.profit) ? "0" : entry.profit.toFixed(2)) + " %" 399 | }`, 400 | border: true, 401 | }, 402 | { 403 | text: `${entry.expectedOutAmount} ${entry.outputToken}`, 404 | border: true, 405 | }, 406 | { 407 | text: `${entry.expectedProfit.toFixed(2)}% ${entry.slippage} BPS`, 408 | border: true, 409 | }, 410 | { 411 | text: `${ 412 | entry.error ? chalk.bold.redBright(entry.error) : "-" 413 | }`, 414 | border: true, 415 | } 416 | ) 417 | ); 418 | } 419 | } 420 | ui.div(""); 421 | 422 | // print UI 423 | console.log(ui.toString()); 424 | } 425 | } catch (error) { 426 | console.log(error); 427 | } 428 | } 429 | 430 | module.exports = printToConsole; 431 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | const CONFIG_INITIAL_STATE = { 2 | showHelp: true, 3 | nav: { 4 | currentStep: 0, 5 | steps: [ 6 | "network", 7 | "rpc", 8 | "strategy", 9 | "tokens", 10 | "trading size", 11 | "profit", 12 | "slippage", 13 | "priority", 14 | "advanced", 15 | "confirm", 16 | ], 17 | }, 18 | config: { 19 | network: { 20 | value: "", 21 | isSet: false, 22 | }, 23 | rpc: { 24 | value: [], 25 | isSet: false, 26 | state: { 27 | items: [ 28 | { 29 | label: process.env.DEFAULT_RPC, 30 | value: process.env.DEFAULT_RPC, 31 | isSelected: true, 32 | }, 33 | ...String(process.env.ALT_RPC_LIST) 34 | .split(",") 35 | .map((item) => ({ 36 | label: item, 37 | value: item, 38 | isSelected: false, 39 | })), 40 | ], 41 | }, 42 | }, 43 | strategy: { 44 | value: "", 45 | isSet: false, 46 | }, 47 | tokens: { 48 | value: { 49 | tokenA: { symbol: "", address: "" }, 50 | tokenB: { symbol: "", address: "" }, 51 | }, 52 | isSet: { 53 | tokenA: false, 54 | tokenB: false, 55 | }, 56 | }, 57 | "trading size": { 58 | value: { 59 | strategy: "", 60 | value: "", 61 | }, 62 | isSet: false, 63 | }, 64 | profit: { 65 | value: 1, 66 | isSet: { 67 | percent: false, 68 | strategy: false, 69 | }, 70 | }, 71 | slippage: { 72 | value: 0, 73 | isSet: false, 74 | }, 75 | priority: { 76 | value: 0, 77 | isSet: false, 78 | }, 79 | advanced: { 80 | value: { 81 | minInterval: 100, 82 | }, 83 | isSet: { 84 | minInterval: false, 85 | }, 86 | }, 87 | }, 88 | }; 89 | 90 | module.exports = { 91 | DISCORD_INVITE_URL: "https://discord.gg/Z8JJCuq4", 92 | CONFIG_INITIAL_STATE, 93 | }; -------------------------------------------------------------------------------- /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 | // check for .env file 9 | const { checkForEnvFile, checkWallet, checkArbReady } = require("./utils"); 10 | checkForEnvFile(); 11 | 12 | require("dotenv").config(); 13 | 14 | checkWallet(); 15 | 16 | const isArbReady = async () => { 17 | try { 18 | // Display the message 19 | await checkArbReady(); 20 | return true; // If checkArbReady completes without errors, return true 21 | } catch (error) { 22 | spinner.text = chalk.black.bgRedBright( 23 | `\n${error.message}\n` 24 | ); 25 | logExit(1, error); 26 | process.exit(1); // Exit the process if there's an error 27 | } 28 | }; 29 | 30 | isArbReady().then((arbReady) => { 31 | if (!arbReady) { 32 | process.exit(1); // Exit the process if ARB is not ready 33 | } 34 | }); 35 | 36 | const wizard = importJsx("./wizard/index"); 37 | 38 | const cli = meow(` 39 | Usage 40 | $ solana-jupiter-bot 41 | 42 | Options 43 | --name Your name 44 | 45 | Examples 46 | $ solana-jupiter-bot --name=Jane 47 | Hello, Master 48 | `); 49 | 50 | console.clear(); 51 | 52 | render(React.createElement(wizard, cli.flags)).waitUntilExit(); -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const chalk = require("chalk"); 2 | const fs = require("fs"); 3 | const ora = require("ora-classic"); 4 | const { logExit } = require("../bot/exit"); 5 | const JSBI = require('jsbi'); 6 | const bs58 = require("bs58"); 7 | const { PublicKey, Connection, Keypair } = require("@solana/web3.js"); 8 | require("dotenv").config(); 9 | 10 | const createTempDir = () => !fs.existsSync("./temp") && fs.mkdirSync("./temp"); 11 | 12 | const getCircularReplacer = () => { 13 | const seen = new WeakSet(); 14 | return (key, value) => { 15 | if (typeof value === "object" && value !== null) { 16 | if (seen.has(value)) { 17 | return; 18 | } 19 | seen.add(value); 20 | } else if (typeof value === "bigint") { 21 | value = value.toString(); 22 | } 23 | return value; 24 | }; 25 | }; 26 | 27 | const storeItInTempAsJSON = (filename, data) => 28 | fs.writeFileSync(`./temp/${filename}.json`, JSON.stringify(data, getCircularReplacer(), 2)); 29 | 30 | const createConfigFile = (config) => { 31 | const configSpinner = ora({ 32 | text: "Creating config...", 33 | discardStdin: false, 34 | }).start(); 35 | 36 | // Set the adaptive slippage setting based on initial configuration 37 | const adaptiveslippage = config?.adaptiveslippage?.value ?? 0; 38 | 39 | const configValues = { 40 | network: config.network.value, 41 | rpc: config.rpc.value, 42 | tradingStrategy: config.strategy.value, 43 | tokenA: config.tokens.value.tokenA, 44 | tokenB: config.tokens.value.tokenB, 45 | slippage: config.slippage.value, 46 | adaptiveSlippage: adaptiveslippage, 47 | priority: config.priority.value, 48 | minPercProfit: config.profit.value, 49 | minInterval: parseInt(config.advanced.value.minInterval), 50 | tradeSize: { 51 | value: parseFloat(config["trading size"].value.value), 52 | strategy: config["trading size"].value.strategy, 53 | }, 54 | ui: { 55 | defaultColor: "cyan", 56 | }, 57 | storeFailedTxInHistory: true, 58 | }; 59 | 60 | fs.writeFileSync("./config.json", JSON.stringify(configValues, null, 2), {}); 61 | configSpinner.succeed("Config created!"); 62 | }; 63 | 64 | const verifyConfig = (config) => { 65 | let result = true; 66 | const badConfig = []; 67 | Object.entries(config).forEach(([key, value]) => { 68 | const isSet = value.isSet; 69 | const isSectionSet = 70 | isSet instanceof Object 71 | ? Object.values(isSet).every((value) => value === true) 72 | : isSet; 73 | 74 | if (!isSectionSet) { 75 | result = false; 76 | badConfig.push(key); 77 | } 78 | }); 79 | return { result, badConfig }; 80 | }; 81 | 82 | /** 83 | * It loads the config file and returns the config object 84 | * @returns The config object 85 | */ 86 | const loadConfigFile = ({ showSpinner = false }) => { 87 | let config = {}; 88 | let spinner; 89 | if (showSpinner) { 90 | spinner = ora({ 91 | text: "Loading config...", 92 | discardStdin: false, 93 | }).start(); 94 | } 95 | 96 | if (fs.existsSync("./config.json")) { 97 | config = JSON.parse(fs.readFileSync("./config.json")); 98 | spinner?.succeed("Config loaded!"); 99 | return config; 100 | } 101 | 102 | spinner?.fail(chalk.redBright("Loading config failed!\n")); 103 | throw new Error("\nNo config.json file found!\n"); 104 | }; 105 | 106 | const calculateProfit = ((oldVal, newVal) => ((newVal - oldVal) / oldVal) * 100); 107 | 108 | const toDecimal = (number, decimals) => 109 | parseFloat(String(number) / 10 ** decimals).toFixed(decimals); 110 | 111 | 112 | const toNumber = (number, decimals) => 113 | Math.floor(String(number) * 10 ** decimals); 114 | 115 | /** 116 | * It calculates the number of iterations per minute and updates the cache. 117 | */ 118 | const updateIterationsPerMin = (cache) => { 119 | const iterationTimer = 120 | (performance.now() - cache.iterationPerMinute.start) / 1000; 121 | 122 | if (iterationTimer >= 60) { 123 | cache.iterationPerMinute.value = Number( 124 | cache.iterationPerMinute.counter.toFixed() 125 | ); 126 | cache.iterationPerMinute.start = performance.now(); 127 | cache.iterationPerMinute.counter = 0; 128 | } else cache.iterationPerMinute.counter++; 129 | }; 130 | 131 | const checkRoutesResponse = (routes) => { 132 | if (Object.hasOwn(routes, "routesInfos")) { 133 | if (routes.routesInfos.length === 0) { 134 | console.log(routes); 135 | logExit(1, { 136 | message: "No routes found or something is wrong with RPC / Jupiter! ", 137 | }); 138 | process.exit(1); 139 | } 140 | } else { 141 | console.log(routes); 142 | logExit(1, { 143 | message: "Something is wrong with RPC / Jupiter! ", 144 | }); 145 | process.exit(1); 146 | } 147 | }; 148 | 149 | function displayMessage(message) { 150 | console.clear(); // Clear console before displaying message 151 | const lineLength = 50; // Length of each line 152 | const paddingLength = Math.max(0, Math.floor((lineLength - message.length) / 2)); // Calculate padding length for centering, ensuring it's non-negative 153 | const padding = "-".repeat(paddingLength); // Create padding string 154 | const displayMessage = `${padding}\x1b[93m${message}\x1b[0m${padding}`; // Create display message with padding and light yellow color ANSI escape codes 155 | 156 | console.log("\n"); 157 | console.log(`\x1b[1m${'ARB PROTOCOL BOT SETUP TESTS'}\x1b[0m\n`); 158 | console.log("\x1b[93m*\x1b[0m".repeat(lineLength / 2)); // Display top border in light yellow 159 | console.log(`\n${displayMessage}\n`); // Display message 160 | console.log("\x1b[93m*\x1b[0m".repeat(lineLength / 2)); // Display bottom border in light yellow 161 | console.log("\n"); 162 | } 163 | 164 | const checkForEnvFile = () => { 165 | if (!fs.existsSync("./.env")) { 166 | displayMessage("Please refer to the readme to set up the Bot properly.\n\nYou have not created the .ENV file yet.\n\nRefer to the .env.example file."); 167 | logExit(1, { 168 | message: "No .env file found! ", 169 | }); 170 | process.exit(1); 171 | } 172 | }; 173 | const checkWallet = () => { 174 | if ( 175 | !process.env.SOLANA_WALLET_PRIVATE_KEY || 176 | (process.env.SOLANA_WALLET_PUBLIC_KEY && 177 | process.env.SOLANA_WALLET_PUBLIC_KEY?.length !== 88) 178 | ) { 179 | displayMessage(`${process.env.SOLANA_WALLET_PUBLIC_KEY} Your wallet is not valid. \n\nCheck the .env file and ensure you have put in the private key in the correct format. \n\ni.e. SOLANA_WALLET_PRIVATE_KEY=3QztVpoRgLNvAmBX9Yo3cjR3bLrXVrJZbPW5BY7GXq8GFvEjR4xEDeVai85a8WtYUCePvMx27eBut5K2kdqN8Hks`); 180 | process.exit(1); 181 | } 182 | } 183 | 184 | const checkArbReady = async () => { 185 | try{ 186 | // Support the community 187 | const ARB_TOKEN = '9tzZzEHsKnwFL1A3DyFJwj36KnZj3gZ7g4srWp9YTEoh'; 188 | 189 | var checkBalance = Number(0); 190 | const connection = new Connection(process.env.DEFAULT_RPC); 191 | wallet = Keypair.fromSecretKey(bs58.decode(process.env.SOLANA_WALLET_PRIVATE_KEY)); 192 | 193 | const tokenAccounts = await connection.getParsedTokenAccountsByOwner(wallet.publicKey, { 194 | mint: new PublicKey(ARB_TOKEN) 195 | }); 196 | 197 | let totalTokenBalance = BigInt(0); 198 | tokenAccounts.value.forEach((accountInfo) => { 199 | const parsedInfo = accountInfo.account.data.parsed.info; 200 | totalTokenBalance += BigInt(parsedInfo.tokenAmount.amount); 201 | }); 202 | 203 | // Do you support the project and the hard work of the developers? 204 | var arb_ready = Number(totalTokenBalance); 205 | if (arb_ready < 10000000000) { 206 | console.clear(); // Clear console before displaying message 207 | displayMessage("You are not ARB ready! You need to hold at least 10K in ARB in your trading wallet to use this bot."); 208 | process.exit(1); 209 | } 210 | 211 | // Check if there are no ATAs for the specified token 212 | if (tokenAccounts.value.length === 0) { 213 | console.clear(); // Clear console before displaying message 214 | displayMessage("You are not ARB ready! You need to hold at least 10K in ARB in your trading wallet to use this bot."); 215 | process.exit(1); 216 | } 217 | return true; 218 | } catch (err){ 219 | console.clear(); // Clear console before displaying message 220 | displayMessage("You do not seem to be ARB ready!\n\nCheck the .ENV file to see your RPC is set up properly and your wallet is set to the correct private key."); 221 | process.exit(1); 222 | } 223 | }; 224 | 225 | module.exports = { 226 | createTempDir, 227 | storeItInTempAsJSON, 228 | createConfigFile, 229 | loadConfigFile, 230 | verifyConfig, 231 | calculateProfit, 232 | toDecimal, 233 | toNumber, 234 | updateIterationsPerMin, 235 | checkRoutesResponse, 236 | checkForEnvFile, 237 | checkArbReady, 238 | checkWallet, 239 | }; 240 | -------------------------------------------------------------------------------- /src/utils/transaction.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { Connection, PublicKey } = require("@solana/web3.js"); 3 | const { setTimeout } = require("timers/promises"); 4 | const cache = require("../bot/cache"); 5 | const { loadConfigFile, toNumber, calculateProfit, toDecimal } = require("./index.js"); 6 | 7 | cache.config = loadConfigFile({ showSpinner: true }); 8 | 9 | // Adding a backup option for the transaction lookup 10 | // This is only needed for some RPCS that are not 11 | // working or are behind at the time of lookup. 12 | const rpc_main = cache.config.rpc[0]; 13 | const rpc_backup = 'https://api.mainnet-beta.solana.com'; 14 | 15 | // Key variables 16 | var transstatus = 0; 17 | var transid = ''; 18 | var transresp = []; 19 | 20 | const WAIT_ERROR_CODE = 1; 21 | const WAIT_SUCCESS_CODE = 0; 22 | 23 | const waitabit = async (ms) => { 24 | try { 25 | await setTimeout(ms); 26 | console.log('Waited for', ms, 'milliseconds.'); 27 | return WAIT_SUCCESS_CODE; 28 | } catch (error) { 29 | console.error('Error occurred while waiting:', error); 30 | return WAIT_ERROR_CODE; 31 | } 32 | }; 33 | 34 | // Main RPC 35 | const connection = new Connection(rpc_main, { 36 | disableRetryOnRateLimit: true, 37 | commitment: 'confirmed', 38 | }); 39 | 40 | // Backup RPC 41 | const connection_backup = new Connection(rpc_backup, { 42 | disableRetryOnRateLimit: false, 43 | commitment: 'confirmed', 44 | }); 45 | 46 | const fetchTransaction = async (rpcConnection, transaction) => { 47 | try { 48 | return await rpcConnection.getParsedTransaction(transaction, { "maxSupportedTransactionVersion": 0 }); 49 | } catch (error) { 50 | // Handle errors, or let the caller handle them. 51 | console.error("Error fetching transaction:", error); 52 | return null; 53 | } 54 | }; 55 | 56 | const checkTransactionStatus = async (transaction, wallet_address) => { 57 | try { 58 | const primaryTransaction = await fetchTransaction(connection, transaction); 59 | 60 | if (!primaryTransaction) { 61 | // If primary RPC fails, try backup RPC 62 | return await fetchTransaction(connection_backup, transaction); 63 | } 64 | 65 | return primaryTransaction; 66 | } catch (error) { 67 | console.error("Error checking transaction status:", error); 68 | return null; 69 | } 70 | 71 | }; 72 | 73 | const checktrans = async (txid,wallet_address) => { 74 | try { 75 | transresp = await checkTransactionStatus(txid, wallet_address); 76 | 77 | if (transresp) { 78 | var transaction_changes = []; 79 | 80 | if (transresp.meta?.status.Err){ 81 | // Failed Transaction 82 | return [transresp.meta.status.err,2]; 83 | } else { 84 | transstatus = 1; 85 | } 86 | 87 | // Check if postTokenBalances is null or empty 88 | if (!transresp.meta.postTokenBalances || transresp.meta.postTokenBalances.length === 0) { 89 | return [null, WAIT_ERROR_CODE]; 90 | } 91 | 92 | var tokenamt=0; 93 | var tokendec=0; 94 | 95 | // Outgoing SOL native mgmt 96 | // Handle transfers of SOL that would not show up due to wrapping 97 | if (transresp.meta.innerInstructions){ 98 | for (instructions of transresp.meta.innerInstructions){ 99 | if(instructions.instructions){ 100 | for (parsed of instructions.instructions){ 101 | //console.log(JSON.stringify(parsed, null, 4)); 102 | if (parsed.parsed){ 103 | if (parsed.parsed.type=='transferChecked'){ 104 | if (parsed.parsed.info.authority==wallet_address && parsed.parsed.info.mint=='So11111111111111111111111111111111111111112'){ 105 | tokenamt = Number(parsed.parsed.info.tokenAmount.amount); 106 | tokendec = parsed.parsed.info.tokenAmount.decimals; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | // SOL Transfer handling 116 | if (tokenamt>0){ 117 | transaction_changes['So11111111111111111111111111111111111111112'] = { status: transstatus, start: tokenamt, decimals: tokendec, end: 0, change: (-1*tokenamt) }; 118 | } 119 | 120 | // Pre Token Balance Handling 121 | for (token of transresp.meta.preTokenBalances){ 122 | if (token.owner==wallet_address){ 123 | transaction_changes[token.mint.toString()] = {status: transstatus, start: token.uiTokenAmount.amount, decimals: token.uiTokenAmount.decimals}; 124 | }; 125 | } 126 | 127 | // Post Token Handling 128 | for (token of transresp.meta.postTokenBalances){ 129 | if (token.owner==wallet_address){ 130 | if (transaction_changes[token.mint]?.start) { 131 | // Case where token account existed already 132 | diff = Number(token.uiTokenAmount.amount)-Number(transaction_changes[token.mint].start); 133 | diffdec = toDecimal(diff,transaction_changes[token.mint].decimals); 134 | transaction_changes[token.mint] = {...transaction_changes[token.mint], end: token.uiTokenAmount.amount, change: diff} 135 | } else { 136 | // Case where token did not exist yet 137 | // Set the initial to 0 138 | transaction_changes[token.mint] = {status: transstatus, start: 0, decimals: token.uiTokenAmount.decimals}; 139 | // Calculate the difference 140 | diff = Number(token.uiTokenAmount.amount)-Number(transaction_changes[token.mint].start); 141 | diffdec = toDecimal(diff,transaction_changes[token.mint].decimals); 142 | transaction_changes[token.mint] = {...transaction_changes[token.mint], end: token.uiTokenAmount.amount, change: diff} 143 | } 144 | } 145 | } 146 | return [transaction_changes, WAIT_SUCCESS_CODE]; 147 | } else { 148 | // Transaction not found or error occurred 149 | return [null, WAIT_ERROR_CODE]; 150 | } 151 | } catch(error) { 152 | console.error('Error checking transaction:', error); 153 | return [null, WAIT_ERROR_CODE]; 154 | } 155 | } 156 | 157 | module.exports = {checktrans}; -------------------------------------------------------------------------------- /src/wizard/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 | -------------------------------------------------------------------------------- /src/wizard/Components/Help.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const chalk = require("chalk"); 4 | const Help = () => { 5 | return ( 6 | 7 | 8 | 9 | {chalk.gray`${chalk.bold.white("[")} previous`} 10 | {chalk.gray`${chalk.bold.white("]")} next`} 11 | {chalk.gray`[${chalk.bold.white("H")}]elp`} 12 | {chalk.gray`[${chalk.bold.hex("#481ede")("ESC")}]`} 13 | 14 | 15 | ); 16 | }; 17 | module.exports = Help; 18 | -------------------------------------------------------------------------------- /src/wizard/Components/Layout.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box } = require("ink"); 3 | 4 | const importJsx = require("import-jsx"); 5 | const { useContext } = require("react"); 6 | const WizardContext = require("../WizardContext"); 7 | const Help = require("import-jsx")("./Help"); 8 | 9 | const WizardHeader = importJsx("./WizardHeader"); 10 | const Menu = importJsx("./Menu"); 11 | const Main = importJsx("./Main"); 12 | 13 | const Layout = () => { 14 | const { showHelp } = useContext(WizardContext); 15 | return ( 16 | <> 17 | {showHelp && } 18 | 19 | 20 | 28 | 29 |
30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | module.exports = Layout; 37 | -------------------------------------------------------------------------------- /src/wizard/Components/Main.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box } = require("ink"); 3 | const WizardContext = require("../WizardContext"); 4 | const { useContext } = require("react"); 5 | const importJsx = require("import-jsx"); 6 | const Divider = require("ink-divider"); 7 | const Router = importJsx("./Router"); 8 | 9 | function Main() { 10 | const { nav } = useContext(WizardContext); 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | module.exports = Main; 21 | -------------------------------------------------------------------------------- /src/wizard/Components/Menu.js: -------------------------------------------------------------------------------- 1 | const { Box, Text } = require("ink"); 2 | const { useContext } = require("react"); 3 | const React = require("react"); 4 | const WizardContext = require("../WizardContext"); 5 | const Menu = () => { 6 | const { nav, config } = useContext(WizardContext); 7 | return ( 8 | 15 | {nav.steps.map((step, index) => { 16 | const isActive = index === nav.currentStep; 17 | const isSet = config[nav.steps[index]]?.isSet; 18 | const isSectionSet = 19 | isSet instanceof Object 20 | ? Object.values(isSet).every((value) => value === true) 21 | : isSet; 22 | 23 | return ( 24 | 32 | {step} 33 | 34 | ); 35 | })} 36 | 37 | ); 38 | }; 39 | 40 | module.exports = Menu; 41 | -------------------------------------------------------------------------------- /src/wizard/Components/Router.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const importJsx = require("import-jsx"); 3 | 4 | const WizardContext = require("../WizardContext"); 5 | const { useContext } = require("react"); 6 | 7 | const Network = importJsx("../Pages/Network"); 8 | const Rpc = importJsx("../Pages/Rpc"); 9 | const Strategy = importJsx("../Pages/Strategy"); 10 | const Tokens = importJsx("../Pages/Tokens"); 11 | const TradingSize = importJsx("../Pages/TradingSize"); 12 | const Profit = importJsx("../Pages/Profit"); 13 | const Slippage = importJsx("../Pages/Slippage"); 14 | const Priority = importJsx("../Pages/Priority"); 15 | const Advanced = importJsx("../Pages/Advanced"); 16 | const Confirm = importJsx("../Pages/Confirm"); 17 | 18 | const ROUTES = { 19 | network: , 20 | rpc: , 21 | strategy: , 22 | tokens: , 23 | "trading size": , 24 | profit: , 25 | slippage: , 26 | priority: , 27 | advanced: , 28 | confirm: , 29 | }; 30 | 31 | const Router = () => { 32 | const { nav } = useContext(WizardContext); 33 | 34 | return <>{ROUTES[nav.steps[nav.currentStep]]}; 35 | }; 36 | module.exports = Router; 37 | -------------------------------------------------------------------------------- /src/wizard/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 | -------------------------------------------------------------------------------- /src/wizard/Components/WizardHeader.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const BigText = require("ink-big-text"); 3 | const Gradient = require("ink-gradient"); 4 | const { Box } = require("ink"); 5 | const { useState, useEffect } = require("react"); 6 | 7 | let colorsSetInitialState = [ 8 | "#cf4884", 9 | "#8832b3", 10 | "#b5b4fa", 11 | "#cdadff", 12 | "#6d29c5", 13 | "#4e21d9", 14 | "#481ede", 15 | "#4b9db0", 16 | "#8deef5", 17 | "#cdd4a2", 18 | "#e2a659", 19 | ]; 20 | 21 | const IntroTitle = () => { 22 | const [colorsSet, setColorSet] = useState(colorsSetInitialState); 23 | useEffect(() => { 24 | const changeColorInterval = setInterval(() => { 25 | const temp = [...colorsSet]; 26 | const a = temp.shift(); 27 | temp.push(a); 28 | setColorSet(temp); 29 | }, 200); 30 | 31 | return () => { 32 | try { 33 | clearInterval(changeColorInterval); 34 | } catch (error) { 35 | console.log("changeColorInterval error: ", error); 36 | } 37 | }; 38 | }, [colorsSet]); 39 | 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | module.exports = IntroTitle; 56 | -------------------------------------------------------------------------------- /src/wizard/Pages/Advanced.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const { useContext, useState, useRef, useEffect } = require("react"); 4 | const WizardContext = require("../WizardContext"); 5 | const { default: TextInput } = require("ink-text-input"); 6 | 7 | function Advanced() { 8 | let isMountedRef = useRef(false); 9 | 10 | const { 11 | config: { 12 | advanced: { value: advancedValue, isSet: advancedIsSet }, 13 | }, 14 | configSetValue, 15 | } = useContext(WizardContext); 16 | 17 | const [tempAdvancedValue, setTempAdvancedValue] = useState(advancedValue); 18 | 19 | const handleSubmit = (key, value) => { 20 | configSetValue("advanced", { 21 | value: { 22 | ...advancedValue, 23 | [key]: value, 24 | }, 25 | isSet: { 26 | ...advancedIsSet, 27 | [key]: true, 28 | }, 29 | }); 30 | }; 31 | 32 | const handleMinIntervalChange = (value) => { 33 | if (!isMountedRef.current) return; 34 | 35 | setTempAdvancedValue({ 36 | ...tempAdvancedValue, 37 | minInterval: value, 38 | }); 39 | }; 40 | 41 | useEffect(() => { 42 | isMountedRef.current = true; 43 | return () => (isMountedRef.current = false); 44 | }, []); 45 | 46 | return ( 47 | 48 | 49 | Advanced settings can be crucial for strategy efficiency. 50 | 51 | 52 | Please make sure you know what you are doing before changing these 53 | settings. 54 | 55 | 56 | 57 | Min Interval:{" "} 58 | {!advancedIsSet.minInterval ? ( 59 | 60 | { 68 | handleSubmit("minInterval", value); 69 | }} 70 | /> 71 | 72 | ) : ( 73 | {tempAdvancedValue?.minInterval} 74 | )} 75 | 76 | 77 | 78 | ); 79 | } 80 | module.exports = Advanced; 81 | -------------------------------------------------------------------------------- /src/wizard/Pages/Confirm.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text, useApp } = require("ink"); 3 | const WizardContext = require("../WizardContext"); 4 | const { useContext, useEffect, useState } = require("react"); 5 | const { default: TextInput } = require("ink-text-input"); 6 | const chalk = require("chalk"); 7 | const { createConfigFile, verifyConfig } = require("../../utils"); 8 | 9 | const Confirm = () => { 10 | const { exit } = useApp(); 11 | const { 12 | config: { 13 | network: { value: network }, 14 | rpc: { value: rpc }, 15 | strategy: { value: strategy }, 16 | tokens: { value: tokens }, 17 | "trading size": { value: tradingSize }, 18 | profit: { value: profit }, 19 | slippage: { value: slippage }, 20 | priority: { value: priority }, 21 | advanced: { value: advanced }, 22 | }, 23 | config, 24 | } = useContext(WizardContext); 25 | const [isConfigOk, setIsConfigOk] = useState({ 26 | result: false, 27 | badConfig: [], 28 | }); 29 | 30 | useEffect(() => { 31 | setIsConfigOk(verifyConfig(config)); 32 | }, []); 33 | 34 | return ( 35 | 36 | Confirm your settings: 37 | 38 | Network: {chalk.greenBright(network)} 39 | RPC: {chalk.greenBright(rpc)} 40 | Strategy: {chalk.bold.greenBright(strategy)} 41 | 42 | Tokens: {chalk.bold.blueBright(tokens.tokenA.symbol)} /{" "} 43 | {chalk.bold.blueBright(tokens.tokenB.symbol)} 44 | 45 | 46 | Trading size: {chalk.bold.greenBright(tradingSize.value)}{" "} 47 | {chalk.gray(tokens.tokenA.symbol)} |{" "} 48 | {chalk.greenBright(tradingSize.strategy)} 49 | 50 | Profit: {chalk.bold.greenBright(profit)} 51 | Slippage: {chalk.bold.greenBright(slippage)} 52 | Priority: {chalk.bold.greenBright(priority)} 53 | 54 | 55 | Min Interval: {chalk.bold.greenBright(advanced.minInterval)} 56 | 57 | 58 | {isConfigOk.result ? ( 59 | { 63 | createConfigFile(config); 64 | exit(); 65 | }} 66 | /> 67 | ) : ( 68 | 69 | Error on step:{" "} 70 | {{isConfigOk.badConfig.join(", ")}} 71 | 72 | )} 73 | 74 | ); 75 | }; 76 | 77 | module.exports = Confirm; 78 | -------------------------------------------------------------------------------- /src/wizard/Pages/Network.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const WizardContext = require("../WizardContext"); 4 | const { useContext } = require("react"); 5 | const { default: SelectInput } = require("ink-select-input"); 6 | const chalk = require("chalk"); 7 | 8 | const NETWORKS = [ 9 | { label: "mainnet-beta", value: "mainnet-beta" }, 10 | ]; 11 | 12 | const Indicator = ({ label, value }) => { 13 | const { 14 | config: { 15 | network: { value: selectedValue }, 16 | }, 17 | } = useContext(WizardContext); 18 | 19 | const isSelected = value == selectedValue; 20 | 21 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 22 | }; 23 | 24 | function Network() { 25 | const { configSetValue } = useContext(WizardContext); 26 | return ( 27 | 28 | 29 | Select Solana Network: 30 | 31 | 32 | configSetValue("network", item.value)} 35 | itemComponent={Indicator} 36 | /> 37 | 38 | 39 | ); 40 | } 41 | module.exports = Network; 42 | -------------------------------------------------------------------------------- /src/wizard/Pages/Priority.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const { default: SelectInput } = require("ink-select-input"); 4 | const { useContext, useState, useEffect, useRef } = require("react"); 5 | const WizardContext = require("../WizardContext"); 6 | 7 | const { default: TextInput } = require("ink-text-input"); 8 | const chalk = require("chalk"); 9 | 10 | const priority_STRATEGIES = [ 11 | { label: "1000 micro Lamports", value: 1000 }, 12 | { label: "10000 micro Lamports", value: 10000 }, 13 | { label: "50000 micro Lamports", value: 50000 }, 14 | { label: "Custom setting", value: "custom" }, 15 | ]; 16 | 17 | const Indicator = ({ label, value }) => { 18 | const { 19 | config: { 20 | priority: { value: selectedValue }, 21 | }, 22 | } = useContext(WizardContext); 23 | 24 | const isSelected = value == selectedValue; 25 | 26 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 27 | }; 28 | 29 | function priority() { 30 | const { configSetValue } = useContext(WizardContext); 31 | let isMountedRef = useRef(false); 32 | 33 | const [temppriorityStrategy, setTemppriorityStrategy] = useState( 34 | priority_STRATEGIES[0] 35 | ); 36 | const [custompriority, setCustompriority] = useState("1"); 37 | const [inputBorderColor, setInputBorderColor] = useState("gray"); 38 | 39 | const handlepriorityStrategySelect = (priority) => { 40 | const value = priority.value; 41 | setTemppriorityStrategy(value); 42 | if (value !== "custom") 43 | configSetValue( 44 | "priority", Number(value) 45 | ); 46 | }; 47 | 48 | const handleCustompriorityChange = (value) => { 49 | const badChars = /[^0-9.]/g; 50 | badChars.test(value) 51 | ? setInputBorderColor("red") 52 | : setInputBorderColor("gray"); 53 | const sanitizedValue = value.replace(badChars, ""); 54 | setCustompriority(sanitizedValue); 55 | setTimeout(() => isMountedRef.current && setInputBorderColor("gray"), 100); 56 | }; 57 | 58 | const handleCustomprioritySubmit = () => { 59 | configSetValue("priority", Number(custompriority)); 60 | }; 61 | 62 | useEffect(() => { 63 | isMountedRef.current = true; 64 | return () => (isMountedRef.current = false); 65 | }, []); 66 | 67 | return ( 68 | 69 | 70 | Set priority strategy 71 | 72 | 73 | 78 | 79 | {temppriorityStrategy === "custom" && ( 80 | 81 | Custom priority: 82 | 87 | 92 | 93 | micro Lamports 94 | 95 | )} 96 | 97 | ); 98 | } 99 | module.exports = priority; 100 | -------------------------------------------------------------------------------- /src/wizard/Pages/Profit.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const { useContext, useState, useRef, useEffect } = require("react"); 4 | const WizardContext = require("../WizardContext"); 5 | 6 | const { default: TextInput } = require("ink-text-input"); 7 | 8 | function MinProfit() { 9 | let isMountedRef = useRef(false); 10 | const { 11 | config: { 12 | profit: { value: profitValue }, 13 | }, 14 | configSetValue, 15 | } = useContext(WizardContext); 16 | 17 | const [minProfit, setMinProfit] = useState(profitValue.toString()); 18 | const [inputBorderColor, setInputBorderColor] = useState("gray"); 19 | 20 | const handleMinProfitSubmit = (value) => { 21 | configSetValue("profit", value); 22 | }; 23 | 24 | const handleMinProfitChange = (value) => { 25 | if (!isMountedRef.current) return; 26 | 27 | const badChars = /[^0-9.]/g; 28 | badChars.test(value) 29 | ? setInputBorderColor("red") 30 | : setInputBorderColor("gray"); 31 | const sanitizedValue = value.replace(badChars, ""); 32 | setMinProfit(sanitizedValue); 33 | setTimeout(() => isMountedRef.current && setInputBorderColor("gray"), 100); 34 | }; 35 | 36 | useEffect(() => { 37 | isMountedRef.current = true; 38 | return () => (isMountedRef.current = false); 39 | }, []); 40 | 41 | return ( 42 | 43 | 44 | Set min. profit value: 45 | 46 | 47 | 48 | Min. Profit: 49 | 50 | 55 | 56 | % 57 | 58 | 59 | ); 60 | } 61 | module.exports = MinProfit; 62 | -------------------------------------------------------------------------------- /src/wizard/Pages/Rpc.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text, useInput, Newline } = require("ink"); 3 | const WizardContext = require("../WizardContext"); 4 | const { useContext, useState } = require("react"); 5 | const { default: SelectInput } = require("ink-select-input"); 6 | const chalk = require("chalk"); 7 | 8 | const Indicator = ({ label: selectedLabel, value: selectedValue }) => { 9 | const { 10 | config: { 11 | rpc: { 12 | value, 13 | state: { items }, 14 | }, 15 | }, 16 | } = useContext(WizardContext); 17 | 18 | const isSelected = items.find( 19 | (item) => item.value === selectedValue 20 | ).isSelected; 21 | 22 | return ( 23 | 24 | {chalk[ 25 | value?.includes(selectedValue) 26 | ? "greenBright" 27 | : isSelected 28 | ? "white" 29 | : "gray" 30 | ](`${isSelected ? "⦿" : "○"} ${selectedLabel}`)} 31 | 32 | ); 33 | }; 34 | 35 | function Rpc() { 36 | const { 37 | config: { 38 | rpc: { state }, 39 | }, 40 | configSetValue, 41 | configSwitchState, 42 | } = useContext(WizardContext); 43 | 44 | const items = state?.items || []; 45 | 46 | const handleSelect = () => { 47 | const valueToSet = items 48 | .filter((item) => item.isSelected) 49 | .map((item) => item.value); 50 | configSetValue("rpc", valueToSet); 51 | }; 52 | 53 | const [highlightedItem, setHighlightedItem] = useState(); 54 | 55 | useInput((input) => { 56 | if (input === " " && highlightedItem) { 57 | configSwitchState("rpc", highlightedItem.value); 58 | } 59 | }); 60 | 61 | const handleHighlight = (item) => setHighlightedItem(item); 62 | return ( 63 | 64 | Please select at least one RPC. 65 | 66 | If You choose more, You can switch between them while the bot is 67 | running. 68 | 69 | 70 | 76 | 77 | ); 78 | } 79 | module.exports = Rpc; 80 | -------------------------------------------------------------------------------- /src/wizard/Pages/Slippage.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const { default: SelectInput } = require("ink-select-input"); 4 | const { useContext, useState, useEffect, useRef } = require("react"); 5 | const WizardContext = require("../WizardContext"); 6 | 7 | const { default: TextInput } = require("ink-text-input"); 8 | const chalk = require("chalk"); 9 | 10 | const SLIPPAGE_STRATEGIES = [ 11 | { label: "1 BPS", value: 1 }, 12 | { label: "10 BPS", value: 10 }, 13 | { label: "30 BPS", value: 30 }, 14 | { label: "Custom BPS", value: "custom" }, 15 | ]; 16 | 17 | const Indicator = ({ label, value }) => { 18 | const { 19 | config: { 20 | slippage: { value: selectedValue }, 21 | }, 22 | } = useContext(WizardContext); 23 | 24 | const isSelected = value == selectedValue; 25 | 26 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 27 | }; 28 | 29 | function Slippage() { 30 | const { configSetValue } = useContext(WizardContext); 31 | let isMountedRef = useRef(false); 32 | 33 | const [tempSlippageStrategy, setTempSlippageStrategy] = useState( 34 | SLIPPAGE_STRATEGIES[0] 35 | ); 36 | const [customSlippage, setCustomSlippage] = useState("1"); 37 | const [inputBorderColor, setInputBorderColor] = useState("gray"); 38 | 39 | const handleSlippageStrategySelect = (slippage) => { 40 | const value = slippage.value; 41 | setTempSlippageStrategy(value); 42 | if (value !== "custom") 43 | configSetValue( 44 | "slippage", Number(value) 45 | ); 46 | }; 47 | 48 | const handleCustomSlippageChange = (value) => { 49 | const badChars = /[^0-9.]/g; 50 | badChars.test(value) 51 | ? setInputBorderColor("red") 52 | : setInputBorderColor("gray"); 53 | const sanitizedValue = value.replace(badChars, ""); 54 | setCustomSlippage(sanitizedValue); 55 | setTimeout(() => isMountedRef.current && setInputBorderColor("gray"), 100); 56 | }; 57 | 58 | const handleCustomSlippageSubmit = () => { 59 | configSetValue("slippage", Number(customSlippage)); 60 | }; 61 | 62 | useEffect(() => { 63 | isMountedRef.current = true; 64 | return () => (isMountedRef.current = false); 65 | }, []); 66 | 67 | return ( 68 | 69 | 70 | Set slippage strategy 71 | 72 | 73 | 78 | 79 | {tempSlippageStrategy === "custom" && ( 80 | 81 | Custom slippage: 82 | 87 | 92 | 93 | BPS 94 | 95 | )} 96 | 97 | ); 98 | } 99 | module.exports = Slippage; 100 | -------------------------------------------------------------------------------- /src/wizard/Pages/Strategy.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const WizardContext = require("../WizardContext"); 4 | const { useContext } = require("react"); 5 | const { default: SelectInput } = require("ink-select-input"); 6 | const chalk = require("chalk"); 7 | 8 | const TRADING_STRATEGIES = [ 9 | { label: "Ping Pong", value: "pingpong" }, 10 | { label: "Arbitrage", value: "arbitrage" }, 11 | ]; 12 | 13 | const Indicator = ({ label, value }) => { 14 | const { 15 | config: { 16 | strategy: { value: selectedValue }, 17 | }, 18 | } = useContext(WizardContext); 19 | 20 | const isSelected = value === selectedValue; 21 | 22 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 23 | }; 24 | 25 | function Strategy() { 26 | const { configSetValue } = useContext(WizardContext); 27 | 28 | const handleTradingStrategySelect = (strategy) => { 29 | configSetValue("strategy", strategy.value); 30 | }; 31 | 32 | return ( 33 | 34 | Select Trading Strategy: 35 | 36 | 41 | 42 | 43 | ); 44 | } 45 | module.exports = Strategy; 46 | -------------------------------------------------------------------------------- /src/wizard/Pages/Tokens.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const React = require("react"); 3 | const { Box, Text } = require("ink"); 4 | const WizardContext = require("../WizardContext"); 5 | const { useContext, useState, useEffect, useRef } = require("react"); 6 | const { default: SelectInput } = require("ink-select-input"); 7 | const chalk = require("chalk"); 8 | const { default: axios } = require("axios"); 9 | const { TOKEN_LIST_URL } = require("@jup-ag/core"); 10 | const { default: TextInput } = require("ink-text-input"); 11 | const fs = require("fs"); 12 | 13 | function Tokens() { 14 | let isMountedRef = useRef(false); 15 | 16 | const { 17 | config: { 18 | strategy: { value: strategy }, 19 | network: { value: network }, 20 | tokens: { value: tokensValue, isSet: tokensIsSet }, 21 | }, 22 | configSetValue, 23 | } = useContext(WizardContext); 24 | const [tokens, setTokens] = useState([]); 25 | const [autocompleteTokens, setAutocompleteTokens] = useState([]); 26 | const [tempTokensValue, setTempTokensValue] = useState(tokensValue); 27 | 28 | const handleSubmit = (tokenId, selectedToken) => { 29 | // go to the next step only if all tokens are set 30 | let goToNextStep = true; 31 | 32 | tokensIsSet[tokenId] = true; 33 | tokensValue[tokenId] = { 34 | symbol: selectedToken.label, 35 | address: selectedToken.value, 36 | }; 37 | 38 | if (strategy === "arbitrage") { 39 | tokensIsSet.tokenB = true; 40 | } 41 | 42 | if (strategy === "pingpong" && tokenId === "tokenA") { 43 | goToNextStep = false; 44 | } 45 | 46 | configSetValue( 47 | "tokens", 48 | { 49 | value: tokensValue, 50 | isSet: tokensIsSet, 51 | }, 52 | goToNextStep 53 | ); 54 | }; 55 | 56 | const handleTokenChange = (tokenId, value) => { 57 | if (!isMountedRef.current) return; 58 | const sanitizedValue = value.replace(/[^a-zA-Z0-9]/g, ""); 59 | const filteredTokens = tokens 60 | .map((t) => ({ 61 | label: t.symbol, 62 | value: t.address, 63 | })) 64 | .filter((t) => 65 | t.label.toLowerCase().includes(sanitizedValue.toLowerCase()) 66 | ); 67 | if (isMountedRef.current) { 68 | setAutocompleteTokens(filteredTokens); 69 | setTempTokensValue({ 70 | ...tempTokensValue, 71 | [tokenId]: { 72 | symbol: sanitizedValue, 73 | }, 74 | }); 75 | } 76 | }; 77 | 78 | if (network === "") { 79 | return ( 80 | 81 | 82 | Plese select network first! 83 | 84 | 85 | ); 86 | } 87 | 88 | useEffect(() => { 89 | // check if tokens.json exist 90 | if (fs.existsSync("./tokens.json")) { 91 | const tokensFromFile = JSON.parse(fs.readFileSync("./config.json")); 92 | tokens.tokensFromFile?.length > 0 && setTokens(tokensFromFile); 93 | } else { 94 | axios.get(TOKEN_LIST_URL[network]).then((res) => { 95 | isMountedRef.current && setTokens(res.data); 96 | // save tokens to tokens.json file 97 | fs.writeFileSync( 98 | "./temp/tokens.json", 99 | JSON.stringify(res.data, null, 2) 100 | ); 101 | }); 102 | } 103 | }, []); 104 | 105 | useEffect(() => { 106 | isMountedRef.current = true; 107 | return () => (isMountedRef.current = false); 108 | }, []); 109 | 110 | return ( 111 | 112 | 113 | Set tokens for Your strategy. There is{" "} 114 | {tokens 115 | ? chalk.magenta(tokens.length) 116 | : chalk.yellowBright("loading...")}{" "} 117 | tokens available. 118 | 119 | Type token symbol and use arrows to select 120 | 121 | 122 | Token A:{" "} 123 | {!tokensIsSet.tokenA ? ( 124 | 125 | 128 | handleTokenChange("tokenA", tokenSymbol) 129 | } 130 | placeholder="USDC" 131 | /> 132 | 133 | ) : ( 134 | 135 | {tokensValue.tokenA.symbol} 136 | 137 | )} 138 | 139 | 140 | 141 | {!tokensIsSet.tokenA && 142 | tempTokensValue?.tokenA?.symbol?.length > 1 && ( 143 | handleSubmit("tokenA", tokenSymbol)} 147 | /> 148 | )} 149 | 150 | 151 | {strategy === "pingpong" && ( 152 | <> 153 | 154 | Token B:{" "} 155 | {tokensIsSet.tokenA && !tokensIsSet.tokenB ? ( 156 | 157 | 160 | handleTokenChange("tokenB", tokenSymbol) 161 | } 162 | placeholder="ARB" 163 | /> 164 | 165 | ) : ( 166 | 167 | {tokensValue.tokenB.symbol} 168 | 169 | )} 170 | 171 | 172 | {!tokensIsSet.tokenB && 173 | tempTokensValue.tokenB?.symbol?.length > 1 && ( 174 | t.label !== tokensValue.tokenA.symbol 177 | )} 178 | limit={4} 179 | onSelect={(tokenSymbol) => 180 | handleSubmit("tokenB", tokenSymbol) 181 | } 182 | /> 183 | )} 184 | 185 | 186 | )} 187 | 188 | 189 | ); 190 | } 191 | module.exports = Tokens; 192 | -------------------------------------------------------------------------------- /src/wizard/Pages/TradingMax.js: -------------------------------------------------------------------------------- 1 | //TO DO!!! 2 | const React = require("react"); 3 | const { Box, Text } = require("ink"); 4 | const WizardContext = require("../WizardContext"); 5 | const { useContext } = require("react"); 6 | const { default: SelectInput } = require("ink-select-input"); 7 | const chalk = require("chalk"); 8 | 9 | const NETWORKS = [ 10 | { label: "mainnet-beta", value: "mainnet-beta" }, 11 | ]; 12 | 13 | const Indicator = ({ label, value }) => { 14 | const { 15 | config: { 16 | network: { value: selectedValue }, 17 | }, 18 | } = useContext(WizardContext); 19 | 20 | const isSelected = value == selectedValue; 21 | 22 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 23 | }; 24 | 25 | function Network() { 26 | const { configSetValue } = useContext(WizardContext); 27 | return ( 28 | 29 | 30 | Select Solana Network: 31 | 32 | 33 | configSetValue("network", item.value)} 36 | itemComponent={Indicator} 37 | /> 38 | 39 | 40 | ); 41 | } 42 | module.exports = Network; 43 | -------------------------------------------------------------------------------- /src/wizard/Pages/TradingSize.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const { Box, Text } = require("ink"); 3 | const { useContext, useState, useRef, useEffect } = require("react"); 4 | const WizardContext = require("../WizardContext"); 5 | const { default: TextInput } = require("ink-text-input"); 6 | const chalk = require("chalk"); 7 | const { default: SelectInput } = require("ink-select-input"); 8 | 9 | const TRADING_SIZE_STRATEGIES = [ 10 | { label: "cumulative", value: "cumulative" }, 11 | { label: "fixed", value: "fixed" }, 12 | ]; 13 | 14 | const Indicator = ({ label, value }) => { 15 | const { 16 | config: { 17 | "trading size": { value: selectedValue }, 18 | }, 19 | } = useContext(WizardContext); 20 | 21 | const isSelected = value === selectedValue.strategy; 22 | 23 | return {chalk[isSelected ? "greenBright" : "white"](`${label}`)}; 24 | }; 25 | 26 | function TradingSize() { 27 | let isMountedRef = useRef(false); 28 | 29 | const { 30 | config: { 31 | tokens: { value: tokensValue }, 32 | "trading size": { value: tradingSizeValue }, 33 | }, 34 | configSetValue, 35 | } = useContext(WizardContext); 36 | 37 | const [tradingSize, setTradingSize] = useState(tradingSizeValue.value); 38 | const [inputBorderColor, setInputBorderColor] = useState("gray"); 39 | 40 | const handleTradingSizeStrategySelect = (selected) => { 41 | configSetValue("trading size", { 42 | value: { 43 | strategy: selected.value, 44 | value: tradingSize, 45 | }, 46 | }); 47 | }; 48 | 49 | const handleTradingSizeChange = (value) => { 50 | if (!isMountedRef.current) return; 51 | 52 | const badChars = /[^0-9.]/g; 53 | badChars.test(value) 54 | ? setInputBorderColor("red") 55 | : setInputBorderColor("gray"); 56 | const sanitizedValue = value.replace(badChars, ""); 57 | setTimeout(() => isMountedRef.current && setInputBorderColor("gray"), 100); 58 | setTradingSize(sanitizedValue); 59 | }; 60 | 61 | useEffect(() => { 62 | isMountedRef.current = true; 63 | return () => (isMountedRef.current = false); 64 | }, []); 65 | 66 | return ( 67 | 68 | 69 | Set trading size strategy: 70 | 71 | 72 | 77 | 78 | 79 | Trading Size: 80 | 81 | handleSubmit("percent", value)} 86 | /> 87 | {tokensValue.tokenA.symbol} 88 | 89 | 90 | 91 | ); 92 | } 93 | module.exports = TradingSize; 94 | -------------------------------------------------------------------------------- /src/wizard/WizardContext.js: -------------------------------------------------------------------------------- 1 | const { createContext } = require("react"); 2 | const { initialState } = require("./reducer"); 3 | 4 | const WizardContext = createContext(initialState); 5 | 6 | module.exports = WizardContext; 7 | -------------------------------------------------------------------------------- /src/wizard/WizardProvider.js: -------------------------------------------------------------------------------- 1 | const { useInput } = require("ink"); 2 | const React = require("react"); 3 | const { useReducer } = require("react"); 4 | const reducer = require("./reducer"); 5 | const WizardContext = require("./WizardContext"); 6 | const { CONFIG_INITIAL_STATE } = require("../constants"); 7 | 8 | const WizardProvider = ({ children }) => { 9 | const [state, dispatch] = useReducer(reducer, CONFIG_INITIAL_STATE); 10 | 11 | useInput((input, key) => { 12 | if (input === "]") dispatch({ type: "NEXT_STEP" }); 13 | if (input === "[") dispatch({ type: "PREV_STEP" }); 14 | if (input === "h") dispatch({ type: "TOGGLE_HELP" }); 15 | if (key.escape) process.exit(); 16 | }); 17 | 18 | const configSetValue = (key, value, goToNextStep = true) => { 19 | dispatch({ type: "CONFIG_SET", key, value }); 20 | goToNextStep && dispatch({ type: "NEXT_STEP" }); 21 | }; 22 | 23 | const configSwitchState = (key, value) => { 24 | dispatch({ type: "CONFIG_SWITCH_STATE", key, value }); 25 | }; 26 | 27 | const providerValue = { 28 | ...state, 29 | configSetValue, 30 | configSwitchState, 31 | }; 32 | return ( 33 | 34 | {children} 35 | 36 | ); 37 | }; 38 | 39 | module.exports = WizardProvider; 40 | -------------------------------------------------------------------------------- /src/wizard/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // check for .env file 4 | const { checkForEnvFile } = require("../utils"); 5 | checkForEnvFile(); 6 | 7 | require("dotenv").config(); 8 | const React = require("react"); 9 | 10 | // create temp dir 11 | const { createTempDir } = require("../utils"); 12 | createTempDir(); 13 | 14 | // import components 15 | const importJsx = require("import-jsx"); 16 | 17 | const WizardProvider = importJsx("./WizardProvider"); 18 | 19 | const Layout = importJsx("./Components/Layout"); 20 | 21 | const App = () => { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | module.exports = App; -------------------------------------------------------------------------------- /src/wizard/reducer.js: -------------------------------------------------------------------------------- 1 | const reducer = (prevState, action) => { 2 | const nav = prevState.nav; 3 | const isSet = 4 | action.value?.isSet instanceof Object ? action.value?.isSet : true; 5 | const value = action.value?.value || action.value; 6 | switch (action.type) { 7 | case "NEXT_STEP": 8 | return { 9 | ...prevState, 10 | nav: { 11 | ...prevState.nav, 12 | currentStep: 13 | nav.currentStep === nav.steps.length - 1 ? 0 : nav.currentStep + 1, 14 | }, 15 | }; 16 | case "PREV_STEP": 17 | return { 18 | ...prevState, 19 | nav: { 20 | ...prevState.nav, 21 | currentStep: 22 | nav.currentStep === 0 ? nav.steps.length - 1 : nav.currentStep - 1, 23 | }, 24 | }; 25 | case "TOGGLE_HELP": 26 | return { 27 | ...prevState, 28 | showHelp: !prevState.showHelp, 29 | }; 30 | case "CONFIG_SET": 31 | return { 32 | ...prevState, 33 | config: { 34 | ...prevState.config, 35 | [action.key]: { 36 | ...prevState.config[action.key], 37 | value: value, 38 | isSet: isSet, 39 | }, 40 | }, 41 | }; 42 | 43 | case "CONFIG_SWITCH_STATE": 44 | return { 45 | ...prevState, 46 | config: { 47 | ...prevState.config, 48 | [action.key]: { 49 | state: { 50 | ...prevState.config[action.key].state, 51 | items: prevState.config[action.key].state.items.map((item) => 52 | item.value === action.value 53 | ? { 54 | ...item, 55 | isSelected: !item.isSelected, 56 | } 57 | : item 58 | ), 59 | }, 60 | isSet: isSet, 61 | }, 62 | }, 63 | }; 64 | 65 | default: 66 | throw new Error(`Unhandled action type: ${action.type}`); 67 | } 68 | }; 69 | 70 | module.exports = reducer; 71 | --------------------------------------------------------------------------------