├── .eslintrc.js ├── .github └── workflows │ ├── main.yml │ ├── on-sdk-update.yml │ └── release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── Anchor.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── deploy-scripts ├── build-devnet.sh └── deploy-devnet.sh ├── package.json ├── programs └── jit-proxy │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── error.rs │ ├── instructions │ ├── arb_perp.rs │ ├── check_order_constraints.rs │ ├── jit.rs │ └── mod.rs │ ├── lib.rs │ └── state.rs ├── python ├── README.md ├── pyproject.toml └── sdk │ ├── examples │ ├── shotgun.py │ └── sniper.py │ └── jit_proxy │ ├── __init__.py │ ├── jit_proxy_client.py │ └── jitter │ ├── base_jitter.py │ ├── jitter_shotgun.py │ └── jitter_sniper.py ├── rust ├── Cargo.lock ├── Cargo.toml ├── README.md ├── run.sh └── src │ ├── jitter.rs │ ├── lib.rs │ ├── main.rs │ └── types.rs ├── ts └── sdk │ ├── Readme.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── jitProxyClient.ts │ ├── jitter │ │ ├── baseJitter.ts │ │ ├── jitterShotgun.ts │ │ └── jitterSniper.ts │ └── types │ │ └── jit_proxy.ts │ ├── tsconfig.json │ └── yarn.lock ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "ignorePatterns": ["**/lib", "**/node_modules", "migrations"], 9 | "plugins": [], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/explicit-function-return-type": "off", 17 | "@typescript-eslint/ban-ts-ignore": "off", 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "@typescript-eslint/no-unused-vars": [ 21 | 2, 22 | { 23 | "argsIgnorePattern": "^_", 24 | "varsIgnorePattern": "^_" 25 | } 26 | ], 27 | "@typescript-eslint/no-var-requires": 0, 28 | "@typescript-eslint/no-empty-function": 0, 29 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 30 | "no-prototype-builtins": "off", 31 | "semi": 2, 32 | "no-restricted-imports": [ 33 | "error", 34 | { 35 | "patterns": [ 36 | { 37 | // Restrict importing BN from bn.js 38 | "group": ["bn.js"], 39 | "message": "Import BN from @drift-labs/sdk instead", 40 | } 41 | ], 42 | }, 43 | ], 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | branches: master 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | working-directory: . 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | RUST_TOOLCHAIN: 1.62.0 17 | SOLANA_VERSION: '1.14.16' 18 | 19 | jobs: 20 | verified-build: 21 | name: Build Verifiable Artifact 22 | runs-on: ubicloud 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Install Solana Verify 28 | run: | 29 | cargo install solana-verify 30 | solana-verify --version 31 | 32 | - name: Verifiable Build 33 | run: | 34 | solana-verify build --library-name jit_proxy --base-image ellipsislabs/solana:1.16.6 35 | 36 | - name: Upload Artifact 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: build 40 | path: target/deploy/jit_proxy.so -------------------------------------------------------------------------------- /.github/workflows/on-sdk-update.yml: -------------------------------------------------------------------------------- 1 | name: Deploy on sdk update 2 | on: 3 | push: 4 | branches: 5 | - master 6 | repository_dispatch: 7 | types: [sdk-update] 8 | 9 | jobs: 10 | update-sdk: 11 | runs-on: ubicloud 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '20.18.x' 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - name: Determine sdk version 23 | id: determine-sdk-version 24 | run: | 25 | if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then 26 | echo "SDK_VERSION=${{ github.event.client_payload.version }}" >> $GITHUB_ENV 27 | else 28 | # Get the current version of sdk used in package.json 29 | CURRENT_VERSION=$(node -e "console.log(require('./ts/sdk/package.json').dependencies['@drift-labs/sdk'])") 30 | echo "SDK_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV 31 | fi 32 | 33 | - name: Install dependencies 34 | run: yarn 35 | working-directory: ts/sdk 36 | 37 | - name: Add specific version of sdk 38 | run: yarn add @drift-labs/sdk@$SDK_VERSION 39 | working-directory: ts/sdk 40 | 41 | - run: yarn build 42 | working-directory: ts/sdk 43 | 44 | - name: Update package version 45 | run: npm version patch 46 | working-directory: ts/sdk 47 | 48 | - name: Git commit 49 | id: publish-jit-sdk 50 | run: | 51 | VERSION=$(node -e "console.log(require('./package.json').version);") 52 | git config user.name "GitHub Actions" 53 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 54 | git add -A 55 | git commit --allow-empty -m "sdk: release v$VERSION" 56 | git push origin HEAD 57 | echo "JIT_VERSION=$VERSION" >> $GITHUB_ENV 58 | working-directory: ts/sdk 59 | 60 | - name: Publish to npm 61 | run: npm publish --access=public 62 | working-directory: ts/sdk 63 | env: 64 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 65 | 66 | 67 | - name: Emit dispatch event 68 | run: | 69 | curl -X POST \ 70 | -H "Accept: application/vnd.github+json" \ 71 | -H "Authorization: token ${{ secrets.GH_PAT }}" \ 72 | -H "X-GitHub-Api-Version: 2022-11-28" \ 73 | "https://api.github.com/repos/drift-labs/keeper-bots-v2/dispatches" \ 74 | -d "{\"event_type\": \"jit-sdk-update\", \"client_payload\": { 75 | \"sdk-version\": \"$SDK_VERSION\", 76 | \"jit-version\": \"$JIT_VERSION\" 77 | }}" 78 | env: 79 | GH_PAT: ${{ secrets.GH_PAT }} 80 | 81 | 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | release: 7 | runs-on: ubicloud 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2.5.0 11 | 12 | - name: Set up Python 13 | uses: actions/setup-python@v4.3.0 14 | with: 15 | python-version: '3.10.10' 16 | 17 | - name: Install and configure Poetry 18 | run: | 19 | cd python 20 | curl -sSL https://install.python-poetry.org | python3 - 21 | env: 22 | POETRY_VERSION: 1.4.2 23 | 24 | - name: Build 25 | run: poetry build 26 | working-directory: python 27 | 28 | - name: Publish 29 | run: poetry publish --username=__token__ --password=${{ secrets.PYPI_TOKEN }} 30 | working-directory: python 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .idea 9 | migrations 10 | ts/sdk/lib/ 11 | ts/sdk/src/**/*.js 12 | ts/sdk/src/**/*.js.map 13 | package-lock.json 14 | python/sdk/examples/.env 15 | python/poetry.lock 16 | __pycache__/ 17 | *.pyc 18 | *.pyo 19 | *.pyd 20 | rust/.env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | cargo fmt -- --check 5 | cargo clippy -p jit-proxy -- -D warnings -D clippy::expect_used -D clippy::panic 6 | cargo clippy -p jit-proxy --tests -- -D warnings 7 | cargo test --quiet 8 | yarn prettify 9 | yarn lint 10 | cd ts/sdk 11 | tsc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | node_modules 6 | dist 7 | build 8 | test-ledger 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'es5', 4 | singleQuote: true, 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: true, 8 | bracketSameLine: false, 9 | }; 10 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [features] 2 | seeds = false 3 | skip-lint = false 4 | [programs.localnet] 5 | jit_proxy = "J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP" 6 | 7 | [registry] 8 | url = "https://api.apr.dev" 9 | 10 | 11 | [provider] 12 | cluster = "Localnet" 13 | wallet = "~/.config/solana/id.json" 14 | 15 | [[test.genesis]] 16 | address = "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" 17 | program = "./deps/drift.so" 18 | 19 | [scripts] 20 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Features 10 | 11 | ### Fixes 12 | 13 | ### Breaking 14 | 15 | ## [0.17.0] - 2025-04-21 16 | 17 | ### Features 18 | 19 | - program update drift-version to v2.119.0[#64](https://github.com/drift-labs/jit-proxy/pull/64) 20 | 21 | ### Fixes 22 | 23 | ### Breaking 24 | 25 | ## [0.16.0] - 2025-04-11 26 | 27 | ### Features 28 | 29 | - program: update drift version to v2.118.0 30 | 31 | ### Fixes 32 | 33 | ### Breaking 34 | 35 | ## [0.15.1] - 2025-03-24 36 | 37 | ### Features 38 | 39 | ### Fixes 40 | 41 | - program: fix program/jit-proxy/Cargo.toml 42 | 43 | ### Breaking 44 | 45 | ## [0.15.0] - 2025-03-23 46 | 47 | ### Features 48 | 49 | ### Fixes 50 | 51 | - program: update drift version to v2.116.0 52 | 53 | ### Breaking 54 | 55 | ## [0.14.0] - 2025-03-10 56 | 57 | ### Features 58 | 59 | - program: rename swift to signed msg 60 | 61 | ### Fixes 62 | 63 | ### Breaking 64 | 65 | ## [0.13.0] - 2025-02-11 66 | 67 | ### Features 68 | 69 | - program/sdk: swift integration ([#52](https://github.com/drift-labs/jit-proxy/pull/52)) 70 | - program: upgrade drift version for pyth lazer ([#53](https://github.com/drift-labs/jit-proxy/pull/53)) 71 | 72 | ### Fixes 73 | 74 | ### Breaking 75 | 76 | 77 | ## [0.12.0] - 2024-12-06 78 | 79 | ### Features 80 | 81 | - program: update to drift v2.103.0 82 | 83 | ### Fixes 84 | 85 | ### Breaking 86 | 87 | ## [0.11.0] - 2024-10-23 88 | 89 | ### Features 90 | 91 | - program: update to drift v2.97.0 92 | 93 | ### Fixes 94 | 95 | ### Breaking 96 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | exclude = [ 6 | "rust" 7 | ] 8 | 9 | [profile.release] 10 | overflow-checks = true 11 | lto = "fat" 12 | codegen-units = 1 13 | [profile.release.build-override] 14 | opt-level = 3 15 | incremental = false 16 | codegen-units = 1 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Drift Labs 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jit Proxy 2 | 3 | The jit proxy is a solana program that aims to make it easier to provide jit liquidity on drift v2. The jit proxy uses an MM's market and maximum position size to automatically create drift perp and spot orders. 4 | 5 | For more information on how interact with the Jit Proxy program, see the README for the [typescript sdk](ts/sdk/Readme.md) or the [python sdk](python/README.md) 6 | -------------------------------------------------------------------------------- /deploy-scripts/build-devnet.sh: -------------------------------------------------------------------------------- 1 | anchor build -- --features "drift/cpi" --no-default-features 2 | -------------------------------------------------------------------------------- /deploy-scripts/deploy-devnet.sh: -------------------------------------------------------------------------------- 1 | anchor upgrade --program-id J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP --provider.cluster devnet --provider.wallet $SOLANA_PATH/$DEVNET_ADMIN target/deploy/jit_proxy.so -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "Apache-2.0", 3 | "scripts": { 4 | "prettify": "prettier --check './ts/sdk/src/**/*.ts'", 5 | "prettify:fix": "prettier --write './ts/sdk/src/**/*.ts'", 6 | "lint": "eslint . --ext ts --quiet", 7 | "lint:fix": "eslint . --ext ts --fix", 8 | "prepare": "husky install", 9 | "update-types": "cp target/types/jit_proxy.ts ts/sdk/src/types/jit_proxy.ts && prettier --write ts/sdk/src/types/jit_proxy.ts", 10 | "anchor-tests": "yarn update-types && yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 11 | }, 12 | "devDependencies": { 13 | "@coral-xyz/anchor": "0.29.0", 14 | "@drift-labs/sdk": "2.120.0-beta.0", 15 | "@solana/web3.js": "1.73.2", 16 | "@types/bn.js": "^5.1.0", 17 | "@types/chai": "^4.3.0", 18 | "@types/mocha": "^9.0.0", 19 | "@typescript-eslint/eslint-plugin": "^4.28.0", 20 | "@typescript-eslint/parser": "^4.28.0", 21 | "chai": "^4.3.4", 22 | "eslint": "^7.29.0", 23 | "eslint-config-prettier": "^8.3.0", 24 | "eslint-plugin-prettier": "^3.4.0", 25 | "husky": "^8.0.0", 26 | "mocha": "^9.0.3", 27 | "prettier": "^2.6.2", 28 | "ts-mocha": "^10.0.0", 29 | "typescript": "^4.5.4" 30 | }, 31 | "engines": { 32 | "node": ">=20.18.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /programs/jit-proxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jit-proxy" 3 | version = "0.17.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "jit_proxy" 10 | 11 | [features] 12 | no-entrypoint = [] 13 | cpi = ["no-entrypoint"] 14 | default = [] 15 | 16 | [dependencies] 17 | anchor-lang = "0.29.0" 18 | anchor-spl = "0.29.0" 19 | bytemuck = { version = "1.4.0" } 20 | drift = { git = "https://github.com/drift-labs/protocol-v2.git", rev = "3a7e11e2beea9118c35bcc83d80b75d91c69a196", features = ["cpi", "mainnet-beta"]} 21 | static_assertions = "1.1.0" 22 | solana-program = "1.16" 23 | ahash = "=0.8.6" 24 | -------------------------------------------------------------------------------- /programs/jit-proxy/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | #[derive(PartialEq, Eq)] 5 | pub enum ErrorCode { 6 | #[msg("BidNotCrossed")] 7 | BidNotCrossed, 8 | #[msg("AskNotCrossed")] 9 | AskNotCrossed, 10 | #[msg("TakerOrderNotFound")] 11 | TakerOrderNotFound, 12 | #[msg("OrderSizeBreached")] 13 | OrderSizeBreached, 14 | #[msg("NoBestBid")] 15 | NoBestBid, 16 | #[msg("NoBestAsk")] 17 | NoBestAsk, 18 | #[msg("NoArbOpportunity")] 19 | NoArbOpportunity, 20 | #[msg("UnprofitableArb")] 21 | UnprofitableArb, 22 | #[msg("PositionLimitBreached")] 23 | PositionLimitBreached, 24 | #[msg("NoFill")] 25 | NoFill, 26 | #[msg("SignedMsgOrderDoesNotExist")] 27 | SignedMsgOrderDoesNotExist, 28 | } 29 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/instructions/arb_perp.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use drift::controller::position::PositionDirection; 3 | use drift::cpi::accounts::PlaceAndTake; 4 | use drift::error::DriftResult; 5 | use drift::instructions::optional_accounts::{load_maps, AccountMaps}; 6 | use drift::math::casting::Cast; 7 | use drift::math::constants::{BASE_PRECISION, MARGIN_PRECISION_U128, QUOTE_PRECISION}; 8 | use drift::math::margin::MarginRequirementType; 9 | use drift::program::Drift; 10 | use drift::state::order_params::{OrderParams, OrderParamsBitFlag, PostOnlyParam}; 11 | use std::collections::BTreeSet; 12 | use std::ops::Deref; 13 | 14 | use drift::math::orders::find_bids_and_asks_from_users; 15 | use drift::math::safe_math::SafeMath; 16 | use drift::state::oracle::OraclePriceData; 17 | use drift::state::state::State; 18 | use drift::state::user::{MarketType, OrderTriggerCondition, OrderType, User, UserStats}; 19 | use drift::state::user_map::load_user_maps; 20 | 21 | use crate::error::ErrorCode; 22 | 23 | pub fn arb_perp<'c: 'info, 'info>( 24 | ctx: Context<'_, '_, 'c, 'info, ArbPerp<'info>>, 25 | market_index: u16, 26 | ) -> Result<()> { 27 | let clock = Clock::get()?; 28 | let slot = clock.slot; 29 | let now = clock.unix_timestamp; 30 | 31 | let taker = ctx.accounts.user.load()?; 32 | 33 | let (base_init, quote_init) = taker 34 | .get_perp_position(market_index) 35 | .map_or((0, 0), |p| (p.base_asset_amount, p.quote_asset_amount)); 36 | 37 | let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); 38 | let AccountMaps { 39 | perp_market_map, 40 | mut oracle_map, 41 | spot_market_map, 42 | } = load_maps( 43 | remaining_accounts_iter, 44 | &BTreeSet::new(), 45 | &BTreeSet::new(), 46 | slot, 47 | None, 48 | )?; 49 | 50 | let quote_asset_token_amount = taker 51 | .get_quote_spot_position() 52 | .get_token_amount(spot_market_map.get_quote_spot_market()?.deref())?; 53 | 54 | let (makers, _) = load_user_maps(remaining_accounts_iter, true)?; 55 | 56 | let perp_market = perp_market_map.get_ref(&market_index)?; 57 | let oracle_price_data = oracle_map.get_price_data(&perp_market.oracle_id())?; 58 | 59 | let (bids, asks) = 60 | find_bids_and_asks_from_users(&perp_market, oracle_price_data, &makers, slot, now)?; 61 | 62 | let best_bid = bids.first().ok_or(ErrorCode::NoBestBid)?; 63 | let best_ask = asks.first().ok_or(ErrorCode::NoBestAsk)?; 64 | 65 | if best_bid.price < best_ask.price { 66 | return Err(ErrorCode::NoArbOpportunity.into()); 67 | } 68 | 69 | let base_asset_amount = best_bid.base_asset_amount.min(best_ask.base_asset_amount); 70 | 71 | let (intermediate_base, start_direction) = if base_init >= 0 { 72 | let intermediate_base = base_init.safe_sub(base_asset_amount.cast()?)?; 73 | (intermediate_base, PositionDirection::Short) 74 | } else { 75 | let intermediate_base = base_init.safe_add(base_asset_amount.cast()?)?; 76 | (intermediate_base, PositionDirection::Long) 77 | }; 78 | 79 | let init_margin_ratio = perp_market.get_margin_ratio( 80 | intermediate_base.unsigned_abs().cast()?, 81 | MarginRequirementType::Initial, 82 | taker.is_high_leverage_mode(), 83 | )?; 84 | 85 | // assumes all free collateral in quote asset token 86 | let max_base_asset_amount = calculate_max_base_asset_amount( 87 | quote_asset_token_amount, 88 | init_margin_ratio, 89 | oracle_price_data, 90 | )?; 91 | 92 | let base_asset_amount = base_asset_amount 93 | .min(max_base_asset_amount.cast()?) 94 | .max(perp_market.amm.min_order_size); 95 | 96 | let get_order_params = |taker_direction: PositionDirection, taker_price: u64| -> OrderParams { 97 | OrderParams { 98 | order_type: OrderType::Limit, 99 | market_type: MarketType::Perp, 100 | direction: taker_direction, 101 | user_order_id: 0, 102 | base_asset_amount, 103 | price: taker_price, 104 | market_index, 105 | reduce_only: false, 106 | post_only: PostOnlyParam::None, 107 | bit_flags: OrderParamsBitFlag::ImmediateOrCancel as u8, 108 | max_ts: None, 109 | trigger_price: None, 110 | trigger_condition: OrderTriggerCondition::Above, 111 | oracle_price_offset: None, 112 | auction_duration: None, 113 | auction_start_price: None, 114 | auction_end_price: None, 115 | } 116 | }; 117 | 118 | let order_params = if start_direction == PositionDirection::Long { 119 | vec![ 120 | get_order_params(PositionDirection::Long, best_ask.price), 121 | get_order_params(PositionDirection::Short, best_bid.price), 122 | ] 123 | } else { 124 | vec![ 125 | get_order_params(PositionDirection::Short, best_bid.price), 126 | get_order_params(PositionDirection::Long, best_ask.price), 127 | ] 128 | }; 129 | 130 | drop(taker); 131 | drop(perp_market); 132 | 133 | place_and_take(&ctx, order_params)?; 134 | 135 | let taker = ctx.accounts.user.load()?; 136 | let (base_end, quote_end) = taker 137 | .get_perp_position(market_index) 138 | .map_or((0, 0), |p| (p.base_asset_amount, p.quote_asset_amount)); 139 | 140 | if base_end != base_init || quote_end <= quote_init { 141 | msg!( 142 | "base_end {} base_init {} quote_end {} quote_init {}", 143 | base_end, 144 | base_init, 145 | quote_end, 146 | quote_init 147 | ); 148 | return Err(ErrorCode::NoArbOpportunity.into()); 149 | } 150 | 151 | msg!("pnl {}", quote_end - quote_init); 152 | 153 | Ok(()) 154 | } 155 | 156 | #[derive(Accounts)] 157 | pub struct ArbPerp<'info> { 158 | pub state: Box>, 159 | #[account(mut)] 160 | pub user: AccountLoader<'info, User>, 161 | #[account(mut)] 162 | pub user_stats: AccountLoader<'info, UserStats>, 163 | pub authority: Signer<'info>, 164 | pub drift_program: Program<'info, Drift>, 165 | } 166 | 167 | fn calculate_max_base_asset_amount( 168 | quote_asset_token_amount: u128, 169 | init_margin_ratio: u32, 170 | oracle_price_data: &OraclePriceData, 171 | ) -> DriftResult { 172 | quote_asset_token_amount 173 | .saturating_sub((quote_asset_token_amount / 100).min(10 * QUOTE_PRECISION)) // room for error 174 | .safe_mul(MARGIN_PRECISION_U128)? 175 | .safe_div(init_margin_ratio.cast()?)? 176 | .safe_mul(BASE_PRECISION)? 177 | .safe_div(oracle_price_data.price.cast()?) 178 | } 179 | 180 | fn place_and_take<'info>( 181 | ctx: &Context<'_, '_, '_, 'info, ArbPerp<'info>>, 182 | orders_params: Vec, 183 | ) -> Result<()> { 184 | for order_params in orders_params { 185 | let drift_program = ctx.accounts.drift_program.to_account_info().clone(); 186 | let cpi_accounts = PlaceAndTake { 187 | state: ctx.accounts.state.to_account_info().clone(), 188 | user: ctx.accounts.user.to_account_info().clone(), 189 | user_stats: ctx.accounts.user_stats.to_account_info().clone(), 190 | authority: ctx.accounts.authority.to_account_info().clone(), 191 | }; 192 | 193 | let cpi_context = CpiContext::new(drift_program, cpi_accounts) 194 | .with_remaining_accounts(ctx.remaining_accounts.into()); 195 | 196 | drift::cpi::place_and_take_perp_order(cpi_context, order_params, None)?; 197 | } 198 | 199 | Ok(()) 200 | } 201 | 202 | #[cfg(test)] 203 | mod test { 204 | use drift::math::constants::{MARGIN_PRECISION, PRICE_PRECISION_I64, QUOTE_PRECISION}; 205 | use drift::state::oracle::OraclePriceData; 206 | 207 | #[test] 208 | pub fn calculate_max_base_asset_amount() { 209 | let quote_asset_token_amount = 100 * QUOTE_PRECISION; 210 | let init_margin_ratio = MARGIN_PRECISION / 10; 211 | let oracle_price_data = OraclePriceData { 212 | price: 100 * PRICE_PRECISION_I64, 213 | ..OraclePriceData::default() 214 | }; 215 | 216 | let max_base_asset_amount = super::calculate_max_base_asset_amount( 217 | quote_asset_token_amount, 218 | init_margin_ratio, 219 | &oracle_price_data, 220 | ) 221 | .unwrap(); 222 | 223 | assert_eq!(max_base_asset_amount, 9900000000); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/instructions/check_order_constraints.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use drift::instructions::optional_accounts::{load_maps, AccountMaps}; 3 | use drift::math::casting::Cast; 4 | use drift::math::safe_math::SafeMath; 5 | use drift::state::user::User; 6 | use std::collections::BTreeSet; 7 | 8 | use crate::error::ErrorCode; 9 | use crate::state::MarketType; 10 | 11 | pub fn check_order_constraints<'c: 'info, 'info>( 12 | ctx: Context<'_, '_, 'c, 'info, CheckOrderConstraints<'info>>, 13 | constraints: Vec, 14 | ) -> Result<()> { 15 | let clock = Clock::get()?; 16 | let slot = clock.slot; 17 | 18 | let user = ctx.accounts.user.load()?; 19 | 20 | let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); 21 | let AccountMaps { 22 | perp_market_map, 23 | spot_market_map, 24 | mut oracle_map, 25 | } = load_maps( 26 | remaining_accounts_iter, 27 | &BTreeSet::new(), 28 | &BTreeSet::new(), 29 | slot, 30 | None, 31 | )?; 32 | 33 | for constraint in constraints.iter() { 34 | if constraint.market_type == MarketType::Spot { 35 | let spot_market = spot_market_map.get_ref(&constraint.market_index)?; 36 | let spot_position = match user.get_spot_position(constraint.market_index) { 37 | Ok(spot_position) => spot_position, 38 | Err(_) => continue, 39 | }; 40 | 41 | let signed_token_amount = spot_position 42 | .get_signed_token_amount(&spot_market)? 43 | .cast::()?; 44 | 45 | constraint.check( 46 | signed_token_amount, 47 | spot_position.open_bids, 48 | spot_position.open_asks, 49 | )?; 50 | } else { 51 | let perp_market = perp_market_map.get_ref(&constraint.market_index)?; 52 | let perp_position = match user.get_perp_position(constraint.market_index) { 53 | Ok(perp_position) => perp_position, 54 | Err(_) => continue, 55 | }; 56 | 57 | let oracle_price = oracle_map.get_price_data(&perp_market.oracle_id())?.price; 58 | 59 | let settled_perp_position = 60 | perp_position.simulate_settled_lp_position(&perp_market, oracle_price)?; 61 | 62 | constraint.check( 63 | settled_perp_position.base_asset_amount, 64 | settled_perp_position.open_bids, 65 | settled_perp_position.open_asks, 66 | )?; 67 | } 68 | } 69 | 70 | Ok(()) 71 | } 72 | 73 | #[derive(Accounts)] 74 | pub struct CheckOrderConstraints<'info> { 75 | pub user: AccountLoader<'info, User>, 76 | } 77 | 78 | #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] 79 | pub struct OrderConstraint { 80 | pub max_position: i64, 81 | pub min_position: i64, 82 | pub market_index: u16, 83 | pub market_type: MarketType, 84 | } 85 | 86 | impl OrderConstraint { 87 | pub fn check(&self, current_position: i64, open_bids: i64, open_asks: i64) -> Result<()> { 88 | let max_long = current_position.safe_add(open_bids)?; 89 | 90 | if max_long > self.max_position { 91 | msg!( 92 | "market index {} market type {:?}", 93 | self.market_index, 94 | self.market_type 95 | ); 96 | msg!( 97 | "max long {} current position {} open bids {}", 98 | max_long, 99 | current_position, 100 | open_bids 101 | ); 102 | return Err(ErrorCode::OrderSizeBreached.into()); 103 | } 104 | 105 | let max_short = current_position.safe_add(open_asks)?; 106 | if max_short < self.min_position { 107 | msg!( 108 | "market index {} market type {:?}", 109 | self.market_index, 110 | self.market_type 111 | ); 112 | msg!( 113 | "max short {} current position {} open asks {}", 114 | max_short, 115 | current_position, 116 | open_asks 117 | ); 118 | return Err(ErrorCode::OrderSizeBreached.into()); 119 | } 120 | 121 | Ok(()) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/instructions/jit.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | use anchor_lang::prelude::*; 3 | use drift::controller::position::PositionDirection; 4 | use drift::cpi::accounts::{PlaceAndMake, PlaceAndMakeSignedMsg}; 5 | use drift::error::DriftResult; 6 | use drift::instructions::optional_accounts::{load_maps, AccountMaps}; 7 | use drift::math::casting::Cast; 8 | use drift::math::safe_math::SafeMath; 9 | use drift::program::Drift; 10 | use drift::state::order_params::{OrderParams, OrderParamsBitFlag}; 11 | use drift::state::perp_market_map::PerpMarketMap; 12 | use drift::state::signed_msg_user::SignedMsgUserOrdersLoader; 13 | use drift::state::spot_market_map::SpotMarketMap; 14 | use drift::state::state::State; 15 | use drift::state::user::Order; 16 | use drift::state::user::{MarketType as DriftMarketType, OrderTriggerCondition, OrderType}; 17 | use drift::state::user::{User, UserStats}; 18 | use std::collections::BTreeSet; 19 | 20 | use crate::error::ErrorCode; 21 | use crate::state::PriceType; 22 | use drift::state::order_params::PostOnlyParam; 23 | 24 | pub fn jit<'c: 'info, 'info>( 25 | ctx: Context<'_, '_, 'c, 'info, Jit<'info>>, 26 | params: JitParams, 27 | ) -> Result<()> { 28 | let clock = Clock::get()?; 29 | let slot = clock.slot; 30 | 31 | let taker = ctx.accounts.taker.load()?; 32 | let maker = ctx.accounts.user.load()?; 33 | 34 | let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); 35 | let AccountMaps { 36 | perp_market_map, 37 | spot_market_map, 38 | mut oracle_map, 39 | } = load_maps( 40 | remaining_accounts_iter, 41 | &BTreeSet::new(), 42 | &BTreeSet::new(), 43 | slot, 44 | None, 45 | )?; 46 | 47 | let taker_order = taker 48 | .get_order(params.taker_order_id) 49 | .ok_or(ErrorCode::TakerOrderNotFound)?; 50 | let market_type = taker_order.market_type; 51 | let market_index = taker_order.market_index; 52 | 53 | let oracle_price = if taker_order.market_type == DriftMarketType::Perp { 54 | let perp_market = perp_market_map.get_ref(&taker_order.market_index)?; 55 | oracle_map.get_price_data(&perp_market.oracle_id())?.price 56 | } else { 57 | let spot_market = spot_market_map.get_ref(&taker_order.market_index)?; 58 | oracle_map.get_price_data(&spot_market.oracle_id())?.price 59 | }; 60 | 61 | let (order_params, taker_base_asset_amount_unfilled, taker_price, maker_price) = process_order( 62 | &maker, 63 | &perp_market_map, 64 | &spot_market_map, 65 | taker_order, 66 | slot, 67 | params.max_position, 68 | params.min_position, 69 | oracle_price, 70 | params.get_worst_price(oracle_price, taker_order.direction)?, 71 | params.post_only.unwrap_or(PostOnlyParam::MustPostOnly), 72 | )?; 73 | 74 | drop(taker); 75 | drop(maker); 76 | 77 | place_and_make(&ctx, params.taker_order_id, order_params)?; 78 | 79 | let taker = ctx.accounts.taker.load()?; 80 | 81 | let taker_base_asset_amount_unfilled_after = match taker.get_order(params.taker_order_id) { 82 | Some(order) => order.get_base_asset_amount_unfilled(None)?, 83 | None => 0, 84 | }; 85 | 86 | if taker_base_asset_amount_unfilled_after == taker_base_asset_amount_unfilled { 87 | // taker order failed to fill 88 | msg!( 89 | "taker price = {} maker price = {} oracle price = {}", 90 | taker_price, 91 | maker_price, 92 | oracle_price 93 | ); 94 | msg!("jit params {:?}", params); 95 | if market_type == DriftMarketType::Perp { 96 | let perp_market = perp_market_map.get_ref(&market_index)?; 97 | let reserve_price = perp_market.amm.reserve_price()?; 98 | let (bid_price, ask_price) = perp_market.amm.bid_ask_price(reserve_price)?; 99 | msg!( 100 | "vamm bid price = {} vamm ask price = {}", 101 | bid_price, 102 | ask_price 103 | ); 104 | } 105 | return Err(ErrorCode::NoFill.into()); 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | pub fn jit_signed_msg<'c: 'info, 'info>( 112 | ctx: Context<'_, '_, 'c, 'info, JitSignedMsg<'info>>, 113 | params: JitSignedMsgParams, 114 | ) -> Result<()> { 115 | let clock = Clock::get()?; 116 | let slot = clock.slot; 117 | 118 | let taker = ctx.accounts.taker.load()?; 119 | let maker = ctx.accounts.user.load()?; 120 | 121 | let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; 122 | let taker_order_id = taker_signed_msg_account 123 | .iter() 124 | .find(|signed_msg_order_id| signed_msg_order_id.uuid == params.signed_msg_order_uuid) 125 | .ok_or(ErrorCode::SignedMsgOrderDoesNotExist)? 126 | .order_id; 127 | let taker_order = taker 128 | .get_order(taker_order_id) 129 | .ok_or(ErrorCode::TakerOrderNotFound)?; 130 | 131 | let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); 132 | let AccountMaps { 133 | perp_market_map, 134 | spot_market_map, 135 | mut oracle_map, 136 | } = load_maps( 137 | remaining_accounts_iter, 138 | &BTreeSet::new(), 139 | &BTreeSet::new(), 140 | slot, 141 | None, 142 | )?; 143 | 144 | let oracle_price = oracle_map 145 | .get_price_data( 146 | &perp_market_map 147 | .get_ref(&taker_order.market_index)? 148 | .oracle_id(), 149 | )? 150 | .price; 151 | 152 | let (order_params, taker_base_asset_amount_unfilled, taker_price, maker_price) = process_order( 153 | &maker, 154 | &perp_market_map, 155 | &spot_market_map, 156 | taker_order, 157 | slot, 158 | params.max_position, 159 | params.min_position, 160 | oracle_price, 161 | params.get_worst_price(oracle_price, taker_order.direction)?, 162 | params.post_only.unwrap_or(PostOnlyParam::MustPostOnly), 163 | )?; 164 | 165 | drop(taker); 166 | drop(maker); 167 | 168 | place_and_make_signed_msg(&ctx, order_params, params.signed_msg_order_uuid)?; 169 | 170 | let taker = ctx.accounts.taker.load()?; 171 | 172 | let taker_base_asset_amount_unfilled_after = match taker.get_order(taker_order_id) { 173 | Some(order) => order.get_base_asset_amount_unfilled(None)?, 174 | None => 0, 175 | }; 176 | 177 | if taker_base_asset_amount_unfilled_after == taker_base_asset_amount_unfilled { 178 | // taker order failed to fill 179 | msg!( 180 | "taker price = {} maker price = {} oracle price = {}", 181 | taker_price, 182 | maker_price, 183 | oracle_price 184 | ); 185 | msg!("jit params {:?}", params); 186 | 187 | let perp_market = perp_market_map.get_ref(&order_params.market_index)?; 188 | let reserve_price = perp_market.amm.reserve_price()?; 189 | let (bid_price, ask_price) = perp_market.amm.bid_ask_price(reserve_price)?; 190 | msg!( 191 | "vamm bid price = {} vamm ask price = {}", 192 | bid_price, 193 | ask_price 194 | ); 195 | 196 | return Err(ErrorCode::NoFill.into()); 197 | } 198 | 199 | Ok(()) 200 | } 201 | 202 | #[allow(clippy::too_many_arguments)] 203 | #[inline(always)] 204 | fn process_order( 205 | maker: &User, 206 | perp_market_map: &PerpMarketMap, 207 | spot_market_map: &SpotMarketMap, 208 | taker_order: &Order, 209 | slot: u64, 210 | max_position: i64, 211 | min_position: i64, 212 | oracle_price: i64, 213 | maker_worst_price: u64, 214 | post_only: PostOnlyParam, 215 | ) -> Result<(OrderParams, u64, u64, u64)> { 216 | let market_type = taker_order.market_type; 217 | let market_index = taker_order.market_index; 218 | let taker_direction = taker_order.direction; 219 | 220 | let slots_left = taker_order 221 | .slot 222 | .safe_add(taker_order.auction_duration.cast()?)? 223 | .cast::()? 224 | .safe_sub(slot.cast()?)?; 225 | msg!( 226 | "slot = {} auction duration = {} slots_left = {}", 227 | slot, 228 | taker_order.auction_duration, 229 | slots_left 230 | ); 231 | 232 | msg!( 233 | "taker order type {:?} auction start {} auction end {} limit price {} oracle price offset {}", 234 | taker_order.order_type, 235 | taker_order.auction_start_price, 236 | taker_order.auction_end_price, 237 | taker_order.price, 238 | taker_order.oracle_price_offset 239 | ); 240 | 241 | let (tick_size, min_order_size, is_prediction_market) = if market_type == DriftMarketType::Perp 242 | { 243 | let perp_market = perp_market_map.get_ref(&market_index)?; 244 | 245 | ( 246 | perp_market.amm.order_tick_size, 247 | perp_market.amm.min_order_size, 248 | perp_market.is_prediction_market(), 249 | ) 250 | } else { 251 | let spot_market = spot_market_map.get_ref(&market_index)?; 252 | 253 | ( 254 | spot_market.order_tick_size, 255 | spot_market.min_order_size, 256 | false, 257 | ) 258 | }; 259 | 260 | let taker_price = match taker_order.get_limit_price( 261 | Some(oracle_price), 262 | None, 263 | slot, 264 | tick_size, 265 | is_prediction_market, 266 | None, 267 | )? { 268 | Some(price) => price, 269 | None if market_type == DriftMarketType::Perp => { 270 | msg!("taker order didnt have price. deriving fallback"); 271 | // if the order doesn't have a price, drift users amm price for taker price 272 | let perp_market = perp_market_map.get_ref(&market_index)?; 273 | let reserve_price = perp_market.amm.reserve_price()?; 274 | match taker_direction { 275 | PositionDirection::Long => perp_market.amm.ask_price(reserve_price)?, 276 | PositionDirection::Short => perp_market.amm.bid_price(reserve_price)?, 277 | } 278 | } 279 | None => { 280 | // Shouldnt be possible for spot 281 | msg!("taker order didnt have price"); 282 | return Err(ErrorCode::TakerOrderNotFound.into()); 283 | } 284 | }; 285 | 286 | let maker_direction = taker_direction.opposite(); 287 | match maker_direction { 288 | PositionDirection::Long => { 289 | if taker_price > maker_worst_price { 290 | msg!( 291 | "taker price {} > worst bid {}", 292 | taker_price, 293 | maker_worst_price 294 | ); 295 | return Err(ErrorCode::BidNotCrossed.into()); 296 | } 297 | } 298 | PositionDirection::Short => { 299 | if taker_price < maker_worst_price { 300 | msg!( 301 | "taker price {} < worst ask {}", 302 | taker_price, 303 | maker_worst_price 304 | ); 305 | return Err(ErrorCode::AskNotCrossed.into()); 306 | } 307 | } 308 | } 309 | 310 | let maker_price = if market_type == DriftMarketType::Perp { 311 | let perp_market = perp_market_map.get_ref(&market_index)?; 312 | let reserve_price = perp_market.amm.reserve_price()?; 313 | 314 | match maker_direction { 315 | PositionDirection::Long => { 316 | let amm_bid_price = perp_market.amm.bid_price(reserve_price)?; 317 | 318 | // if amm price is better than maker, use amm price to ensure fill 319 | if taker_price <= amm_bid_price { 320 | amm_bid_price.min(maker_worst_price) 321 | } else { 322 | taker_price 323 | } 324 | } 325 | PositionDirection::Short => { 326 | let amm_ask_price = perp_market.amm.ask_price(reserve_price)?; 327 | 328 | if taker_price >= amm_ask_price { 329 | amm_ask_price.max(maker_worst_price) 330 | } else { 331 | taker_price 332 | } 333 | } 334 | } 335 | } else { 336 | taker_price 337 | }; 338 | 339 | let taker_base_asset_amount_unfilled = taker_order 340 | .get_base_asset_amount_unfilled(None)? 341 | .max(min_order_size); 342 | let maker_existing_position = if market_type == DriftMarketType::Perp { 343 | let perp_market = perp_market_map.get_ref(&market_index)?; 344 | let perp_position = maker.get_perp_position(market_index); 345 | match perp_position { 346 | Ok(perp_position) => { 347 | perp_position 348 | .simulate_settled_lp_position(&perp_market, oracle_price)? 349 | .base_asset_amount 350 | } 351 | Err(_) => 0, 352 | } 353 | } else { 354 | let spot_market = spot_market_map.get_ref(&market_index)?; 355 | maker 356 | .get_spot_position(market_index) 357 | .map_or(0, |p| p.get_signed_token_amount(&spot_market).unwrap()) 358 | .cast::()? 359 | }; 360 | 361 | let maker_base_asset_amount = match check_position_limits( 362 | max_position, 363 | min_position, 364 | maker_direction, 365 | taker_base_asset_amount_unfilled, 366 | maker_existing_position, 367 | min_order_size, 368 | ) { 369 | Ok(size) => size, 370 | Err(e) => { 371 | return Err(e); 372 | } 373 | }; 374 | 375 | let order_params = OrderParams { 376 | order_type: OrderType::Limit, 377 | market_type, 378 | direction: maker_direction, 379 | user_order_id: 0, 380 | base_asset_amount: maker_base_asset_amount, 381 | price: maker_price, 382 | market_index, 383 | reduce_only: false, 384 | post_only, 385 | bit_flags: OrderParamsBitFlag::ImmediateOrCancel as u8, 386 | max_ts: None, 387 | trigger_price: None, 388 | trigger_condition: OrderTriggerCondition::Above, 389 | oracle_price_offset: None, 390 | auction_duration: None, 391 | auction_start_price: None, 392 | auction_end_price: None, 393 | }; 394 | Ok(( 395 | order_params, 396 | taker_base_asset_amount_unfilled, 397 | taker_price, 398 | maker_price, 399 | )) 400 | } 401 | 402 | #[derive(Accounts)] 403 | pub struct Jit<'info> { 404 | pub state: Box>, 405 | #[account(mut)] 406 | pub user: AccountLoader<'info, User>, 407 | #[account(mut)] 408 | pub user_stats: AccountLoader<'info, UserStats>, 409 | #[account(mut)] 410 | pub taker: AccountLoader<'info, User>, 411 | #[account(mut)] 412 | pub taker_stats: AccountLoader<'info, UserStats>, 413 | pub authority: Signer<'info>, 414 | pub drift_program: Program<'info, Drift>, 415 | } 416 | 417 | #[derive(Accounts)] 418 | pub struct JitSignedMsg<'info> { 419 | pub state: Box>, 420 | #[account(mut)] 421 | pub user: AccountLoader<'info, User>, 422 | #[account(mut)] 423 | pub user_stats: AccountLoader<'info, UserStats>, 424 | #[account(mut)] 425 | pub taker: AccountLoader<'info, User>, 426 | #[account(mut)] 427 | pub taker_stats: AccountLoader<'info, UserStats>, 428 | /// CHECK: checked in SignedMsgUserOrdersZeroCopy checks 429 | #[account(mut)] 430 | pub taker_signed_msg_user_orders: AccountInfo<'info>, 431 | pub authority: Signer<'info>, 432 | pub drift_program: Program<'info, Drift>, 433 | } 434 | 435 | #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] 436 | pub struct JitParams { 437 | pub taker_order_id: u32, 438 | pub max_position: i64, 439 | pub min_position: i64, 440 | pub bid: i64, 441 | pub ask: i64, 442 | pub price_type: PriceType, 443 | pub post_only: Option, 444 | } 445 | 446 | impl Default for JitParams { 447 | fn default() -> Self { 448 | Self { 449 | taker_order_id: 0, 450 | max_position: 0, 451 | min_position: 0, 452 | bid: 0, 453 | ask: 0, 454 | price_type: PriceType::Limit, 455 | post_only: None, 456 | } 457 | } 458 | } 459 | 460 | impl JitParams { 461 | pub fn get_worst_price( 462 | self, 463 | oracle_price: i64, 464 | taker_direction: PositionDirection, 465 | ) -> DriftResult { 466 | match (taker_direction, self.price_type) { 467 | (PositionDirection::Long, PriceType::Limit) => Ok(self.ask.unsigned_abs()), 468 | (PositionDirection::Short, PriceType::Limit) => Ok(self.bid.unsigned_abs()), 469 | (PositionDirection::Long, PriceType::Oracle) => { 470 | Ok(oracle_price.safe_add(self.ask)?.unsigned_abs()) 471 | } 472 | (PositionDirection::Short, PriceType::Oracle) => { 473 | Ok(oracle_price.safe_add(self.bid)?.unsigned_abs()) 474 | } 475 | } 476 | } 477 | } 478 | 479 | #[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] 480 | pub struct JitSignedMsgParams { 481 | pub signed_msg_order_uuid: [u8; 8], 482 | pub max_position: i64, 483 | pub min_position: i64, 484 | pub bid: i64, 485 | pub ask: i64, 486 | pub price_type: PriceType, 487 | pub post_only: Option, 488 | } 489 | 490 | impl Default for JitSignedMsgParams { 491 | fn default() -> Self { 492 | Self { 493 | signed_msg_order_uuid: [0; 8], 494 | max_position: 0, 495 | min_position: 0, 496 | bid: 0, 497 | ask: 0, 498 | price_type: PriceType::Limit, 499 | post_only: None, 500 | } 501 | } 502 | } 503 | 504 | impl JitSignedMsgParams { 505 | pub fn get_worst_price( 506 | self, 507 | oracle_price: i64, 508 | taker_direction: PositionDirection, 509 | ) -> DriftResult { 510 | match (taker_direction, self.price_type) { 511 | (PositionDirection::Long, PriceType::Limit) => Ok(self.ask.unsigned_abs()), 512 | (PositionDirection::Short, PriceType::Limit) => Ok(self.bid.unsigned_abs()), 513 | (PositionDirection::Long, PriceType::Oracle) => { 514 | Ok(oracle_price.safe_add(self.ask)?.unsigned_abs()) 515 | } 516 | (PositionDirection::Short, PriceType::Oracle) => { 517 | Ok(oracle_price.safe_add(self.bid)?.unsigned_abs()) 518 | } 519 | } 520 | } 521 | } 522 | 523 | fn check_position_limits( 524 | max_position: i64, 525 | min_position: i64, 526 | maker_direction: PositionDirection, 527 | taker_base_asset_amount_unfilled: u64, 528 | maker_existing_position: i64, 529 | min_order_size: u64, 530 | ) -> Result { 531 | if maker_direction == PositionDirection::Long { 532 | let size = max_position.safe_sub(maker_existing_position)?; 533 | 534 | if size <= min_order_size.cast()? { 535 | msg!( 536 | "maker existing position {} >= max position {} + min order size {}", 537 | maker_existing_position, 538 | max_position, 539 | min_order_size 540 | ); 541 | return Err(ErrorCode::PositionLimitBreached.into()); 542 | } 543 | 544 | Ok(size.unsigned_abs().min(taker_base_asset_amount_unfilled)) 545 | } else { 546 | let size = maker_existing_position.safe_sub(min_position)?; 547 | 548 | if size <= min_order_size.cast()? { 549 | msg!( 550 | "maker existing position {} <= min position {} + min order size {}", 551 | maker_existing_position, 552 | min_position, 553 | min_order_size 554 | ); 555 | return Err(ErrorCode::PositionLimitBreached.into()); 556 | } 557 | 558 | Ok(size.unsigned_abs().min(taker_base_asset_amount_unfilled)) 559 | } 560 | } 561 | 562 | fn place_and_make<'info>( 563 | ctx: &Context<'_, '_, '_, 'info, Jit<'info>>, 564 | taker_order_id: u32, 565 | order_params: OrderParams, 566 | ) -> Result<()> { 567 | let drift_program = ctx.accounts.drift_program.to_account_info().clone(); 568 | let cpi_accounts = PlaceAndMake { 569 | state: ctx.accounts.state.to_account_info().clone(), 570 | user: ctx.accounts.user.to_account_info().clone(), 571 | user_stats: ctx.accounts.user_stats.to_account_info().clone(), 572 | authority: ctx.accounts.authority.to_account_info().clone(), 573 | taker: ctx.accounts.taker.to_account_info().clone(), 574 | taker_stats: ctx.accounts.taker_stats.to_account_info().clone(), 575 | }; 576 | 577 | let cpi_context = CpiContext::new(drift_program, cpi_accounts) 578 | .with_remaining_accounts(ctx.remaining_accounts.into()); 579 | 580 | if order_params.market_type == DriftMarketType::Perp { 581 | drift::cpi::place_and_make_perp_order(cpi_context, order_params, taker_order_id)?; 582 | } else { 583 | drift::cpi::place_and_make_spot_order(cpi_context, order_params, taker_order_id, None)?; 584 | } 585 | 586 | Ok(()) 587 | } 588 | 589 | fn place_and_make_signed_msg<'info>( 590 | ctx: &Context<'_, '_, '_, 'info, JitSignedMsg<'info>>, 591 | order_params: OrderParams, 592 | signed_msg_order_uuid: [u8; 8], 593 | ) -> Result<()> { 594 | let drift_program = ctx.accounts.drift_program.to_account_info(); 595 | let state = ctx.accounts.state.to_account_info(); 596 | let taker = ctx.accounts.taker.to_account_info(); 597 | let taker_stats = ctx.accounts.taker_stats.to_account_info(); 598 | let taker_signed_msg_user_orders = ctx.accounts.taker_signed_msg_user_orders.to_account_info(); 599 | 600 | let cpi_accounts_place_and_make = PlaceAndMakeSignedMsg { 601 | state, 602 | user: ctx.accounts.user.to_account_info().clone(), 603 | user_stats: ctx.accounts.user_stats.to_account_info().clone(), 604 | authority: ctx.accounts.authority.to_account_info().clone(), 605 | taker, 606 | taker_stats, 607 | taker_signed_msg_user_orders, 608 | }; 609 | 610 | let cpi_context_place_and_make = CpiContext::new(drift_program, cpi_accounts_place_and_make) 611 | .with_remaining_accounts(ctx.remaining_accounts.into()); 612 | 613 | drift::cpi::place_and_make_signed_msg_perp_order( 614 | cpi_context_place_and_make, 615 | order_params, 616 | signed_msg_order_uuid, 617 | )?; 618 | Ok(()) 619 | } 620 | 621 | #[cfg(test)] 622 | mod tests { 623 | use super::*; 624 | 625 | #[test] 626 | fn test_check_position_limits() { 627 | let max_position: i64 = 100; 628 | let min_position: i64 = -100; 629 | 630 | // same direction, doesn't breach 631 | let result = check_position_limits( 632 | max_position, 633 | min_position, 634 | PositionDirection::Long, 635 | 10, 636 | 40, 637 | 0, 638 | ); 639 | assert!(result.is_ok()); 640 | assert_eq!(result.unwrap(), 10); 641 | let result = check_position_limits( 642 | max_position, 643 | min_position, 644 | PositionDirection::Short, 645 | 10, 646 | -40, 647 | 0, 648 | ); 649 | assert!(result.is_ok()); 650 | assert_eq!(result.unwrap(), 10); 651 | 652 | // same direction, whole order breaches, only takes enough to hit limit 653 | let result = check_position_limits( 654 | max_position, 655 | min_position, 656 | PositionDirection::Long, 657 | 100, 658 | 40, 659 | 0, 660 | ); 661 | assert!(result.is_ok()); 662 | assert_eq!(result.unwrap(), 60); 663 | let result = check_position_limits( 664 | max_position, 665 | min_position, 666 | PositionDirection::Short, 667 | 100, 668 | -40, 669 | 0, 670 | ); 671 | assert!(result.is_ok()); 672 | assert_eq!(result.unwrap(), 60); 673 | 674 | // opposite direction, doesn't breach 675 | let result = check_position_limits( 676 | max_position, 677 | min_position, 678 | PositionDirection::Long, 679 | 10, 680 | -40, 681 | 0, 682 | ); 683 | assert!(result.is_ok()); 684 | assert_eq!(result.unwrap(), 10); 685 | let result = check_position_limits( 686 | max_position, 687 | min_position, 688 | PositionDirection::Short, 689 | 10, 690 | 40, 691 | 0, 692 | ); 693 | assert!(result.is_ok()); 694 | assert_eq!(result.unwrap(), 10); 695 | 696 | // opposite direction, whole order breaches, only takes enough to take flipped limit 697 | let result = check_position_limits( 698 | max_position, 699 | min_position, 700 | PositionDirection::Long, 701 | 200, 702 | -40, 703 | 0, 704 | ); 705 | assert!(result.is_ok()); 706 | assert_eq!(result.unwrap(), 140); 707 | let result = check_position_limits( 708 | max_position, 709 | min_position, 710 | PositionDirection::Short, 711 | 200, 712 | 40, 713 | 0, 714 | ); 715 | assert!(result.is_ok()); 716 | assert_eq!(result.unwrap(), 140); 717 | 718 | // opposite direction, maker already breached, allows reducing 719 | let result = check_position_limits( 720 | max_position, 721 | min_position, 722 | PositionDirection::Long, 723 | 200, 724 | -150, 725 | 0, 726 | ); 727 | assert!(result.is_ok()); 728 | assert_eq!(result.unwrap(), 200); 729 | let result = check_position_limits( 730 | max_position, 731 | min_position, 732 | PositionDirection::Short, 733 | 200, 734 | 150, 735 | 0, 736 | ); 737 | assert!(result.is_ok()); 738 | assert_eq!(result.unwrap(), 200); 739 | 740 | // same direction, maker already breached, errors 741 | let result = check_position_limits( 742 | max_position, 743 | min_position, 744 | PositionDirection::Long, 745 | 200, 746 | 150, 747 | 0, 748 | ); 749 | assert!(result.is_err()); 750 | let result = check_position_limits( 751 | max_position, 752 | min_position, 753 | PositionDirection::Short, 754 | 200, 755 | -150, 756 | 0, 757 | ); 758 | assert!(result.is_err()); 759 | } 760 | } 761 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | mod arb_perp; 2 | mod check_order_constraints; 3 | mod jit; 4 | 5 | pub use arb_perp::*; 6 | pub use check_order_constraints::*; 7 | pub use jit::*; 8 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | pub mod instructions; 5 | pub mod state; 6 | 7 | use instructions::*; 8 | 9 | declare_id!("J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP"); 10 | 11 | #[program] 12 | pub mod jit_proxy { 13 | use super::*; 14 | 15 | pub fn jit<'c: 'info, 'info>( 16 | ctx: Context<'_, '_, 'c, 'info, Jit<'info>>, 17 | params: JitParams, 18 | ) -> Result<()> { 19 | instructions::jit(ctx, params) 20 | } 21 | 22 | pub fn jit_signed_msg<'c: 'info, 'info>( 23 | ctx: Context<'_, '_, 'c, 'info, JitSignedMsg<'info>>, 24 | params: JitSignedMsgParams, 25 | ) -> Result<()> { 26 | instructions::jit_signed_msg(ctx, params) 27 | } 28 | 29 | pub fn check_order_constraints<'c: 'info, 'info>( 30 | ctx: Context<'_, '_, 'c, 'info, CheckOrderConstraints<'info>>, 31 | constraints: Vec, 32 | ) -> Result<()> { 33 | instructions::check_order_constraints(ctx, constraints) 34 | } 35 | 36 | pub fn arb_perp<'c: 'info, 'info>( 37 | ctx: Context<'_, '_, 'c, 'info, ArbPerp<'info>>, 38 | market_index: u16, 39 | ) -> Result<()> { 40 | instructions::arb_perp(ctx, market_index) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /programs/jit-proxy/src/state.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use borsh::{BorshDeserialize, BorshSerialize}; 3 | use drift::state::order_params::PostOnlyParam as DriftPostOnlyParam; 4 | use drift::state::user::MarketType as DriftMarketType; 5 | 6 | #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] 7 | pub enum PostOnlyParam { 8 | None, 9 | MustPostOnly, // Tx fails if order can't be post only 10 | TryPostOnly, // Tx succeeds and order not placed if can't be post only 11 | Slide, // Modify price to be post only if can't be post only 12 | } 13 | 14 | impl PostOnlyParam { 15 | pub fn to_drift_param(self) -> DriftPostOnlyParam { 16 | match self { 17 | PostOnlyParam::None => DriftPostOnlyParam::None, 18 | PostOnlyParam::MustPostOnly => DriftPostOnlyParam::MustPostOnly, 19 | PostOnlyParam::TryPostOnly => DriftPostOnlyParam::TryPostOnly, 20 | PostOnlyParam::Slide => DriftPostOnlyParam::Slide, 21 | } 22 | } 23 | } 24 | 25 | #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] 26 | pub enum PriceType { 27 | Limit, 28 | Oracle, 29 | } 30 | 31 | #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] 32 | pub enum MarketType { 33 | Perp, 34 | Spot, 35 | } 36 | 37 | impl MarketType { 38 | pub fn to_drift_param(self) -> DriftMarketType { 39 | match self { 40 | MarketType::Spot => DriftMarketType::Spot, 41 | MarketType::Perp => DriftMarketType::Perp, 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Python JIT Proxy SDK 2 | 3 | ## QuickStart Guide 4 | 5 | 1. Run `poetry install` in the `/python` directory 6 | 2. Add ```.env``` file in ```/sdk/examples``` with your ```RPC_URL``` and ```PRIVATE_KEY``` as a byte array 7 | 3. Customize ```JitParams``` in either ```shotgun.py``` or ```sniper.py``` 8 | 4. Run ```poetry run python -m examples.sniper``` or ```poetry run python -m examples.shotgun``` while in the ```sdk``` directory 9 | 10 | ## Shotgun vs Sniper 11 | 12 | The ```JitterShotgun``` will immediately spray transactions when it detects an eligible auction. 13 | It'll try up to 10 times to fulfill the order, retrying if the order doesn't yet cross or the oracle is stale. 14 | 15 | The ```JitterSniper``` will wait until it detects an order that has a high chance of crossing the ```JitParams``` and retry up to 3 times on errors. It won't send transactions immediately like the ```JitterShotgun``` does, but will try to determine that the order will cross the bid/ask during the auction before sending a transaction. 16 | 17 | 18 | ## How to set up a ```JitProxyClient``` and ```Jitter``` 19 | 20 | This example uses the ```JitterShotgun```, but the same logic follows for the ```JitterSniper```. 21 | 22 | However, the ```JitterSniper``` also requires a ```SlotSubscriber``` in its constructor. 23 | 24 | ``` 25 | jit_proxy_client = JitProxyClient( 26 | drift_client, 27 | # JIT program ID 28 | Pubkey.from_string("J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP"), 29 | ) 30 | 31 | jitter_shotgun = JitterShotgun(drift_client, auction_subscriber, jit_proxy_client, True) # The boolean is logging verbosity, True = verbose. 32 | 33 | jit_params = JitParams( 34 | bid=-1_000_000, 35 | ask=1_010_000, 36 | min_position=0, 37 | max_position=2, 38 | price_type=PriceType.Oracle(), 39 | sub_account_id=None, 40 | ) 41 | 42 | # Add your parameters to the Jitter before subscribing 43 | jitter_shotgun.update_perp_params(0, jit_params) 44 | 45 | await jitter_shotgun.subscribe() 46 | ``` -------------------------------------------------------------------------------- /python/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "drift_jit_proxy" 3 | version = "0.1.6" 4 | description = "python sdk for drift protocol v2 jit proxy program" 5 | authors = ["Frank "] 6 | readme = "README.md" 7 | packages = [ 8 | { include = "jit_proxy", from = "sdk" } 9 | ] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.10" 13 | python-dotenv = "^1.0.0" 14 | solana = "^0.34.0" 15 | anchorpy = "^0.20.1" 16 | driftpy = ">=0.7.0" 17 | 18 | [build-system] 19 | requires = ["poetry-core"] 20 | build-backend = "poetry.core.masonry.api" 21 | 22 | # 23 | -------------------------------------------------------------------------------- /python/sdk/examples/shotgun.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | from solana.rpc.async_api import AsyncClient 7 | from solders.pubkey import Pubkey 8 | 9 | from anchorpy import Wallet 10 | 11 | from driftpy.drift_client import DriftClient 12 | from driftpy.account_subscription_config import AccountSubscriptionConfig 13 | from driftpy.auction_subscriber.auction_subscriber import AuctionSubscriber 14 | from driftpy.auction_subscriber.types import AuctionSubscriberConfig 15 | from driftpy.keypair import load_keypair 16 | 17 | from jit_proxy.jitter.jitter_shotgun import JitterShotgun # type: ignore 18 | from jit_proxy.jitter.base_jitter import JitParams # type: ignore 19 | from jit_proxy.jit_proxy_client import JitProxyClient, PriceType #type: ignore 20 | 21 | 22 | async def main(): 23 | load_dotenv() 24 | secret = os.getenv("PRIVATE_KEY") 25 | url = os.getenv("RPC_URL") 26 | 27 | kp = load_keypair(secret) 28 | wallet = Wallet(kp) 29 | 30 | connection = AsyncClient(url) 31 | drift_client = DriftClient( 32 | connection, 33 | wallet, 34 | "mainnet", 35 | account_subscription=AccountSubscriptionConfig("websocket"), 36 | ) 37 | 38 | auction_subscriber_config = AuctionSubscriberConfig(drift_client) 39 | auction_subscriber = AuctionSubscriber(auction_subscriber_config) 40 | 41 | jit_proxy_client = JitProxyClient( 42 | drift_client, 43 | # JIT program ID 44 | Pubkey.from_string("J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP"), 45 | ) 46 | 47 | jitter_shotgun = JitterShotgun(drift_client, auction_subscriber, jit_proxy_client, True) 48 | 49 | jit_params = JitParams( 50 | bid=-1_000_000, 51 | ask=1_010_000, 52 | min_position=0, 53 | max_position=2, 54 | price_type=PriceType.Oracle(), 55 | sub_account_id=None, 56 | ) 57 | 58 | jitter_shotgun.update_spot_params(0, jit_params) 59 | jitter_shotgun.update_perp_params(0, jit_params) 60 | 61 | print(f"Added JitParams: {jit_params} to JitterShotgun") 62 | 63 | await jitter_shotgun.subscribe() 64 | 65 | print("Subscribed to JitterShotgun successfully!") 66 | 67 | # quick & dirty way to keep event loop open 68 | try: 69 | while True: 70 | await asyncio.sleep(3600) 71 | except asyncio.CancelledError: 72 | pass 73 | 74 | 75 | if __name__ == "__main__": 76 | asyncio.run(main()) 77 | -------------------------------------------------------------------------------- /python/sdk/examples/sniper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from dotenv import load_dotenv 5 | 6 | from solana.rpc.async_api import AsyncClient 7 | from solders.pubkey import Pubkey 8 | 9 | from anchorpy import Wallet 10 | 11 | from driftpy.drift_client import DriftClient 12 | from driftpy.account_subscription_config import AccountSubscriptionConfig 13 | from driftpy.auction_subscriber.auction_subscriber import AuctionSubscriber 14 | from driftpy.auction_subscriber.types import AuctionSubscriberConfig 15 | from driftpy.slot.slot_subscriber import SlotSubscriber 16 | from driftpy.keypair import load_keypair 17 | 18 | from jit_proxy.jitter.jitter_sniper import JitterSniper 19 | from jit_proxy.jitter.base_jitter import JitParams 20 | from jit_proxy.jit_proxy_client import JitProxyClient, PriceType 21 | 22 | 23 | async def main(): 24 | load_dotenv() 25 | secret = os.getenv("PRIVATE_KEY") 26 | url = os.getenv("RPC_URL") 27 | 28 | kp = load_keypair(secret) 29 | wallet = Wallet(kp) 30 | 31 | connection = AsyncClient(url) 32 | drift_client = DriftClient( 33 | connection, 34 | wallet, 35 | "mainnet", 36 | account_subscription=AccountSubscriptionConfig("websocket"), 37 | ) 38 | 39 | auction_subscriber_config = AuctionSubscriberConfig(drift_client) 40 | auction_subscriber = AuctionSubscriber(auction_subscriber_config) 41 | 42 | slot_subscriber = SlotSubscriber(drift_client) 43 | 44 | jit_proxy_client = JitProxyClient( 45 | drift_client, 46 | # JIT program ID 47 | Pubkey.from_string("J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP"), 48 | ) 49 | 50 | jitter_sniper = JitterSniper( 51 | drift_client, slot_subscriber, auction_subscriber, jit_proxy_client, True 52 | ) 53 | 54 | jit_params = JitParams( 55 | bid=-1_000_000, 56 | ask=1_010_000, 57 | min_position=0, 58 | max_position=2, 59 | price_type=PriceType.Oracle(), 60 | sub_account_id=None, 61 | ) 62 | 63 | jitter_sniper.update_spot_params(1, jit_params) 64 | jitter_sniper.update_perp_params(1, jit_params) 65 | 66 | print(f"Added JitParams: {jit_params} to JitterSniper") 67 | 68 | await jitter_sniper.subscribe() 69 | 70 | print("Subscribed to JitterSniper successfully!") 71 | 72 | # quick & dirty way to keep event loop open 73 | try: 74 | while True: 75 | await asyncio.sleep(3600) 76 | except asyncio.CancelledError: 77 | pass 78 | 79 | 80 | if __name__ == "__main__": 81 | asyncio.run(main()) 82 | -------------------------------------------------------------------------------- /python/sdk/jit_proxy/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" 2 | -------------------------------------------------------------------------------- /python/sdk/jit_proxy/jit_proxy_client.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional, cast 3 | 4 | from borsh_construct.enum import _rust_enum 5 | from sumtypes import constructor # type: ignore 6 | 7 | from solders.pubkey import Pubkey # type: ignore 8 | 9 | from anchorpy import Context, Program 10 | 11 | from solana.transaction import AccountMeta 12 | 13 | from driftpy.types import ( 14 | UserAccount, 15 | PostOnlyParams, 16 | ReferrerInfo, 17 | MarketType, 18 | MakerInfo, 19 | is_variant, 20 | ) 21 | from driftpy.drift_client import DriftClient 22 | from driftpy.constants.numeric_constants import QUOTE_SPOT_MARKET_INDEX 23 | 24 | 25 | @_rust_enum 26 | class PriceType: 27 | Limit = constructor() 28 | Oracle = constructor() 29 | 30 | 31 | @dataclass 32 | class JitIxParams: 33 | taker_key: Pubkey 34 | taker_stats_key: Pubkey 35 | taker: UserAccount 36 | taker_order_id: int 37 | max_position: int 38 | min_position: int 39 | bid: int 40 | ask: int 41 | price_type: Optional[PriceType] 42 | referrer_info: Optional[ReferrerInfo] 43 | sub_account_id: Optional[int] 44 | post_only: PostOnlyParams = PostOnlyParams.MustPostOnly() 45 | 46 | 47 | @dataclass 48 | class ArbIxParams: 49 | maker_infos: list[MakerInfo] 50 | market_index: int 51 | 52 | 53 | @dataclass 54 | class OrderConstraint: 55 | max_position: int 56 | min_position: int 57 | market_index: int 58 | market_type: MarketType 59 | 60 | 61 | class JitProxyClient: 62 | def __init__(self, drift_client: DriftClient, program_id: Pubkey): 63 | self.program_id = program_id 64 | self.drift_client = drift_client 65 | self.program = None 66 | 67 | async def init(self): 68 | self.program = await Program.at( 69 | self.program_id, self.drift_client.program.provider 70 | ) 71 | 72 | async def jit(self, params: JitIxParams): 73 | if self.program is None: 74 | await self.init() 75 | 76 | sub_account_id = self.drift_client.get_sub_account_id_for_ix( 77 | params.sub_account_id # type: ignore 78 | ) 79 | 80 | order = next( 81 | ( 82 | order 83 | for order in params.taker.orders 84 | if order.order_id == params.taker_order_id 85 | ), 86 | None, 87 | ) 88 | remaining_accounts = self.drift_client.get_remaining_accounts( 89 | user_accounts=[ 90 | params.taker, 91 | self.drift_client.get_user_account(sub_account_id), 92 | ], 93 | writable_spot_market_indexes=[order.market_index, QUOTE_SPOT_MARKET_INDEX] # type: ignore 94 | if is_variant(order.market_type, "Spot") # type: ignore 95 | else [], 96 | writable_perp_market_indexes=[order.market_index] # type: ignore 97 | if is_variant(order.market_type, "Perp") # type: ignore 98 | else [], 99 | ) 100 | 101 | if params.referrer_info is not None: 102 | remaining_accounts.append( 103 | AccountMeta( 104 | pubkey=params.referrer_info.referrer, 105 | is_writable=True, 106 | is_signer=False, 107 | ) 108 | ) 109 | remaining_accounts.append( 110 | AccountMeta( 111 | pubkey=params.referrer_info.referrer_stats, 112 | is_writable=True, 113 | is_signer=False, 114 | ) 115 | ) 116 | 117 | if is_variant(order.market_type, "Spot"): # type: ignore 118 | remaining_accounts.append( 119 | AccountMeta( 120 | pubkey=self.drift_client.get_spot_market_account( # type: ignore 121 | order.market_index # type: ignore 122 | ).vault, 123 | is_writable=False, 124 | is_signer=False, 125 | ) 126 | ) 127 | remaining_accounts.append( 128 | AccountMeta( 129 | pubkey=self.drift_client.get_quote_spot_market_account().vault, # type: ignore 130 | is_writable=False, 131 | is_signer=False, 132 | ) 133 | ) 134 | 135 | jit_params = self.program.type["JitParams"]( # type: ignore 136 | taker_order_id=params.taker_order_id, 137 | max_position=cast(int, params.max_position), 138 | min_position=cast(int, params.min_position), 139 | bid=cast(int, params.bid), 140 | ask=cast(int, params.ask), 141 | price_type=self.get_price_type(params.price_type), # type: ignore 142 | post_only=self.get_post_only(params.post_only), 143 | ) 144 | 145 | ix = self.program.instruction["jit"]( # type: ignore 146 | jit_params, 147 | ctx=Context( 148 | accounts={ 149 | "state": self.drift_client.get_state_public_key(), 150 | "user": self.drift_client.get_user_account_public_key( 151 | sub_account_id 152 | ), 153 | "user_stats": self.drift_client.get_user_stats_public_key(), 154 | "taker": params.taker_key, 155 | "taker_stats": params.taker_stats_key, 156 | "authority": self.drift_client.wallet.public_key, 157 | "drift_program": self.drift_client.program_id, 158 | }, 159 | signers={self.drift_client.wallet}, # type: ignore 160 | remaining_accounts=remaining_accounts, 161 | ), 162 | ) 163 | 164 | tx_sig_and_slot = await self.drift_client.send_ixs(ix) 165 | 166 | return tx_sig_and_slot.tx_sig 167 | 168 | def get_price_type(self, price_type: PriceType): 169 | if is_variant(price_type, "Oracle"): 170 | return self.program.type["PriceType"].Oracle() # type: ignore 171 | elif is_variant(price_type, "Limit"): 172 | return self.program.type["PriceType"].Limit() # type: ignore 173 | else: 174 | raise ValueError(f"Unknown price type: {str(price_type)}") 175 | 176 | def get_post_only(self, post_only: PostOnlyParams): 177 | if is_variant(post_only, "MustPostOnly"): 178 | return self.program.type["PostOnlyParam"].MustPostOnly() # type: ignore 179 | elif is_variant(post_only, "TryPostOnly"): 180 | return self.program.type["PostOnlyParam"].TryPostOnly() # type: ignore 181 | elif is_variant(post_only, "Slide"): 182 | return self.program.type["PostOnlyParam"].Slide() # type: ignore 183 | -------------------------------------------------------------------------------- /python/sdk/jit_proxy/jitter/base_jitter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from typing import Callable, Dict, Optional 5 | from abc import ABC, abstractmethod 6 | from dataclasses import dataclass 7 | 8 | from solders.pubkey import Pubkey # type: ignore 9 | 10 | from driftpy.types import is_variant, UserAccount, Order, UserStatsAccount, ReferrerInfo 11 | from driftpy.drift_client import DriftClient 12 | from driftpy.auction_subscriber.auction_subscriber import AuctionSubscriber 13 | from driftpy.addresses import get_user_stats_account_public_key, get_user_account_public_key 14 | from driftpy.math.orders import has_auction_price 15 | from driftpy.math.conversion import convert_to_number 16 | 17 | from jit_proxy.jit_proxy_client import JitProxyClient, PriceType 18 | 19 | UserFilter = Callable[[UserAccount, str, Order], bool] 20 | 21 | @dataclass 22 | class JitParams: 23 | bid: int 24 | ask: int 25 | min_position: int 26 | max_position: int 27 | price_type: PriceType 28 | sub_account_id: Optional[int] 29 | 30 | 31 | class BaseJitter(ABC): 32 | @abstractmethod 33 | def __init__( 34 | self, 35 | drift_client: DriftClient, 36 | auction_subscriber: AuctionSubscriber, 37 | jit_proxy_client: JitProxyClient, 38 | verbose: bool, 39 | ): 40 | self.drift_client = drift_client 41 | self.auction_subscriber = auction_subscriber 42 | self.jit_proxy_client = jit_proxy_client 43 | self.perp_params: Dict[int, JitParams] = {} 44 | self.spot_params: Dict[int, JitParams] = {} 45 | self.ongoing_auctions: Dict[str, asyncio.Future] = {} 46 | self.user_filter: Optional[UserFilter] = None 47 | 48 | if verbose: 49 | logging_level = logging.INFO 50 | else: 51 | logging_level = logging.WARNING 52 | 53 | logging.basicConfig(level=logging_level) 54 | self.logger = logging.getLogger(__name__) 55 | 56 | 57 | @abstractmethod 58 | async def subscribe(self): 59 | await self.drift_client.subscribe() 60 | await self.auction_subscriber.subscribe() 61 | 62 | self.auction_subscriber.event_emitter.on_account_update += ( 63 | self.on_account_update_sync 64 | ) 65 | 66 | def on_account_update_sync(self, taker: UserAccount, taker_key: Pubkey, slot: int): 67 | asyncio.create_task(self.on_account_update(taker, taker_key, slot)) 68 | 69 | async def on_account_update(self, taker: UserAccount, taker_key: Pubkey, slot: int): 70 | self.logger.info("Auction received!") 71 | self.logger.info("----------------------------") 72 | taker_key_str = str(taker_key) 73 | 74 | taker_stats_key = get_user_stats_account_public_key( 75 | self.drift_client.program_id, taker.authority # type: ignore 76 | ) 77 | 78 | self.logger.info(f"Taker: {taker.authority}") 79 | 80 | for order in taker.orders: 81 | if not is_variant(order.status, "Open"): 82 | continue 83 | 84 | if not has_auction_price(order, slot): 85 | continue 86 | 87 | if self.user_filter is not None: 88 | if self.user_filter(taker, taker_key_str, order): 89 | self.logger.info("User filtered out.") 90 | return 91 | 92 | order_sig = self.get_order_signatures(taker_key_str, order.order_id) 93 | 94 | self.logger.info(f"Order sig: {order_sig}") 95 | 96 | if order_sig in self.ongoing_auctions: 97 | continue 98 | 99 | if is_variant(order.market_type, "Perp"): 100 | self.logger.info("Perp Auction") 101 | if not order.market_index in self.perp_params: 102 | self.logger.info(f"Jitter not listening to {order.market_index}") 103 | return 104 | 105 | self.log_details(order) 106 | 107 | perp_market_account = self.drift_client.get_perp_market_account( 108 | order.market_index 109 | ) 110 | 111 | if ( 112 | order.base_asset_amount - order.base_asset_amount_filled 113 | <= perp_market_account.amm.min_order_size # type: ignore 114 | ): 115 | self.logger.info("Order filled within min_order_size") 116 | self.logger.info("----------------------------") 117 | return 118 | 119 | future = asyncio.create_task( 120 | self.create_try_fill( 121 | taker, taker_key, taker_stats_key, order, order_sig 122 | ) 123 | ) 124 | self.ongoing_auctions[order_sig] = future 125 | 126 | else: 127 | self.logger.info("Spot Auction") 128 | if not order.market_index in self.spot_params: 129 | self.logger.info(f"Jitter not listening to {order.market_index}") 130 | self.logger.info("----------------------------") 131 | return 132 | 133 | self.log_details(order) 134 | 135 | spot_market_account = self.drift_client.get_spot_market_account( 136 | order.market_index 137 | ) 138 | 139 | if ( 140 | order.base_asset_amount - order.base_asset_amount_filled 141 | <= spot_market_account.min_order_size # type: ignore 142 | ): 143 | self.logger.info("Order filled within min_order_size") 144 | self.logger.info("----------------------------") 145 | return 146 | 147 | future = asyncio.create_task( 148 | self.create_try_fill( 149 | taker, taker_key, taker_stats_key, order, order_sig 150 | ) 151 | ) 152 | self.ongoing_auctions[order_sig] = future 153 | 154 | def log_details(self, order: Order): 155 | self.logger.info(f"Market Type: {str(order.market_type)}") 156 | self.logger.info(f"Market Index: {order.market_index}") 157 | self.logger.info(f"Order Price: {convert_to_number(order.price)}") 158 | self.logger.info(f"Order Type: {str(order.order_type)}") 159 | self.logger.info(f"Order Direction: {str(order.direction)}") 160 | self.logger.info( 161 | f"Auction Start Price: {convert_to_number(order.auction_start_price)}" 162 | ) 163 | self.logger.info(f"Auction End Price: {convert_to_number(order.auction_end_price)}") 164 | self.logger.info( 165 | f"Order Base Asset Amount: {convert_to_number(order.base_asset_amount)}" 166 | ) 167 | self.logger.info( 168 | f"Order Base Asset Amount Filled: {convert_to_number(order.base_asset_amount_filled)}" 169 | ) 170 | 171 | @abstractmethod 172 | async def create_try_fill( 173 | self, 174 | taker: UserAccount, 175 | taker_key: Pubkey, 176 | taker_stats_key: Pubkey, 177 | order: Order, 178 | order_sig: str, 179 | ): 180 | future = asyncio.Future() # type: ignore 181 | future.set_result(None) 182 | return future 183 | 184 | def get_order_signatures(self, taker_key: str, order_id: int) -> str: 185 | return f"{taker_key}-{order_id}" 186 | 187 | def update_perp_params(self, market_index: int, params: JitParams): 188 | self.perp_params[market_index] = params 189 | 190 | def update_spot_params(self, market_index: int, params: JitParams): 191 | self.spot_params[market_index] = params 192 | 193 | def set_user_filter(self, user_filter: Optional[UserFilter]): 194 | self.user_filter = user_filter 195 | 196 | def get_referrer_info( 197 | self, taker_stats: UserStatsAccount 198 | ) -> Optional[ReferrerInfo]: 199 | if taker_stats.referrer == Pubkey.default(): 200 | return None 201 | else: 202 | return ReferrerInfo( 203 | get_user_account_public_key( 204 | self.drift_client.program_id, # type: ignore 205 | taker_stats.referrer, 206 | 0 207 | ), 208 | get_user_stats_account_public_key( 209 | self.drift_client.program_id, taker_stats.referrer # type: ignore 210 | ), 211 | ) 212 | -------------------------------------------------------------------------------- /python/sdk/jit_proxy/jitter/jitter_shotgun.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typing import Any, Coroutine 4 | 5 | from solders.pubkey import Pubkey # type: ignore 6 | 7 | from driftpy.drift_client import DriftClient 8 | from driftpy.auction_subscriber.auction_subscriber import AuctionSubscriber 9 | from driftpy.types import is_variant, UserAccount, Order, PostOnlyParams 10 | from driftpy.accounts.get_accounts import get_user_stats_account 11 | 12 | from jit_proxy.jitter.base_jitter import BaseJitter 13 | from jit_proxy.jit_proxy_client import JitIxParams, JitProxyClient 14 | 15 | 16 | class JitterShotgun(BaseJitter): 17 | def __init__( 18 | self, 19 | drift_client: DriftClient, 20 | auction_subscriber: AuctionSubscriber, 21 | jit_proxy_client: JitProxyClient, 22 | verbose: bool, 23 | ): 24 | super().__init__(drift_client, auction_subscriber, jit_proxy_client, verbose) 25 | 26 | async def subscribe(self): 27 | await super().subscribe() 28 | 29 | async def create_try_fill( 30 | self, 31 | taker: UserAccount, 32 | taker_key: Pubkey, 33 | taker_stats_key: Pubkey, 34 | order: Order, 35 | order_sig: str, 36 | ) -> Coroutine[Any, Any, None]: 37 | self.logger.info("JitterShotgun: Creating Try Fill") 38 | 39 | async def try_fill(): 40 | taker_stats = await get_user_stats_account( 41 | self.drift_client.program, taker.authority 42 | ) 43 | 44 | referrer_info = self.get_referrer_info(taker_stats) 45 | 46 | for i in range(order.auction_duration): 47 | params = ( 48 | self.perp_params.get(order.market_index) 49 | if is_variant(order.market_type, "Perp") 50 | else self.spot_params.get(order.market_index) 51 | ) 52 | 53 | if params is None: 54 | self.ongoing_auctions.pop(order_sig) 55 | return 56 | 57 | self.logger.info(f"Trying to fill {order_sig} -> Attempt: {i + 1}") 58 | 59 | try: 60 | if params.max_position == 0 and params.min_position == 0: 61 | break 62 | 63 | sig = await self.jit_proxy_client.jit( 64 | JitIxParams( 65 | taker_key, 66 | taker_stats_key, 67 | taker, 68 | order.order_id, 69 | params.max_position, 70 | params.min_position, 71 | params.bid, 72 | params.ask, 73 | params.price_type, 74 | referrer_info, 75 | params.sub_account_id, 76 | PostOnlyParams.MustPostOnly(), 77 | ) 78 | ) 79 | 80 | self.logger.info(f"Filled Order: {order_sig}") 81 | self.logger.info(f"Signature: {sig}") 82 | await asyncio.sleep(10) # sleep for 10 seconds 83 | del self.ongoing_auctions[order_sig] 84 | return 85 | except Exception as e: 86 | self.logger.error(f"Failed to fill Order: {order_sig}") 87 | if "0x1770" in str(e) or "0x1771" in str(e): 88 | self.logger.error(f"Order: {order_sig} does not cross params yet, retrying") 89 | elif "0x1779" in str(e): 90 | self.logger.error(f"Order: {order_sig} could not fill, retrying") 91 | elif "0x1793" in str(e): 92 | self.logger.error("Oracle invalid, retrying") 93 | elif "0x1772" in str(e): 94 | self.logger.error(f"Order: {order_sig} already filled") 95 | # we don't want to retry if the order is filled 96 | break 97 | else: 98 | self.logger.error(f"Error: {e}") 99 | await asyncio.sleep(10) # sleep for 10 seconds 100 | del self.ongoing_auctions[order_sig] 101 | return 102 | if order_sig in self.ongoing_auctions: 103 | del self.ongoing_auctions[order_sig] 104 | 105 | return await try_fill() 106 | -------------------------------------------------------------------------------- /python/sdk/jit_proxy/jitter/jitter_sniper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from dataclasses import dataclass 4 | from typing import Any, Coroutine 5 | 6 | from solders.pubkey import Pubkey # type: ignore 7 | 8 | from driftpy.drift_client import DriftClient 9 | from driftpy.auction_subscriber.auction_subscriber import AuctionSubscriber 10 | from driftpy.slot.slot_subscriber import SlotSubscriber 11 | from driftpy.accounts.get_accounts import get_user_stats_account 12 | from driftpy.types import is_variant, OraclePriceData, Order, UserAccount, PostOnlyParams 13 | from driftpy.math.conversion import convert_to_number 14 | from driftpy.math.auction import ( 15 | get_auction_price_for_oracle_offset_auction, 16 | get_auction_price, 17 | ) 18 | from driftpy.constants.numeric_constants import PRICE_PRECISION 19 | 20 | from jit_proxy.jitter.base_jitter import BaseJitter 21 | from jit_proxy.jit_proxy_client import JitProxyClient 22 | 23 | 24 | @dataclass 25 | class AuctionAndOrderDetails: 26 | slots_until_cross: int 27 | will_cross: bool 28 | bid: int 29 | ask: int 30 | auction_start_price: int 31 | auction_end_price: int 32 | step_size: int 33 | oracle_price: OraclePriceData 34 | 35 | 36 | class JitterSniper(BaseJitter): 37 | def __init__( 38 | self, 39 | drift_client: DriftClient, 40 | slot_subscriber: SlotSubscriber, 41 | auction_subscriber: AuctionSubscriber, 42 | jit_proxy_client: JitProxyClient, 43 | verbose: bool, 44 | ): 45 | super().__init__(drift_client, auction_subscriber, jit_proxy_client, verbose) 46 | self.slot_subscriber = slot_subscriber 47 | 48 | async def subscribe(self): 49 | await super().subscribe() 50 | await self.slot_subscriber.subscribe() 51 | 52 | async def create_try_fill( 53 | self, 54 | taker: UserAccount, 55 | taker_key: Pubkey, 56 | taker_stats_key: Pubkey, 57 | order: Order, 58 | order_sig: str, 59 | ) -> Coroutine[Any, Any, None]: 60 | self.logger.info("JitterSniper: Creating Try Fill") 61 | 62 | async def try_fill(): 63 | params = ( 64 | self.perp_params.get(order.market_index) 65 | if is_variant(order.market_type, "Perp") 66 | else self.spot_params.get(order.market_index) 67 | ) 68 | 69 | if params is None: 70 | del self.ongoing_auctions[order_sig] 71 | return 72 | 73 | taker_stats = await get_user_stats_account( 74 | self.drift_client.program, taker.authority 75 | ) 76 | 77 | referrer_info = self.get_referrer_info(taker_stats) 78 | 79 | details = self.get_auction_and_order_details(order) 80 | 81 | if is_variant(order.market_type, "Perp"): 82 | current_perp_pos = self.drift_client.get_user().get_perp_position( 83 | order.market_index 84 | ) 85 | if current_perp_pos.base_asset_amount < 0 and is_variant( 86 | order.direction, "Short" 87 | ): 88 | if current_perp_pos.base_asset_amount <= params.min_position: 89 | self.logger.warning( 90 | f"Order would increase existing short (mkt \ 91 | {str(order.market_type)}-{order.market_index} \ 92 | ) too much" 93 | ) 94 | del self.ongoing_auctions[order_sig] 95 | return 96 | elif current_perp_pos.base_asset_amount > 0 and is_variant( 97 | order.direction, "Long" 98 | ): 99 | if current_perp_pos.base_asset_amount >= params.max_position: 100 | self.logger.warning( 101 | f"Order would increase existing long (mkt \ 102 | {str(order.market_type)}-{order.market_index} \ 103 | ) too much" 104 | ) 105 | del self.ongoing_auctions[order_sig] 106 | return 107 | 108 | self.logger.info( 109 | f""" 110 | Taker wants to {order.direction}, order slot is {order.slot}, 111 | My market: {details.bid}@{details.ask}, 112 | Auction: {details.auction_start_price} -> {details.auction_end_price}, step size {details.step_size} 113 | Current slot: {self.slot_subscriber.current_slot}, Order slot: {order.slot}, 114 | Will cross?: {details.will_cross} 115 | Slots to wait: {details.slots_until_cross}. Target slot = {order.slot + details.slots_until_cross} 116 | """ 117 | ) 118 | 119 | target_slot = ( 120 | order.slot + details.slots_until_cross 121 | if details.will_cross 122 | else order.slot + order.auction_duration + 1 123 | ) 124 | 125 | (slot, updated_details) = await self.wait_for_slot_or_cross_or_expiry( 126 | target_slot, order, details 127 | ) 128 | 129 | if slot == -1: 130 | self.logger.info("Auction expired without crossing") 131 | if order_sig in self.ongoing_auctions: 132 | del self.ongoing_auctions[order_sig] 133 | return 134 | 135 | params = ( 136 | self.perp_params.get(order.market_index) 137 | if is_variant(order.market_type, "Perp") 138 | else self.spot_params.get(order.market_index) 139 | ) 140 | 141 | bid = ( 142 | convert_to_number(details.oracle_price + params.bid, PRICE_PRECISION) 143 | if is_variant(params.price_type, "Oracle") 144 | else convert_to_number(params.bid, PRICE_PRECISION) 145 | ) 146 | 147 | ask = ( 148 | convert_to_number(details.oracle_price + params.ask, PRICE_PRECISION) 149 | if is_variant(params.price_type, "Oracle") 150 | else convert_to_number(params.ask, PRICE_PRECISION) 151 | ) 152 | 153 | auction_price = convert_to_number( 154 | get_auction_price(order, slot, updated_details.oracle_price.price), 155 | PRICE_PRECISION, 156 | ) 157 | 158 | self.logger.info( 159 | f""" 160 | Expected auction price: {details.auction_start_price + details.slots_until_cross * details.step_size} 161 | Actual auction price: {auction_price} 162 | ----------------- 163 | Looking for slot {order.slot + details.slots_until_cross} 164 | Got slot {slot} 165 | """ 166 | ) 167 | 168 | self.logger.info( 169 | f""" 170 | Trying to fill {order_sig} with: 171 | market: {bid}@{ask} 172 | auction price: {auction_price} 173 | submitting: {convert_to_number(params.bid, PRICE_PRECISION)}@{convert_to_number(params.ask, PRICE_PRECISION)} 174 | """ 175 | ) 176 | 177 | for _ in range(3): 178 | try: 179 | if params.max_position == 0 and params.min_position == 0: 180 | break 181 | 182 | tx_sig_and_slot = await self.jit_proxy_client.jit( 183 | { 184 | taker_key, 185 | taker_stats_key, 186 | taker, 187 | order.order_id, 188 | params.max_position, 189 | params.min_position, 190 | params.bid, 191 | params.ask, 192 | params.price_type, 193 | referrer_info, 194 | params.sub_account_id, 195 | PostOnlyParams.TryPostOnly(), 196 | } 197 | ) 198 | 199 | self.logger.info(f"Filled {order_sig}") 200 | self.logger.info(f"tx signature: {tx_sig_and_slot.tx_sig}") 201 | await asyncio.sleep(3) # Sleep for 3 seconds 202 | del self.ongoing_auctions[order_sig] 203 | return 204 | except Exception as e: 205 | self.logger.error(f"Failed to fill {order_sig}: {e}") 206 | if "0x1770" in str(e) or "0x1771" in str(e): 207 | self.logger.error("Order does not cross params yet") 208 | elif "0x1779" in str(e): 209 | self.logger.error("Order could not fill, retrying") 210 | elif "0x1793" in str(e): 211 | self.logger.error("Oracle invalid") 212 | elif "0x1772" in str(e): 213 | self.logger.error("Order already filled") 214 | # we don't want to retry if the order is filled 215 | break 216 | else: 217 | await asyncio.sleep(3) # sleep for 3 seconds 218 | del self.ongoing_auctions[order_sig] 219 | return 220 | 221 | await asyncio.sleep(0.05) # 50ms 222 | if order_sig in self.ongoing_auctions: 223 | del self.on_going_auctions[order_sig] 224 | 225 | return await try_fill() 226 | 227 | def get_auction_and_order_details(self, order: Order) -> AuctionAndOrderDetails: 228 | params = ( 229 | self.perp_params.get(order.market_index) 230 | if is_variant(order.market_type, "Perp") 231 | else self.spot_params.get(order.market_index) 232 | ) 233 | 234 | oracle_price = ( 235 | self.drift_client.get_oracle_price_data_for_perp_market(order.market_index) 236 | if is_variant(order.market_type, "Perp") 237 | else self.drift_client.get_oracle_price_data_for_spot_market( 238 | order.market_index 239 | ) 240 | ) 241 | 242 | maker_order_dir = "sell" if is_variant(order.direction, "Long") else "buy" 243 | 244 | auction_start_price = convert_to_number( 245 | get_auction_price_for_oracle_offset_auction( 246 | order, order.slot, oracle_price.price # type: ignore 247 | ) 248 | if is_variant(order.order_type, "Oracle") 249 | else order.auction_start_price, 250 | PRICE_PRECISION, 251 | ) 252 | 253 | auction_end_price = convert_to_number( 254 | get_auction_price_for_oracle_offset_auction( 255 | order, order.slot + order.auction_duration - 1, oracle_price.price # type: ignore 256 | ) 257 | if is_variant(order.order_type, "Oracle") 258 | else order.auction_end_price, 259 | PRICE_PRECISION, 260 | ) 261 | 262 | bid = ( 263 | convert_to_number(oracle_price.price + params.bid, PRICE_PRECISION) # type: ignore 264 | if is_variant(params.price_type, "Oracle") # type: ignore 265 | else convert_to_number(params.bid, PRICE_PRECISION) # type: ignore 266 | ) 267 | 268 | ask = ( 269 | convert_to_number(oracle_price.price + params.ask, PRICE_PRECISION) # type: ignore 270 | if is_variant(params.price_type, "Oracle") # type: ignore 271 | else convert_to_number(params.ask, PRICE_PRECISION) # type: ignore 272 | ) 273 | 274 | slots_until_cross = 0 275 | will_cross = False 276 | step_size = (auction_end_price - auction_start_price) // ( 277 | order.auction_duration - 1 278 | ) 279 | 280 | while slots_until_cross < order.auction_duration: 281 | if maker_order_dir == "buy": 282 | if ( 283 | convert_to_number( 284 | get_auction_price( 285 | order, order.slot + slots_until_cross, oracle_price.price # type: ignore 286 | ), 287 | PRICE_PRECISION, 288 | ) 289 | <= bid 290 | ): 291 | will_cross = True 292 | break 293 | else: 294 | if ( 295 | convert_to_number( 296 | get_auction_price( 297 | order, order.slot + slots_until_cross, oracle_price.price # type: ignore 298 | ), 299 | PRICE_PRECISION, 300 | ) 301 | >= ask 302 | ): 303 | will_cross = True 304 | break 305 | slots_until_cross += 1 306 | 307 | return AuctionAndOrderDetails( 308 | slots_until_cross, 309 | will_cross, 310 | bid, 311 | ask, 312 | auction_start_price, 313 | auction_end_price, 314 | step_size, 315 | oracle_price, # type: ignore 316 | ) 317 | 318 | async def wait_for_slot_or_cross_or_expiry( 319 | self, target_slot: int, order: Order, initial_details: AuctionAndOrderDetails 320 | ) -> (int, AuctionAndOrderDetails): # type: ignore 321 | auction_end_slot = order.auction_duration + order.slot 322 | current_details: AuctionAndOrderDetails = initial_details 323 | will_cross = initial_details.will_cross 324 | 325 | if self.slot_subscriber.current_slot > auction_end_slot: 326 | return (-1, current_details) 327 | 328 | def slot_listener(slot): 329 | if slot >= target_slot and will_cross: 330 | return (slot, current_details) 331 | 332 | self.slot_subscriber.event_emitter.on_slot_change += slot_listener 333 | 334 | while True: 335 | if self.slot_subscriber.current_slot >= auction_end_slot: 336 | self.slot_subscriber.event_emitter.on_slot_change -= slot_listener 337 | return (-1, current_details) 338 | 339 | current_details = self.get_auction_and_order_details(order) 340 | will_cross = current_details.will_cross 341 | if will_cross: 342 | target_slot = order.slot + current_details.slots_until_cross 343 | 344 | await asyncio.sleep(0.05) # 50 ms 345 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jitter-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-trait = "0.1.86" 8 | dashmap = "6" 9 | dotenv = "0.15.0" 10 | drift-rs = { git = "https://github.com/drift-labs/drift-rs", rev = "5e1b5f8b" } 11 | env_logger = "0.11" 12 | futures = "0.3" 13 | futures-util = "0.3" 14 | log = "0.4" 15 | solana-sdk = "2" 16 | thiserror = "1" 17 | tokio = { version = "1.42.0", features = ["full"] } 18 | -------------------------------------------------------------------------------- /rust/README.md: -------------------------------------------------------------------------------- 1 | # QUICK START 2 | 3 | Example rust jitter. 4 | It subscribes to orders over Ws and tries to fill with some margin from oracle prices. 5 | It is provided as an example show casing the usage of drift-rs + jit-proxy. 6 | 7 | ## Run 8 | 1. Create .env file in rust/ with your `RPC_URL` (example uses Helius) and your `PRIVATE_KEY` (as a [u8, u8, u8, u8]) 9 | 2. run `bash run.sh` to start the jitter 10 | -------------------------------------------------------------------------------- /rust/run.sh: -------------------------------------------------------------------------------- 1 | RUST_LOG=info cargo run --release -------------------------------------------------------------------------------- /rust/src/jitter.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | str::FromStr, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | Arc, 7 | }, 8 | }; 9 | 10 | use async_trait::async_trait; 11 | use dashmap::DashMap; 12 | use drift_rs::{ 13 | auction_subscriber::{AuctionSubscriber, AuctionSubscriberConfig}, 14 | jit_client::{ComputeBudgetParams, JitIxParams, JitProxyClient, JitTakerParams}, 15 | slot_subscriber::SlotSubscriber, 16 | swift_order_subscriber::SignedOrderInfo, 17 | types::{ 18 | accounts::{User, UserStats}, 19 | CommitmentConfig, MarketType, Order, OrderStatus, ReferrerInfo, RpcSendTransactionConfig, 20 | }, 21 | websocket_program_account_subscriber::ProgramAccountUpdate, 22 | DriftClient, Pubkey, Wallet, 23 | }; 24 | use futures::StreamExt; 25 | use solana_sdk::signature::Signature; 26 | use tokio::task::JoinHandle; 27 | 28 | use crate::types::{JitError, JitResult}; 29 | 30 | pub type ExcludeAuctionFn = dyn Fn(&User, &String, Order) -> bool + Send + Sync; 31 | 32 | #[inline(always)] 33 | fn log_details(order: &Order) { 34 | log::info!( 35 | "Order Details:\n\ 36 | Market Type: {:?}\n\ 37 | Market index: {}\n\ 38 | Order price: {}\n\ 39 | Order direction: {:?}\n\ 40 | Auction start price: {}\n\ 41 | Auction end price: {}\n\ 42 | Auction duration: {} slots\n\ 43 | Order base asset amount: {}\n\ 44 | Order base asset amount filled: {}", 45 | order.market_type, 46 | order.market_index, 47 | order.price, 48 | order.direction, 49 | order.auction_start_price, 50 | order.auction_end_price, 51 | order.auction_duration, 52 | order.base_asset_amount, 53 | order.base_asset_amount_filled 54 | ); 55 | } 56 | 57 | #[inline(always)] 58 | fn check_err(err: String, order_sig: &str) -> Option<()> { 59 | if err.contains("0x1770") || err.contains("0x1771") { 60 | log::error!("Order: {order_sig} does not cross params yet, retrying"); 61 | None 62 | } else if err.contains("0x1779") { 63 | log::error!("Order: {order_sig} could not fill, retrying"); 64 | None 65 | } else if err.contains("0x1793") { 66 | log::error!("Oracle invalid, retrying: {order_sig}"); 67 | None 68 | } else if err.contains("0x1772") { 69 | log::error!("Order: {order_sig} already filled"); 70 | Some(()) 71 | } else { 72 | log::error!("Order: {order_sig}, Error: {err}"); 73 | Some(()) 74 | } 75 | } 76 | 77 | pub struct Jitter { 78 | drift_client: DriftClient, 79 | perp_params: DashMap, 80 | spot_params: DashMap, 81 | ongoing_auctions: DashMap>, 82 | exclusion_criteria: AtomicBool, 83 | jitter: Arc, 84 | } 85 | 86 | #[async_trait] 87 | pub trait JitterStrategy { 88 | async fn try_fill( 89 | &self, 90 | taker: User, 91 | taker_key: Pubkey, 92 | taker_stats_key: Pubkey, 93 | order: Order, 94 | order_sig: String, 95 | referrer_info: Option, 96 | params: JitIxParams, 97 | ) -> JitResult<()>; 98 | async fn try_swift_fill( 99 | &self, 100 | signed_order_info: &SignedOrderInfo, 101 | order_sig: String, 102 | taker_params: &JitTakerParams, 103 | jit_params: &JitIxParams, 104 | ) -> JitResult<()>; 105 | } 106 | 107 | impl Jitter { 108 | pub fn new( 109 | drift_client: DriftClient, 110 | jitter: Arc, 111 | ) -> Arc { 112 | Arc::new(Jitter { 113 | drift_client, 114 | perp_params: DashMap::new(), 115 | spot_params: DashMap::new(), 116 | ongoing_auctions: DashMap::new(), 117 | exclusion_criteria: AtomicBool::new(false), 118 | jitter, 119 | }) 120 | } 121 | 122 | /// Set up a Jitter with the Shotgun strategy 123 | pub fn new_with_shotgun( 124 | drift_client: DriftClient, 125 | config: Option, 126 | cu_params: Option, 127 | ) -> Arc { 128 | let jit_proxy_client = JitProxyClient::new(drift_client.clone(), config, cu_params); 129 | let shotgun = Arc::new(Shotgun { 130 | jit_proxy_client, 131 | authority: *drift_client.wallet().authority(), 132 | }); 133 | 134 | Arc::new(Jitter { 135 | drift_client, 136 | perp_params: DashMap::new(), 137 | spot_params: DashMap::new(), 138 | ongoing_auctions: DashMap::new(), 139 | exclusion_criteria: AtomicBool::new(false), 140 | jitter: shotgun, 141 | }) 142 | } 143 | 144 | // Subscribe to auction events and start listening for them 145 | pub async fn subscribe(self: Arc, url: String) -> JitResult { 146 | // start swift order subscriber 147 | let markets = self.drift_client.get_all_perp_market_ids(); 148 | let mut swift_order_stream = self 149 | .drift_client 150 | .subscribe_swift_orders(&markets) 151 | .await 152 | .map_err(|err| { 153 | log::warn!("failed to start swift subscriber: {err:?}"); 154 | JitError::Sdk(err.to_string()) 155 | })?; 156 | 157 | let self_ref = Arc::clone(&self); 158 | tokio::spawn(async move { 159 | while let Some(signed_order_info) = swift_order_stream.next().await { 160 | if let Err(err) = self_ref.on_swift_order(signed_order_info).await { 161 | log::warn!("processing swift order failed: {err:?}"); 162 | } 163 | } 164 | }); 165 | 166 | // start jit order subscriber 167 | let auction_subscriber_config = AuctionSubscriberConfig { 168 | commitment: CommitmentConfig::processed(), 169 | resub_timeout_ms: None, 170 | url, 171 | }; 172 | let auction_subscriber = AuctionSubscriber::new(auction_subscriber_config); 173 | auction_subscriber.subscribe(move |auction| { 174 | let self_ref = Arc::clone(&self); 175 | let auction = auction.clone(); 176 | tokio::spawn({ 177 | async move { 178 | if let Err(err) = self_ref.on_auction(auction).await { 179 | log::warn!("processing auction failed: {err:?}"); 180 | } 181 | } 182 | }); 183 | }); 184 | 185 | Ok(auction_subscriber) 186 | } 187 | 188 | // Process the auction event & attempt to fill with JIT if possible 189 | pub async fn on_auction(&self, auction: ProgramAccountUpdate) -> JitResult<()> { 190 | log::info!("Auction received"); 191 | let user = auction.data_and_slot.data; 192 | let user_pubkey = &auction.pubkey; 193 | let user_stats_key = Wallet::derive_stats_account(&user.authority); 194 | 195 | for order in user.orders { 196 | if order.status != OrderStatus::Open 197 | || order.auction_duration == 0 198 | || self.exclusion_criteria.load(Ordering::Relaxed) 199 | { 200 | continue; 201 | } 202 | 203 | let order_sig = self.get_order_signatures(user_pubkey, order.order_id); 204 | 205 | if self.ongoing_auctions.contains_key(&order_sig) { 206 | continue; 207 | } 208 | 209 | match order.market_type { 210 | MarketType::Perp => { 211 | if let Some(param) = self.perp_params.get(&order.market_index) { 212 | let perp_market = self 213 | .drift_client 214 | .program_data() 215 | .perp_market_config_by_index(order.market_index) 216 | .unwrap(); 217 | let remaining = order.base_asset_amount - order.base_asset_amount_filled; 218 | let min_order_size = perp_market.amm.min_order_size; 219 | 220 | if remaining < min_order_size { 221 | log::warn!( 222 | "Order filled within min order size\nRemaining: {}\nMinimum order size: {}", 223 | remaining, 224 | min_order_size 225 | ); 226 | return Ok(()); 227 | } 228 | 229 | if (remaining as i128) < param.min_position.into() { 230 | log::warn!( 231 | "Order filled within min position\nRemaining: {}\nMin position: {}", 232 | remaining, 233 | param.min_position 234 | ); 235 | return Ok(()); 236 | } 237 | 238 | let jitter = Arc::clone(&self.jitter); 239 | let taker = Pubkey::from_str(&user_pubkey).unwrap(); 240 | let order_signature = order_sig.clone(); 241 | 242 | let taker_stats: UserStats = self 243 | .drift_client 244 | .get_user_stats(&user.authority) 245 | .await 246 | .expect("user stats"); 247 | let referrer_info = ReferrerInfo::get_referrer_info(taker_stats); 248 | 249 | let param = param.clone(); 250 | let ongoing_auction = tokio::spawn(async move { 251 | let _ = jitter 252 | .try_fill( 253 | user, 254 | taker, 255 | user_stats_key, 256 | order, 257 | order_signature, 258 | referrer_info, 259 | param, 260 | ) 261 | .await; 262 | }); 263 | 264 | self.ongoing_auctions.insert(order_sig, ongoing_auction); 265 | } else { 266 | log::warn!("Jitter not listening to {}", order.market_index); 267 | return Ok(()); 268 | } 269 | } 270 | MarketType::Spot => { 271 | if let Some(param) = self.spot_params.get(&order.market_index) { 272 | let spot_market = self 273 | .drift_client 274 | .program_data() 275 | .spot_market_config_by_index(order.market_index) 276 | .expect("spot market"); 277 | 278 | if order.base_asset_amount - order.base_asset_amount_filled 279 | < spot_market.min_order_size 280 | { 281 | log::warn!( 282 | "Order filled within min order size\nRemaining: {}\nMinimum order size: {}", 283 | order.base_asset_amount - order.base_asset_amount_filled, 284 | spot_market.min_order_size 285 | ); 286 | return Ok(()); 287 | } 288 | 289 | if (order.base_asset_amount as i128) 290 | - (order.base_asset_amount_filled as i128) 291 | < param.min_position.into() 292 | { 293 | log::warn!( 294 | "Order filled within min order size\nRemaining: {}\nMinimum order size: {}", 295 | order.base_asset_amount - order.base_asset_amount_filled, 296 | spot_market.min_order_size 297 | ); 298 | return Ok(()); 299 | } 300 | 301 | let jitter = Arc::clone(&self.jitter); 302 | let taker = Pubkey::from_str(&user_pubkey).unwrap(); 303 | let order_signature = order_sig.clone(); 304 | 305 | let taker_stats: UserStats = 306 | self.drift_client.get_user_stats(&user.authority).await?; 307 | let referrer_info = ReferrerInfo::get_referrer_info(taker_stats); 308 | 309 | let param = param.clone(); 310 | let ongoing_auction = tokio::spawn(async move { 311 | let _ = jitter 312 | .try_fill( 313 | user, 314 | taker, 315 | user_stats_key, 316 | order, 317 | order_signature, 318 | referrer_info, 319 | param, 320 | ) 321 | .await; 322 | }); 323 | 324 | self.ongoing_auctions.insert(order_sig, ongoing_auction); 325 | } else { 326 | log::warn!("Jitter not listening to {}", order.market_index); 327 | return Ok(()); 328 | } 329 | } 330 | } 331 | } 332 | 333 | Ok(()) 334 | } 335 | 336 | // Process the swift order & attempt to fill if possible 337 | pub async fn on_swift_order(&self, signed_order_info: SignedOrderInfo) -> JitResult<()> { 338 | log::info!("Signed order received"); 339 | let taker_authority = &signed_order_info.taker_authority; 340 | let taker_pubkey = 341 | Wallet::derive_user_account(taker_authority, signed_order_info.taker_subaccount_id()); 342 | let taker_stats_key = Wallet::derive_stats_account(taker_authority); 343 | 344 | let order = signed_order_info.order_params(); 345 | if order.auction_duration.is_some_and(|d| d == 0) || order.auction_duration.is_none() { 346 | return Ok(()); 347 | } 348 | 349 | let order_sig = self.get_order_signatures( 350 | &taker_pubkey.to_string(), 351 | signed_order_info.order_uuid_str(), 352 | ); 353 | 354 | // TODO: is this necessary?? 355 | if self.ongoing_auctions.contains_key(&order_sig) { 356 | return Ok(()); 357 | } 358 | 359 | let (taker, taker_stats) = tokio::try_join!( 360 | self.drift_client.get_user_account(&taker_pubkey), 361 | self.drift_client.get_user_stats(taker_authority), 362 | )?; 363 | 364 | match order.market_type { 365 | MarketType::Spot => { 366 | log::warn!("spot market swift unimplemented"); 367 | return Ok(()); 368 | } 369 | MarketType::Perp => { 370 | if let Some(param) = self.perp_params.get(&order.market_index) { 371 | let perp_market = self 372 | .drift_client 373 | .program_data() 374 | .perp_market_config_by_index(order.market_index) 375 | .unwrap(); 376 | let remaining = order.base_asset_amount; 377 | let min_order_size = perp_market.amm.min_order_size; 378 | 379 | if remaining < min_order_size { 380 | log::warn!( 381 | "Order filled within min order size\nRemaining: {}\nMinimum order size: {}", 382 | remaining, 383 | min_order_size 384 | ); 385 | return Ok(()); 386 | } 387 | 388 | if (remaining as i128) < param.min_position.into() { 389 | log::warn!( 390 | "Order filled within min position\nRemaining: {}\nMin position: {}", 391 | remaining, 392 | param.min_position 393 | ); 394 | return Ok(()); 395 | } 396 | 397 | let jitter = Arc::clone(&self.jitter); 398 | let order_signature = order_sig.clone(); 399 | let referrer_info = ReferrerInfo::get_referrer_info(taker_stats); 400 | let jit_ix_params = param.clone(); 401 | 402 | let ongoing_auction = tokio::spawn(async move { 403 | let _ = jitter 404 | .try_swift_fill( 405 | &signed_order_info, 406 | order_signature, 407 | &JitTakerParams::new( 408 | taker_pubkey, 409 | taker_stats_key, 410 | taker, 411 | referrer_info, 412 | ), 413 | &jit_ix_params, 414 | ) 415 | .await; 416 | }); 417 | 418 | self.ongoing_auctions.insert(order_sig, ongoing_auction); 419 | } else { 420 | log::warn!("Jitter not listening to {}", order.market_index); 421 | return Ok(()); 422 | } 423 | } 424 | } 425 | Ok(()) 426 | } 427 | 428 | // Helper functions 429 | pub fn update_perp_params(&self, market_index: u16, params: JitIxParams) { 430 | self.perp_params.insert(market_index, params); 431 | } 432 | 433 | pub fn update_spot_params(&self, market_index: u16, params: JitIxParams) { 434 | self.spot_params.insert(market_index, params); 435 | } 436 | 437 | pub fn set_exclusion_criteria(&self, exclusion_criteria: bool) { 438 | self.exclusion_criteria 439 | .store(exclusion_criteria, Ordering::Relaxed); 440 | } 441 | 442 | pub fn get_order_signatures(&self, taker_key: &str, order_id: T) -> String { 443 | format!("{}-{}", taker_key, order_id) 444 | } 445 | } 446 | 447 | /// Aggressive JIT making strategy 448 | pub struct Shotgun { 449 | authority: Pubkey, 450 | pub jit_proxy_client: JitProxyClient, 451 | } 452 | 453 | /// Implementing the Sniper is left as an exercise for the reader. 454 | /// Good luck! 455 | pub struct Sniper { 456 | pub jit_proxy_client: JitProxyClient, 457 | pub slot_subscriber: SlotSubscriber, 458 | } 459 | 460 | #[async_trait] 461 | impl JitterStrategy for Shotgun { 462 | async fn try_fill( 463 | &self, 464 | taker: User, 465 | taker_key: Pubkey, 466 | taker_stats_key: Pubkey, 467 | order: Order, 468 | order_sig: String, 469 | referrer_info: Option, 470 | params: JitIxParams, 471 | ) -> JitResult<()> { 472 | log::info!("Trying to fill with Shotgun:"); 473 | log_details(&order); 474 | 475 | for i in 0..order.auction_duration { 476 | let referrer_info = referrer_info.clone(); 477 | log::info!("Trying to fill: {:?} -> Attempt: {}", &order_sig, i + 1); 478 | 479 | if params.max_position == 0 || params.min_position == 0 { 480 | log::warn!( 481 | "min or max position is 0 -> min: {} max: {}", 482 | params.min_position, 483 | params.max_position 484 | ); 485 | return Ok(()); 486 | } 487 | 488 | let jit_ix_params = JitIxParams::new( 489 | params.max_position, 490 | params.min_position, 491 | params.bid, 492 | params.ask, 493 | params.price_type, 494 | None, 495 | ); 496 | 497 | let jit_result = self 498 | .jit_proxy_client 499 | .jit( 500 | order.order_id, 501 | &JitTakerParams::new(taker_key, taker_stats_key, taker, referrer_info), 502 | jit_ix_params, 503 | &self.authority, 504 | None, 505 | ) 506 | .await; 507 | 508 | match jit_result { 509 | Ok(sig) => { 510 | if sig == Signature::default() { 511 | continue; 512 | } 513 | log::info!("Order filled"); 514 | log::info!("Signature: {:?}", sig); 515 | return Ok(()); 516 | } 517 | Err(e) => { 518 | let err = e.to_string(); 519 | match check_err(err, &order_sig) { 520 | Some(_) => return Ok(()), 521 | None => { 522 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 523 | continue; 524 | } 525 | } 526 | } 527 | } 528 | } 529 | 530 | Ok(()) 531 | } 532 | async fn try_swift_fill( 533 | &self, 534 | signed_order_info: &SignedOrderInfo, 535 | order_sig: String, 536 | taker_params: &JitTakerParams, 537 | jit_ix_params: &JitIxParams, 538 | ) -> JitResult<()> { 539 | log::info!("Trying to fill with Shotgun:"); 540 | // TODO: log the order deets 541 | // log_details(&order); 542 | 543 | let auction_duration = signed_order_info 544 | .order_params() 545 | .auction_duration 546 | .expect("auction duration set"); 547 | for i in 0..auction_duration { 548 | log::info!("Trying to fill: {:?} -> Attempt: {}", &order_sig, i + 1); 549 | 550 | if jit_ix_params.max_position == 0 || jit_ix_params.min_position == 0 { 551 | log::warn!( 552 | "min or max position is 0 -> min: {} max: {}", 553 | jit_ix_params.min_position, 554 | jit_ix_params.max_position 555 | ); 556 | return Ok(()); 557 | } 558 | 559 | let jit_result = self 560 | .jit_proxy_client 561 | .try_swift_fill( 562 | signed_order_info, 563 | taker_params, 564 | jit_ix_params, 565 | &self.authority, 566 | None, 567 | ) 568 | .await; 569 | 570 | match jit_result { 571 | Ok(sig) => { 572 | if sig == Signature::default() { 573 | continue; 574 | } 575 | log::info!("Order filled"); 576 | log::info!("Signature: {:?}", sig); 577 | return Ok(()); 578 | } 579 | Err(e) => { 580 | let err = e.to_string(); 581 | match check_err(err, &order_sig) { 582 | Some(_) => return Ok(()), 583 | None => { 584 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 585 | continue; 586 | } 587 | } 588 | } 589 | } 590 | } 591 | 592 | Ok(()) 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use drift_rs::jit_client::JitProxyClient; 2 | pub mod jitter; 3 | pub mod types; 4 | -------------------------------------------------------------------------------- /rust/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Simple example JIT(er) 2 | //! 3 | //! It watches jit auctions and fastlane orders, and tries to fill them using the jit-proxy program. 4 | //! jit-proxy program will ensure fills at set prices and max/min positions (otherwise fail) 5 | //! prices are set at a fixed margin from oracle, fills are attempted every slot until an auction is complete 6 | //! 7 | //! This is provided as an example of the overall flow of JITing, a successful jit maker will require tuning 8 | //! for optimal prices and tx inclusion. 9 | //! 10 | use std::env; 11 | 12 | use drift_rs::{ 13 | event_subscriber::RpcClient, 14 | jit_client::{ComputeBudgetParams, JitIxParams, PriceType}, 15 | types::{CommitmentConfig, Context, RpcSendTransactionConfig}, 16 | utils::get_ws_url, 17 | DriftClient, Wallet, 18 | }; 19 | 20 | pub mod jitter; 21 | pub mod types; 22 | 23 | use crate::jitter::Jitter; 24 | 25 | #[tokio::main] 26 | async fn main() { 27 | env_logger::init(); 28 | let _ = dotenv::dotenv(); 29 | 30 | let rpc_url = env::var("RPC_URL").expect("RPC_URL must be set"); 31 | let private_key = env::var("PRIVATE_KEY").expect("PRIVATE_KEY must be set"); 32 | let wallet = Wallet::try_from_str(&private_key).expect("loaded wallet"); 33 | 34 | let drift_client = DriftClient::new( 35 | Context::MainNet, 36 | RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed()), 37 | wallet, 38 | ) 39 | .await 40 | .unwrap(); 41 | 42 | let config = RpcSendTransactionConfig::default(); 43 | let cu_params = ComputeBudgetParams::new(100_000, 1_400_000); 44 | let jitter = Jitter::new_with_shotgun(drift_client.clone(), Some(config), Some(cu_params)); 45 | // some fixed width from oracle 46 | // IRL adjust per market and requirements 47 | let jit_params = JitIxParams::new(0, 0, -1_000_000, 1_000_000, PriceType::Oracle, None); 48 | 49 | // try to JIT make on the first 10 perp markets 50 | for market_idx in 0..=10 { 51 | jitter.update_perp_params(market_idx, jit_params.clone()); 52 | } 53 | let fwog_perp = drift_client.market_lookup("fwog-perp").unwrap(); 54 | jitter.update_perp_params(fwog_perp.index(), jit_params.clone()); 55 | 56 | let _auction_subscriber = jitter 57 | .subscribe(get_ws_url(&rpc_url).expect("valid RPC url")) 58 | .await 59 | .unwrap(); 60 | 61 | let _ = tokio::signal::ctrl_c().await; 62 | log::info!("jitter shutting down..."); 63 | } 64 | -------------------------------------------------------------------------------- /rust/src/types.rs: -------------------------------------------------------------------------------- 1 | use drift_rs::types::SdkError; 2 | use thiserror::Error; 3 | 4 | pub type JitResult = Result; 5 | 6 | #[derive(Debug, Error)] 7 | pub enum JitError { 8 | #[error("{0}")] 9 | Drift(String), 10 | #[error("{0}")] 11 | Sdk(String), 12 | } 13 | 14 | impl From for JitError { 15 | fn from(error: drift_rs::drift_idl::errors::ErrorCode) -> Self { 16 | JitError::Drift(error.to_string()) 17 | } 18 | } 19 | 20 | impl From for JitError { 21 | fn from(error: SdkError) -> Self { 22 | JitError::Sdk(error.to_string()) 23 | } 24 | } 25 | 26 | #[derive(Clone, Copy)] 27 | pub struct ComputeBudgetParams { 28 | microlamports_per_cu: u64, 29 | cu_limit: u32, 30 | } 31 | 32 | impl ComputeBudgetParams { 33 | pub fn new(microlamports_per_cu: u64, cu_limit: u32) -> Self { 34 | Self { 35 | microlamports_per_cu, 36 | cu_limit, 37 | } 38 | } 39 | 40 | pub fn microlamports_per_cu(&self) -> u64 { 41 | self.microlamports_per_cu 42 | } 43 | 44 | pub fn cu_limit(&self) -> u32 { 45 | self.cu_limit 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ts/sdk/Readme.md: -------------------------------------------------------------------------------- 1 | # Jit Proxy SDK 2 | 3 | ## Jit Proxy Client 4 | 5 | ```JitProxyClient``` will create and send transactions to the jit proxy program. Instantiate a jit proxy client with a drift client and a program Id. The current public key for the jit proxy program is 6 | 7 | ```J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP```. Example instantiation: 8 | 9 | ``` 10 | const jitProxyClient = new JitProxyClient({ 11 | driftClient, 12 | programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'), 13 | }); 14 | ``` 15 | 16 | ## Jitter 17 | 18 | A jitter takes ```JitParams``` and uses them to determine when and how to use the ```JitProxyClient``` to send a transaction. For bots to make use of the jitter, they should create a jitter instance, and then set the ```JitParams``` according to their strategy for each market they are market making. ```JitParams``` are set on the Jitter object using ```updatePerpParams()``` and ```updateSpotParams()```. There are two kinds of Jitters that can be insantiated: ```JitterShotgun``` and ```JitterSniper```. The difference between the two is how orders are sent. 19 | 20 | For ```JitterShotgun```, orders are sent immediately when detecting a new eligible auction. The JitterShotgun will try up to 10 times to fill the order, retrying every time the it receives back an error due to the order not crossing the bid/ask in the ```JitParams``` or the oracle price being invalid. 21 | 22 | For ```JitterSniper```, orders are sent only when it detects that an order might cross the bid/ask of the ```JitParams```, waiting until the right slot, before sending up to 3 orders (retrying on errors). It will not send orders if the price of the order does not cross the bid/ask during the auction, unlike the JitterShotgun, which will immediately attempt. 23 | 24 | ## Jit Params 25 | 26 | Type definition for the JitParmas is below: 27 | 28 | ``` 29 | export type JitParams = { 30 | bid: BN; 31 | ask: BN; 32 | minPosition: BN; 33 | maxPosition: BN; 34 | priceType: PriceType; 35 | subAccountId?: number; 36 | }; 37 | ``` 38 | 39 | PriceType options are ```ORACLE``` and ```LIMIT```. Limit price type is the BN representaiton of an absolute price; i.e., price type of LIMIT and a market of bid: 10, ask: 11 means your market is 10@11. Oracle price types are offsets relative to the oralce price for a given slot. They are always added to the oracle price, so if the oracle price is 10.5, to get a market of 10@11 when price type is oracle, bid and ask are -0.5 and 0.5 respectively (Remember that bid and ask are of BN type, this example is for illustration purposes only. Remember to use BN math operations). 40 | 41 | ## Example set up 42 | 43 | Example set up for the JitterSniper (assuming parameters are already initialized/subscribed to). JitterShotgun is instantiated and initialized in a similar manner. 44 | 45 | ``` 46 | const jitProxyClient = new JitProxyClient({ 47 | driftClient, 48 | programId: new PublicKey('J1TnP8zvVxbtF5KFp5xRmWuvG9McnhzmBd9XGfCyuxFP'), 49 | }); 50 | 51 | const jitter = new JitterSniper({ 52 | auctionSubscriber, 53 | driftClient, 54 | slotSubscriber, 55 | jitProxyClient, 56 | }); 57 | await jitter.subscribe(); 58 | ``` 59 | -------------------------------------------------------------------------------- /ts/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@drift-labs/jit-proxy", 3 | "version": "0.17.46", 4 | "scripts": { 5 | "clean": "rm -rf lib", 6 | "build": "yarn clean && tsc" 7 | }, 8 | "dependencies": { 9 | "@coral-xyz/anchor": "0.29.0", 10 | "@drift-labs/sdk": "2.123.0-beta.0", 11 | "@solana/web3.js": "1.91.7", 12 | "tweetnacl-util": "^0.15.1" 13 | }, 14 | "engines": { 15 | "node": ">=18" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ts/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types/jit_proxy'; 2 | export * from './jitProxyClient'; 3 | export * from './jitter/jitterSniper'; 4 | export * from './jitter/jitterShotgun'; 5 | -------------------------------------------------------------------------------- /ts/sdk/src/jitProxyClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BN, 3 | DriftClient, 4 | getSignedMsgUserAccountPublicKey, 5 | isVariant, 6 | MakerInfo, 7 | MarketType, 8 | PostOnlyParams, 9 | QUOTE_SPOT_MARKET_INDEX, 10 | ReferrerInfo, 11 | SignedMsgOrderParams, 12 | TxParams, 13 | UserAccount, 14 | } from '@drift-labs/sdk'; 15 | import { IDL, JitProxy } from './types/jit_proxy'; 16 | import { 17 | ComputeBudgetProgram, 18 | PublicKey, 19 | TransactionInstruction, 20 | TransactionMessage, 21 | VersionedTransaction, 22 | } from '@solana/web3.js'; 23 | import { Program } from '@coral-xyz/anchor'; 24 | import { TxSigAndSlot } from '@drift-labs/sdk'; 25 | 26 | export const DEFAULT_CU_LIMIT = 1_400_000; 27 | 28 | export type JitIxParams = { 29 | takerKey: PublicKey; 30 | takerStatsKey: PublicKey; 31 | taker: UserAccount; 32 | takerOrderId: number; 33 | maxPosition: BN; 34 | minPosition: BN; 35 | bid: BN; 36 | ask: BN; 37 | postOnly: PostOnlyParams | null; 38 | priceType?: PriceType; 39 | referrerInfo?: ReferrerInfo; 40 | subAccountId?: number; 41 | }; 42 | 43 | export type JitSignedMsgIxParams = JitIxParams & { 44 | authorityToUse: PublicKey; 45 | signedMsgOrderParams: SignedMsgOrderParams; 46 | uuid: Uint8Array; 47 | marketIndex: number; 48 | }; 49 | 50 | export class PriceType { 51 | static readonly LIMIT = { limit: {} }; 52 | static readonly ORACLE = { oracle: {} }; 53 | } 54 | 55 | export type OrderConstraint = { 56 | maxPosition: BN; 57 | minPosition: BN; 58 | marketIndex: number; 59 | marketType: MarketType; 60 | }; 61 | 62 | export class JitProxyClient { 63 | private driftClient: DriftClient; 64 | private program: Program; 65 | 66 | constructor({ 67 | driftClient, 68 | programId, 69 | }: { 70 | driftClient: DriftClient; 71 | programId: PublicKey; 72 | }) { 73 | this.driftClient = driftClient; 74 | this.program = new Program(IDL, programId, driftClient.provider); 75 | } 76 | 77 | public async jit( 78 | params: JitIxParams, 79 | txParams?: TxParams 80 | ): Promise { 81 | const ix = await this.getJitIx(params); 82 | const tx = await this.driftClient.buildTransaction([ix], txParams); 83 | return await this.driftClient.sendTransaction(tx); 84 | } 85 | 86 | public async jitSignedMsg( 87 | params: JitSignedMsgIxParams, 88 | computeBudgetParams?: { 89 | computeUnits: number; 90 | computeUnitsPrice: number; 91 | } 92 | ): Promise { 93 | const ixs = [ 94 | ComputeBudgetProgram.setComputeUnitLimit({ 95 | units: computeBudgetParams?.computeUnits || DEFAULT_CU_LIMIT, 96 | }), 97 | ComputeBudgetProgram.setComputeUnitPrice({ 98 | microLamports: computeBudgetParams?.computeUnitsPrice || 0, 99 | }), 100 | ]; 101 | 102 | const signedMsgTakerIxs = 103 | await this.driftClient.getPlaceSignedMsgTakerPerpOrderIxs( 104 | params.signedMsgOrderParams, 105 | params.marketIndex, 106 | { 107 | taker: params.takerKey, 108 | takerStats: params.takerStatsKey, 109 | takerUserAccount: params.taker, 110 | signingAuthority: params.authorityToUse, 111 | }, 112 | ixs 113 | ); 114 | ixs.push(...signedMsgTakerIxs); 115 | 116 | const ix = await this.getJitSignedMsgIx(params); 117 | ixs.push(ix); 118 | 119 | const v0Message = new TransactionMessage({ 120 | instructions: ixs, 121 | payerKey: this.driftClient.wallet.publicKey, 122 | recentBlockhash: ( 123 | await this.driftClient.txHandler.getLatestBlockhashForTransaction() 124 | ).blockhash, 125 | }).compileToV0Message(await this.driftClient.fetchAllLookupTableAccounts()); 126 | const tx = new VersionedTransaction(v0Message); 127 | 128 | return await this.driftClient.txSender.sendVersionedTransaction(tx); 129 | } 130 | 131 | public async getJitIx({ 132 | takerKey, 133 | takerStatsKey, 134 | taker, 135 | takerOrderId, 136 | maxPosition, 137 | minPosition, 138 | bid, 139 | ask, 140 | postOnly = null, 141 | priceType = PriceType.LIMIT, 142 | referrerInfo, 143 | subAccountId, 144 | }: JitIxParams): Promise { 145 | subAccountId = 146 | subAccountId !== undefined 147 | ? subAccountId 148 | : this.driftClient.activeSubAccountId; 149 | const order = taker.orders.find((order) => order.orderId === takerOrderId); 150 | const remainingAccounts = this.driftClient.getRemainingAccounts({ 151 | userAccounts: [taker, this.driftClient.getUserAccount(subAccountId)], 152 | writableSpotMarketIndexes: isVariant(order.marketType, 'spot') 153 | ? [order.marketIndex, QUOTE_SPOT_MARKET_INDEX] 154 | : [], 155 | writablePerpMarketIndexes: isVariant(order.marketType, 'perp') 156 | ? [order.marketIndex] 157 | : [], 158 | }); 159 | 160 | if (referrerInfo) { 161 | remainingAccounts.push({ 162 | pubkey: referrerInfo.referrer, 163 | isWritable: true, 164 | isSigner: false, 165 | }); 166 | remainingAccounts.push({ 167 | pubkey: referrerInfo.referrerStats, 168 | isWritable: true, 169 | isSigner: false, 170 | }); 171 | } 172 | 173 | if (isVariant(order.marketType, 'spot')) { 174 | remainingAccounts.push({ 175 | pubkey: this.driftClient.getSpotMarketAccount(order.marketIndex).vault, 176 | isWritable: false, 177 | isSigner: false, 178 | }); 179 | remainingAccounts.push({ 180 | pubkey: this.driftClient.getQuoteSpotMarketAccount().vault, 181 | isWritable: false, 182 | isSigner: false, 183 | }); 184 | } 185 | 186 | const jitParams = { 187 | takerOrderId, 188 | maxPosition, 189 | minPosition, 190 | bid, 191 | ask, 192 | postOnly, 193 | priceType, 194 | }; 195 | 196 | return this.program.methods 197 | .jit(jitParams as any) 198 | .accounts({ 199 | taker: takerKey, 200 | takerStats: takerStatsKey, 201 | state: await this.driftClient.getStatePublicKey(), 202 | user: await this.driftClient.getUserAccountPublicKey(subAccountId), 203 | userStats: this.driftClient.getUserStatsAccountPublicKey(), 204 | driftProgram: this.driftClient.program.programId, 205 | }) 206 | .remainingAccounts(remainingAccounts) 207 | .instruction(); 208 | } 209 | 210 | public async getJitSignedMsgIx({ 211 | takerKey, 212 | takerStatsKey, 213 | taker, 214 | maxPosition, 215 | minPosition, 216 | bid, 217 | ask, 218 | postOnly = null, 219 | priceType = PriceType.LIMIT, 220 | referrerInfo, 221 | subAccountId, 222 | uuid, 223 | marketIndex, 224 | }: JitSignedMsgIxParams): Promise { 225 | subAccountId = 226 | subAccountId !== undefined 227 | ? subAccountId 228 | : this.driftClient.activeSubAccountId; 229 | const remainingAccounts = this.driftClient.getRemainingAccounts({ 230 | userAccounts: [taker, this.driftClient.getUserAccount(subAccountId)], 231 | writableSpotMarketIndexes: [], 232 | writablePerpMarketIndexes: [marketIndex], 233 | }); 234 | 235 | if (referrerInfo) { 236 | remainingAccounts.push({ 237 | pubkey: referrerInfo.referrer, 238 | isWritable: true, 239 | isSigner: false, 240 | }); 241 | remainingAccounts.push({ 242 | pubkey: referrerInfo.referrerStats, 243 | isWritable: true, 244 | isSigner: false, 245 | }); 246 | } 247 | 248 | const jitSignedMsgParams = { 249 | signedMsgOrderUuid: Array.from(uuid), 250 | maxPosition, 251 | minPosition, 252 | bid, 253 | ask, 254 | postOnly, 255 | priceType, 256 | }; 257 | 258 | return this.program.methods 259 | .jitSignedMsg(jitSignedMsgParams as any) 260 | .accounts({ 261 | taker: takerKey, 262 | takerStats: takerStatsKey, 263 | takerSignedMsgUserOrders: getSignedMsgUserAccountPublicKey( 264 | this.driftClient.program.programId, 265 | taker.authority 266 | ), 267 | authority: this.driftClient.wallet.payer.publicKey, 268 | state: await this.driftClient.getStatePublicKey(), 269 | user: await this.driftClient.getUserAccountPublicKey(subAccountId), 270 | userStats: this.driftClient.getUserStatsAccountPublicKey(), 271 | driftProgram: this.driftClient.program.programId, 272 | }) 273 | .remainingAccounts(remainingAccounts) 274 | .instruction(); 275 | } 276 | 277 | public async getCheckOrderConstraintIx({ 278 | subAccountId, 279 | orderConstraints, 280 | }: { 281 | subAccountId: number; 282 | orderConstraints: OrderConstraint[]; 283 | }): Promise { 284 | subAccountId = 285 | subAccountId !== undefined 286 | ? subAccountId 287 | : this.driftClient.activeSubAccountId; 288 | 289 | const readablePerpMarketIndex = []; 290 | const readableSpotMarketIndexes = []; 291 | for (const orderConstraint of orderConstraints) { 292 | if (isVariant(orderConstraint.marketType, 'perp')) { 293 | readablePerpMarketIndex.push(orderConstraint.marketIndex); 294 | } else { 295 | readableSpotMarketIndexes.push(orderConstraint.marketIndex); 296 | } 297 | } 298 | 299 | const remainingAccounts = this.driftClient.getRemainingAccounts({ 300 | userAccounts: [this.driftClient.getUserAccount(subAccountId)], 301 | readableSpotMarketIndexes, 302 | readablePerpMarketIndex, 303 | }); 304 | 305 | return this.program.methods 306 | .checkOrderConstraints(orderConstraints as any) 307 | .accounts({ 308 | user: await this.driftClient.getUserAccountPublicKey(subAccountId), 309 | }) 310 | .remainingAccounts(remainingAccounts) 311 | .instruction(); 312 | } 313 | 314 | public async arbPerp( 315 | params: { 316 | makerInfos: MakerInfo[]; 317 | marketIndex: number; 318 | }, 319 | txParams?: TxParams 320 | ): Promise { 321 | const ix = await this.getArbPerpIx(params); 322 | const tx = await this.driftClient.buildTransaction([ix], txParams); 323 | return await this.driftClient.sendTransaction(tx); 324 | } 325 | 326 | public async getArbPerpIx({ 327 | makerInfos, 328 | marketIndex, 329 | referrerInfo, 330 | }: { 331 | makerInfos: MakerInfo[]; 332 | marketIndex: number; 333 | referrerInfo?: ReferrerInfo; 334 | }): Promise { 335 | const userAccounts = [this.driftClient.getUserAccount()]; 336 | for (const makerInfo of makerInfos) { 337 | userAccounts.push(makerInfo.makerUserAccount); 338 | } 339 | 340 | const remainingAccounts = this.driftClient.getRemainingAccounts({ 341 | userAccounts, 342 | writablePerpMarketIndexes: [marketIndex], 343 | }); 344 | 345 | for (const makerInfo of makerInfos) { 346 | remainingAccounts.push({ 347 | pubkey: makerInfo.maker, 348 | isWritable: true, 349 | isSigner: false, 350 | }); 351 | remainingAccounts.push({ 352 | pubkey: makerInfo.makerStats, 353 | isWritable: true, 354 | isSigner: false, 355 | }); 356 | } 357 | 358 | if (referrerInfo) { 359 | const referrerIsMaker = 360 | makerInfos.find((maker) => 361 | maker.maker.equals(referrerInfo.referrer) 362 | ) !== undefined; 363 | if (!referrerIsMaker) { 364 | remainingAccounts.push({ 365 | pubkey: referrerInfo.referrer, 366 | isWritable: true, 367 | isSigner: false, 368 | }); 369 | remainingAccounts.push({ 370 | pubkey: referrerInfo.referrerStats, 371 | isWritable: true, 372 | isSigner: false, 373 | }); 374 | } 375 | } 376 | 377 | return this.program.methods 378 | .arbPerp(marketIndex) 379 | .accounts({ 380 | state: await this.driftClient.getStatePublicKey(), 381 | user: await this.driftClient.getUserAccountPublicKey(), 382 | userStats: this.driftClient.getUserStatsAccountPublicKey(), 383 | driftProgram: this.driftClient.program.programId, 384 | }) 385 | .remainingAccounts(remainingAccounts) 386 | .instruction(); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /ts/sdk/src/jitter/baseJitter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { JitProxyClient, PriceType } from '../jitProxyClient'; 3 | import { PublicKey } from '@solana/web3.js'; 4 | import { 5 | AuctionSubscriber, 6 | BN, 7 | BulkAccountLoader, 8 | DriftClient, 9 | getAuctionPrice, 10 | getUserAccountPublicKey, 11 | getUserStatsAccountPublicKey, 12 | hasAuctionPrice, 13 | isVariant, 14 | MarketType, 15 | Order, 16 | OrderStatus, 17 | PositionDirection, 18 | PostOnlyParams, 19 | SwiftOrderSubscriber, 20 | SlotSubscriber, 21 | SignedMsgOrderParams, 22 | UserAccount, 23 | UserStatsMap, 24 | ZERO, 25 | isSignedMsgOrder, 26 | OrderTriggerCondition, 27 | SignedMsgOrderParamsDelegateMessage, 28 | SignedMsgOrderParamsMessage, 29 | OrderParamsBitFlag, 30 | } from '@drift-labs/sdk'; 31 | import { decodeUTF8 } from 'tweetnacl-util'; 32 | 33 | export type UserFilter = ( 34 | userAccount: UserAccount, 35 | userKey: string, 36 | order: Order 37 | ) => boolean; 38 | 39 | export type JitParams = { 40 | bid: BN; 41 | ask: BN; 42 | minPosition: BN; 43 | maxPosition; 44 | priceType: PriceType; 45 | subAccountId?: number; 46 | postOnlyParams?: PostOnlyParams; 47 | }; 48 | 49 | export abstract class BaseJitter { 50 | auctionSubscriber: AuctionSubscriber; 51 | swiftOrderSubscriber: SwiftOrderSubscriber; 52 | slotSubscriber: SlotSubscriber; 53 | driftClient: DriftClient; 54 | jitProxyClient: JitProxyClient; 55 | auctionSubscriberIgnoresSwiftOrders?: boolean; 56 | userStatsMap: UserStatsMap; 57 | 58 | perpParams = new Map(); 59 | spotParams = new Map(); 60 | 61 | seenOrders = new Set(); 62 | onGoingAuctions = new Map>(); 63 | 64 | userFilter: UserFilter; 65 | 66 | computeUnits: number; 67 | computeUnitsPrice: number; 68 | 69 | constructor({ 70 | auctionSubscriber, 71 | jitProxyClient, 72 | driftClient, 73 | userStatsMap, 74 | swiftOrderSubscriber, 75 | slotSubscriber, 76 | auctionSubscriberIgnoresSwiftOrders, 77 | }: { 78 | driftClient: DriftClient; 79 | auctionSubscriber: AuctionSubscriber; 80 | jitProxyClient: JitProxyClient; 81 | userStatsMap: UserStatsMap; 82 | swiftOrderSubscriber?: SwiftOrderSubscriber; 83 | slotSubscriber?: SlotSubscriber; 84 | auctionSubscriberIgnoresSwiftOrders?: boolean; 85 | }) { 86 | this.auctionSubscriber = auctionSubscriber; 87 | this.driftClient = driftClient; 88 | this.jitProxyClient = jitProxyClient; 89 | this.userStatsMap = 90 | userStatsMap || 91 | new UserStatsMap( 92 | this.driftClient, 93 | new BulkAccountLoader(this.driftClient.connection, 'confirmed', 0) 94 | ); 95 | this.slotSubscriber = slotSubscriber; 96 | this.swiftOrderSubscriber = swiftOrderSubscriber; 97 | this.auctionSubscriberIgnoresSwiftOrders = 98 | auctionSubscriberIgnoresSwiftOrders; 99 | 100 | if (this.swiftOrderSubscriber && !this.slotSubscriber) { 101 | throw new Error( 102 | 'Slot subscriber is required for signedMsg order subscriber' 103 | ); 104 | } 105 | 106 | if (!this.swiftOrderSubscriber.userAccountGetter) { 107 | throw new Error( 108 | 'User account getter is required in swift order subscriber for jit integration' 109 | ); 110 | } 111 | } 112 | 113 | async subscribe(): Promise { 114 | await this.driftClient.subscribe(); 115 | 116 | await this.auctionSubscriber.subscribe(); 117 | this.auctionSubscriber.eventEmitter.on( 118 | 'onAccountUpdate', 119 | async (taker, takerKey, slot) => { 120 | const takerKeyString = takerKey.toBase58(); 121 | 122 | const takerStatsKey = getUserStatsAccountPublicKey( 123 | this.driftClient.program.programId, 124 | taker.authority 125 | ); 126 | for (const order of taker.orders) { 127 | if (!isVariant(order.status, 'open')) { 128 | continue; 129 | } 130 | 131 | if (!hasAuctionPrice(order, slot)) { 132 | continue; 133 | } 134 | 135 | if ( 136 | this.auctionSubscriberIgnoresSwiftOrders && 137 | isSignedMsgOrder(order) 138 | ) { 139 | continue; 140 | } 141 | 142 | if (this.userFilter) { 143 | if (this.userFilter(taker, takerKeyString, order)) { 144 | return; 145 | } 146 | } 147 | 148 | const orderSignature = this.getOrderSignatures( 149 | takerKeyString, 150 | order.orderId 151 | ); 152 | 153 | if (this.seenOrders.has(orderSignature)) { 154 | continue; 155 | } 156 | this.seenOrders.add(orderSignature); 157 | 158 | if (this.onGoingAuctions.has(orderSignature)) { 159 | continue; 160 | } 161 | 162 | if (isVariant(order.marketType, 'perp')) { 163 | if (!this.perpParams.has(order.marketIndex)) { 164 | return; 165 | } 166 | 167 | const perpMarketAccount = this.driftClient.getPerpMarketAccount( 168 | order.marketIndex 169 | ); 170 | if ( 171 | order.baseAssetAmount 172 | .sub(order.baseAssetAmountFilled) 173 | .lte(perpMarketAccount.amm.minOrderSize) 174 | ) { 175 | return; 176 | } 177 | 178 | const promise = this.createTryFill( 179 | taker, 180 | takerKey, 181 | takerStatsKey, 182 | order, 183 | orderSignature 184 | ).bind(this)(); 185 | this.onGoingAuctions.set(orderSignature, promise); 186 | } else { 187 | if (!this.spotParams.has(order.marketIndex)) { 188 | return; 189 | } 190 | 191 | const spotMarketAccount = this.driftClient.getSpotMarketAccount( 192 | order.marketIndex 193 | ); 194 | if ( 195 | order.baseAssetAmount 196 | .sub(order.baseAssetAmountFilled) 197 | .lte(spotMarketAccount.minOrderSize) 198 | ) { 199 | return; 200 | } 201 | 202 | const promise = this.createTryFill( 203 | taker, 204 | takerKey, 205 | takerStatsKey, 206 | order, 207 | orderSignature 208 | ).bind(this)(); 209 | this.onGoingAuctions.set(orderSignature, promise); 210 | } 211 | } 212 | } 213 | ); 214 | await this.slotSubscriber?.subscribe(); 215 | await this.swiftOrderSubscriber?.subscribe( 216 | async (orderMessageRaw, signedMessage, isDelegateSigner) => { 217 | const signedMsgOrderParams = signedMessage.signedMsgOrderParams; 218 | 219 | if ( 220 | !signedMsgOrderParams.auctionDuration || 221 | !signedMsgOrderParams.auctionStartPrice || 222 | !signedMsgOrderParams.auctionEndPrice 223 | ) { 224 | return; 225 | } 226 | 227 | if (signedMsgOrderParams.baseAssetAmount.eq(ZERO)) { 228 | return; 229 | } 230 | 231 | const signedMsgOrderParamsBufHex = Buffer.from( 232 | orderMessageRaw['order_message'] 233 | ); 234 | 235 | const takerAuthority = new PublicKey( 236 | orderMessageRaw['taker_authority'] 237 | ); 238 | const signingAuthority = new PublicKey( 239 | orderMessageRaw['signing_authority'] 240 | ); 241 | const takerUserPubkey = isDelegateSigner 242 | ? (signedMessage as SignedMsgOrderParamsDelegateMessage).takerPubkey 243 | : await getUserAccountPublicKey( 244 | this.driftClient.program.programId, 245 | takerAuthority, 246 | (signedMessage as SignedMsgOrderParamsMessage).subAccountId 247 | ); 248 | const takerUserPubkeyString = takerUserPubkey.toBase58(); 249 | const takerUserAccount = 250 | await this.swiftOrderSubscriber.userAccountGetter.mustGetUserAccount( 251 | takerUserPubkey.toString() 252 | ); 253 | const orderSlot = Math.min( 254 | signedMessage.slot.toNumber(), 255 | this.slotSubscriber.getSlot() 256 | ); 257 | 258 | /** 259 | * Base asset amount equalling u64::max is a special case that signals to program 260 | * to bring taker to max leverage. Program will calculate the max base asset amount to do this 261 | * once the tx lands on chain. 262 | * 263 | * You will see this is base asset amount is ffffffffffffffff 264 | */ 265 | const signedMsgOrder: Order = { 266 | status: OrderStatus.OPEN, 267 | orderType: signedMsgOrderParams.orderType, 268 | orderId: this.convertUuidToNumber(orderMessageRaw['uuid']), 269 | slot: new BN(orderSlot), 270 | marketIndex: signedMsgOrderParams.marketIndex, 271 | marketType: MarketType.PERP, 272 | baseAssetAmount: signedMsgOrderParams.baseAssetAmount, 273 | auctionDuration: signedMsgOrderParams.auctionDuration, 274 | auctionStartPrice: signedMsgOrderParams.auctionStartPrice, 275 | auctionEndPrice: signedMsgOrderParams.auctionEndPrice, 276 | immediateOrCancel: 277 | (signedMsgOrderParams.bitFlags & 278 | OrderParamsBitFlag.ImmediateOrCancel) !== 279 | 0, 280 | bitFlags: signedMsgOrderParams.bitFlags, 281 | direction: signedMsgOrderParams.direction, 282 | postOnly: false, 283 | oraclePriceOffset: signedMsgOrderParams.oraclePriceOffset ?? 0, 284 | maxTs: signedMsgOrderParams.maxTs ?? ZERO, 285 | reduceOnly: signedMsgOrderParams.reduceOnly ?? false, 286 | triggerCondition: 287 | signedMsgOrderParams.triggerCondition ?? 288 | OrderTriggerCondition.ABOVE, 289 | price: signedMsgOrderParams.price ?? ZERO, 290 | userOrderId: signedMsgOrderParams.userOrderId ?? 0, 291 | // Rest are not necessary and set for type conforming 292 | existingPositionDirection: PositionDirection.LONG, 293 | triggerPrice: ZERO, 294 | baseAssetAmountFilled: ZERO, 295 | quoteAssetAmountFilled: ZERO, 296 | quoteAssetAmount: ZERO, 297 | postedSlotTail: 0, 298 | }; 299 | 300 | if (this.userFilter) { 301 | if ( 302 | this.userFilter( 303 | takerUserAccount, 304 | takerUserPubkeyString, 305 | signedMsgOrder 306 | ) 307 | ) { 308 | return; 309 | } 310 | } 311 | 312 | const orderSignature = this.getOrderSignatures( 313 | takerUserPubkeyString, 314 | signedMsgOrder.orderId 315 | ); 316 | 317 | if (this.seenOrders.has(orderSignature)) { 318 | return; 319 | } 320 | this.seenOrders.add(orderSignature); 321 | 322 | if (this.onGoingAuctions.has(orderSignature)) { 323 | return; 324 | } 325 | 326 | if (!this.perpParams.has(signedMsgOrder.marketIndex)) { 327 | return; 328 | } 329 | 330 | const perpMarketAccount = this.driftClient.getPerpMarketAccount( 331 | signedMsgOrder.marketIndex 332 | ); 333 | if ( 334 | signedMsgOrder.baseAssetAmount.lt(perpMarketAccount.amm.minOrderSize) 335 | ) { 336 | return; 337 | } 338 | 339 | const promise = this.createTrySignedMsgFill( 340 | signingAuthority, 341 | { 342 | orderParams: signedMsgOrderParamsBufHex, 343 | signature: Buffer.from( 344 | orderMessageRaw['order_signature'], 345 | 'base64' 346 | ), 347 | }, 348 | decodeUTF8(orderMessageRaw['uuid']), 349 | takerUserAccount, 350 | takerUserPubkey, 351 | getUserStatsAccountPublicKey( 352 | this.driftClient.program.programId, 353 | takerUserAccount.authority 354 | ), 355 | signedMsgOrder, 356 | orderSignature, 357 | orderMessageRaw['market_index'] 358 | ).bind(this)(); 359 | this.onGoingAuctions.set(orderSignature, promise); 360 | } 361 | ); 362 | } 363 | 364 | createTryFill( 365 | taker: UserAccount, 366 | takerKey: PublicKey, 367 | takerStatsKey: PublicKey, 368 | order: Order, 369 | orderSignature: string 370 | ): () => Promise { 371 | throw new Error('Not implemented'); 372 | } 373 | 374 | createTrySignedMsgFill( 375 | authorityToUse: PublicKey, 376 | signedMsgOrderParams: SignedMsgOrderParams, 377 | uuid: Uint8Array, 378 | taker: UserAccount, 379 | takerKey: PublicKey, 380 | takerStatsKey: PublicKey, 381 | order: Order, 382 | orderSignature: string, 383 | marketIndex: number 384 | ): () => Promise { 385 | throw new Error('Not implemented'); 386 | } 387 | 388 | deleteOnGoingAuction(orderSignature: string): void { 389 | this.onGoingAuctions.delete(orderSignature); 390 | this.seenOrders.delete(orderSignature); 391 | } 392 | 393 | getOrderSignatures(takerKey: string, orderId: number): string { 394 | return `${takerKey}-${orderId}`; 395 | } 396 | 397 | private convertUuidToNumber(uuid: string): number { 398 | return uuid 399 | .split('') 400 | .reduce( 401 | (n, c) => 402 | n * 64 + 403 | '_~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf( 404 | c 405 | ), 406 | 0 407 | ); 408 | } 409 | 410 | public updatePerpParams(marketIndex: number, params: JitParams): void { 411 | this.perpParams.set(marketIndex, params); 412 | } 413 | 414 | public updateSpotParams(marketIndex: number, params: JitParams): void { 415 | this.spotParams.set(marketIndex, params); 416 | } 417 | 418 | public setUserFilter(userFilter: UserFilter | undefined): void { 419 | this.userFilter = userFilter; 420 | } 421 | 422 | public setComputeUnits(computeUnits: number): void { 423 | this.computeUnits = computeUnits; 424 | } 425 | 426 | public setComputeUnitsPrice(computeUnitsPrice: number): void { 427 | this.computeUnitsPrice = computeUnitsPrice; 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /ts/sdk/src/jitter/jitterShotgun.ts: -------------------------------------------------------------------------------- 1 | import { JitProxyClient } from '../jitProxyClient'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { 4 | AuctionSubscriber, 5 | DriftClient, 6 | Order, 7 | PostOnlyParams, 8 | SlotSubscriber, 9 | SignedMsgOrderParams, 10 | SwiftOrderSubscriber, 11 | UserAccount, 12 | UserStatsMap, 13 | } from '@drift-labs/sdk'; 14 | import { BaseJitter } from './baseJitter'; 15 | 16 | export class JitterShotgun extends BaseJitter { 17 | constructor({ 18 | auctionSubscriber, 19 | jitProxyClient, 20 | driftClient, 21 | userStatsMap, 22 | swiftOrderSubscriber, 23 | slotSubscriber, 24 | auctionSubscriberIgnoresSwiftOrders, 25 | }: { 26 | driftClient: DriftClient; 27 | auctionSubscriber: AuctionSubscriber; 28 | jitProxyClient: JitProxyClient; 29 | userStatsMap?: UserStatsMap; 30 | swiftOrderSubscriber?: SwiftOrderSubscriber; 31 | slotSubscriber?: SlotSubscriber; 32 | auctionSubscriberIgnoresSwiftOrders?: boolean; 33 | }) { 34 | super({ 35 | auctionSubscriber, 36 | jitProxyClient, 37 | driftClient, 38 | userStatsMap, 39 | swiftOrderSubscriber, 40 | slotSubscriber, 41 | auctionSubscriberIgnoresSwiftOrders, 42 | }); 43 | } 44 | 45 | createTryFill( 46 | taker: UserAccount, 47 | takerKey: PublicKey, 48 | takerStatsKey: PublicKey, 49 | order: Order, 50 | orderSignature: string 51 | ): () => Promise { 52 | return async () => { 53 | let i = 0; 54 | 55 | const takerStats = await this.userStatsMap.mustGet( 56 | taker.authority.toString() 57 | ); 58 | const referrerInfo = takerStats.getReferrerInfo(); 59 | 60 | // assumes each preflight simulation takes ~1 slot 61 | while (i < order.auctionDuration) { 62 | const params = this.perpParams.get(order.marketIndex); 63 | if (!params) { 64 | this.deleteOnGoingAuction(orderSignature); 65 | return; 66 | } 67 | 68 | const txParams = { 69 | computeUnits: this.computeUnits, 70 | computeUnitsPrice: this.computeUnitsPrice, 71 | }; 72 | 73 | console.log(`Trying to fill ${orderSignature}`); 74 | try { 75 | const { txSig } = await this.jitProxyClient.jit( 76 | { 77 | takerKey, 78 | takerStatsKey, 79 | taker, 80 | takerOrderId: order.orderId, 81 | maxPosition: params.maxPosition, 82 | minPosition: params.minPosition, 83 | bid: params.bid, 84 | ask: params.ask, 85 | postOnly: params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, 86 | priceType: params.priceType, 87 | referrerInfo, 88 | subAccountId: params.subAccountId, 89 | }, 90 | txParams 91 | ); 92 | 93 | console.log( 94 | `Successfully sent tx for ${orderSignature} txSig ${txSig}` 95 | ); 96 | await sleep(10000); 97 | this.deleteOnGoingAuction(orderSignature); 98 | return; 99 | } catch (e) { 100 | console.error(`Failed to fill ${orderSignature}`); 101 | if (e.message.includes('0x1770') || e.message.includes('0x1771')) { 102 | console.log('Order does not cross params yet, retrying'); 103 | } else if (e.message.includes('0x1779')) { 104 | console.log('Order could not fill'); 105 | } else if (e.message.includes('0x1793')) { 106 | console.log('Oracle invalid, retrying'); 107 | } else { 108 | await sleep(10000); 109 | this.deleteOnGoingAuction(orderSignature); 110 | return; 111 | } 112 | } 113 | i++; 114 | } 115 | 116 | this.deleteOnGoingAuction(orderSignature); 117 | }; 118 | } 119 | 120 | createTrySignedMsgFill( 121 | authorityToUse: PublicKey, 122 | signedMsgOrderParams: SignedMsgOrderParams, 123 | uuid: Uint8Array, 124 | taker: UserAccount, 125 | takerKey: PublicKey, 126 | takerStatsKey: PublicKey, 127 | order: Order, 128 | orderSignature: string, 129 | marketIndex: number 130 | ): () => Promise { 131 | return async () => { 132 | let i = 0; 133 | 134 | const takerStats = await this.userStatsMap.mustGet( 135 | taker.authority.toString() 136 | ); 137 | const referrerInfo = takerStats.getReferrerInfo(); 138 | 139 | // assumes each preflight simulation takes ~1 slot 140 | while (i < order.auctionDuration) { 141 | const params = this.perpParams.get(order.marketIndex); 142 | if (!params) { 143 | this.deleteOnGoingAuction(orderSignature); 144 | return; 145 | } 146 | 147 | const txParams = { 148 | computeUnits: this.computeUnits, 149 | computeUnitsPrice: this.computeUnitsPrice, 150 | }; 151 | 152 | console.log(`Trying to fill ${orderSignature}`); 153 | try { 154 | const { txSig } = await this.jitProxyClient.jitSignedMsg( 155 | { 156 | takerKey, 157 | takerStatsKey, 158 | taker, 159 | takerOrderId: order.orderId, 160 | maxPosition: params.maxPosition, 161 | minPosition: params.minPosition, 162 | bid: params.bid, 163 | ask: params.ask, 164 | postOnly: params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, 165 | priceType: params.priceType, 166 | referrerInfo, 167 | subAccountId: params.subAccountId, 168 | authorityToUse, 169 | signedMsgOrderParams, 170 | uuid, 171 | marketIndex, 172 | }, 173 | txParams 174 | ); 175 | 176 | console.log( 177 | `Successfully sent tx for ${orderSignature} txSig ${txSig}` 178 | ); 179 | await sleep(10000); 180 | this.deleteOnGoingAuction(orderSignature); 181 | return; 182 | } catch (e) { 183 | console.error(`Failed to fill ${orderSignature}`); 184 | console.log(e); 185 | if (e.message.includes('0x1770') || e.message.includes('0x1771')) { 186 | console.log('Order does not cross params yet, retrying'); 187 | } else if (e.message.includes('0x1779')) { 188 | console.log('Order could not fill'); 189 | } else if (e.message.includes('0x1793')) { 190 | console.log('Oracle invalid, retrying'); 191 | } else { 192 | await sleep(10000); 193 | this.deleteOnGoingAuction(orderSignature); 194 | return; 195 | } 196 | } 197 | i++; 198 | } 199 | 200 | this.deleteOnGoingAuction(orderSignature); 201 | }; 202 | } 203 | } 204 | 205 | function sleep(ms: number): Promise { 206 | return new Promise((resolve) => setTimeout(resolve, ms)); 207 | } 208 | -------------------------------------------------------------------------------- /ts/sdk/src/jitter/jitterSniper.ts: -------------------------------------------------------------------------------- 1 | import { JitProxyClient } from '../jitProxyClient'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { 4 | AuctionSubscriber, 5 | convertToNumber, 6 | DriftClient, 7 | getAuctionPrice, 8 | getAuctionPriceForOracleOffsetAuction, 9 | getLimitPrice, 10 | getVariant, 11 | isVariant, 12 | OraclePriceData, 13 | Order, 14 | PostOnlyParams, 15 | PRICE_PRECISION, 16 | SignedMsgOrderParams, 17 | SwiftOrderSubscriber, 18 | SlotSubscriber, 19 | UserAccount, 20 | UserStatsMap, 21 | ZERO, 22 | } from '@drift-labs/sdk'; 23 | import { BaseJitter } from './baseJitter'; 24 | 25 | type AuctionAndOrderDetails = { 26 | slotsTilCross: number; 27 | willCross: boolean; 28 | bid: number; 29 | ask: number; 30 | auctionStartPrice: number; 31 | auctionEndPrice: number; 32 | stepSize: number; 33 | oraclePrice: OraclePriceData; 34 | }; 35 | 36 | export class JitterSniper extends BaseJitter { 37 | slotSubscriber: SlotSubscriber; 38 | userStatsMap: UserStatsMap; 39 | 40 | constructor({ 41 | auctionSubscriber, 42 | slotSubscriber, 43 | jitProxyClient, 44 | driftClient, 45 | userStatsMap, 46 | swiftOrderSubscriber, 47 | auctionSubscriberIgnoresSwiftOrders, 48 | }: { 49 | driftClient: DriftClient; 50 | slotSubscriber: SlotSubscriber; 51 | auctionSubscriber: AuctionSubscriber; 52 | jitProxyClient: JitProxyClient; 53 | userStatsMap?: UserStatsMap; 54 | swiftOrderSubscriber?: SwiftOrderSubscriber; 55 | auctionSubscriberIgnoresSwiftOrders?: boolean; 56 | }) { 57 | super({ 58 | auctionSubscriber, 59 | jitProxyClient, 60 | driftClient, 61 | userStatsMap, 62 | swiftOrderSubscriber, 63 | slotSubscriber, 64 | auctionSubscriberIgnoresSwiftOrders, 65 | }); 66 | this.slotSubscriber = slotSubscriber; 67 | } 68 | 69 | createTryFill( 70 | taker: UserAccount, 71 | takerKey: PublicKey, 72 | takerStatsKey: PublicKey, 73 | order: Order, 74 | orderSignature: string 75 | ): () => Promise { 76 | return async () => { 77 | const params = this.perpParams.get(order.marketIndex); 78 | if (!params) { 79 | this.deleteOnGoingAuction(orderSignature); 80 | return; 81 | } 82 | 83 | const takerStats = await this.userStatsMap.mustGet( 84 | taker.authority.toString() 85 | ); 86 | const referrerInfo = takerStats.getReferrerInfo(); 87 | 88 | const { 89 | slotsTilCross, 90 | willCross, 91 | bid, 92 | ask, 93 | auctionStartPrice, 94 | auctionEndPrice, 95 | stepSize, 96 | oraclePrice, 97 | } = this.getAuctionAndOrderDetails(order); 98 | 99 | // don't increase risk if we're past max positions 100 | if (isVariant(order.marketType, 'perp')) { 101 | const currPerpPos = 102 | this.driftClient.getUser().getPerpPosition(order.marketIndex) || 103 | this.driftClient.getUser().getEmptyPosition(order.marketIndex); 104 | if ( 105 | currPerpPos.baseAssetAmount.lt(ZERO) && 106 | isVariant(order.direction, 'short') 107 | ) { 108 | if (currPerpPos.baseAssetAmount.lte(params.minPosition)) { 109 | console.log( 110 | `Order would increase existing short (mkt ${getVariant( 111 | order.marketType 112 | )}-${order.marketIndex}) too much` 113 | ); 114 | this.deleteOnGoingAuction(orderSignature); 115 | return; 116 | } 117 | } else if ( 118 | currPerpPos.baseAssetAmount.gt(ZERO) && 119 | isVariant(order.direction, 'long') 120 | ) { 121 | if (currPerpPos.baseAssetAmount.gte(params.maxPosition)) { 122 | console.log( 123 | `Order would increase existing long (mkt ${getVariant( 124 | order.marketType 125 | )}-${order.marketIndex}) too much` 126 | ); 127 | this.deleteOnGoingAuction(orderSignature); 128 | return; 129 | } 130 | } 131 | } 132 | 133 | console.log(` 134 | Taker wants to ${JSON.stringify( 135 | order.direction 136 | )}, order slot is ${order.slot.toNumber()}, 137 | My market: ${bid}@${ask}, 138 | Auction: ${auctionStartPrice} -> ${auctionEndPrice}, step size ${stepSize} 139 | Current slot: ${ 140 | this.slotSubscriber.currentSlot 141 | }, Order slot: ${order.slot.toNumber()}, 142 | Will cross?: ${willCross} 143 | Slots to wait: ${slotsTilCross}. Target slot = ${ 144 | order.slot.toNumber() + slotsTilCross 145 | } 146 | `); 147 | 148 | this.waitForSlotOrCrossOrExpiry( 149 | willCross 150 | ? order.slot.toNumber() + slotsTilCross 151 | : order.slot.toNumber() + order.auctionDuration + 1, 152 | order, 153 | { 154 | slotsTilCross, 155 | willCross, 156 | bid, 157 | ask, 158 | auctionStartPrice, 159 | auctionEndPrice, 160 | stepSize, 161 | oraclePrice, 162 | } 163 | ).then(async ({ slot, updatedDetails }) => { 164 | if (slot === -1) { 165 | console.log('Auction expired without crossing'); 166 | this.deleteOnGoingAuction(orderSignature); 167 | return; 168 | } 169 | 170 | const params = isVariant(order.marketType, 'perp') 171 | ? this.perpParams.get(order.marketIndex) 172 | : this.spotParams.get(order.marketIndex); 173 | const bid = isVariant(params.priceType, 'oracle') 174 | ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION) 175 | : convertToNumber(params.bid, PRICE_PRECISION); 176 | const ask = isVariant(params.priceType, 'oracle') 177 | ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION) 178 | : convertToNumber(params.ask, PRICE_PRECISION); 179 | const auctionPrice = convertToNumber( 180 | getAuctionPrice(order, slot, updatedDetails.oraclePrice.price), 181 | PRICE_PRECISION 182 | ); 183 | console.log(` 184 | Expected auction price: ${auctionStartPrice + slotsTilCross * stepSize} 185 | Actual auction price: ${auctionPrice} 186 | ----------------- 187 | Looking for slot ${order.slot.toNumber() + slotsTilCross} 188 | Got slot ${slot} 189 | `); 190 | 191 | console.log(`Trying to fill ${orderSignature} with: 192 | market: ${bid}@${ask} 193 | auction price: ${auctionPrice} 194 | submitting" ${convertToNumber(params.bid, PRICE_PRECISION)}@${convertToNumber( 195 | params.ask, 196 | PRICE_PRECISION 197 | )} 198 | `); 199 | let i = 0; 200 | while (i < 10) { 201 | try { 202 | const txParams = { 203 | computeUnits: this.computeUnits, 204 | computeUnitsPrice: this.computeUnitsPrice, 205 | }; 206 | const { txSig } = await this.jitProxyClient.jit( 207 | { 208 | takerKey, 209 | takerStatsKey, 210 | taker, 211 | takerOrderId: order.orderId, 212 | maxPosition: params.maxPosition, 213 | minPosition: params.minPosition, 214 | bid: params.bid, 215 | ask: params.ask, 216 | postOnly: 217 | params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, 218 | priceType: params.priceType, 219 | referrerInfo, 220 | subAccountId: params.subAccountId, 221 | }, 222 | txParams 223 | ); 224 | 225 | console.log(`Filled ${orderSignature} txSig ${txSig}`); 226 | await sleep(3000); 227 | this.deleteOnGoingAuction(orderSignature); 228 | return; 229 | } catch (e) { 230 | console.error(`Failed to fill ${orderSignature}`); 231 | if (e.message.includes('0x1770') || e.message.includes('0x1771')) { 232 | console.log('Order does not cross params yet'); 233 | } else if (e.message.includes('0x1779')) { 234 | console.log('Order could not fill'); 235 | } else if (e.message.includes('0x1793')) { 236 | console.log('Oracle invalid'); 237 | } else { 238 | await sleep(3000); 239 | this.deleteOnGoingAuction(orderSignature); 240 | return; 241 | } 242 | } 243 | await sleep(200); 244 | i++; 245 | } 246 | }); 247 | this.deleteOnGoingAuction(orderSignature); 248 | }; 249 | } 250 | 251 | createTrySignedMsgFill( 252 | authorityToUse: PublicKey, 253 | signedMsgOrderParams: SignedMsgOrderParams, 254 | uuid: Uint8Array, 255 | taker: UserAccount, 256 | takerKey: PublicKey, 257 | takerStatsKey: PublicKey, 258 | order: Order, 259 | orderSignature: string, 260 | marketIndex: number 261 | ): () => Promise { 262 | return async () => { 263 | const params = this.perpParams.get(order.marketIndex); 264 | if (!params) { 265 | this.deleteOnGoingAuction(orderSignature); 266 | return; 267 | } 268 | 269 | const takerStats = await this.userStatsMap.mustGet( 270 | taker.authority.toString() 271 | ); 272 | const referrerInfo = takerStats.getReferrerInfo(); 273 | 274 | const { 275 | slotsTilCross, 276 | willCross, 277 | bid, 278 | ask, 279 | auctionStartPrice, 280 | auctionEndPrice, 281 | stepSize, 282 | oraclePrice, 283 | } = this.getAuctionAndOrderDetails(order); 284 | 285 | // don't increase risk if we're past max positions 286 | if (isVariant(order.marketType, 'perp')) { 287 | const currPerpPos = 288 | this.driftClient.getUser().getPerpPosition(order.marketIndex) || 289 | this.driftClient.getUser().getEmptyPosition(order.marketIndex); 290 | if ( 291 | currPerpPos.baseAssetAmount.lt(ZERO) && 292 | isVariant(order.direction, 'short') 293 | ) { 294 | if (currPerpPos.baseAssetAmount.lte(params.minPosition)) { 295 | console.log( 296 | `Order would increase existing short (mkt ${getVariant( 297 | order.marketType 298 | )}-${order.marketIndex}) too much` 299 | ); 300 | this.deleteOnGoingAuction(orderSignature); 301 | return; 302 | } 303 | } else if ( 304 | currPerpPos.baseAssetAmount.gt(ZERO) && 305 | isVariant(order.direction, 'long') 306 | ) { 307 | if (currPerpPos.baseAssetAmount.gte(params.maxPosition)) { 308 | console.log( 309 | `Order would increase existing long (mkt ${getVariant( 310 | order.marketType 311 | )}-${order.marketIndex}) too much` 312 | ); 313 | this.deleteOnGoingAuction(orderSignature); 314 | return; 315 | } 316 | } 317 | } 318 | 319 | console.log(` 320 | Taker wants to ${JSON.stringify( 321 | order.direction 322 | )}, order slot is ${order.slot.toNumber()}, 323 | My market: ${bid}@${ask}, 324 | Auction: ${auctionStartPrice} -> ${auctionEndPrice}, step size ${stepSize} 325 | Current slot: ${ 326 | this.slotSubscriber.currentSlot 327 | }, Order slot: ${order.slot.toNumber()}, 328 | Will cross?: ${willCross} 329 | Slots to wait: ${slotsTilCross}. Target slot = ${ 330 | order.slot.toNumber() + slotsTilCross 331 | } 332 | `); 333 | 334 | this.waitForSlotOrCrossOrExpiry( 335 | willCross 336 | ? order.slot.toNumber() + slotsTilCross 337 | : order.slot.toNumber() + order.auctionDuration + 1, 338 | order, 339 | { 340 | slotsTilCross, 341 | willCross, 342 | bid, 343 | ask, 344 | auctionStartPrice, 345 | auctionEndPrice, 346 | stepSize, 347 | oraclePrice, 348 | } 349 | ).then(async ({ slot, updatedDetails }) => { 350 | if (slot === -1) { 351 | console.log('Auction expired without crossing'); 352 | this.deleteOnGoingAuction(orderSignature); 353 | return; 354 | } 355 | 356 | const params = isVariant(order.marketType, 'perp') 357 | ? this.perpParams.get(order.marketIndex) 358 | : this.spotParams.get(order.marketIndex); 359 | const bid = isVariant(params.priceType, 'oracle') 360 | ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION) 361 | : convertToNumber(params.bid, PRICE_PRECISION); 362 | const ask = isVariant(params.priceType, 'oracle') 363 | ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION) 364 | : convertToNumber(params.ask, PRICE_PRECISION); 365 | const auctionPrice = convertToNumber( 366 | getAuctionPrice(order, slot, updatedDetails.oraclePrice.price), 367 | PRICE_PRECISION 368 | ); 369 | console.log(` 370 | Expected auction price: ${auctionStartPrice + slotsTilCross * stepSize} 371 | Actual auction price: ${auctionPrice} 372 | ----------------- 373 | Looking for slot ${order.slot.toNumber() + slotsTilCross} 374 | Got slot ${slot} 375 | `); 376 | 377 | console.log(`Trying to fill ${orderSignature} with: 378 | market: ${bid}@${ask} 379 | auction price: ${auctionPrice} 380 | submitting" ${convertToNumber(params.bid, PRICE_PRECISION)}@${convertToNumber( 381 | params.ask, 382 | PRICE_PRECISION 383 | )} 384 | `); 385 | let i = 0; 386 | while (i < 10) { 387 | try { 388 | const txParams = { 389 | computeUnits: this.computeUnits, 390 | computeUnitsPrice: this.computeUnitsPrice, 391 | }; 392 | const { txSig } = await this.jitProxyClient.jitSignedMsg( 393 | { 394 | takerKey, 395 | takerStatsKey, 396 | taker, 397 | takerOrderId: order.orderId, 398 | maxPosition: params.maxPosition, 399 | minPosition: params.minPosition, 400 | bid: params.bid, 401 | ask: params.ask, 402 | postOnly: 403 | params.postOnlyParams ?? PostOnlyParams.MUST_POST_ONLY, 404 | priceType: params.priceType, 405 | referrerInfo, 406 | subAccountId: params.subAccountId, 407 | authorityToUse, 408 | signedMsgOrderParams, 409 | uuid, 410 | marketIndex, 411 | }, 412 | txParams 413 | ); 414 | 415 | console.log(`Filled ${orderSignature} txSig ${txSig}`); 416 | await sleep(3000); 417 | this.deleteOnGoingAuction(orderSignature); 418 | return; 419 | } catch (e) { 420 | console.error(`Failed to fill ${orderSignature}`); 421 | if (e.message.includes('0x1770') || e.message.includes('0x1771')) { 422 | console.log('Order does not cross params yet'); 423 | } else if (e.message.includes('0x1779')) { 424 | console.log('Order could not fill'); 425 | } else if (e.message.includes('0x1793')) { 426 | console.log('Oracle invalid'); 427 | } else { 428 | await sleep(3000); 429 | this.deleteOnGoingAuction(orderSignature); 430 | return; 431 | } 432 | } 433 | await sleep(200); 434 | i++; 435 | } 436 | }); 437 | this.deleteOnGoingAuction(orderSignature); 438 | }; 439 | } 440 | 441 | getAuctionAndOrderDetails(order: Order): AuctionAndOrderDetails { 442 | // Find number of slots until the order is expected to be in cross 443 | const params = isVariant(order.marketType, 'perp') 444 | ? this.perpParams.get(order.marketIndex) 445 | : this.spotParams.get(order.marketIndex); 446 | const oraclePrice = isVariant(order.marketType, 'perp') 447 | ? this.driftClient.getOracleDataForPerpMarket(order.marketIndex) 448 | : this.driftClient.getOracleDataForSpotMarket(order.marketIndex); 449 | 450 | const makerOrderDir = isVariant(order.direction, 'long') ? 'sell' : 'buy'; 451 | const auctionStartPrice = convertToNumber( 452 | isVariant(order.orderType, 'oracle') 453 | ? getAuctionPriceForOracleOffsetAuction( 454 | order, 455 | order.slot.toNumber(), 456 | oraclePrice.price 457 | ) 458 | : order.auctionStartPrice, 459 | PRICE_PRECISION 460 | ); 461 | const auctionEndPrice = convertToNumber( 462 | isVariant(order.orderType, 'oracle') 463 | ? getAuctionPriceForOracleOffsetAuction( 464 | order, 465 | order.slot.toNumber() + order.auctionDuration - 1, 466 | oraclePrice.price 467 | ) 468 | : order.auctionEndPrice, 469 | PRICE_PRECISION 470 | ); 471 | 472 | const bid = isVariant(params.priceType, 'oracle') 473 | ? convertToNumber(oraclePrice.price.add(params.bid), PRICE_PRECISION) 474 | : convertToNumber(params.bid, PRICE_PRECISION); 475 | const ask = isVariant(params.priceType, 'oracle') 476 | ? convertToNumber(oraclePrice.price.add(params.ask), PRICE_PRECISION) 477 | : convertToNumber(params.ask, PRICE_PRECISION); 478 | 479 | let slotsTilCross = 0; 480 | let willCross = false; 481 | const stepSize = 482 | (auctionEndPrice - auctionStartPrice) / (order.auctionDuration - 1); 483 | while (slotsTilCross < order.auctionDuration) { 484 | if (makerOrderDir === 'buy') { 485 | if ( 486 | convertToNumber( 487 | getAuctionPrice( 488 | order, 489 | order.slot.toNumber() + slotsTilCross, 490 | oraclePrice.price 491 | ), 492 | PRICE_PRECISION 493 | ) <= bid 494 | ) { 495 | willCross = true; 496 | break; 497 | } 498 | } else { 499 | if ( 500 | convertToNumber( 501 | getAuctionPrice( 502 | order, 503 | order.slot.toNumber() + slotsTilCross, 504 | oraclePrice.price 505 | ), 506 | PRICE_PRECISION 507 | ) >= ask 508 | ) { 509 | willCross = true; 510 | break; 511 | } 512 | } 513 | slotsTilCross++; 514 | } 515 | 516 | // if it doesnt cross during auction, check if limit price crosses 517 | if (!willCross) { 518 | const slotAfterAuction = 519 | order.slot.toNumber() + order.auctionDuration + 1; 520 | const limitPrice = getLimitPrice(order, oraclePrice, slotAfterAuction); 521 | if (!limitPrice) { 522 | willCross = true; 523 | slotsTilCross = order.auctionDuration + 1; 524 | } else { 525 | const limitPriceNum = convertToNumber(limitPrice, PRICE_PRECISION); 526 | if (makerOrderDir === 'buy' || limitPriceNum <= bid) { 527 | willCross = true; 528 | slotsTilCross = order.auctionDuration + 1; 529 | } else if (makerOrderDir === 'sell' || limitPriceNum >= ask) { 530 | willCross = true; 531 | slotsTilCross = order.auctionDuration + 1; 532 | } 533 | } 534 | } 535 | 536 | return { 537 | slotsTilCross, 538 | willCross, 539 | bid, 540 | ask, 541 | auctionStartPrice, 542 | auctionEndPrice, 543 | stepSize, 544 | oraclePrice, 545 | }; 546 | } 547 | 548 | async waitForSlotOrCrossOrExpiry( 549 | targetSlot: number, 550 | order: Order, 551 | initialDetails: AuctionAndOrderDetails 552 | ): Promise<{ slot: number; updatedDetails: AuctionAndOrderDetails }> { 553 | let currentDetails: AuctionAndOrderDetails = initialDetails; 554 | let willCross = initialDetails.willCross; 555 | if (this.slotSubscriber.currentSlot > targetSlot) { 556 | const slot = willCross ? this.slotSubscriber.currentSlot : -1; 557 | return new Promise((resolve) => 558 | resolve({ slot, updatedDetails: currentDetails }) 559 | ); 560 | } 561 | 562 | return new Promise((resolve) => { 563 | // Immediately return if we are past target slot 564 | 565 | const slotListener = (slot: number) => { 566 | if (slot >= targetSlot && willCross) { 567 | resolve({ slot, updatedDetails: currentDetails }); 568 | } 569 | }; 570 | 571 | // Otherwise listen for new slots in case we hit the target slot and we're gonna cross 572 | this.slotSubscriber.eventEmitter.once('newSlot', slotListener); 573 | 574 | // Update target slot as the bid/ask and the oracle changes 575 | const intervalId = setInterval(async () => { 576 | if (this.slotSubscriber.currentSlot >= targetSlot) { 577 | this.slotSubscriber.eventEmitter.removeListener( 578 | 'newSlot', 579 | slotListener 580 | ); 581 | clearInterval(intervalId); 582 | const slot = willCross ? this.slotSubscriber.currentSlot : -1; 583 | resolve({ slot, updatedDetails: currentDetails }); 584 | } 585 | 586 | currentDetails = this.getAuctionAndOrderDetails(order); 587 | willCross = currentDetails.willCross; 588 | if (willCross) { 589 | targetSlot = order.slot.toNumber() + currentDetails.slotsTilCross; 590 | } 591 | }, 50); 592 | }); 593 | } 594 | } 595 | 596 | function sleep(ms: number): Promise { 597 | return new Promise((resolve) => setTimeout(resolve, ms)); 598 | } 599 | -------------------------------------------------------------------------------- /ts/sdk/src/types/jit_proxy.ts: -------------------------------------------------------------------------------- 1 | export type JitProxy = { 2 | version: '0.17.0'; 3 | name: 'jit_proxy'; 4 | instructions: [ 5 | { 6 | name: 'jit'; 7 | accounts: [ 8 | { 9 | name: 'state'; 10 | isMut: false; 11 | isSigner: false; 12 | }, 13 | { 14 | name: 'user'; 15 | isMut: true; 16 | isSigner: false; 17 | }, 18 | { 19 | name: 'userStats'; 20 | isMut: true; 21 | isSigner: false; 22 | }, 23 | { 24 | name: 'taker'; 25 | isMut: true; 26 | isSigner: false; 27 | }, 28 | { 29 | name: 'takerStats'; 30 | isMut: true; 31 | isSigner: false; 32 | }, 33 | { 34 | name: 'authority'; 35 | isMut: false; 36 | isSigner: true; 37 | }, 38 | { 39 | name: 'driftProgram'; 40 | isMut: false; 41 | isSigner: false; 42 | } 43 | ]; 44 | args: [ 45 | { 46 | name: 'params'; 47 | type: { 48 | defined: 'JitParams'; 49 | }; 50 | } 51 | ]; 52 | }, 53 | { 54 | name: 'jitSignedMsg'; 55 | accounts: [ 56 | { 57 | name: 'state'; 58 | isMut: false; 59 | isSigner: false; 60 | }, 61 | { 62 | name: 'user'; 63 | isMut: true; 64 | isSigner: false; 65 | }, 66 | { 67 | name: 'userStats'; 68 | isMut: true; 69 | isSigner: false; 70 | }, 71 | { 72 | name: 'taker'; 73 | isMut: true; 74 | isSigner: false; 75 | }, 76 | { 77 | name: 'takerStats'; 78 | isMut: true; 79 | isSigner: false; 80 | }, 81 | { 82 | name: 'takerSignedMsgUserOrders'; 83 | isMut: true; 84 | isSigner: false; 85 | }, 86 | { 87 | name: 'authority'; 88 | isMut: false; 89 | isSigner: true; 90 | }, 91 | { 92 | name: 'driftProgram'; 93 | isMut: false; 94 | isSigner: false; 95 | } 96 | ]; 97 | args: [ 98 | { 99 | name: 'params'; 100 | type: { 101 | defined: 'JitSignedMsgParams'; 102 | }; 103 | } 104 | ]; 105 | }, 106 | { 107 | name: 'checkOrderConstraints'; 108 | accounts: [ 109 | { 110 | name: 'user'; 111 | isMut: false; 112 | isSigner: false; 113 | } 114 | ]; 115 | args: [ 116 | { 117 | name: 'constraints'; 118 | type: { 119 | vec: { 120 | defined: 'OrderConstraint'; 121 | }; 122 | }; 123 | } 124 | ]; 125 | }, 126 | { 127 | name: 'arbPerp'; 128 | accounts: [ 129 | { 130 | name: 'state'; 131 | isMut: false; 132 | isSigner: false; 133 | }, 134 | { 135 | name: 'user'; 136 | isMut: true; 137 | isSigner: false; 138 | }, 139 | { 140 | name: 'userStats'; 141 | isMut: true; 142 | isSigner: false; 143 | }, 144 | { 145 | name: 'authority'; 146 | isMut: false; 147 | isSigner: true; 148 | }, 149 | { 150 | name: 'driftProgram'; 151 | isMut: false; 152 | isSigner: false; 153 | } 154 | ]; 155 | args: [ 156 | { 157 | name: 'marketIndex'; 158 | type: 'u16'; 159 | } 160 | ]; 161 | } 162 | ]; 163 | types: [ 164 | { 165 | name: 'OrderConstraint'; 166 | type: { 167 | kind: 'struct'; 168 | fields: [ 169 | { 170 | name: 'maxPosition'; 171 | type: 'i64'; 172 | }, 173 | { 174 | name: 'minPosition'; 175 | type: 'i64'; 176 | }, 177 | { 178 | name: 'marketIndex'; 179 | type: 'u16'; 180 | }, 181 | { 182 | name: 'marketType'; 183 | type: { 184 | defined: 'MarketType'; 185 | }; 186 | } 187 | ]; 188 | }; 189 | }, 190 | { 191 | name: 'JitParams'; 192 | type: { 193 | kind: 'struct'; 194 | fields: [ 195 | { 196 | name: 'takerOrderId'; 197 | type: 'u32'; 198 | }, 199 | { 200 | name: 'maxPosition'; 201 | type: 'i64'; 202 | }, 203 | { 204 | name: 'minPosition'; 205 | type: 'i64'; 206 | }, 207 | { 208 | name: 'bid'; 209 | type: 'i64'; 210 | }, 211 | { 212 | name: 'ask'; 213 | type: 'i64'; 214 | }, 215 | { 216 | name: 'priceType'; 217 | type: { 218 | defined: 'PriceType'; 219 | }; 220 | }, 221 | { 222 | name: 'postOnly'; 223 | type: { 224 | option: { 225 | defined: 'PostOnlyParam'; 226 | }; 227 | }; 228 | } 229 | ]; 230 | }; 231 | }, 232 | { 233 | name: 'JitSignedMsgParams'; 234 | type: { 235 | kind: 'struct'; 236 | fields: [ 237 | { 238 | name: 'signedMsgOrderUuid'; 239 | type: { 240 | array: ['u8', 8]; 241 | }; 242 | }, 243 | { 244 | name: 'maxPosition'; 245 | type: 'i64'; 246 | }, 247 | { 248 | name: 'minPosition'; 249 | type: 'i64'; 250 | }, 251 | { 252 | name: 'bid'; 253 | type: 'i64'; 254 | }, 255 | { 256 | name: 'ask'; 257 | type: 'i64'; 258 | }, 259 | { 260 | name: 'priceType'; 261 | type: { 262 | defined: 'PriceType'; 263 | }; 264 | }, 265 | { 266 | name: 'postOnly'; 267 | type: { 268 | option: { 269 | defined: 'PostOnlyParam'; 270 | }; 271 | }; 272 | } 273 | ]; 274 | }; 275 | }, 276 | { 277 | name: 'PostOnlyParam'; 278 | type: { 279 | kind: 'enum'; 280 | variants: [ 281 | { 282 | name: 'None'; 283 | }, 284 | { 285 | name: 'MustPostOnly'; 286 | }, 287 | { 288 | name: 'TryPostOnly'; 289 | }, 290 | { 291 | name: 'Slide'; 292 | } 293 | ]; 294 | }; 295 | }, 296 | { 297 | name: 'PriceType'; 298 | type: { 299 | kind: 'enum'; 300 | variants: [ 301 | { 302 | name: 'Limit'; 303 | }, 304 | { 305 | name: 'Oracle'; 306 | } 307 | ]; 308 | }; 309 | }, 310 | { 311 | name: 'MarketType'; 312 | type: { 313 | kind: 'enum'; 314 | variants: [ 315 | { 316 | name: 'Perp'; 317 | }, 318 | { 319 | name: 'Spot'; 320 | } 321 | ]; 322 | }; 323 | } 324 | ]; 325 | errors: [ 326 | { 327 | code: 6000; 328 | name: 'BidNotCrossed'; 329 | msg: 'BidNotCrossed'; 330 | }, 331 | { 332 | code: 6001; 333 | name: 'AskNotCrossed'; 334 | msg: 'AskNotCrossed'; 335 | }, 336 | { 337 | code: 6002; 338 | name: 'TakerOrderNotFound'; 339 | msg: 'TakerOrderNotFound'; 340 | }, 341 | { 342 | code: 6003; 343 | name: 'OrderSizeBreached'; 344 | msg: 'OrderSizeBreached'; 345 | }, 346 | { 347 | code: 6004; 348 | name: 'NoBestBid'; 349 | msg: 'NoBestBid'; 350 | }, 351 | { 352 | code: 6005; 353 | name: 'NoBestAsk'; 354 | msg: 'NoBestAsk'; 355 | }, 356 | { 357 | code: 6006; 358 | name: 'NoArbOpportunity'; 359 | msg: 'NoArbOpportunity'; 360 | }, 361 | { 362 | code: 6007; 363 | name: 'UnprofitableArb'; 364 | msg: 'UnprofitableArb'; 365 | }, 366 | { 367 | code: 6008; 368 | name: 'PositionLimitBreached'; 369 | msg: 'PositionLimitBreached'; 370 | }, 371 | { 372 | code: 6009; 373 | name: 'NoFill'; 374 | msg: 'NoFill'; 375 | }, 376 | { 377 | code: 6010; 378 | name: 'SignedMsgOrderDoesNotExist'; 379 | msg: 'SignedMsgOrderDoesNotExist'; 380 | } 381 | ]; 382 | }; 383 | 384 | export const IDL: JitProxy = { 385 | version: '0.17.0', 386 | name: 'jit_proxy', 387 | instructions: [ 388 | { 389 | name: 'jit', 390 | accounts: [ 391 | { 392 | name: 'state', 393 | isMut: false, 394 | isSigner: false, 395 | }, 396 | { 397 | name: 'user', 398 | isMut: true, 399 | isSigner: false, 400 | }, 401 | { 402 | name: 'userStats', 403 | isMut: true, 404 | isSigner: false, 405 | }, 406 | { 407 | name: 'taker', 408 | isMut: true, 409 | isSigner: false, 410 | }, 411 | { 412 | name: 'takerStats', 413 | isMut: true, 414 | isSigner: false, 415 | }, 416 | { 417 | name: 'authority', 418 | isMut: false, 419 | isSigner: true, 420 | }, 421 | { 422 | name: 'driftProgram', 423 | isMut: false, 424 | isSigner: false, 425 | }, 426 | ], 427 | args: [ 428 | { 429 | name: 'params', 430 | type: { 431 | defined: 'JitParams', 432 | }, 433 | }, 434 | ], 435 | }, 436 | { 437 | name: 'jitSignedMsg', 438 | accounts: [ 439 | { 440 | name: 'state', 441 | isMut: false, 442 | isSigner: false, 443 | }, 444 | { 445 | name: 'user', 446 | isMut: true, 447 | isSigner: false, 448 | }, 449 | { 450 | name: 'userStats', 451 | isMut: true, 452 | isSigner: false, 453 | }, 454 | { 455 | name: 'taker', 456 | isMut: true, 457 | isSigner: false, 458 | }, 459 | { 460 | name: 'takerStats', 461 | isMut: true, 462 | isSigner: false, 463 | }, 464 | { 465 | name: 'takerSignedMsgUserOrders', 466 | isMut: true, 467 | isSigner: false, 468 | }, 469 | { 470 | name: 'authority', 471 | isMut: false, 472 | isSigner: true, 473 | }, 474 | { 475 | name: 'driftProgram', 476 | isMut: false, 477 | isSigner: false, 478 | }, 479 | ], 480 | args: [ 481 | { 482 | name: 'params', 483 | type: { 484 | defined: 'JitSignedMsgParams', 485 | }, 486 | }, 487 | ], 488 | }, 489 | { 490 | name: 'checkOrderConstraints', 491 | accounts: [ 492 | { 493 | name: 'user', 494 | isMut: false, 495 | isSigner: false, 496 | }, 497 | ], 498 | args: [ 499 | { 500 | name: 'constraints', 501 | type: { 502 | vec: { 503 | defined: 'OrderConstraint', 504 | }, 505 | }, 506 | }, 507 | ], 508 | }, 509 | { 510 | name: 'arbPerp', 511 | accounts: [ 512 | { 513 | name: 'state', 514 | isMut: false, 515 | isSigner: false, 516 | }, 517 | { 518 | name: 'user', 519 | isMut: true, 520 | isSigner: false, 521 | }, 522 | { 523 | name: 'userStats', 524 | isMut: true, 525 | isSigner: false, 526 | }, 527 | { 528 | name: 'authority', 529 | isMut: false, 530 | isSigner: true, 531 | }, 532 | { 533 | name: 'driftProgram', 534 | isMut: false, 535 | isSigner: false, 536 | }, 537 | ], 538 | args: [ 539 | { 540 | name: 'marketIndex', 541 | type: 'u16', 542 | }, 543 | ], 544 | }, 545 | ], 546 | types: [ 547 | { 548 | name: 'OrderConstraint', 549 | type: { 550 | kind: 'struct', 551 | fields: [ 552 | { 553 | name: 'maxPosition', 554 | type: 'i64', 555 | }, 556 | { 557 | name: 'minPosition', 558 | type: 'i64', 559 | }, 560 | { 561 | name: 'marketIndex', 562 | type: 'u16', 563 | }, 564 | { 565 | name: 'marketType', 566 | type: { 567 | defined: 'MarketType', 568 | }, 569 | }, 570 | ], 571 | }, 572 | }, 573 | { 574 | name: 'JitParams', 575 | type: { 576 | kind: 'struct', 577 | fields: [ 578 | { 579 | name: 'takerOrderId', 580 | type: 'u32', 581 | }, 582 | { 583 | name: 'maxPosition', 584 | type: 'i64', 585 | }, 586 | { 587 | name: 'minPosition', 588 | type: 'i64', 589 | }, 590 | { 591 | name: 'bid', 592 | type: 'i64', 593 | }, 594 | { 595 | name: 'ask', 596 | type: 'i64', 597 | }, 598 | { 599 | name: 'priceType', 600 | type: { 601 | defined: 'PriceType', 602 | }, 603 | }, 604 | { 605 | name: 'postOnly', 606 | type: { 607 | option: { 608 | defined: 'PostOnlyParam', 609 | }, 610 | }, 611 | }, 612 | ], 613 | }, 614 | }, 615 | { 616 | name: 'JitSignedMsgParams', 617 | type: { 618 | kind: 'struct', 619 | fields: [ 620 | { 621 | name: 'signedMsgOrderUuid', 622 | type: { 623 | array: ['u8', 8], 624 | }, 625 | }, 626 | { 627 | name: 'maxPosition', 628 | type: 'i64', 629 | }, 630 | { 631 | name: 'minPosition', 632 | type: 'i64', 633 | }, 634 | { 635 | name: 'bid', 636 | type: 'i64', 637 | }, 638 | { 639 | name: 'ask', 640 | type: 'i64', 641 | }, 642 | { 643 | name: 'priceType', 644 | type: { 645 | defined: 'PriceType', 646 | }, 647 | }, 648 | { 649 | name: 'postOnly', 650 | type: { 651 | option: { 652 | defined: 'PostOnlyParam', 653 | }, 654 | }, 655 | }, 656 | ], 657 | }, 658 | }, 659 | { 660 | name: 'PostOnlyParam', 661 | type: { 662 | kind: 'enum', 663 | variants: [ 664 | { 665 | name: 'None', 666 | }, 667 | { 668 | name: 'MustPostOnly', 669 | }, 670 | { 671 | name: 'TryPostOnly', 672 | }, 673 | { 674 | name: 'Slide', 675 | }, 676 | ], 677 | }, 678 | }, 679 | { 680 | name: 'PriceType', 681 | type: { 682 | kind: 'enum', 683 | variants: [ 684 | { 685 | name: 'Limit', 686 | }, 687 | { 688 | name: 'Oracle', 689 | }, 690 | ], 691 | }, 692 | }, 693 | { 694 | name: 'MarketType', 695 | type: { 696 | kind: 'enum', 697 | variants: [ 698 | { 699 | name: 'Perp', 700 | }, 701 | { 702 | name: 'Spot', 703 | }, 704 | ], 705 | }, 706 | }, 707 | ], 708 | errors: [ 709 | { 710 | code: 6000, 711 | name: 'BidNotCrossed', 712 | msg: 'BidNotCrossed', 713 | }, 714 | { 715 | code: 6001, 716 | name: 'AskNotCrossed', 717 | msg: 'AskNotCrossed', 718 | }, 719 | { 720 | code: 6002, 721 | name: 'TakerOrderNotFound', 722 | msg: 'TakerOrderNotFound', 723 | }, 724 | { 725 | code: 6003, 726 | name: 'OrderSizeBreached', 727 | msg: 'OrderSizeBreached', 728 | }, 729 | { 730 | code: 6004, 731 | name: 'NoBestBid', 732 | msg: 'NoBestBid', 733 | }, 734 | { 735 | code: 6005, 736 | name: 'NoBestAsk', 737 | msg: 'NoBestAsk', 738 | }, 739 | { 740 | code: 6006, 741 | name: 'NoArbOpportunity', 742 | msg: 'NoArbOpportunity', 743 | }, 744 | { 745 | code: 6007, 746 | name: 'UnprofitableArb', 747 | msg: 'UnprofitableArb', 748 | }, 749 | { 750 | code: 6008, 751 | name: 'PositionLimitBreached', 752 | msg: 'PositionLimitBreached', 753 | }, 754 | { 755 | code: 6009, 756 | name: 'NoFill', 757 | msg: 'NoFill', 758 | }, 759 | { 760 | code: 6010, 761 | name: 'SignedMsgOrderDoesNotExist', 762 | msg: 'SignedMsgOrderDoesNotExist', 763 | }, 764 | ], 765 | }; 766 | -------------------------------------------------------------------------------- /ts/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2019", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "tests"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } --------------------------------------------------------------------------------