├── .circleci └── config.yml ├── .flake8 ├── .gitignore ├── .gitmodules ├── .pyre_configuration ├── .watchmanconfig ├── LICENSE ├── Makefile ├── README.md ├── bxsolana ├── __init__.py ├── examples │ ├── __init__.py │ ├── constants.py │ ├── order_lifecycle.py │ ├── order_utils.py │ ├── request_utils.py │ ├── stream_utils.py │ └── transaction_request_utils.py ├── provider │ ├── __init__.py │ ├── base.py │ ├── constants.py │ ├── grpc.py │ ├── http.py │ ├── http_error.py │ ├── jsonrpc_patch.py │ ├── package_info.py │ └── ws.py ├── transaction │ ├── __init__.py │ ├── memo.py │ ├── private_txs.py │ └── signing.py └── utils │ ├── __init__.py │ └── timestamp.py ├── example ├── README.md ├── bundles │ └── main.py ├── provider │ └── main.py └── transaction │ └── main.py ├── helpers.py ├── menu.py ├── pyproject.toml ├── requirements.txt ├── sdk.py ├── setup.cfg └── test ├── __init__.py ├── integration ├── test_grpc.py └── test_ws.py └── unit ├── __init__.py ├── transaction ├── __init__.py ├── test_memo.py └── test_signing.py └── ws └── test_validation.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | bxpython: 4 | docker: 5 | - image: cimg/python:3.12.0 6 | environment: 7 | RUN_TRADES: false 8 | RUN_SLOW_STREAMS: false 9 | working_directory: ~/ws 10 | jobs: 11 | init: 12 | executor: bxpython 13 | steps: 14 | - attach_workspace: 15 | at: ~/ws 16 | - checkout: 17 | path: solana-trader-client-python 18 | - restore_cache: 19 | key: deps-{{ checksum "~/ws/solana-trader-client-python/requirements.txt" }} 20 | - run: 21 | name: Install awscli 22 | command: | 23 | sudo apt update 24 | sudo apt install awscli 25 | - run: 26 | name: Install dependencies 27 | command: | 28 | cd ~/ws/solana-trader-client-python 29 | python -m virtualenv --clear venv 30 | . venv/bin/activate 31 | pip install -r requirements.txt 32 | make environment-integration 33 | - save_cache: 34 | key: deps-{{ checksum "~/ws/solana-trader-client-python/requirements.txt" }} 35 | paths: 36 | - ~/ws/solana-trader-client-python/venv 37 | - persist_to_workspace: 38 | root: ~/ws 39 | paths: 40 | - solana-trader-client-python 41 | lint: 42 | executor: bxpython 43 | steps: 44 | - attach_workspace: 45 | at: ~/ws 46 | - run: 47 | name: lint 48 | command: | 49 | cd ~/ws/solana-trader-client-python 50 | . venv/bin/activate 51 | pip install pyre-check black flake8 # adjust based on your needs 52 | echo "Current directory contents:" 53 | python --version 54 | ls -la 55 | echo "Pyre configuration contents:" 56 | cat .pyre_configuration 57 | make lint 58 | unit: 59 | executor: bxpython 60 | steps: 61 | - attach_workspace: 62 | at: ~/ws 63 | - run: 64 | name: Unit test 65 | command: | 66 | cd ~/ws/solana-trader-client-python 67 | . venv/bin/activate 68 | make test 69 | mainnet_examples: 70 | executor: bxpython 71 | steps: 72 | - attach_workspace: 73 | at: ~/ws 74 | - run: 75 | name: Export build details 76 | command: | 77 | cd ~/ws/solana-trader-client-python 78 | echo 'export COMMIT_HASH=$(git rev-parse HEAD)' >> $BASH_ENV 79 | echo 'export COMMIT_AUTHOR="$(git --no-pager log -1 --pretty=format:'%an')"' >> $BASH_ENV 80 | source $BASH_ENV 81 | - run: 82 | name: Mainnet 83 | command: | 84 | unset PRIVATE_KEY 85 | cd ~/ws/solana-trader-client-python 86 | . venv/bin/activate 87 | API_ENV=mainnet AUTH_HEADER=$AUTH_HEADER_MAINNET python example/provider/main.py 88 | workflows: 89 | version: 2 90 | test-branch: 91 | when: 92 | not: 93 | equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] 94 | jobs: 95 | - init: 96 | context: 97 | - circleci 98 | # 2024/10/31: Taking out lint for now 99 | # as we do not know how to address violations. 100 | # Jira ticket has been created to address. 101 | # - lint: 102 | # requires: 103 | # - init 104 | - unit: 105 | requires: 106 | - init 107 | nightly: 108 | when: 109 | equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] 110 | jobs: 111 | - init: 112 | context: 113 | - circleci 114 | - unit: 115 | requires: 116 | - init -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = bxsolana/proto 3 | ignore = E203, E501 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | test_state.json 132 | .vscode/ 133 | 134 | myenv 135 | **/.DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "solana-trader-proto"] 2 | path = solana-trader-proto 3 | url = git@github.com:bloXroute-Labs/solana-trader-proto.git 4 | -------------------------------------------------------------------------------- /.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "site_package_search_strategy": "all", 3 | "source_directories": [ 4 | "bxsolana", 5 | "example" 6 | ], 7 | "ignore_all_errors": [ 8 | "bxsolana/proto/" 9 | ], 10 | "python_version": "3.11" 11 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 bloXroute Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean pkg release proto lint fmt fmt-check analyze typecheck test 2 | .PHONY: environment-integration 3 | 4 | all: clean pkg 5 | 6 | clean: 7 | rm -rf dist build *.egg-info 8 | 9 | pkg: 10 | python -m build 11 | 12 | release: all 13 | python -m twine upload dist/* 14 | 15 | lint: typecheck analyze fmt-check 16 | 17 | fmt: 18 | black bxsolana 19 | black example 20 | 21 | fmt-check: 22 | black bxsolana --check && black example --check 23 | 24 | analyze: 25 | flake8 bxsolana 26 | 27 | typecheck: 28 | pyre check 29 | 30 | test: 31 | python -m unittest discover test/unit 32 | 33 | environment-integration: 34 | aws s3 cp s3://files.bloxroute.com/trader-api/test_state.json $(CURDIR)/test_state.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Trader Python Client 2 | 3 | Provides a Python SDK for bloXroute's Solana Trader API. 4 | 5 | ## Installation 6 | 7 | ``` 8 | $ pip install bxsolana-trader 9 | ``` 10 | 11 | # `solana-trader-client-python` SDK examples 12 | 13 | - Historically, examples are run from this directory in the 'bundles', 'provider', and 'transaction' directory, calling a set of functions from the 'bxsolana' package. We have added `sdk.py` to streamline running examples with this SDK, allowing you to run each endpoint/stream individually, on a per provider (WS, GRPC, HTTP) basis. If you would like to modify the examples to change parameters, amounts, etc, feel free to do so in the example functions in the file and rerun. 14 | - If certain examples submit transactions on chain, and you don't see transactions landing, modify parameters of `computeLimit`, `computePrice` and `tip` parameters. These adjust the tip amount to be sent to RPCs as well as priority fees. You can read more about it here: [Trader API Docs](https://docs.bloxroute.com/solana/trader-api-v2) 15 | 16 | ## How to Run SDK 17 | 18 | Set up your Environment Variables: 19 | ``` 20 | AUTH_HEADER: bloXRoute Auth Header 21 | PRIVATE_KEY: solana signing key to be used for examples 22 | PUBLIC_KEY: solana public key to be used for examples (default `payer` if not specified) 23 | PAYER: payer responsible for transaction fees (optional) 24 | OPEN_ORDERS: openbook open orders address (optional) 25 | ``` 26 | 27 | Once your environment is set run 28 | 29 | `python sdk.py` 30 | 31 | After this, follow menu to select whatever you want. This should give you a feeling of the services trader-api provides. 32 | 33 | ## Usage 34 | 35 | This library supports HTTP, websockets, and GRPC interfaces. You can use it with 36 | a context manager or handle open/closing yourself. 37 | 38 | 39 | For any methods involving transaction creation you will need to provide your 40 | Solana private key. You can provide this via the environment variable 41 | `PRIVATE_KEY`, or specify it via the provider configuration if you want to load 42 | it with some other mechanism. See samples for more information. 43 | As a general note on this: methods named `post_*` (e.g. `post_order`) typically 44 | do not sign/submit the transaction, only return the raw unsigned transaction. 45 | This isn't very useful to most users (unless you want to write a signer in a 46 | different language), and you'll typically want the similarly named `submit_*` 47 | methods (e.g. `submit_order`). These methods generate, sign, and submit the 48 | transaction all at once. 49 | 50 | You will also need your bloXroute authorization header to use these endpoints. By default, this is loaded from the 51 | `AUTH_HEADER` environment variable. 52 | 53 | Context manager: 54 | 55 | ```python 56 | from bxsolana import provider 57 | 58 | async with provider.http() as api: 59 | print(await api.get_orderbook(market="ETHUSDT")) 60 | 61 | async with provider.ws() as api: 62 | async for update in api.get_orderbooks_stream(market="ETHUSDT"): 63 | print(update) 64 | ``` 65 | 66 | Manual: 67 | 68 | ```python 69 | import bxsolana 70 | 71 | from bxsolana import provider 72 | 73 | p = provider.grpc() 74 | api = await bxsolana.trader_api(p) 75 | 76 | try: 77 | await api.get_orderbook(market="ETHUSDT") 78 | finally: 79 | await p.close() 80 | ``` 81 | 82 | Refer to the `examples/` for more info. 83 | 84 | ## Development 85 | 86 | bloXroute Solana Trader API's interfaces are primarily powered by protobuf, so you will 87 | need to install it for your system: https://grpc.io/docs/protoc-installation/ 88 | 89 | Clone project and install dependencies: 90 | 91 | ``` 92 | $ git clone https://github.com/bloXroute-Labs/solana-trader-client-python.git 93 | ``` 94 | You can build the **solana-trader-proto-python** directory using these steps: 95 | 96 | - update **setup.cfg**, set the new version of bxsolana-trader-proto, e.g. 97 | ``` 98 | bxsolana-trader-proto==0.0.89 99 | ``` 100 | - run: 101 | ``` 102 | $ pip install -r requirements.txt 103 | ``` 104 | 105 | Run tests: 106 | ``` 107 | $ make test 108 | ``` 109 | Linting: 110 | ``` 111 | $ make lint 112 | ``` 113 | -------------------------------------------------------------------------------- /bxsolana/__init__.py: -------------------------------------------------------------------------------- 1 | from . import provider 2 | from . import examples 3 | 4 | Provider = provider.Provider 5 | 6 | 7 | async def trader_api(connection_provider: Provider) -> Provider: 8 | await connection_provider.connect() 9 | return connection_provider 10 | 11 | 12 | __all__ = [ 13 | "examples", 14 | "Provider", 15 | "trader_api", 16 | ] 17 | -------------------------------------------------------------------------------- /bxsolana/examples/__init__.py: -------------------------------------------------------------------------------- 1 | from .request_utils import do_requests 2 | from .transaction_request_utils import do_transaction_requests 3 | from .stream_utils import do_stream 4 | from .constants import ( 5 | PUBLIC_KEY, 6 | USDC_WALLET, 7 | OPEN_ORDERS, 8 | ORDER_ID, 9 | MARKET, 10 | ) 11 | from .order_utils import ( 12 | cancel_order, 13 | cancel_all_orders, 14 | replace_order_by_client_order_id, 15 | ) 16 | from .order_lifecycle import order_lifecycle 17 | 18 | __all__ = [ 19 | "do_requests", 20 | "do_transaction_requests", 21 | "do_stream", 22 | "cancel_all_orders", 23 | "cancel_order", 24 | "replace_order_by_client_order_id", 25 | "order_lifecycle", 26 | "PUBLIC_KEY", 27 | "USDC_WALLET", 28 | "OPEN_ORDERS", 29 | "ORDER_ID", 30 | "MARKET", 31 | ] 32 | -------------------------------------------------------------------------------- /bxsolana/examples/constants.py: -------------------------------------------------------------------------------- 1 | # sample keys to run integration/regression tests with 2 | # maintained by bloxroute team 3 | import json 4 | 5 | SIDE_BID = "bid" 6 | SIDE_ASK = "ask" 7 | TYPE_LIMIT = "limit" 8 | TYPE_IOC = "ioc" 9 | TYPE_POST_ONLY = "postonly" 10 | 11 | PUBLIC_KEY = "BgJ8uyf9yhLJaUVESRrqffzwVyQgRi9YvWmpEFaH14kw" 12 | USDC_WALLET = "6QRBKhLeJQNpPqRUz1L1nwARJ1YGsH3QpmVapn5PeWky" 13 | OPEN_ORDERS = "FpLJoV6WkBoAq7VRNWhfFCua64UZobfqyQG1z8ceTaz2" 14 | MARKET = "SOLUSDC" 15 | ORDER_ID = "" 16 | 17 | try: 18 | with open("test_state.json") as f: 19 | cfg = json.load(f) 20 | PUBLIC_KEY = cfg["publicKey"] 21 | OPEN_ORDERS = cfg["openOrders"] 22 | USDC_WALLET = cfg["usdcWallet"] 23 | MARKET = cfg["market"] 24 | ORDER_ID = cfg["expectedOrderId"] 25 | except Exception: 26 | # ignore 27 | pass 28 | -------------------------------------------------------------------------------- /bxsolana/examples/order_lifecycle.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import async_timeout # type: ignore 3 | 4 | from bxsolana import provider 5 | from bxsolana_trader_proto import api as proto 6 | from .order_utils import cancel_order, place_order, settle_funds 7 | 8 | crank_timeout = 60 9 | 10 | 11 | async def order_lifecycle( 12 | p1: provider.Provider, 13 | p2: provider.Provider, 14 | owner_addr, 15 | payer_addr, 16 | market_addr, 17 | order_side, 18 | order_type, 19 | order_amount, 20 | order_price, 21 | open_orders_addr, 22 | base_token_wallet, 23 | quote_token_wallet, 24 | ): 25 | print("order lifecycle test\n") 26 | 27 | oss = p2.get_order_status_stream( 28 | get_order_status_stream_request=proto.GetOrderStatusStreamRequest( 29 | market=market_addr, owner_address=owner_addr 30 | ) 31 | ) 32 | 33 | # pyre-ignore[6]: 34 | task = asyncio.create_task(oss.__anext__()) 35 | await asyncio.sleep(10) 36 | 37 | # Place Order => `Open` 38 | client_order_id = await place_order( 39 | p1, 40 | owner_addr, 41 | payer_addr, 42 | market_addr, 43 | order_side, 44 | order_type, 45 | order_amount, 46 | order_price, 47 | open_orders_addr, 48 | ) 49 | try: 50 | print(f"waiting {crank_timeout}s for place order to be cranked") 51 | async with async_timeout.timeout(crank_timeout): 52 | response = await task 53 | if response.order_info.order_status == proto.OrderStatus.OS_OPEN: 54 | print("order went to orderbook (`OPEN`) successfully") 55 | else: 56 | print( 57 | "order should be `OPEN` but is " 58 | + response.order_info.order_status.__str__() # noqa: W503 59 | ) 60 | except asyncio.TimeoutError: 61 | raise Exception("no updates after placing order") 62 | print() 63 | 64 | await asyncio.sleep(10) 65 | 66 | # Cancel Order => `Cancelled` 67 | await cancel_order( 68 | p1, client_order_id, market_addr, owner_addr, open_orders_addr 69 | ) 70 | try: 71 | print(f"waiting {crank_timeout}s for cancel order to be cranked") 72 | async with async_timeout.timeout(crank_timeout): 73 | response = await oss.__anext__() 74 | if ( 75 | response.order_info.order_status 76 | == proto.OrderStatus.OS_CANCELLED # noqa: W503 77 | ): 78 | print("order cancelled (`CANCELLED`) successfully") 79 | else: 80 | print( 81 | "order should be `CANCELLED` but is " 82 | + response.order_info.order_status.__str__() # noqa: W503 83 | ) 84 | except asyncio.TimeoutError: 85 | raise Exception("no updates after cancelling order") 86 | print() 87 | 88 | # Settle Funds 89 | await settle_funds( 90 | p1, 91 | owner_addr, 92 | market_addr, 93 | base_token_wallet, 94 | quote_token_wallet, 95 | open_orders_addr, 96 | ) 97 | print() 98 | -------------------------------------------------------------------------------- /bxsolana/examples/order_utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | from bxsolana_trader_proto import api as proto 5 | 6 | from bxsolana import provider 7 | from bxsolana.transaction import signing 8 | 9 | crank_timeout = 60 10 | 11 | 12 | async def place_order( 13 | p: provider.Provider, 14 | owner_addr, 15 | payer_addr, 16 | market_addr, 17 | order_side, 18 | order_type, 19 | order_amount, 20 | order_price, 21 | open_orders_addr, 22 | ) -> int: 23 | print("starting place order") 24 | 25 | client_order_id = random.randint(0, 1000000) 26 | post_order_response = await p.post_order( 27 | post_order_request=proto.PostOrderRequest( 28 | owner_address=owner_addr, 29 | payer_address=payer_addr, 30 | market=market_addr, 31 | side=order_side, 32 | type=[order_type], 33 | amount=order_amount, 34 | price=order_price, 35 | open_orders_address=open_orders_addr, 36 | client_order_id=client_order_id, 37 | ) 38 | ) 39 | print("place order transaction created successfully") 40 | 41 | signed_tx = signing.sign_tx(post_order_response.transaction.content) 42 | 43 | post_submit_response = await p.post_submit( 44 | post_submit_request=proto.PostSubmitRequest( 45 | transaction=proto.TransactionMessage(content=signed_tx), 46 | skip_pre_flight=True, 47 | ) 48 | ) 49 | 50 | print( 51 | f"placing order with clientOrderID {client_order_id.__str__()}, " 52 | f" response signature: {post_submit_response.signature}" 53 | ) 54 | 55 | return client_order_id 56 | 57 | 58 | async def place_order_with_tip( 59 | p: provider.Provider, 60 | owner_addr, 61 | payer_addr, 62 | market_addr, 63 | order_side, 64 | order_type, 65 | order_amount, 66 | order_price, 67 | open_orders_addr, 68 | ) -> int: 69 | print("starting place order") 70 | 71 | client_order_id = random.randint(0, 1000000) 72 | post_order_response = await p.post_order( 73 | post_order_request=proto.PostOrderRequest( 74 | owner_address=owner_addr, 75 | payer_address=payer_addr, 76 | market=market_addr, 77 | side=order_side, 78 | type=[order_type], 79 | amount=order_amount, 80 | price=order_price, 81 | open_orders_address=open_orders_addr, 82 | client_order_id=client_order_id, 83 | tip=1030, 84 | ) 85 | ) 86 | print("place order transaction created successfully") 87 | 88 | signed_tx = signing.sign_tx(post_order_response.transaction.content) 89 | 90 | post_submit_response = await p.post_submit( 91 | post_submit_request=proto.PostSubmitRequest( 92 | transaction=proto.TransactionMessage(content=signed_tx), 93 | skip_pre_flight=True, 94 | front_running_protection=True, 95 | use_staked_rp_cs=True, 96 | ) 97 | ) 98 | 99 | print( 100 | f"placing order with clientOrderID {client_order_id.__str__()}, " 101 | f" response signature: {post_submit_response.signature}" 102 | ) 103 | 104 | return client_order_id 105 | 106 | 107 | async def cancel_order( 108 | p: provider.Provider, 109 | client_order_id: int, 110 | market_addr: str, 111 | owner_addr: str, 112 | open_orders_addr: str, 113 | ): 114 | print("starting cancel order") 115 | 116 | cancel_order_response = await p.post_cancel_by_client_order_id( 117 | post_cancel_by_client_order_id_request=proto.PostCancelByClientOrderIdRequest( 118 | client_order_id=client_order_id, 119 | market_address=market_addr, 120 | owner_address=owner_addr, 121 | open_orders_address=open_orders_addr, 122 | ) 123 | ) 124 | print("cancel order transaction created successfully") 125 | 126 | signed_tx = signing.sign_tx(cancel_order_response.transaction.content) 127 | 128 | post_submit_response = await p.post_submit( 129 | post_submit_request=proto.PostSubmitRequest( 130 | transaction=proto.TransactionMessage(content=signed_tx), 131 | skip_pre_flight=True, 132 | ) 133 | ) 134 | print( 135 | f"cancelling order with clientOrderID {client_order_id.__str__()}, " 136 | f" response signature: {post_submit_response.signature}" 137 | ) 138 | 139 | 140 | async def settle_funds( 141 | p: provider.Provider, 142 | owner_addr: str, 143 | market_addr: str, 144 | base_token_wallet: str, 145 | quote_token_wallet: str, 146 | open_orders_addr: str, 147 | ): 148 | print("starting settle funds") 149 | 150 | post_settle_response = await p.post_settle( 151 | post_settle_request=proto.PostSettleRequest( 152 | owner_address=owner_addr, 153 | market=market_addr, 154 | base_token_wallet=base_token_wallet, 155 | quote_token_wallet=quote_token_wallet, 156 | open_orders_address=open_orders_addr, 157 | ) 158 | ) 159 | print("settle transaction created successfully") 160 | 161 | signed_settle_tx = signing.sign_tx(post_settle_response.transaction.content) 162 | 163 | post_submit_response = await p.post_submit( 164 | post_submit_request=proto.PostSubmitRequest( 165 | transaction=proto.TransactionMessage(content=signed_settle_tx), 166 | skip_pre_flight=True, 167 | ) 168 | ) 169 | 170 | print( 171 | "settling funds, response signature: " + post_submit_response.signature 172 | ) 173 | 174 | 175 | async def cancel_all_orders( 176 | p: provider.Provider, 177 | owner_addr, 178 | payer_addr, 179 | order_side, 180 | order_type, 181 | order_amount, 182 | order_price, 183 | open_orders_addr, 184 | market_addr, 185 | ): 186 | print("cancel all test\n") 187 | 188 | print("placing order #1") 189 | client_order_id_1 = await place_order( 190 | p, 191 | owner_addr, 192 | payer_addr, 193 | market_addr, 194 | order_side, 195 | order_type, 196 | order_amount, 197 | order_price, 198 | open_orders_addr, 199 | ) 200 | print() 201 | 202 | print("placing order #2") 203 | client_order_id_2 = await place_order( 204 | p, 205 | owner_addr, 206 | payer_addr, 207 | market_addr, 208 | order_side, 209 | order_type, 210 | order_amount, 211 | order_price, 212 | open_orders_addr, 213 | ) 214 | print() 215 | 216 | print(f"waiting {crank_timeout}s for place orders to be cranked") 217 | time.sleep(crank_timeout) 218 | 219 | o = await p.get_open_orders( 220 | get_open_orders_request=proto.GetOpenOrdersRequest( 221 | market=market_addr, address=owner_addr 222 | ) 223 | ) 224 | found1 = False 225 | found2 = False 226 | 227 | for order in o.orders: 228 | if order.client_order_id == str(client_order_id_1): 229 | found1 = True 230 | elif order.client_order_id == str(client_order_id_2): 231 | found2 = True 232 | 233 | if not found1 or not found2: 234 | raise Exception("one/both orders not found in orderbook") 235 | print("2 orders placed successfully\n") 236 | 237 | await cancel_all(p, owner_addr, open_orders_addr, market_addr) 238 | 239 | print(f"\nwaiting {crank_timeout}s for cancel order(s) to be cranked") 240 | time.sleep(crank_timeout) 241 | 242 | o = await p.get_open_orders( 243 | get_open_orders_request=proto.GetOpenOrdersRequest( 244 | market=market_addr, address=owner_addr 245 | ) 246 | ) 247 | if len(o.orders) != 0: 248 | print(f"{len(o.orders)} orders in orderbook not cancelled") 249 | else: 250 | print("orders in orderbook cancelled") 251 | print() 252 | 253 | 254 | async def cancel_all( 255 | p: provider.Provider, owner_addr, open_orders_addr, market_addr 256 | ): 257 | print("starting cancel all") 258 | 259 | open_orders_addresses = [""] 260 | if open_orders_addresses is None: 261 | open_orders_addresses.append(open_orders_addr) 262 | 263 | cancel_all_response = await p.post_cancel_all( 264 | post_cancel_all_request=proto.PostCancelAllRequest( 265 | market=market_addr, 266 | owner_address=owner_addr, 267 | open_orders_addresses=open_orders_addresses, 268 | ) 269 | ) 270 | print("cancel all transaction created successfully") 271 | 272 | signatures = [] 273 | for transaction in cancel_all_response.transactions: 274 | signed_tx = signing.sign_tx(transaction.content) 275 | post_submit_response = await p.post_submit( 276 | post_submit_request=proto.PostSubmitRequest( 277 | transaction=proto.TransactionMessage(content=signed_tx), 278 | skip_pre_flight=True, 279 | ) 280 | ) 281 | signatures.append(post_submit_response.signature) 282 | 283 | signatures_string = ", ".join(signatures) 284 | print(f"cancelling all orders, response signature(s): {signatures_string}") 285 | 286 | 287 | async def replace_order_by_client_order_id( 288 | p: provider.Provider, 289 | owner_addr, 290 | payer_addr, 291 | market_addr, 292 | order_side, 293 | order_type, 294 | order_amount, 295 | order_price, 296 | open_orders_addr, 297 | ) -> int: 298 | print("starting replace order by client order ID") 299 | 300 | client_order_id = random.randint(0, 1000000) 301 | post_order_response = await p.post_replace_by_client_order_id( 302 | post_order_request=proto.PostOrderRequest( 303 | owner_address=owner_addr, 304 | payer_address=payer_addr, 305 | market=market_addr, 306 | side=order_side, 307 | type=[order_type], 308 | amount=order_amount, 309 | price=order_price, 310 | open_orders_address=open_orders_addr, 311 | client_order_id=client_order_id, 312 | ) 313 | ) 314 | print("replace order transaction created successfully") 315 | 316 | signed_tx = signing.sign_tx(post_order_response.transaction.content) 317 | 318 | post_submit_response = await p.post_submit( 319 | post_submit_request=proto.PostSubmitRequest( 320 | transaction=proto.TransactionMessage(content=signed_tx), 321 | skip_pre_flight=True, 322 | ) 323 | ) 324 | print( 325 | f"replacing order with clientOrderID {client_order_id.__str__()}, " 326 | f" response signature: {post_submit_response.signature}" 327 | ) 328 | 329 | return client_order_id 330 | -------------------------------------------------------------------------------- /bxsolana/examples/request_utils.py: -------------------------------------------------------------------------------- 1 | from bxsolana_trader_proto import api as proto 2 | 3 | from .. import provider 4 | 5 | 6 | async def do_requests( 7 | api: provider.Provider, 8 | pumpny_api: provider.Provider, 9 | public_key: str, 10 | open_orders: str, 11 | order_id: str, 12 | usdc_wallet: str, 13 | sol_usdc_market: str, 14 | ): 15 | print("fetching Raydium pool reserve") 16 | print( 17 | ( 18 | await api.get_raydium_pool_reserve( 19 | get_raydium_pool_reserve_request=proto.GetRaydiumPoolReserveRequest( 20 | pairs_or_addresses=[ 21 | "HZ1znC9XBasm9AMDhGocd9EHSyH8Pyj1EUdiPb4WnZjo", 22 | "D8wAxwpH2aKaEGBKfeGdnQbCc2s54NrRvTDXCK98VAeT", 23 | "DdpuaJgjB2RptGMnfnCZVmC4vkKsMV6ytRa2gggQtCWt", 24 | ] 25 | ) 26 | ) 27 | ).to_json() 28 | ) 29 | 30 | # prints too much info, that's why it's commented 31 | # print("fetching Raydium pools") 32 | # print( 33 | # ( 34 | # await api.get_raydium_pools( 35 | # get_raydium_pools_request=proto.GetRaydiumPoolsRequest() 36 | # ) 37 | # ).to_json() 38 | # ) 39 | 40 | print("fetching Raydium CLMM pools") 41 | print( 42 | ( 43 | await api.get_raydium_clmm_pools( 44 | get_raydium_clmm_pools_request=proto.GetRaydiumClmmPoolsRequest() 45 | ) 46 | ).to_json() 47 | ) 48 | 49 | print("getting transaction") 50 | print( 51 | ( 52 | await api.get_transaction( 53 | get_transaction_request=proto.GetTransactionRequest( 54 | signature="2s48MnhH54GfJbRwwiEK7iWKoEh3uNbS2zDEVBPNu7DaCjPXe3bfqo6RuCg9NgHRFDn3L28sMVfEh65xevf4o5W3" 55 | ) 56 | ) 57 | ).to_json() 58 | ) 59 | 60 | print("getting ratelimit") 61 | print( 62 | ( 63 | await api.get_rate_limit( 64 | get_rate_limit_request=proto.GetRateLimitRequest() 65 | ) 66 | ).to_json() 67 | ) 68 | 69 | print("fetching market depth") 70 | print( 71 | ( 72 | await api.get_market_depth_v2( 73 | get_market_depth_request_v2=proto.GetMarketDepthRequestV2( 74 | limit=1, market="SOLUSDC" 75 | ) 76 | ) 77 | ).to_json() 78 | ) 79 | 80 | print("fetching priority fee") 81 | print( 82 | ( 83 | await api.get_priority_fee( 84 | get_priority_fee_request=proto.GetPriorityFeeRequest( 85 | project=proto.Project.P_RAYDIUM 86 | ) 87 | ) 88 | ).to_json() 89 | ) 90 | 91 | print("fetching priority fee by program") 92 | print( 93 | ( 94 | await api.get_priority_fee_by_program( 95 | get_priority_fee_by_program_request=proto.GetPriorityFeeByProgramRequest( 96 | programs=[ 97 | "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", 98 | "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C", 99 | ] 100 | ) 101 | ) 102 | ).to_json() 103 | ) 104 | 105 | # markets API 106 | print("fetching all markets") 107 | print( 108 | ( 109 | await api.get_markets_v2( 110 | get_markets_request_v2=proto.GetMarketsRequestV2() 111 | ) 112 | ).to_json() 113 | ) 114 | 115 | print("fetching SOL/USDC orderbook") 116 | print( 117 | ( 118 | await api.get_orderbook_v2( 119 | get_orderbook_request_v2=proto.GetOrderbookRequestV2( 120 | market="SOLUSDC" 121 | ) 122 | ) 123 | ).to_json() 124 | ) 125 | 126 | print("fetching SOL/USDC ticker") 127 | print( 128 | ( 129 | await api.get_tickers_v2( 130 | get_tickers_request_v2=proto.GetTickersRequestV2( 131 | market="SOLUSDC" 132 | ) 133 | ) 134 | ).to_json() 135 | ) 136 | 137 | print("fetching all tickers") 138 | print( 139 | ( 140 | await api.get_tickers_v2( 141 | get_tickers_request_v2=proto.GetTickersRequestV2() 142 | ) 143 | ).to_json() 144 | ) 145 | 146 | print("fetching prices") 147 | 148 | print( 149 | ( 150 | await api.get_price( 151 | get_price_request=proto.GetPriceRequest( 152 | tokens=[ 153 | "So11111111111111111111111111111111111111112", 154 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 155 | ] 156 | ) 157 | ) 158 | ).to_json() 159 | ) 160 | 161 | print("fetching Raydium prices") 162 | 163 | print( 164 | ( 165 | await api.get_raydium_prices( 166 | get_raydium_prices_request=proto.GetRaydiumPricesRequest( 167 | tokens=[ 168 | "So11111111111111111111111111111111111111112", 169 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 170 | ] 171 | ) 172 | ) 173 | ).to_json() 174 | ) 175 | 176 | print("fetching Jupiter prices") 177 | 178 | print( 179 | ( 180 | await api.get_jupiter_prices( 181 | get_jupiter_prices_request=proto.GetJupiterPricesRequest( 182 | tokens=[ 183 | "So11111111111111111111111111111111111111112", 184 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 185 | ] 186 | ) 187 | ) 188 | ).to_json() 189 | ) 190 | 191 | print("fetching pools") 192 | print( 193 | ( 194 | await api.get_pools( 195 | get_pools_request=proto.GetPoolsRequest( 196 | projects=[proto.Project.P_RAYDIUM] 197 | ) 198 | ) 199 | ).to_json() 200 | ) 201 | 202 | print("fetching Raydium pools") 203 | print( 204 | ( 205 | await api.get_raydium_pools( 206 | get_raydium_pools_request=proto.GetRaydiumPoolsRequest() 207 | ) 208 | ).to_json() 209 | ) 210 | 211 | print("fetching quotes") 212 | print( 213 | ( 214 | await api.get_quotes( 215 | get_quotes_request=proto.GetQuotesRequest( 216 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 217 | out_token="So11111111111111111111111111111111111111112", 218 | in_amount=0.01, 219 | slippage=10, 220 | limit=1, 221 | projects=[proto.Project.P_RAYDIUM], 222 | ) 223 | ) 224 | ).to_json() 225 | ) 226 | 227 | print("fetching Raydium quotes") 228 | print( 229 | ( 230 | await api.get_raydium_quotes( 231 | get_raydium_quotes_request=proto.GetRaydiumQuotesRequest( 232 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 233 | out_token="So11111111111111111111111111111111111111112", 234 | in_amount=0.01, 235 | slippage=10, 236 | ) 237 | ) 238 | ).to_json() 239 | ) 240 | 241 | print("fetching PumpFun quotes") 242 | print( 243 | ( 244 | await pumpny_api.get_pump_fun_quotes( 245 | get_pump_fun_quotes_request=proto.GetPumpFunQuotesRequest( 246 | bonding_curve_address=( 247 | "Dga6eouREJ4kLHMqWWtccGGPsGebexuBYrcepBVd494q" 248 | ), 249 | mint_address="9QG5NHnfqQCyZ9SKhz7BzfjPseTFWaApmAtBTziXLanY", 250 | amount=0.01, 251 | quote_type="buy", 252 | ) 253 | ) 254 | ).to_json() 255 | ) 256 | 257 | print("fetching Raydium CLMM quotes") 258 | print( 259 | ( 260 | await api.get_raydium_clmm_quotes( 261 | get_raydium_clmm_quotes_request=proto.GetRaydiumClmmQuotesRequest( 262 | in_token="USDC", 263 | out_token="SOL", 264 | in_amount=32, 265 | slippage=10, 266 | ) 267 | ) 268 | ).to_json() 269 | ) 270 | 271 | print("fetching Raydium CPMM quotes") 272 | print( 273 | ( 274 | await api.get_raydium_cpmm_quotes( 275 | get_raydium_cpmm_quotes_request=proto.GetRaydiumCpmmQuotesRequest( 276 | in_token="USDC", 277 | out_token="SOL", 278 | in_amount=32, 279 | slippage=10, 280 | ) 281 | ) 282 | ).to_json() 283 | ) 284 | 285 | print("fetching Jupiter quotes") 286 | print( 287 | ( 288 | await api.get_jupiter_quotes( 289 | get_jupiter_quotes_request=proto.GetJupiterQuotesRequest( 290 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 291 | out_token="So11111111111111111111111111111111111111112", 292 | in_amount=0.01, 293 | slippage=10, 294 | ) 295 | ) 296 | ).to_json() 297 | ) 298 | 299 | # trade API 300 | print("fetching open orders for account") 301 | print( 302 | ( 303 | await api.get_open_orders_v2( 304 | get_open_orders_request_v2=proto.GetOpenOrdersRequestV2( 305 | order_id="", 306 | client_order_id=0, 307 | market="SOLUSDC", 308 | address=public_key, 309 | limit=0, 310 | ) 311 | ) 312 | ).to_json() 313 | ) 314 | 315 | print("fetching unsettled amounts") 316 | print( 317 | ( 318 | await api.get_unsettled_v2( 319 | get_unsettled_request_v2=proto.GetUnsettledRequestV2( 320 | market="SOLUSDC", 321 | owner_address=public_key, 322 | ) 323 | ) 324 | ).to_json() 325 | ) 326 | 327 | print("fetching account balance amounts") 328 | print( 329 | ( 330 | await api.get_account_balance( 331 | get_account_balance_request=proto.GetAccountBalanceRequest( 332 | owner_address=public_key 333 | ) 334 | ) 335 | ).to_json() 336 | ) 337 | 338 | print("fetching token accounts and balances") 339 | print( 340 | ( 341 | await api.get_token_accounts( 342 | get_token_accounts_request=proto.GetTokenAccountsRequest( 343 | owner_address=public_key 344 | ) 345 | ) 346 | ).to_json() 347 | ) 348 | 349 | print( 350 | "generating unsigned order (no sign or submission) to sell 0.1 SOL for" 351 | " USDC at 150_000 USD/SOL" 352 | ) 353 | print( 354 | ( 355 | await api.post_order_v2( 356 | post_order_request_v2=proto.PostOrderRequestV2( 357 | owner_address=public_key, 358 | payer_address=public_key, 359 | market="SOLUSDC", 360 | side="ASK", 361 | amount=0.1, 362 | price=150_000, 363 | type="limit", 364 | # optional, but much faster if known 365 | open_orders_address=open_orders, 366 | # optional, for identification 367 | client_order_id=0, 368 | ) 369 | ) 370 | ).to_json() 371 | ) 372 | if order_id != "": 373 | print("generate cancel order") 374 | print( 375 | ( 376 | await api.post_cancel_order_v2( 377 | post_cancel_order_request_v2=proto.PostCancelOrderRequestV2( 378 | order_id=order_id, 379 | side="ASK", 380 | market_address="SOLUSDC", 381 | owner_address=public_key, 382 | open_orders_address=open_orders, 383 | client_order_id=0, 384 | ) 385 | ) 386 | ).to_json() 387 | ) 388 | 389 | print("generate cancel order by client ID") 390 | print( 391 | await api.post_cancel_order_v2( 392 | post_cancel_order_request_v2=proto.PostCancelOrderRequestV2( 393 | client_order_id=123, 394 | market_address=sol_usdc_market, 395 | owner_address=public_key, 396 | open_orders_address=open_orders, 397 | ) 398 | ) 399 | ) 400 | 401 | print("generate settle order") 402 | print( 403 | await api.post_settle_v2( 404 | post_settle_request_v2=proto.PostSettleRequestV2( 405 | owner_address=public_key, 406 | market="SOLUSDC", 407 | base_token_wallet=public_key, 408 | quote_token_wallet=usdc_wallet, 409 | open_orders_address=open_orders, 410 | ) 411 | ) 412 | ) 413 | 414 | print("generate replace by client order id") 415 | print( 416 | ( 417 | await api.post_replace_order_v2( 418 | post_replace_order_request_v2=proto.PostReplaceOrderRequestV2( 419 | owner_address=public_key, 420 | payer_address=public_key, 421 | market="SOLUSDC", 422 | side="ASK", 423 | type="limit", 424 | amount=0.1, 425 | price=150_000, 426 | # optional, but much faster if known 427 | open_orders_address=open_orders, 428 | # optional, for identification 429 | client_order_id=123, 430 | ) 431 | ) 432 | ).to_json() 433 | ) 434 | if order_id != "": 435 | print("generate replace by order id") 436 | print( 437 | ( 438 | await api.post_replace_order_v2( 439 | post_replace_order_request_v2=proto.PostReplaceOrderRequestV2( 440 | owner_address=public_key, 441 | payer_address=public_key, 442 | market="SOLUSDC", 443 | side="ASK", 444 | amount=0.1, 445 | price=150_000, 446 | # optional, but much faster if known 447 | open_orders_address=open_orders, 448 | # optional, for identification 449 | client_order_id=0, 450 | order_id=order_id, 451 | ) 452 | ) 453 | ).to_json() 454 | ) 455 | 456 | print("generate trade swap") 457 | print( 458 | ( 459 | await api.post_trade_swap( 460 | trade_swap_request=proto.TradeSwapRequest( 461 | project=proto.Project.P_RAYDIUM, 462 | owner_address=public_key, 463 | in_token="So11111111111111111111111111111111111111112", 464 | in_amount=0.01, 465 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 466 | slippage=0.01, 467 | ) 468 | ) 469 | ) 470 | ) 471 | 472 | print("generate raydium swap") 473 | print( 474 | ( 475 | await api.post_raydium_swap( 476 | post_raydium_swap_request=proto.PostRaydiumSwapRequest( 477 | owner_address=public_key, 478 | in_token="So11111111111111111111111111111111111111112", 479 | in_amount=0.01, 480 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 481 | slippage=0.01, 482 | ) 483 | ) 484 | ) 485 | ) 486 | 487 | print("generate raydium CLMM swap") 488 | print( 489 | ( 490 | await api.post_raydium_clmm_swap( 491 | post_raydium_swap_request=proto.PostRaydiumSwapRequest( 492 | owner_address=public_key, 493 | in_token="SOL", 494 | in_amount=1, 495 | out_token="USDC", 496 | slippage=10, 497 | ) 498 | ) 499 | ) 500 | ) 501 | 502 | print("generate raydium CPMM swap") 503 | print( 504 | ( 505 | await api.post_raydium_cpmm_swap( 506 | post_raydium_cpmm_swap_request=proto.PostRaydiumCpmmSwapRequest( 507 | owner_address=public_key, 508 | in_token="SOL", 509 | in_amount=1, 510 | out_token="USDC", 511 | slippage=10, 512 | ) 513 | ) 514 | ) 515 | ) 516 | 517 | print("generate raydium swap") 518 | print( 519 | ( 520 | await api.post_raydium_swap( 521 | post_raydium_swap_request=proto.PostRaydiumSwapRequest( 522 | owner_address=public_key, 523 | in_token="So11111111111111111111111111111111111111112", 524 | in_amount=0.01, 525 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 526 | slippage=0.01, 527 | ) 528 | ) 529 | ) 530 | ) 531 | 532 | print("generate pump_fun swap") 533 | print( 534 | ( 535 | await pumpny_api.post_pump_fun_swap( 536 | post_pump_fun_swap_request=proto.PostPumpFunSwapRequest( 537 | user_address=public_key, 538 | bonding_curve_address=( 539 | "7BcRpqUC7AF5Xsc3QEpCb8xmoi2X1LpwjUBNThbjWvyo" 540 | ), 541 | token_address=( 542 | "BAHY8ocERNc5j6LqkYav1Prr8GBGsHvBV5X3dWPhsgXw" 543 | ), 544 | token_amount=10, 545 | sol_threshold=0.0001, 546 | is_buy=True, 547 | ) 548 | ) 549 | ) 550 | ) 551 | 552 | print("generate jupiter swap") 553 | print( 554 | ( 555 | await api.post_jupiter_swap( 556 | post_jupiter_swap_request=proto.PostJupiterSwapRequest( 557 | owner_address=public_key, 558 | in_token="So11111111111111111111111111111111111111112", 559 | in_amount=0.01, 560 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 561 | slippage=0.01, 562 | ) 563 | ) 564 | ) 565 | ) 566 | 567 | print("generate route swap") 568 | step = proto.RaydiumRouteStep( 569 | in_token="So11111111111111111111111111111111111111112", 570 | in_amount=0.01, 571 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 572 | out_amount=0.007505, 573 | out_amount_min=0.0074, 574 | project=proto.StepProject( 575 | label="Raydium", id="58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" 576 | ), 577 | ) 578 | 579 | print("generate raydium CLMM route swap") 580 | print( 581 | ( 582 | await api.post_raydium_clmm_route_swap( 583 | post_raydium_route_swap_request=proto.PostRaydiumRouteSwapRequest( 584 | owner_address=public_key, 585 | slippage=10, 586 | steps=[step], 587 | ) 588 | ) 589 | ).to_json() 590 | ) 591 | 592 | step = proto.RouteStep( 593 | in_token="So11111111111111111111111111111111111111112", 594 | in_amount=0.01, 595 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 596 | out_amount=0.007505, 597 | out_amount_min=0.0074, 598 | project=proto.StepProject( 599 | label="Raydium", id="58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" 600 | ), 601 | ) 602 | 603 | print( 604 | ( 605 | await api.post_route_trade_swap( 606 | route_trade_swap_request=proto.RouteTradeSwapRequest( 607 | project=proto.Project.P_RAYDIUM, 608 | owner_address=public_key, 609 | slippage=0.1, 610 | steps=[step], 611 | ) 612 | ) 613 | ).to_json() 614 | ) 615 | 616 | print("generate raydium route swap") 617 | step = proto.RaydiumRouteStep( 618 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 619 | in_amount=0.01, 620 | out_token="So11111111111111111111111111111111111111112", 621 | out_amount=0.01, 622 | out_amount_min=0.01, 623 | project=proto.StepProject( 624 | label="Raydium", id="58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" 625 | ), 626 | ) 627 | print( 628 | ( 629 | await api.post_raydium_route_swap( 630 | post_raydium_route_swap_request=proto.PostRaydiumRouteSwapRequest( 631 | owner_address=public_key, 632 | slippage=0.1, 633 | steps=[step], 634 | ) 635 | ) 636 | ).to_json() 637 | ) 638 | 639 | print("fetching Recent Block Hash") 640 | print( 641 | ( 642 | await api.get_recent_block_hash( 643 | get_recent_block_hash_request=proto.GetRecentBlockHashRequest() 644 | ) 645 | ).to_json() 646 | ) 647 | 648 | print("fetching Recent Block Hash V2 without offset") 649 | print( 650 | ( 651 | await api.get_recent_block_hash_v2( 652 | get_recent_block_hash_request_v2=proto.GetRecentBlockHashRequestV2() 653 | ) 654 | ).to_json() 655 | ) 656 | 657 | print("fetching Recent Block Hash V2 with offset") 658 | print( 659 | ( 660 | await api.get_recent_block_hash_v2( 661 | get_recent_block_hash_request_v2=proto.GetRecentBlockHashRequestV2( 662 | offset=1 663 | ) 664 | ) 665 | ).to_json() 666 | ) 667 | -------------------------------------------------------------------------------- /bxsolana/examples/stream_utils.py: -------------------------------------------------------------------------------- 1 | from bxsolana_trader_proto import api as proto 2 | from .. import provider 3 | 4 | 5 | async def do_stream( 6 | api: provider.Provider, pump: provider.Provider, run_slow: bool = False 7 | ): 8 | item_count = 0 9 | print("streaming pump fun new tokens...") 10 | async for response in pump.get_pump_fun_new_tokens_stream( 11 | get_pump_fun_new_tokens_stream_request=proto.GetPumpFunNewTokensStreamRequest() 12 | ): 13 | print(response.to_json()) 14 | async for sresponse in pump.get_pump_fun_swaps_stream( 15 | get_pump_fun_swaps_stream_request=proto.GetPumpFunSwapsStreamRequest( 16 | tokens=[response.mint] 17 | ) 18 | ): 19 | print(sresponse.to_json()) 20 | item_count += 1 21 | if item_count == 1: 22 | item_count = 0 23 | break 24 | 25 | item_count += 1 26 | if item_count == 1: 27 | item_count = 0 28 | break 29 | 30 | print("streaming market depth updates...") 31 | async for response in api.get_market_depths_stream( 32 | get_market_depths_request=proto.GetMarketDepthsRequest( 33 | markets=["SOLUSDC"], limit=10, project=proto.Project.P_OPENBOOK 34 | ) 35 | ): 36 | print(response.to_json()) 37 | item_count += 1 38 | if item_count == 1: 39 | item_count = 0 40 | break 41 | 42 | print("streaming orderbook updates...") 43 | async for response in api.get_orderbooks_stream( 44 | get_orderbooks_request=proto.GetOrderbooksRequest( 45 | markets=["SOLUSDC"], project=proto.Project.P_OPENBOOK 46 | ) 47 | ): 48 | print(response.to_json()) 49 | item_count += 1 50 | if item_count == 1: 51 | item_count = 0 52 | break 53 | 54 | if run_slow: 55 | print("streaming ticker updates...") 56 | async for response in api.get_tickers_stream( 57 | get_tickers_stream_request=proto.GetTickersStreamRequest( 58 | markets=[ 59 | "BONK/SOL", 60 | "wSOL/RAY", 61 | "BONK/RAY", 62 | "RAY/USDC", 63 | "SOL/USDC", 64 | "SOL/USDC", 65 | "RAY/USDC", 66 | "USDT/USDC", 67 | ], 68 | project=proto.Project.P_OPENBOOK, 69 | ) 70 | ): 71 | print(response.to_json()) 72 | item_count += 1 73 | if item_count == 1: 74 | item_count = 0 75 | break 76 | 77 | if run_slow: 78 | print("streaming trade updates...") 79 | async for response in api.get_trades_stream( 80 | get_trades_request=proto.GetTradesRequest( 81 | market="SOLUSDC", project=proto.Project.P_OPENBOOK 82 | ) 83 | ): 84 | print(response.to_json()) 85 | item_count += 1 86 | if item_count == 1: 87 | item_count = 0 88 | break 89 | 90 | if run_slow: 91 | print("streaming swap events...") 92 | async for response in api.get_swaps_stream( 93 | get_swaps_stream_request=proto.GetSwapsStreamRequest( 94 | projects=[proto.Project.P_RAYDIUM], 95 | # RAY-SOL , ETH-SOL, SOL-USDC, SOL-USDT 96 | pools=[ 97 | "AVs9TA4nWDzfPJE9gGVNJMVhcQy3V9PGazuz33BfG2RA", 98 | "9Hm8QX7ZhE9uB8L2arChmmagZZBtBmnzBbpfxzkQp85D", 99 | "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", 100 | "7XawhbbxtsRcQA8KTkHT9f9nc6d69UwqCDh6U5EEbEmX", 101 | ], 102 | include_failed=True, 103 | ) 104 | ): 105 | print(response.to_json()) 106 | item_count += 1 107 | if item_count == 1: 108 | item_count = 0 109 | break 110 | 111 | if run_slow: 112 | print("streaming pool reserves...") 113 | async for response in api.get_pool_reserves_stream( 114 | get_pool_reserves_stream_request=proto.GetPoolReservesStreamRequest( 115 | projects=[proto.Project.P_RAYDIUM], 116 | pools=[ 117 | "GHGxSHVHsUNcGuf94rqFDsnhzGg3qbN1dD1z6DHZDfeQ", 118 | "HZ1znC9XBasm9AMDhGocd9EHSyH8Pyj1EUdiPb4WnZjo", 119 | "D8wAxwpH2aKaEGBKfeGdnQbCc2s54NrRvTDXCK98VAeT", 120 | "DdpuaJgjB2RptGMnfnCZVmC4vkKsMV6ytRa2gggQtCWt", 121 | ], 122 | ) 123 | ): 124 | print(response.to_json()) 125 | item_count += 1 126 | if item_count == 1: 127 | item_count = 0 128 | break 129 | 130 | if run_slow: 131 | print("streaming price streams...") 132 | async for response in api.get_prices_stream( 133 | get_prices_stream_request=proto.GetPricesStreamRequest( 134 | projects=[proto.Project.P_RAYDIUM], 135 | tokens=[ 136 | "So11111111111111111111111111111111111111112", 137 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 138 | ], 139 | ) 140 | ): 141 | print(response.to_json()) 142 | item_count += 1 143 | if item_count == 1: 144 | item_count = 0 145 | break 146 | 147 | if run_slow: 148 | if run_slow: 149 | print("streaming raydium new pool updates without cpmm pools...") 150 | async for response in api.get_new_raydium_pools_stream( 151 | get_new_raydium_pools_request=proto.GetNewRaydiumPoolsRequest() 152 | ): 153 | print(response.to_json()) 154 | item_count += 1 155 | if item_count == 1: 156 | item_count = 0 157 | break 158 | 159 | if run_slow: 160 | print("streaming raydium new pool updates with cpmm pools...") 161 | async for response in api.get_new_raydium_pools_stream( 162 | get_new_raydium_pools_request=proto.GetNewRaydiumPoolsRequest( 163 | include_cpmm=True 164 | ) 165 | ): 166 | print(response.to_json()) 167 | item_count += 1 168 | if item_count == 1: 169 | item_count = 0 170 | break 171 | 172 | if run_slow: 173 | print("streaming priority fee updates...") 174 | async for response in api.get_priority_fee_stream( 175 | get_priority_fee_request=proto.GetPriorityFeeRequest() 176 | ): 177 | print(response.to_json()) 178 | item_count += 1 179 | if item_count == 1: 180 | item_count = 0 181 | break 182 | 183 | if run_slow: 184 | print("streaming bundle tip updates...") 185 | async for response in api.get_bundle_tip_stream( 186 | get_bundle_tip_request=proto.GetBundleTipRequest() 187 | ): 188 | print(response.to_json()) 189 | item_count += 1 190 | if item_count == 1: 191 | item_count = 0 192 | break 193 | 194 | if run_slow: 195 | print("streaming new pump swap amm pools") 196 | async for response in api.get_pump_fun_new_amm_pool_stream( 197 | get_pump_fun_new_amm_pool_stream_request=proto.GetPumpFunNewAmmPoolStreamRequest() 198 | ): 199 | print(response.to_json()) 200 | item_count+=1 201 | if item_count == 1: 202 | item_count = 0 203 | break 204 | 205 | -------------------------------------------------------------------------------- /bxsolana/examples/transaction_request_utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import sys 3 | from typing import Tuple 4 | 5 | from bxsolana_trader_proto import api as proto 6 | from bxsolana_trader_proto.common import OrderType 7 | 8 | from .. import provider 9 | from .. import transaction 10 | 11 | 12 | # if you run this example with the integration wallet be sure to clean up your work afterward 13 | async def do_transaction_requests( 14 | api: provider.Provider, 15 | run_trades: bool, 16 | owner_addr: str, 17 | payer_addr: str, 18 | open_orders_addr: str, 19 | order_id: str, 20 | usdc_wallet: str, 21 | market: str, 22 | ): 23 | if not run_trades: 24 | print("skipping transaction requests: set by environment") 25 | return 26 | 27 | async def submit_order_with_client_id(client_id: int) -> str: 28 | return await api.submit_order( 29 | owner_address=owner_addr, 30 | payer_address=payer_addr, 31 | market=market, 32 | side=proto.Side.S_ASK, 33 | types=[OrderType.OT_LIMIT], 34 | amount=0.1, 35 | price=150_000, 36 | project=proto.Project.P_OPENBOOK, 37 | # optional, but much faster if known 38 | open_orders_address=open_orders_addr, 39 | # optional, for identification 40 | client_order_id=client_id, 41 | ) 42 | 43 | async def submit_order() -> Tuple[int, str]: 44 | _client_order_id = random.randint(1, sys.maxsize) 45 | _signature = await submit_order_with_client_id(_client_order_id) 46 | return _client_order_id, _signature 47 | 48 | print( 49 | "submitting order (generate + sign) to sell 0.1 SOL for USDC at 150_000" 50 | " USD/SOL" 51 | ) 52 | 53 | client_order_id, signature = await submit_order() 54 | print(signature) 55 | 56 | print( 57 | "submitting replace order by client ID (generate + sign) to sell 0.1" 58 | " SOL for USDC at 150_000 USD/SOL" 59 | ) 60 | print( 61 | await api.submit_replace_by_client_order_id( 62 | owner_address=owner_addr, 63 | payer_address=payer_addr, 64 | market=market, 65 | side=proto.Side.S_ASK, 66 | types=[OrderType.OT_LIMIT], 67 | amount=0.1, 68 | price=150_000, 69 | project=proto.Project.P_OPENBOOK, 70 | # optional, but much faster if known 71 | open_orders_address=open_orders_addr, 72 | # optional, for identification 73 | client_order_id=client_order_id, 74 | skip_pre_flight=True, 75 | ) 76 | ) 77 | 78 | print( 79 | "submitting replace order (generate + sign) to sell 0.1 SOL for USDC at" 80 | " 150_000 USD/SOL" 81 | ) 82 | print( 83 | await api.submit_replace_order( 84 | owner_address=owner_addr, 85 | payer_address=payer_addr, 86 | market=market, 87 | side=proto.Side.S_ASK, 88 | types=[OrderType.OT_LIMIT], 89 | amount=0.1, 90 | price=150_000, 91 | project=proto.Project.P_OPENBOOK, 92 | # optional, but much faster if known 93 | open_orders_address=open_orders_addr, 94 | # optional, for identification 95 | client_order_id=0, 96 | order_id=order_id, 97 | ) 98 | ) 99 | 100 | # cancel order example: comment out if want to try replace example 101 | print("submit cancel order") 102 | print( 103 | await api.submit_cancel_order( 104 | order_id=order_id, 105 | side=proto.Side.S_ASK, 106 | market_address=market, 107 | owner_address=owner_addr, 108 | project=proto.Project.P_SERUM, 109 | open_orders_address=open_orders_addr, 110 | ) 111 | ) 112 | 113 | # cancel by client order ID example: comment out if want to try replace example 114 | print("submit cancel order by client ID") 115 | print( 116 | await api.submit_cancel_by_client_order_id( 117 | client_order_id=client_order_id, 118 | market_address=market, 119 | owner_address=owner_addr, 120 | project=proto.Project.P_SERUM, 121 | open_orders_address=open_orders_addr, 122 | skip_pre_flight=True, 123 | ) 124 | ) 125 | 126 | print("submit settle order") 127 | print( 128 | await api.submit_settle( 129 | owner_address=owner_addr, 130 | market=market, 131 | base_token_wallet=owner_addr, 132 | quote_token_wallet=usdc_wallet, 133 | project=proto.Project.P_SERUM, 134 | open_orders_address="", # optional 135 | ) 136 | ) 137 | 138 | print("marking transactions with memo") 139 | await mark_transaction_with_memo(api) 140 | 141 | 142 | async def mark_transaction_with_memo(api: provider.Provider): 143 | private_key = transaction.load_private_key_from_env() 144 | public_key = private_key.pubkey() 145 | response = await api.post_trade_swap( 146 | trade_swap_request=proto.TradeSwapRequest( 147 | project=proto.Project.P_JUPITER, 148 | owner_address=str(public_key), 149 | in_token="USDT", 150 | in_amount=0.01, 151 | out_token="USDC", 152 | slippage=0.01, 153 | ) 154 | ) 155 | 156 | tx = response.transactions[0].content 157 | tx = transaction.add_memo_to_serialized_txn(tx) 158 | 159 | signed_tx = transaction.sign_tx_with_private_key(tx, private_key) 160 | 161 | post_submit_response = await api.post_submit( 162 | post_submit_request=proto.PostSubmitRequest( 163 | transaction=proto.TransactionMessage(signed_tx), 164 | skip_pre_flight=True, 165 | ) 166 | ) 167 | print("signature for single memo txn", post_submit_response.signature) 168 | -------------------------------------------------------------------------------- /bxsolana/provider/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Provider 2 | from .grpc import ( 3 | GrpcProvider, 4 | grpc, 5 | grpc_local, 6 | grpc_testnet, 7 | grpc_devnet, 8 | grpc_pump_ny, 9 | ) 10 | from .http import ( 11 | HttpProvider, 12 | http, 13 | http_local, 14 | http_testnet, 15 | http_devnet, 16 | http_pump_ny, 17 | ) 18 | from .http_error import HttpError 19 | from .ws import WsProvider, ws, ws_local, ws_testnet, ws_devnet, ws_pump_ny 20 | 21 | __all__ = [ 22 | "Provider", 23 | "GrpcProvider", 24 | "grpc", 25 | "grpc_devnet", 26 | "grpc_local", 27 | "grpc_testnet", 28 | "grpc_pump_ny", 29 | "HttpProvider", 30 | "HttpError", 31 | "http", 32 | "http_devnet", 33 | "http_local", 34 | "http_testnet", 35 | "WsProvider", 36 | "ws", 37 | "ws_devnet", 38 | "ws_local", 39 | "ws_testnet", 40 | "ws_pump_ny", 41 | "http_pump_ny", 42 | ] 43 | -------------------------------------------------------------------------------- /bxsolana/provider/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import List, Optional 3 | 4 | from bxsolana_trader_proto import api 5 | from bxsolana_trader_proto.common import OrderType 6 | from solders import keypair as kp # pyre-ignore[21]: module is too hard to find 7 | from ..utils.timestamp import timestamp 8 | 9 | from .. import transaction 10 | 11 | 12 | class Provider(api.ApiStub, ABC): 13 | async def __aenter__(self): 14 | await self.connect() 15 | return self 16 | 17 | async def __aexit__(self, *exc_info): 18 | await self.close() 19 | 20 | @abstractmethod 21 | async def connect(self): 22 | pass 23 | 24 | @abstractmethod 25 | def private_key(self) -> Optional[kp.Keypair]: # pyre-ignore[11]: annotation 26 | pass 27 | 28 | @abstractmethod 29 | async def close(self): 30 | pass 31 | 32 | def require_private_key(self) -> kp.Keypair: 33 | kp = self.private_key() 34 | if kp is None: 35 | raise EnvironmentError("private key has not been set in provider") 36 | return kp 37 | 38 | async def submit_order( 39 | self, 40 | owner_address: str, 41 | payer_address: str, 42 | market: str, 43 | side: "api.Side", 44 | types: List["OrderType"], 45 | amount: float, 46 | price: float, 47 | open_orders_address: str = "", 48 | client_order_id: int = 0, 49 | compute_limit: int = 0, 50 | compute_price: int = 0, 51 | project: api.Project = api.Project.P_UNKNOWN, 52 | skip_pre_flight: bool = True, 53 | ) -> str: 54 | pk = self.require_private_key() 55 | order = await self.post_order( 56 | post_order_request=api.PostOrderRequest( 57 | owner_address=owner_address, 58 | payer_address=payer_address, 59 | market=market, 60 | side=side, 61 | type=types, 62 | amount=amount, 63 | price=price, 64 | open_orders_address=open_orders_address, 65 | client_order_id=client_order_id, 66 | compute_limit=compute_limit, 67 | compute_price=compute_price, 68 | project=project, 69 | ) 70 | ) 71 | signed_tx = transaction.sign_tx_message_with_private_key( 72 | order.transaction, pk 73 | ) 74 | result = await self.post_submit( 75 | post_submit_request=api.PostSubmitRequest( 76 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 77 | ) 78 | ) 79 | return result.signature 80 | 81 | async def submit_cancel_order( 82 | self, 83 | order_id: str = "", 84 | side: api.Side = api.Side.S_UNKNOWN, 85 | market_address: str = "", 86 | owner_address: str = "", 87 | open_orders_address: str = "", 88 | compute_limit: int = 0, 89 | compute_price: int = 0, 90 | project: api.Project = api.Project.P_UNKNOWN, 91 | skip_pre_flight: bool = True, 92 | ) -> str: 93 | pk = self.require_private_key() 94 | order = await self.post_cancel_order( 95 | post_cancel_order_request=api.PostCancelOrderRequest( 96 | order_id=order_id, 97 | side=side, 98 | market_address=market_address, 99 | owner_address=owner_address, 100 | open_orders_address=open_orders_address, 101 | compute_limit=compute_limit, 102 | compute_price=compute_price, 103 | project=project, 104 | ) 105 | ) 106 | signed_tx = transaction.sign_tx_message_with_private_key( 107 | order.transaction, pk 108 | ) 109 | result = await self.post_submit( 110 | post_submit_request=api.PostSubmitRequest( 111 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 112 | ) 113 | ) 114 | return result.signature 115 | 116 | async def submit_cancel_by_client_order_id( 117 | self, 118 | client_order_id: int = 0, 119 | market_address: str = "", 120 | owner_address: str = "", 121 | open_orders_address: str = "", 122 | compute_limit: int = 0, 123 | compute_price: int = 0, 124 | project: api.Project = api.Project.P_UNKNOWN, 125 | skip_pre_flight: bool = True, 126 | ) -> str: 127 | pk = self.require_private_key() 128 | 129 | order = await self.post_cancel_by_client_order_id( 130 | post_cancel_by_client_order_id_request=api.PostCancelByClientOrderIdRequest( 131 | client_order_id=client_order_id, 132 | market_address=market_address, 133 | owner_address=owner_address, 134 | open_orders_address=open_orders_address, 135 | compute_limit=compute_limit, 136 | compute_price=compute_price, 137 | project=project, 138 | ) 139 | ) 140 | signed_tx = transaction.sign_tx_message_with_private_key( 141 | order.transaction, pk 142 | ) 143 | result = await self.post_submit( 144 | post_submit_request=api.PostSubmitRequest( 145 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 146 | ) 147 | ) 148 | return result.signature 149 | 150 | async def submit_cancel_all( 151 | self, 152 | market: str = "", 153 | owner_address: str = "", 154 | open_orders_addresses: Optional[List[str]] = None, 155 | compute_limit: int = 0, 156 | compute_price: int = 0, 157 | project: api.Project = api.Project.P_UNKNOWN, 158 | skip_pre_flight: bool = True, 159 | ) -> List[str]: 160 | if open_orders_addresses is None: 161 | open_orders_addresses = [] 162 | 163 | pk = self.require_private_key() 164 | response = await self.post_cancel_all( 165 | post_cancel_all_request=api.PostCancelAllRequest( 166 | market=market, 167 | owner_address=owner_address, 168 | open_orders_addresses=open_orders_addresses, 169 | compute_limit=compute_limit, 170 | compute_price=compute_price, 171 | project=project, 172 | ) 173 | ) 174 | 175 | signatures = [] 176 | for tx in response.transactions: 177 | signed_tx = transaction.sign_tx_message_with_private_key(tx, pk) 178 | result = await self.post_submit( 179 | post_submit_request=api.PostSubmitRequest( 180 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 181 | ) 182 | ) 183 | signatures.append(result.signature) 184 | 185 | return signatures 186 | 187 | async def submit_settle( 188 | self, 189 | owner_address: str = "", 190 | market: str = "", 191 | base_token_wallet: str = "", 192 | quote_token_wallet: str = "", 193 | open_orders_address: str = "", 194 | compute_limit: int = 0, 195 | compute_price: int = 0, 196 | project: api.Project = api.Project.P_UNKNOWN, 197 | skip_pre_flight: bool = True, 198 | ) -> str: 199 | pk = self.require_private_key() 200 | response = await self.post_settle( 201 | post_settle_request=api.PostSettleRequest( 202 | owner_address=owner_address, 203 | market=market, 204 | base_token_wallet=base_token_wallet, 205 | quote_token_wallet=quote_token_wallet, 206 | open_orders_address=open_orders_address, 207 | compute_limit=compute_limit, 208 | compute_price=compute_price, 209 | project=project, 210 | ) 211 | ) 212 | signed_tx = transaction.sign_tx_message_with_private_key( 213 | response.transaction, pk 214 | ) 215 | result = await self.post_submit( 216 | post_submit_request=api.PostSubmitRequest( 217 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 218 | ) 219 | ) 220 | return result.signature 221 | 222 | async def submit_replace_by_client_order_id( 223 | self, 224 | owner_address: str, 225 | payer_address: str, 226 | market: str, 227 | side: "api.Side", 228 | types: List["OrderType"], 229 | amount: float, 230 | price: float, 231 | open_orders_address: str = "", 232 | client_order_id: int = 0, 233 | compute_limit: int = 0, 234 | compute_price: int = 0, 235 | project: api.Project = api.Project.P_UNKNOWN, 236 | skip_pre_flight: bool = True, 237 | ) -> str: 238 | pk = self.require_private_key() 239 | order = await self.post_replace_by_client_order_id( 240 | post_order_request=api.PostOrderRequest( 241 | owner_address=owner_address, 242 | payer_address=payer_address, 243 | market=market, 244 | side=side, 245 | type=types, 246 | amount=amount, 247 | price=price, 248 | open_orders_address=open_orders_address, 249 | client_order_id=client_order_id, 250 | compute_limit=compute_limit, 251 | compute_price=compute_price, 252 | project=project, 253 | ) 254 | ) 255 | signed_tx = transaction.sign_tx_message_with_private_key( 256 | order.transaction, pk 257 | ) 258 | result = await self.post_submit( 259 | post_submit_request=api.PostSubmitRequest( 260 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 261 | ) 262 | ) 263 | return result.signature 264 | 265 | async def submit_replace_order( 266 | self, 267 | order_id: str, 268 | owner_address: str, 269 | payer_address: str, 270 | market: str, 271 | side: "api.Side", 272 | types: List["OrderType"], 273 | amount: float, 274 | price: float, 275 | open_orders_address: str = "", 276 | client_order_id: int = 0, 277 | compute_limit: int = 0, 278 | compute_price: int = 0, 279 | project: api.Project = api.Project.P_UNKNOWN, 280 | skip_pre_flight: bool = True, 281 | ) -> str: 282 | pk = self.require_private_key() 283 | order = await self.post_replace_order( 284 | post_replace_order_request=api.PostReplaceOrderRequest( 285 | owner_address=owner_address, 286 | payer_address=payer_address, 287 | market=market, 288 | side=side, 289 | type=types, 290 | amount=amount, 291 | price=price, 292 | open_orders_address=open_orders_address, 293 | client_order_id=client_order_id, 294 | order_id=order_id, 295 | compute_limit=compute_limit, 296 | compute_price=compute_price, 297 | project=project, 298 | ) 299 | ) 300 | signed_tx = transaction.sign_tx_message_with_private_key( 301 | order.transaction, pk 302 | ) 303 | result = await self.post_submit( 304 | post_submit_request=api.PostSubmitRequest( 305 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 306 | ) 307 | ) 308 | return result.signature 309 | 310 | async def submit_post_trade_swap( 311 | self, 312 | *, 313 | project: api.Project = api.Project.P_UNKNOWN, 314 | owner_address: str = "", 315 | in_token: str = "", 316 | out_token: str = "", 317 | in_amount: float = 0, 318 | slippage: float = 0, 319 | compute_limit: int = 0, 320 | compute_price: int = 0, 321 | tip: int = 0, 322 | skip_pre_flight: bool = True, 323 | submit_strategy: api.SubmitStrategy = api.SubmitStrategy.P_ABORT_ON_FIRST_ERROR, 324 | ) -> api.PostSubmitBatchResponse: 325 | pk = self.require_private_key() 326 | result = await self.post_trade_swap( 327 | trade_swap_request=api.TradeSwapRequest( 328 | project=project, 329 | owner_address=owner_address, 330 | in_token=in_token, 331 | out_token=out_token, 332 | in_amount=in_amount, 333 | slippage=slippage, 334 | compute_limit=compute_limit, 335 | compute_price=compute_price, 336 | tip=tip 337 | ) 338 | ) 339 | 340 | signed_txs: List[api.PostSubmitRequestEntry] = [] 341 | for tx in result.transactions: 342 | signed_tx = transaction.sign_tx_message_with_private_key(tx, pk) 343 | signed_txs.append( 344 | api.PostSubmitRequestEntry( 345 | transaction=signed_tx, skip_pre_flight=skip_pre_flight 346 | ) 347 | ) 348 | 349 | return await self.post_submit_batch( 350 | post_submit_batch_request=api.PostSubmitBatchRequest( 351 | entries=signed_txs, submit_strategy=submit_strategy, timestamp=timestamp() 352 | ) 353 | ) 354 | 355 | async def submit_post_route_trade_swap( 356 | self, 357 | *, 358 | project: api.Project = api.Project.P_UNKNOWN, 359 | owner_address: str = "", 360 | steps: List["api.RouteStep"] = [], 361 | slippage: float = 0, 362 | compute_limit: int = 0, 363 | compute_price: int = 0, 364 | tip: int = 0, 365 | skip_pre_flight: bool = True, 366 | submit_strategy: api.SubmitStrategy = api.SubmitStrategy.P_ABORT_ON_FIRST_ERROR, 367 | ) -> api.PostSubmitBatchResponse: 368 | pk = self.require_private_key() 369 | result = await self.post_route_trade_swap( 370 | route_trade_swap_request=api.RouteTradeSwapRequest( 371 | project=project, 372 | owner_address=owner_address, 373 | steps=steps, 374 | slippage=slippage, 375 | compute_limit=compute_limit, 376 | compute_price=compute_price, 377 | tip=tip, 378 | ) 379 | ) 380 | 381 | signed_txs: List[api.PostSubmitRequestEntry] = [] 382 | for tx in result.transactions: 383 | signed_tx = transaction.sign_tx_message_with_private_key(tx, pk) 384 | signed_txs.append( 385 | api.PostSubmitRequestEntry( 386 | transaction=signed_tx, skip_pre_flight=skip_pre_flight 387 | ) 388 | ) 389 | 390 | return await self.post_submit_batch( 391 | post_submit_batch_request=api.PostSubmitBatchRequest( 392 | entries=signed_txs, submit_strategy=submit_strategy, timestamp=timestamp() 393 | ) 394 | ) 395 | 396 | async def submit_raydium_swap( 397 | self, 398 | owner_address: str, 399 | in_token: str, 400 | out_token: str, 401 | in_amount: float, 402 | slippage: float = 0, 403 | compute_limit: int = 0, 404 | compute_price: int = 0, 405 | tip: int = 0, 406 | skip_pre_flight: bool = True, 407 | ) -> str: 408 | pk = self.require_private_key() 409 | swap = await self.post_raydium_swap(post_raydium_swap_request=api.PostRaydiumSwapRequest( 410 | owner_address=owner_address, 411 | in_token=in_token, 412 | out_token=out_token, 413 | in_amount=in_amount, 414 | slippage=slippage, 415 | compute_limit=compute_limit, 416 | compute_price=compute_price, 417 | tip=tip 418 | )) 419 | 420 | signed_tx = transaction.sign_tx_message_with_private_key( 421 | swap.transactions[0], pk 422 | ) 423 | 424 | result = await self.post_submit( 425 | post_submit_request=api.PostSubmitRequest( 426 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 427 | ) 428 | ) 429 | 430 | return result.signature 431 | 432 | async def submit_raydium_swap_cpmm( 433 | self, 434 | owner_address: str, 435 | in_token: str, 436 | out_token: str, 437 | in_amount: float, 438 | slippage: float = 0, 439 | compute_limit: int = 0, 440 | compute_price: int = 0, 441 | tip: int = 0, 442 | skip_pre_flight: bool = True, 443 | ) -> str: 444 | pk = self.require_private_key() 445 | swap = await self.post_raydium_cpmm_swap(post_raydium_cpmm_swap_request=api.PostRaydiumCpmmSwapRequest( 446 | owner_address=owner_address, 447 | in_token=in_token, 448 | out_token=out_token, 449 | in_amount=in_amount, 450 | slippage=slippage, 451 | compute_limit=compute_limit, 452 | compute_price=compute_price, 453 | tip=tip 454 | )) 455 | 456 | signed_tx = transaction.sign_tx_message_with_private_key( 457 | swap.transaction, pk 458 | ) 459 | 460 | result = await self.post_submit( 461 | post_submit_request=api.PostSubmitRequest( 462 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 463 | ) 464 | ) 465 | 466 | return result.signature 467 | 468 | async def submit_raydium_swap_clmm( 469 | self, 470 | owner_address: str, 471 | in_token: str, 472 | out_token: str, 473 | in_amount: float, 474 | slippage: float = 0, 475 | compute_limit: int = 0, 476 | compute_price: int = 0, 477 | tip: int = 0, 478 | skip_pre_flight: bool = True, 479 | ) -> str: 480 | pk = self.require_private_key() 481 | swap = await self.post_raydium_clmm_swap(post_raydium_swap_request=api.PostRaydiumSwapRequest( 482 | owner_address=owner_address, 483 | in_token=in_token, 484 | out_token=out_token, 485 | in_amount=in_amount, 486 | slippage=slippage, 487 | compute_limit=compute_limit, 488 | compute_price=compute_price, 489 | tip=tip 490 | )) 491 | 492 | signed_tx = transaction.sign_tx_message_with_private_key( 493 | swap.transactions[0], pk 494 | ) 495 | 496 | result = await self.post_submit( 497 | post_submit_request=api.PostSubmitRequest( 498 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 499 | ) 500 | ) 501 | 502 | return result.signature 503 | 504 | async def submit_jupiter_swap( 505 | self, 506 | owner_address: str, 507 | in_token: str, 508 | out_token: str, 509 | in_amount: float, 510 | slippage: float = 0, 511 | compute_limit: int = 0, 512 | compute_price: int = 0, 513 | tip: int = 0, 514 | skip_pre_flight: bool = True, 515 | ) -> str: 516 | pk = self.require_private_key() 517 | swap = await self.post_jupiter_swap(post_jupiter_swap_request=api.PostJupiterSwapRequest( 518 | owner_address=owner_address, 519 | in_token=in_token, 520 | out_token=out_token, 521 | in_amount=in_amount, 522 | slippage=slippage, 523 | compute_limit=compute_limit, 524 | compute_price=compute_price, 525 | tip=tip 526 | )) 527 | 528 | signed_tx = transaction.sign_tx_message_with_private_key( 529 | swap.transactions[0], pk 530 | ) 531 | 532 | result = await self.post_submit( 533 | post_submit_request=api.PostSubmitRequest( 534 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 535 | ) 536 | ) 537 | 538 | return result.signature 539 | 540 | async def submit_pump_fun_swap( 541 | self, 542 | owner_address: str, 543 | bonding_curve_address: str, 544 | token_address: str, 545 | creator: str, 546 | token_amount: float, 547 | sol_threshold: float, 548 | is_buy: bool, 549 | compute_limit: int = 0, 550 | compute_price: int = 0, 551 | tip: int = 0, 552 | skip_pre_flight: bool = True, 553 | ) -> str: 554 | pk = self.require_private_key() 555 | swap = await self.post_pump_fun_swap(post_pump_fun_swap_request=api.PostPumpFunSwapRequest( 556 | user_address=owner_address, 557 | bonding_curve_address=bonding_curve_address, 558 | token_address=token_address, 559 | creator=creator, 560 | token_amount=token_amount, 561 | sol_threshold=sol_threshold, 562 | is_buy=is_buy, 563 | compute_limit=compute_limit, 564 | compute_price=compute_price, 565 | tip=tip 566 | )) 567 | 568 | signed_tx = transaction.sign_tx_message_with_private_key_v2( 569 | swap.transaction, pk 570 | ) 571 | 572 | result = await self.post_submit( 573 | post_submit_request=api.PostSubmitRequest( 574 | transaction=signed_tx, skip_pre_flight=skip_pre_flight, timestamp=timestamp() 575 | ) 576 | ) 577 | 578 | return result.signature 579 | 580 | async def submit_snipe( 581 | self, 582 | transactions: List[str], 583 | use_staked_rpcs: bool = False, 584 | skip_pre_flight: bool = False, 585 | ) -> List[str]: 586 | pk = self.require_private_key() 587 | 588 | entries = [] 589 | for tx in transactions: 590 | signed_tx = transaction.sign_tx_message_with_private_key(tx, pk) 591 | entries.append( 592 | api.PostSubmitRequestEntry( 593 | transaction=signed_tx, 594 | skip_pre_flight=skip_pre_flight 595 | ) 596 | ) 597 | 598 | result = await self.post_submit_snipe_v2( 599 | post_submit_snipe_request=api.PostSubmitSnipeRequest( 600 | entries=entries, 601 | use_staked_rp_cs=use_staked_rpcs, 602 | timestamp=timestamp() 603 | ) 604 | ) 605 | 606 | return [ 607 | entry.signature 608 | for entry in result.transactions 609 | if entry.submitted 610 | ] 611 | 612 | async def submit_paladin( 613 | self, 614 | signed_tx: str, 615 | revert_protection: bool = False, 616 | ) -> str: 617 | result = await self.post_submit_paladin_v2( 618 | post_submit_paladin_request=api.PostSubmitPaladinRequest( 619 | transaction=api.TransactionMessageV2( 620 | content=signed_tx 621 | ), 622 | revert_protection = revert_protection, 623 | timestamp=timestamp() 624 | ) 625 | ) 626 | 627 | return result.signature 628 | 629 | 630 | class NotConnectedException(Exception): 631 | pass 632 | -------------------------------------------------------------------------------- /bxsolana/provider/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | _mainnet_ny = "ny.solana.dex.blxrbdn.com" 4 | _mainnet_uk = "uk.solana.dex.blxrbdn.com" 5 | _mainnet_la = "la.solana.dex.blxrbdn.com" 6 | _mainnet_frankfurt = "germany.solana.dex.blxrbdn.com" 7 | _mainnet_amsterdam = "amsterdam.solana.dex.blxrbdn.com" 8 | _mainnet_tokyo = "tokyo.solana.dex.blxrbdn.com" 9 | _mainnet_pump_ny = "pump-ny.solana.dex.blxrbdn.com" 10 | _mainnet_pump_uk = "pump-uk.solana.dex.blxrbdn.com" 11 | _testnet = "solana.dex.bxrtest.com" 12 | _devnet = "solana-trader-api-nlb-6b0f765f2fc759e1.elb.us-east-1.amazonaws.com" 13 | 14 | class Region(Enum): 15 | NY = "NY" 16 | UK = "UK" 17 | 18 | def http_endpoint(base: str, secure: bool) -> str: 19 | prefix = "http" 20 | if secure: 21 | prefix = "https" 22 | return f"{prefix}://{base}" 23 | 24 | 25 | def ws_endpoint(base: str, secure: bool) -> str: 26 | prefix = "ws" 27 | if secure: 28 | prefix = "wss" 29 | return f"{prefix}://{base}/ws" 30 | 31 | 32 | MAINNET_API_GRPC_PORT = 443 33 | 34 | 35 | # TODO: Create Provider level functionality based on region type 36 | MAINNET_API_NY_HTTP = http_endpoint(_mainnet_ny, True) 37 | MAINNET_API_NY_WS = ws_endpoint(_mainnet_ny, True) 38 | MAINNET_API_NY_GRPC_HOST = _mainnet_ny 39 | 40 | MAINNET_API_UK_HTTP = http_endpoint(_mainnet_uk, True) 41 | MAINNET_API_UK_WS = ws_endpoint(_mainnet_uk, True) 42 | MAINNET_API_UK_GRPC_HOST = _mainnet_uk 43 | 44 | # Pump Only Regions 45 | # The following URLs are used for Trader API instances that support Pump Fun streams (Raydium is disabled) 46 | # Not all documented trader API endpoints are supported 47 | # See documentation: https://docs.bloxroute.com/solana/trader-api/introduction 48 | MAINNET_API_PUMP_NY_HTTP = http_endpoint(_mainnet_pump_ny, True) 49 | MAINNET_API_PUMP_NY_WS = ws_endpoint(_mainnet_pump_ny, True) 50 | MAINNET_API_PUMP_NY_GRPC_HOST = _mainnet_pump_ny 51 | 52 | # Submit Only Regions 53 | # Most functionality of Solana Trader API is disabled on the following URLs, however they give coverage to 54 | # tx submissions in many different endpoints in the world 55 | # see documentation: https://docs.bloxroute.com/solana/trader-api/introduction/regions 56 | MAINNET_API_LA_HTTP = http_endpoint(_mainnet_la, True) 57 | MAINNET_API_LA_WS = ws_endpoint(_mainnet_la, True) 58 | MAINNET_API_LA_GRPC_HOST = _mainnet_la 59 | 60 | MAINNET_API_AMS_HTTP = http_endpoint(_mainnet_amsterdam, True) 61 | MAINNET_API_AMS_WS = ws_endpoint(_mainnet_amsterdam, True) 62 | MAINNET_API_AMS_GRPC_HOST = _mainnet_amsterdam 63 | 64 | MAINNET_API_TOKYO_HTTP = http_endpoint(_mainnet_tokyo, True) 65 | MAINNET_API_TOKYO_WS = ws_endpoint(_mainnet_tokyo, True) 66 | MAINNET_API_TOKYO_GRPC_HOST = _mainnet_tokyo 67 | 68 | MAINNET_API_FRANKFURT_HTTP = http_endpoint(_mainnet_frankfurt, True) 69 | MAINNET_API_FRANKFURT_WS = ws_endpoint(_mainnet_frankfurt, True) 70 | MAINNET_API_FRANKFURT_GRPC_HOST = _mainnet_frankfurt 71 | 72 | # TESTNET and DEVNET are not stable - Please use with caution 73 | TESTNET_API_HTTP = http_endpoint(_testnet, True) 74 | TESTNET_API_WS = ws_endpoint(_testnet, True) 75 | TESTNET_API_GRPC_HOST = _testnet 76 | TESTNET_API_GRPC_PORT = 443 77 | 78 | DEVNET_API_HTTP = http_endpoint(_devnet, False) 79 | DEVNET_API_WS = ws_endpoint(_devnet, False) 80 | DEVNET_API_GRPC_HOST = _devnet 81 | DEVNET_API_GRPC_PORT = 80 82 | 83 | LOCAL_API_HTTP = "http://127.0.0.1:9000" 84 | LOCAL_API_WS = "ws://127.0.0.1:9000/ws" 85 | LOCAL_API_GRPC_HOST = "127.0.0.1" 86 | LOCAL_API_GRPC_PORT = 9000 87 | -------------------------------------------------------------------------------- /bxsolana/provider/grpc.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | from grpclib import client 5 | from solders import keypair as kp # pyre-ignore[21]: module is too hard to find 6 | 7 | from .. import transaction 8 | from . import constants 9 | from .base import Provider 10 | from .package_info import NAME, VERSION 11 | 12 | 13 | class GrpcProvider(Provider): 14 | # pyre-ignore[15]: overriding to force context manager hooks 15 | channel: Optional[client.Channel] = None # pyre-ignore[11]: annotation 16 | 17 | _host: str # pyre-ignore[11]: annotation 18 | _port: int # pyre-ignore[11]: annotation 19 | _auth_header: str # pyre-ignore[11]: annotation 20 | _use_ssl: bool # pyre-ignore[11]: annotation 21 | _private_key: Optional[kp.Keypair] # pyre-ignore[11]: annotation 22 | 23 | def __init__( 24 | self, 25 | host: str = constants.MAINNET_API_UK_GRPC_HOST, 26 | port: int = constants.MAINNET_API_GRPC_PORT, 27 | private_key: Optional[str] = None, 28 | auth_header: Optional[str] = None, 29 | use_ssl: bool = False, 30 | *, 31 | timeout: Optional[float] = None, 32 | ): 33 | self._host = host 34 | self._port = port 35 | self._use_ssl = use_ssl 36 | 37 | if private_key is None: 38 | try: 39 | self._private_key = transaction.load_private_key_from_env() 40 | except EnvironmentError: 41 | self._private_key = None 42 | else: 43 | self._private_key = transaction.load_private_key(private_key) 44 | 45 | if auth_header is None: 46 | self._auth_header = os.environ["AUTH_HEADER"] 47 | else: 48 | self._auth_header = auth_header 49 | 50 | super().__init__( 51 | # pyre-ignore[6]: overriding to force context manager hooks 52 | None, 53 | timeout=timeout, 54 | ) 55 | 56 | async def connect(self): 57 | if self.channel is None: 58 | self.channel = client.Channel( 59 | self._host, self._port, ssl=self._use_ssl 60 | ) 61 | self.metadata = { 62 | "authorization": self._auth_header, 63 | "x-sdk": NAME, 64 | "s-sdk-version": VERSION, 65 | } 66 | 67 | def private_key(self) -> Optional[kp.Keypair]: 68 | return self._private_key 69 | 70 | async def close(self): 71 | channel = self.channel 72 | if channel is not None: 73 | self.channel.close() 74 | 75 | 76 | def grpc(auth_header: Optional[str] = None, region: Optional[constants.Region] = None) -> GrpcProvider: 77 | # Default to UK if no region specified 78 | if region is None or region == constants.Region.UK: 79 | host = constants.MAINNET_API_UK_GRPC_HOST 80 | elif region == constants.Region.NY: 81 | host = constants.MAINNET_API_NY_GRPC_HOST 82 | else: 83 | raise ValueError(f"Unsupported region: {region}") 84 | 85 | return GrpcProvider( 86 | host=host, 87 | port=constants.MAINNET_API_GRPC_PORT, 88 | use_ssl=True 89 | ) 90 | 91 | def grpc_pump_ny(auth_header: Optional[str] = None) -> Provider: 92 | return GrpcProvider( 93 | host=constants.MAINNET_API_PUMP_NY_GRPC_HOST, 94 | auth_header=auth_header, 95 | use_ssl=True, 96 | ) 97 | 98 | 99 | def grpc_testnet(auth_header: Optional[str] = None) -> Provider: 100 | return GrpcProvider( 101 | host=constants.TESTNET_API_GRPC_HOST, 102 | port=constants.TESTNET_API_GRPC_PORT, 103 | auth_header=auth_header, 104 | ) 105 | 106 | 107 | def grpc_devnet(auth_header: Optional[str] = None) -> Provider: 108 | return GrpcProvider( 109 | host=constants.DEVNET_API_GRPC_HOST, 110 | port=constants.DEVNET_API_GRPC_PORT, 111 | auth_header=auth_header, 112 | ) 113 | 114 | 115 | def grpc_local(auth_header: Optional[str] = None) -> Provider: 116 | return GrpcProvider( 117 | host=constants.LOCAL_API_GRPC_HOST, 118 | port=constants.LOCAL_API_GRPC_PORT, 119 | auth_header=auth_header, 120 | ) 121 | -------------------------------------------------------------------------------- /bxsolana/provider/http_error.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | 3 | import aiohttp 4 | import betterproto 5 | 6 | 7 | class HttpError(Exception): 8 | code: int 9 | message: str 10 | details: List[str] 11 | 12 | def __init__(self, code: int, message: str, details: List[str]): 13 | super().__init__() 14 | 15 | self.code = code 16 | self.message = message 17 | self.details = details 18 | 19 | def __str__(self): 20 | return f"HttpError[{self.code}]: {self.message} ({self.details})" 21 | 22 | @classmethod 23 | def from_json(cls, payload: Dict[str, Any]) -> "HttpError": 24 | return cls( 25 | payload["code"], 26 | payload["message"], 27 | payload["details"], 28 | ) 29 | 30 | 31 | async def map_response( 32 | response: aiohttp.ClientResponse, destination: betterproto.Message 33 | ): 34 | if response.status != 200: 35 | response_text = await response.text() 36 | raise HttpError(code=response.status, message=response_text, details=[]) 37 | response_json = await response.json() 38 | try: 39 | http_error = HttpError.from_json(response_json) 40 | raise http_error 41 | except KeyError: 42 | return destination.from_dict(response_json) 43 | -------------------------------------------------------------------------------- /bxsolana/provider/jsonrpc_patch.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from jsonrpc.types.server_error import RpcErrorCode, message_map, RpcError 3 | 4 | 5 | class NewRpcErrorCode(enum.Enum): 6 | PARSE_ERROR = -32700 7 | INVALID_REQUEST = -32600 8 | METHOD_NOT_FOUND = -32601 9 | INVALID_PARAMS = -32602 10 | INTERNAL_ERROR = -32603 11 | AUTHORIZATION_ERROR = -32004 12 | RATE_LIMIT_ERROR = -32005 13 | STREAM_LIMIT_ERROR = -32006 14 | 15 | def message(self) -> str: 16 | return message_map[self] 17 | 18 | 19 | RpcErrorCode = NewRpcErrorCode # noqa: F811 20 | 21 | message_map = { # noqa: F811 22 | NewRpcErrorCode.PARSE_ERROR: "Parse error", 23 | NewRpcErrorCode.INVALID_REQUEST: "Invalid request", 24 | NewRpcErrorCode.METHOD_NOT_FOUND: "Invalid method", 25 | NewRpcErrorCode.INVALID_PARAMS: "Invalid params", 26 | NewRpcErrorCode.INTERNAL_ERROR: "Internal error", 27 | NewRpcErrorCode.AUTHORIZATION_ERROR: "Invalid account ID", 28 | NewRpcErrorCode.RATE_LIMIT_ERROR: "Rate limit reached", 29 | NewRpcErrorCode.STREAM_LIMIT_ERROR: "Max number of subscriptions error", 30 | } 31 | 32 | # Save the original from_json method 33 | original_from_json = RpcError.from_json 34 | 35 | 36 | # Define a new from_json method 37 | @classmethod 38 | def new_from_json(cls, payload: dict): 39 | code = payload.get("code") 40 | message = payload.get("message", "") 41 | data = payload.get("data") 42 | request_id = payload.get("id") 43 | 44 | if code in [e.value for e in RpcErrorCode]: 45 | return cls(RpcErrorCode(code), request_id, data, message=message) 46 | else: 47 | # Fallback (shouldn't happen) 48 | return original_from_json(cls, payload) 49 | 50 | 51 | # Patch the RpcError class with the new from_json method 52 | RpcError.from_json = new_from_json 53 | -------------------------------------------------------------------------------- /bxsolana/provider/package_info.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | dist = pkg_resources.get_distribution("bxsolana-trader") 4 | NAME = dist.project_name 5 | VERSION = dist.version 6 | -------------------------------------------------------------------------------- /bxsolana/provider/ws.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import os 3 | import re 4 | from typing import AsyncGenerator, Dict, Optional, TYPE_CHECKING, Type 5 | 6 | import jsonrpc 7 | from . import jsonrpc_patch # noqa: F401, Used for side-effect patching 8 | from solders import keypair as kp # pyre-ignore[21]: module is too hard to find 9 | 10 | from . import Provider, constants 11 | from .. import transaction 12 | from .package_info import NAME, VERSION 13 | 14 | from grpclib.metadata import Deadline 15 | from grpclib.metadata import _MetadataLike as MetadataLike 16 | 17 | if TYPE_CHECKING: 18 | # noinspection PyUnresolvedReferences,PyProtectedMember 19 | # pyre-ignore[21]: module is too hard to find 20 | from grpclib._protocols import IProtoMessage 21 | 22 | # noinspection PyProtectedMember 23 | from betterproto import T 24 | 25 | 26 | class WsProvider(Provider): 27 | _ws: jsonrpc.WsRpcConnection # pyre-ignore[11]: annotation 28 | 29 | _endpoint: str # pyre-ignore[11]: annotation 30 | _private_key: Optional[kp.Keypair] # pyre-ignore[11]: annotation 31 | 32 | 33 | def __init__( 34 | self, 35 | endpoint: str = constants.MAINNET_API_UK_WS, 36 | auth_header: Optional[str] = None, 37 | private_key: Optional[str] = None, 38 | request_timeout_s: Optional[int] = None, 39 | ): 40 | self._endpoint = endpoint 41 | 42 | if auth_header is None: 43 | auth_header = os.environ["AUTH_HEADER"] 44 | 45 | opts = jsonrpc.WsRpcOpts( 46 | headers={ 47 | "authorization": auth_header, 48 | "x-sdk": NAME, 49 | "x-sdk-version": VERSION, 50 | }, 51 | request_timeout_s=request_timeout_s, 52 | ) 53 | self._ws = jsonrpc.WsRpcConnection(endpoint, opts) 54 | 55 | if private_key is None: 56 | try: 57 | self._private_key = transaction.load_private_key_from_env() 58 | except EnvironmentError: 59 | self._private_key = None 60 | else: 61 | self._private_key = transaction.load_private_key(private_key) 62 | 63 | async def connect(self): 64 | await self._ws.connect() 65 | 66 | def private_key(self) -> Optional[kp.Keypair]: 67 | return self._private_key 68 | 69 | async def close(self): 70 | await self._ws.close() 71 | 72 | async def _unary_unary( 73 | self, 74 | route: str, 75 | # pyre-ignore[11]: type is too hard to find 76 | request: "IProtoMessage", 77 | response_type: Type["T"], 78 | *, 79 | timeout: Optional[float] = None, 80 | deadline: Optional["Deadline"] = None, 81 | metadata: Optional["MetadataLike"] = None, 82 | ) -> "T": 83 | request_dict = request.to_dict(include_default_values=False) 84 | if "clientOrderId" in request_dict: 85 | request_dict["clientOrderID"] = request_dict.pop("clientOrderId") 86 | 87 | if "orderId" in request_dict: 88 | request_dict["orderID"] = request_dict.pop("orderId") 89 | 90 | if "useStakedRpCs" in request_dict: 91 | request_dict["useStakedRPCs"] = request_dict.pop("useStakedRpCs") 92 | 93 | result = await self._ws.call(_ws_endpoint(route), request_dict) 94 | response = _validated_response(result, response_type) 95 | return response 96 | 97 | async def _unary_stream( 98 | self, 99 | route: str, 100 | request: "IProtoMessage", 101 | response_type: Type["T"], 102 | *, 103 | timeout: Optional[float] = None, 104 | deadline: Optional["Deadline"] = None, 105 | metadata: Optional["MetadataLike"] = None, 106 | ) -> AsyncGenerator["T", None]: 107 | subscription_id = await self._ws.subscribe( 108 | _ws_endpoint(route), request.to_dict() 109 | ) 110 | async for update in self._ws.notifications_for_id(subscription_id): 111 | response = _validated_response(update, response_type) 112 | yield response 113 | 114 | 115 | def _ws_endpoint(route: str) -> str: 116 | return route.split("/")[-1] 117 | 118 | 119 | def ws(region: Optional[constants.Region] = None) -> WsProvider: 120 | # Default to UK if no region specified 121 | if region is None or region == constants.Region.UK: 122 | endpoint = constants.MAINNET_API_UK_WS 123 | elif region == constants.Region.NY: 124 | endpoint = constants.MAINNET_API_NY_WS 125 | else: 126 | raise ValueError(f"Unsupported region: {region}") 127 | 128 | # Pass the appropriate endpoint to WsProvider 129 | return WsProvider(endpoint=endpoint) 130 | 131 | def ws_pump_ny() -> Provider: 132 | return WsProvider(endpoint=constants.MAINNET_API_PUMP_NY_WS) 133 | 134 | 135 | def ws_testnet() -> Provider: 136 | return WsProvider(endpoint=constants.TESTNET_API_WS) 137 | 138 | 139 | def ws_devnet() -> Provider: 140 | return WsProvider(endpoint=constants.DEVNET_API_WS) 141 | 142 | 143 | def ws_local() -> Provider: 144 | return WsProvider(endpoint=constants.LOCAL_API_WS) 145 | 146 | 147 | def _validated_response(response: Dict, response_type: Type["T"]) -> "T": 148 | if not isinstance(response, dict): 149 | raise Exception(f"response {response} was not a dictionary") 150 | 151 | if "message" in response: 152 | raise Exception(response["message"]) 153 | 154 | message = response_type().from_dict(response) 155 | 156 | fields = list(dataclasses.fields(message)) 157 | field_names = [field.name for field in fields] 158 | 159 | for field in field_names: 160 | if camelcase(field) not in response: 161 | raise Exception( 162 | f"didn't find field {camelcase(field)}, {field}, response" 163 | f" {response} was not of type {response_type}" 164 | ) 165 | 166 | return message 167 | 168 | 169 | def camelcase(string): 170 | """Convert string into camel case. 171 | 172 | Args: 173 | string: String to convert. 174 | 175 | Returns: 176 | string: Camel case string. 177 | 178 | """ 179 | 180 | string = re.sub(r"\w[\s\W]+\w", "", str(string)) 181 | if not string: 182 | return string 183 | val = (string[0]).lower() + re.sub( 184 | r"[\-_\.\s]([a-z])", 185 | lambda matched: str(matched.group(1)).upper(), 186 | string[1:], 187 | ) 188 | return re.sub(r"account[iI][dD]", "accountID", val, flags=re.IGNORECASE) 189 | -------------------------------------------------------------------------------- /bxsolana/transaction/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from .private_txs import ( 4 | create_trader_api_tip_instruction, # noqa: F401 5 | create_trader_api_tip_tx_signed # noqa: F401 6 | ) 7 | 8 | from .memo import ( 9 | create_trader_api_memo_instruction, 10 | add_memo_to_serialized_txn, 11 | ) 12 | 13 | from .signing import ( 14 | load_private_key, 15 | load_private_key_from_env, 16 | sign_tx, 17 | sign_tx_with_private_key, 18 | sign_tx_message_with_private_key, 19 | load_open_orders, sign_tx_message_with_private_key_v2, 20 | ) 21 | 22 | __all__ = [ 23 | "load_private_key", 24 | "load_private_key_from_env", 25 | "sign_tx", 26 | "sign_tx_with_private_key", 27 | "sign_tx_message_with_private_key", 28 | "sign_tx_message_with_private_key_v2", 29 | "load_open_orders", 30 | "create_trader_api_memo_instruction", 31 | "add_memo_to_serialized_txn", 32 | 33 | ] 34 | -------------------------------------------------------------------------------- /bxsolana/transaction/memo.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from solders import pubkey as pk # pyre-ignore[21]: module is too hard to find 4 | from solders import instruction as inst # pyre-ignore[21]: module is too hard to find 5 | from solders import transaction as solders_tx # pyre-ignore[21]: module is too hard to find 6 | from solders import message as solders_msg # pyre-ignore[21]: module is too hard to find 7 | 8 | BxMemoMarkerMsg = "Powered by bloXroute Trader Api" 9 | TraderAPIMemoProgram = pk.Pubkey.from_string( 10 | "HQ2UUt18uJqKaQFJhgV9zaTdQxUZjNrsKFgoEDquBkcx" 11 | ) 12 | 13 | 14 | # create_trader_api_memo_instruction generates a transaction instruction that places a memo in the transaction log 15 | # Having a memo instruction with signals Trader-API usage is required 16 | def create_trader_api_memo_instruction( 17 | msg: str, 18 | ) -> inst.Instruction: # pyre-ignore[11]: annotation 19 | if msg == "": 20 | msg = BxMemoMarkerMsg 21 | 22 | data = bytes(msg, "utf-8") 23 | instruction = inst.Instruction(TraderAPIMemoProgram, data, []) 24 | 25 | return instruction 26 | 27 | 28 | def create_compiled_memo_instruction( 29 | program_id_index: int, 30 | ) -> inst.CompiledInstruction: # pyre-ignore[11]: annotation 31 | data = bytes(BxMemoMarkerMsg, "utf-8") 32 | instruction = inst.CompiledInstruction(program_id_index, data, bytes([])) 33 | 34 | return instruction 35 | 36 | 37 | def add_memo( 38 | tx: solders_tx.VersionedTransaction, # pyre-ignore[11]: annotation 39 | ) -> solders_tx.VersionedTransaction: 40 | instructions = tx.message.instructions 41 | accounts = tx.message.account_keys 42 | msg = tx.message 43 | 44 | cutoff = len(tx.message.account_keys) 45 | 46 | for i in range(len(instructions)): 47 | idxs = list(instructions[i].accounts) 48 | for j in range(len(idxs)): 49 | if idxs[j] >= cutoff: 50 | idxs[j] = idxs[j] + 1 51 | 52 | memo = create_compiled_memo_instruction(cutoff) 53 | 54 | accounts.append(TraderAPIMemoProgram) 55 | instructions.append(memo) 56 | if isinstance(msg, solders_msg.MessageV0): 57 | message = solders_msg.MessageV0( 58 | msg.header, 59 | accounts, 60 | msg.recent_blockhash, 61 | instructions, 62 | msg.address_table_lookups, 63 | ) 64 | return solders_tx.VersionedTransaction.populate(message, tx.signatures) 65 | else: 66 | message = solders_msg.Message.new_with_compiled_instructions( 67 | msg.header.num_required_signatures, 68 | msg.header.num_readonly_signed_accounts, 69 | msg.header.num_readonly_unsigned_accounts, 70 | accounts, 71 | msg.recent_blockhash, 72 | instructions, 73 | ) 74 | return solders_tx.VersionedTransaction.populate(message, tx.signatures) 75 | 76 | 77 | # add_memo_to_serialized_txn adds memo instruction to a serialized transaction, it's primarily used if the user 78 | # doesn't want to interact with Trader-API directly 79 | def add_memo_to_serialized_txn(tx_base64: str) -> str: 80 | b = base64.b64decode(tx_base64) 81 | 82 | raw_tx = solders_tx.VersionedTransaction.from_bytes(b) 83 | 84 | tx = add_memo(raw_tx) 85 | 86 | return base64.b64encode(bytes(tx)).decode("utf-8") 87 | -------------------------------------------------------------------------------- /bxsolana/transaction/private_txs.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from bxsolana_trader_proto import api as proto 3 | 4 | from numpy import uint64 5 | from solders import pubkey as pk # pyre-ignore[21]: module is too hard to find 6 | from solders import instruction as inst # pyre-ignore[21]: module is too hard to find 7 | from solders import transaction as solders_tx # pyre-ignore[21]: module is too hard to find 8 | from solders.hash import Hash 9 | from solders.keypair import Keypair 10 | from solders.message import MessageV0 11 | from solders.pubkey import Pubkey 12 | from solders.system_program import transfer, TransferParams 13 | from solders.transaction import VersionedTransaction 14 | from solders import message as msg # pyre-ignore[21]: module is too hard to find 15 | 16 | 17 | # as of 2/12/2024, this is the bloxRoute tip wallet... check docs to see latest up to date tip wallet: 18 | # https://docs.bloxroute.com/solana/trader-api-v2/front-running-protection-and-transaction-bundle 19 | BloxrouteTipWallet = pk.Pubkey.from_string( 20 | "HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY" 21 | ) 22 | 23 | 24 | # create_trader_api_tip_instruction creates a tip instruction to send to bloxRoute. This is used if a user wants to send 25 | # bundles or wants front running protection. If using bloXroute API, this instruction must be included in the last 26 | # transaction sent to the API 27 | def create_trader_api_tip_instruction( 28 | tip_amount: uint64, 29 | sender_address: Pubkey, # pyre-ignore[11]: annotation 30 | ) -> inst.Instruction: # pyre-ignore[11]: annotation 31 | instruction = transfer( 32 | TransferParams( 33 | from_pubkey=sender_address, 34 | to_pubkey=BloxrouteTipWallet, 35 | lamports=int(tip_amount), 36 | ) 37 | ) 38 | 39 | return instruction 40 | 41 | 42 | # create_trader_api_tip_instruction creates a tip transaction to send to bloxRoute. This is used if a user wants to send 43 | # bundles or wants front running protection. If using bloXroute API, this transaction must be the last transaction sent 44 | # to the api 45 | def create_trader_api_tip_tx_signed( 46 | tip_amount: int, sender_address: Keypair, blockhash: Hash, # pyre-ignore[11]: annotation 47 | ) -> proto.TransactionMessage: 48 | transfer_ix = create_trader_api_tip_instruction( 49 | uint64(tip_amount), sender_address.pubkey() 50 | ) 51 | 52 | message = MessageV0.try_compile( 53 | payer=sender_address.pubkey(), 54 | instructions=[transfer_ix], 55 | address_lookup_table_accounts=[], 56 | recent_blockhash=blockhash, 57 | ) 58 | 59 | tx = VersionedTransaction(message, [sender_address]) 60 | 61 | signature = sender_address.sign_message(msg.to_bytes_versioned(tx.message)) 62 | signatures = [signature] 63 | 64 | if len(tx.signatures) > 1: 65 | signatures.extend(list(tx.signatures[1:])) 66 | 67 | tx = solders_tx.VersionedTransaction.populate(tx.message, signatures) 68 | 69 | # convert transaction back to base64 70 | signed_tx_bytes_base64 = base64.b64encode(bytes(tx)) 71 | 72 | return proto.TransactionMessage(content=signed_tx_bytes_base64.decode('utf-8'), is_cleanup=False) 73 | -------------------------------------------------------------------------------- /bxsolana/transaction/signing.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base58 3 | import base64 4 | 5 | from solders import keypair as kp # pyre-ignore[21]: module is too hard to find 6 | from solders import transaction as solders_tx # pyre-ignore[21]: module is too hard to find 7 | from solders import message as msg # pyre-ignore[21]: module is too hard to find 8 | 9 | from bxsolana_trader_proto import api as proto 10 | 11 | 12 | def load_private_key(pkey_str: str) -> kp.Keypair: # pyre-ignore[11]: annotation 13 | # convert base58 private key string to a keypair 14 | pkey_bytes = bytes(pkey_str, encoding="utf-8") 15 | pkey_bytes_base58 = base58.b58decode(pkey_bytes) 16 | return kp.Keypair.from_bytes(pkey_bytes_base58) 17 | 18 | 19 | def load_private_key_from_env() -> kp.Keypair: 20 | # get base58 encoded private key 21 | pkey_str = os.getenv("PRIVATE_KEY") 22 | if pkey_str is None: 23 | raise EnvironmentError("env variable `PRIVATE_KEY` not set") 24 | 25 | return load_private_key(pkey_str) 26 | 27 | 28 | def load_open_orders() -> str: 29 | open_orders = os.getenv("OPEN_ORDERS") 30 | if open_orders is None: 31 | raise EnvironmentError("env variable `OPEN_ORDERS` not set") 32 | 33 | return open_orders 34 | 35 | 36 | def sign_tx(unsigned_tx_base64: str) -> str: 37 | """ 38 | Uses environment variable `PRIVATE_KEY` to sign message content and replace zero signatures. 39 | 40 | :param unsigned_tx_base64: transaction bytes in base64 41 | :return: signed transaction 42 | """ 43 | keypair = load_private_key_from_env() 44 | return sign_tx_with_private_key(unsigned_tx_base64, keypair) 45 | 46 | 47 | def sign_tx_with_private_key( 48 | unsigned_tx_base64: str, keypair: kp.Keypair 49 | ) -> str: 50 | """ 51 | Signs message content and replaces placeholder zero signature with signature. 52 | 53 | :param unsigned_tx_base64: transaction bytes in base64 54 | :param keypair: key pair to sign with 55 | :return: signed transaction 56 | """ 57 | b = base64.b64decode(unsigned_tx_base64) 58 | 59 | raw_tx = solders_tx.VersionedTransaction.from_bytes(b) 60 | 61 | signature = keypair.sign_message(msg.to_bytes_versioned(raw_tx.message)) 62 | signatures = [signature] 63 | 64 | if len(raw_tx.signatures) > 1: 65 | signatures.extend(list(raw_tx.signatures[1:])) 66 | 67 | tx = solders_tx.VersionedTransaction.populate(raw_tx.message, signatures) 68 | 69 | # convert transaction back to base64 70 | signed_tx_bytes_base64 = base64.b64encode(bytes(tx)) 71 | return signed_tx_bytes_base64.decode("utf-8") 72 | 73 | 74 | def sign_tx_message_with_private_key( 75 | tx_message: proto.TransactionMessage, keypair: kp.Keypair 76 | ) -> proto.TransactionMessage: 77 | return proto.TransactionMessage( 78 | sign_tx_with_private_key(tx_message.content, keypair), 79 | tx_message.is_cleanup, 80 | ) 81 | 82 | 83 | def sign_tx_message_with_private_key_v2( 84 | tx_message: proto.TransactionMessageV2, keypair: kp.Keypair 85 | ) -> proto.TransactionMessage: 86 | return proto.TransactionMessage( 87 | sign_tx_with_private_key(tx_message.content, keypair), 88 | ) 89 | -------------------------------------------------------------------------------- /bxsolana/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .timestamp import timestamp, timestamp_rfc3339 2 | 3 | __all__ = [ 4 | "timestamp", 5 | "timestamp_rfc3339" 6 | ] -------------------------------------------------------------------------------- /bxsolana/utils/timestamp.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from betterproto import Timestamp 3 | 4 | 5 | def timestamp(): 6 | """ 7 | Creates and returns a Protocol Buffer Timestamp object based on the current time. 8 | 9 | This function captures the current time and converts it to a Protocol Buffer 10 | Timestamp object with seconds since epoch and nanoseconds precision. 11 | 12 | Returns: 13 | Timestamp: A Protocol Buffer Timestamp object representing the current time 14 | with seconds and nanoseconds components. 15 | """ 16 | now = datetime.datetime.now() 17 | timestamp = Timestamp( 18 | seconds=int(now.timestamp()), 19 | nanos=now.microsecond * 1000 20 | ) 21 | print(timestamp) 22 | return timestamp 23 | 24 | 25 | def timestamp_rfc3339(): 26 | """ 27 | Returns the current time as an RFC 3339 formatted string suitable for 28 | Protocol Buffer Timestamp JSON serialization. 29 | 30 | Reference: https://protobuf.dev/reference/php/api-docs/Google/Protobuf/Timestamp.html 31 | 32 | The format is: "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 33 | - All components except year are zero-padded to two digits 34 | - Year is expressed using four digits 35 | - Fractional seconds can go up to 9 digits (nanosecond precision) 36 | - The "Z" suffix indicates UTC timezone 37 | 38 | Returns: 39 | str: Current time in RFC 3339 format with UTC timezone (Z) 40 | """ 41 | 42 | now = datetime.datetime.now(datetime.timezone.utc) 43 | formatted_time = now.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + '000Z' 44 | 45 | return formatted_time -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # `solana-trader-client-python` SDK examples 2 | 3 | - Historically, examples are run from this directory in the 'bundles', 'provider', and 'transaction' directory, calling a set of functions from the 'bxsolana' package. We have added `sdk.py` to streamline running examples with this SDK, allowing you to run each endpoint/stream individually, on a per provider (WS, GRPC, HTTP) basis. If you would like to modify the examples to change parameters, amounts, etc, feel free to do so in the example functions in the file and rerun. 4 | - If certain examples submit transactions on chian, and you don't see transactions landing, modify parameters of `computeLimit`, `computePrice` and `tip` parameters. These adjust the tip amount to be sent to RPCs as well as priority fees. You can read more about it here: [Trader API Docs](https://docs.bloxroute.com/solana/trader-api-v2) 5 | 6 | ## How to Run SDK 7 | 8 | Set up your Environment Variables: 9 | ``` 10 | AUTH_HEADER: bloXRoute Auth Header 11 | PRIVATE_KEY: solana signing key to be used for examples 12 | PUBLIC_KEY: solana public key to be used for examples (default `payer` if not specified) 13 | PAYER: payer responsible for transaction fees (optional) 14 | OPEN_ORDERS: openbook open orders address (optional) 15 | ``` 16 | 17 | Once your environment is set run 18 | 19 | `python sdk.py` 20 | 21 | After this, follow menu to select whatever you want. This should give you a feeling of the services trader-api provides. 22 | -------------------------------------------------------------------------------- /example/bundles/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from bxsolana_trader_proto import api as proto 5 | from bxsolana_trader_proto.common import OrderType 6 | 7 | from bxsolana.transaction import signing 8 | 9 | from bxsolana import provider, examples 10 | 11 | # TODO: Add some logic here to indicate to user if missing needed environment variables for tests 12 | public_key = os.getenv("PUBLIC_KEY") 13 | private_key = os.getenv("PRIVATE_KEY") 14 | open_orders = os.getenv("OPEN_ORDERS") 15 | base_token_wallet = os.getenv("BASE_TOKEN_WALLET") 16 | quote_token_wallet = os.getenv("QUOTE_TOKEN_WALLET") 17 | 18 | market_addr = "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT" # SOL/USDC 19 | order_side = proto.Side.S_ASK 20 | order_type = OrderType.OT_LIMIT 21 | order_price = 170200 22 | order_amount = 0.1 23 | 24 | 25 | async def main(): 26 | await ws() 27 | await grpc() 28 | await http() 29 | 30 | 31 | async def ws(): 32 | print("\n*** WS Example using Openbook bundles ***\n") 33 | async with provider.ws_testnet() as api: 34 | openbook_bundle_tx = await api.post_order_v2( 35 | post_order_request_v2=proto.PostOrderRequestV2( 36 | owner_address=public_key, 37 | payer_address=public_key, 38 | market="SOLUSDC", 39 | side="ASK", 40 | amount=0.01, 41 | price=150_000, 42 | type="limit", 43 | tip=1030, 44 | ) 45 | ) 46 | 47 | print( 48 | "created OPENBOOK tx with bundle tip of 1030:" 49 | f" {openbook_bundle_tx.transaction.content}" 50 | ) 51 | 52 | signed_tx = signing.sign_tx(openbook_bundle_tx.transaction.content) 53 | 54 | post_submit_response = await api.post_submit( 55 | post_submit_request=proto.PostSubmitRequest( 56 | transaction=proto.TransactionMessage(content=signed_tx), 57 | skip_pre_flight=True, 58 | front_running_protection=True, 59 | ) 60 | ) 61 | 62 | print( 63 | "submitted OPENBOOK tx with front running protection:" 64 | f" {openbook_bundle_tx.transaction.content}" 65 | ) 66 | 67 | 68 | async def grpc(): 69 | print("\n*** GRPC Test ***\n") 70 | async with provider.grpc_testnet() as api: 71 | raydium_bundle_tx = await api.post_raydium_swap( 72 | proto.PostRaydiumSwapRequest( 73 | owner_address=public_key, 74 | in_token="SOL", 75 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 76 | slippage=0.2, 77 | in_amount=0.01, 78 | tip=1030, 79 | ) 80 | ) 81 | 82 | print( 83 | "created RAYDIUM swap tx with bundle tip of 1030:" 84 | f" {raydium_bundle_tx.transactions[0].content}" 85 | ) 86 | 87 | signed_tx = signing.sign_tx(raydium_bundle_tx.transactions[0].content) 88 | 89 | post_submit_response = await api.post_submit( 90 | post_submit_request=proto.PostSubmitRequest( 91 | transaction=proto.TransactionMessage(content=signed_tx), 92 | skip_pre_flight=True, 93 | front_running_protection=True, 94 | ) 95 | ) 96 | 97 | print( 98 | "submitted RAYDIUM tx with front running protection:" 99 | f" {post_submit_response.signature}" 100 | ) 101 | 102 | 103 | async def http(): 104 | print("\n*** HTTP Test ***\n") 105 | async with provider.http_testnet() as api: 106 | raydium_bundle_tx = await api.post_raydium_swap( 107 | proto.PostRaydiumSwapRequest( 108 | owner_address=public_key, 109 | in_token="SOL", 110 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 111 | slippage=0.2, 112 | in_amount=0.01, 113 | tip=1030, 114 | ) 115 | ) 116 | 117 | print( 118 | "created RAYDIUM swap tx with bundle tip of 1030:" 119 | f" {raydium_bundle_tx.transactions[0].content}" 120 | ) 121 | 122 | signed_tx = signing.sign_tx(raydium_bundle_tx.transactions[0].content) 123 | 124 | post_submit_response = await api.post_submit( 125 | post_submit_request=proto.PostSubmitRequest( 126 | transaction=proto.TransactionMessage(content=signed_tx), 127 | skip_pre_flight=True, 128 | front_running_protection=True, 129 | ) 130 | ) 131 | 132 | print( 133 | "submitted RAYDIUM tx with front running protection:" 134 | f" {post_submit_response.signature}" 135 | ) 136 | 137 | 138 | if __name__ == "__main__": 139 | asyncio.run(main()) 140 | -------------------------------------------------------------------------------- /example/provider/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import bxsolana 5 | from bxsolana import provider 6 | from bxsolana import examples 7 | 8 | API_ENV = os.environ.get("API_ENV", "testnet") 9 | if API_ENV not in ["mainnet", "testnet", "local"]: 10 | raise EnvironmentError( 11 | f'invalid API_ENV value: {API_ENV} (valid values: "mainnet", "testnet",' 12 | ' "local)' 13 | ) 14 | 15 | # trades stream is infrequent in terms of updates 16 | RUN_SLOW_STREAMS = os.environ.get("RUN_SLOW_STREAMS", "true") 17 | if RUN_SLOW_STREAMS == "false": 18 | RUN_SLOW_STREAMS = False 19 | else: 20 | RUN_SLOW_STREAMS = True 21 | 22 | RUN_TRADES = os.environ.get("RUN_TRADES", "false") 23 | if RUN_TRADES == "false": 24 | RUN_TRADES = False 25 | else: 26 | RUN_TRADES = True 27 | 28 | 29 | async def main(): 30 | await http() 31 | await ws() 32 | await grpc() 33 | 34 | 35 | async def http(): 36 | # private keys are loaded from environment variable `PRIVATE_KEY` by default 37 | # alternatively, can specify the key manually in base58 str if loaded from other source 38 | # p = provider.HttpProvider("127.0.0.1", 9000, private_key="...") 39 | 40 | if API_ENV == "mainnet": 41 | p = provider.http() 42 | elif API_ENV == "local": 43 | p = provider.http_local() 44 | else: 45 | p = provider.http_testnet() 46 | api = await bxsolana.trader_api(p) 47 | pny = provider.http_pump_ny() 48 | pny_api = await bxsolana.trader_api(pny) 49 | # either `try`/`finally` or `async with` work with each type of provider 50 | try: 51 | await examples.do_requests( 52 | api, 53 | pny_api, 54 | examples.PUBLIC_KEY, 55 | examples.OPEN_ORDERS, 56 | examples.ORDER_ID, 57 | examples.USDC_WALLET, 58 | examples.MARKET, 59 | ) 60 | await examples.do_transaction_requests( 61 | api, 62 | RUN_TRADES, 63 | examples.PUBLIC_KEY, 64 | examples.PUBLIC_KEY, 65 | examples.OPEN_ORDERS, 66 | examples.ORDER_ID, 67 | examples.USDC_WALLET, 68 | examples.MARKET, 69 | ) 70 | except Exception as e: 71 | print(e) 72 | raise e 73 | finally: 74 | await p.close() 75 | 76 | 77 | async def ws(): 78 | if API_ENV == "mainnet": 79 | p = provider.ws() 80 | elif API_ENV == "local": 81 | p = provider.ws_local() 82 | else: 83 | p = provider.ws_testnet() 84 | 85 | pny = provider.ws_pump_ny() 86 | 87 | async with pny as pnyy: 88 | async with p as api: 89 | await examples.do_requests( 90 | api, 91 | pnyy, 92 | examples.PUBLIC_KEY, 93 | examples.OPEN_ORDERS, 94 | examples.ORDER_ID, 95 | examples.USDC_WALLET, 96 | examples.MARKET, 97 | ) 98 | await examples.do_transaction_requests( 99 | api, 100 | RUN_TRADES, 101 | examples.PUBLIC_KEY, 102 | examples.PUBLIC_KEY, 103 | examples.OPEN_ORDERS, 104 | examples.ORDER_ID, 105 | examples.USDC_WALLET, 106 | examples.MARKET, 107 | ) 108 | 109 | await examples.do_stream(api, pnyy, RUN_SLOW_STREAMS) 110 | 111 | 112 | async def grpc(): 113 | if API_ENV == "mainnet": 114 | p = provider.grpc() 115 | elif API_ENV == "local": 116 | p = provider.grpc_local() 117 | else: 118 | p = provider.grpc_testnet() 119 | api = await bxsolana.trader_api(p) 120 | 121 | pumpny = provider.grpc_pump_ny() 122 | pumpny_api = await bxsolana.trader_api(pumpny) 123 | try: 124 | await examples.do_requests( 125 | api, 126 | pumpny_api, 127 | examples.PUBLIC_KEY, 128 | examples.OPEN_ORDERS, 129 | examples.ORDER_ID, 130 | examples.USDC_WALLET, 131 | examples.MARKET, 132 | ) 133 | await examples.do_transaction_requests( 134 | api, 135 | RUN_TRADES, 136 | examples.PUBLIC_KEY, 137 | examples.PUBLIC_KEY, 138 | examples.OPEN_ORDERS, 139 | examples.ORDER_ID, 140 | examples.USDC_WALLET, 141 | examples.MARKET, 142 | ) 143 | await examples.do_stream(api, pumpny_api, RUN_SLOW_STREAMS) 144 | finally: 145 | await p.close() 146 | 147 | 148 | if __name__ == "__main__": 149 | asyncio.run(main()) 150 | -------------------------------------------------------------------------------- /example/transaction/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from bxsolana_trader_proto import api as proto 5 | from bxsolana_trader_proto.common import OrderType 6 | 7 | from bxsolana import provider, examples 8 | 9 | # TODO: Add some logic here to indicate to user if missing needed environment variables for tests 10 | public_key = os.getenv("PUBLIC_KEY") 11 | private_key = os.getenv("PRIVATE_KEY") 12 | open_orders = os.getenv("OPEN_ORDERS") 13 | base_token_wallet = os.getenv("BASE_TOKEN_WALLET") 14 | quote_token_wallet = os.getenv("QUOTE_TOKEN_WALLET") 15 | 16 | market_addr = "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT" # SOL/USDC 17 | order_side = proto.Side.S_ASK 18 | order_type = OrderType.OT_LIMIT 19 | order_price = 170200 20 | order_amount = 0.1 21 | 22 | 23 | async def main(): 24 | await ws() 25 | await grpc() 26 | await http() 27 | 28 | 29 | async def ws(): 30 | print("\n*** WS Test ***\n") 31 | async with provider.ws() as api: 32 | async with provider.ws() as api2: # TODO use same provider when WS streams are separated 33 | await examples.order_lifecycle( 34 | p1=api, 35 | p2=api2, 36 | owner_addr=public_key, 37 | payer_addr=public_key, 38 | market_addr=market_addr, 39 | order_side=order_side, 40 | order_type=order_type, 41 | order_amount=order_amount, 42 | order_price=order_price, 43 | open_orders_addr=open_orders, 44 | base_token_wallet=base_token_wallet, 45 | quote_token_wallet=quote_token_wallet, 46 | ) 47 | 48 | await examples.cancel_all_orders( 49 | api, 50 | owner_addr=public_key, 51 | payer_addr=public_key, 52 | order_side=order_side, 53 | order_type=order_type, 54 | order_amount=order_amount, 55 | order_price=order_price, 56 | open_orders_addr=open_orders, 57 | market_addr=market_addr, 58 | ) 59 | 60 | await examples.replace_order_by_client_order_id( 61 | api, 62 | owner_addr=public_key, 63 | payer_addr=public_key, 64 | market_addr=market_addr, 65 | order_side=order_side, 66 | order_type=order_type, 67 | order_amount=order_amount, 68 | order_price=order_price, 69 | open_orders_addr=open_orders, 70 | ) 71 | 72 | 73 | async def grpc(): 74 | print("\n*** GRPC Test ***\n") 75 | async with provider.grpc() as api: 76 | await examples.order_lifecycle( 77 | p1=api, 78 | p2=api, 79 | owner_addr=public_key, 80 | payer_addr=public_key, 81 | market_addr=market_addr, 82 | order_side=order_side, 83 | order_type=order_type, 84 | order_amount=order_amount, 85 | order_price=order_price, 86 | open_orders_addr=open_orders, 87 | base_token_wallet=base_token_wallet, 88 | quote_token_wallet=quote_token_wallet, 89 | ) 90 | 91 | await examples.cancel_all_orders( 92 | api, 93 | owner_addr=public_key, 94 | payer_addr=public_key, 95 | order_side=order_side, 96 | order_type=order_type, 97 | order_amount=order_amount, 98 | order_price=order_price, 99 | open_orders_addr=open_orders, 100 | market_addr=market_addr, 101 | ) 102 | 103 | await examples.replace_order_by_client_order_id( 104 | api, 105 | owner_addr=public_key, 106 | payer_addr=public_key, 107 | market_addr=market_addr, 108 | order_side=order_side, 109 | order_type=order_type, 110 | order_amount=order_amount, 111 | order_price=order_price, 112 | open_orders_addr=open_orders, 113 | ) 114 | 115 | 116 | async def http(): 117 | print("\n*** HTTP Test ***\n") 118 | async with provider.http() as api: 119 | await examples.cancel_all_orders( 120 | api, 121 | owner_addr=public_key, 122 | payer_addr=public_key, 123 | order_side=order_side, 124 | order_type=order_type, 125 | order_amount=order_amount, 126 | order_price=order_price, 127 | open_orders_addr=open_orders, 128 | market_addr=market_addr, 129 | ) 130 | 131 | await examples.replace_order_by_client_order_id( 132 | api, 133 | owner_addr=public_key, 134 | payer_addr=public_key, 135 | market_addr=market_addr, 136 | order_side=order_side, 137 | order_type=order_type, 138 | order_amount=order_amount, 139 | order_price=order_price, 140 | open_orders_addr=open_orders, 141 | ) 142 | 143 | 144 | if __name__ == "__main__": 145 | asyncio.run(main()) 146 | -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | from asyncio.log import logger 2 | import base64 3 | from collections.abc import Callable, Awaitable 4 | from pprint import pprint 5 | 6 | from solders.hash import Hash 7 | 8 | from bxsolana import provider 9 | from bxsolana_trader_proto import api as proto 10 | 11 | import os 12 | 13 | from bxsolana.transaction import create_trader_api_tip_tx_signed, load_private_key_from_env 14 | 15 | from solders import pubkey as pk # pyre-ignore[21]: module is too hard to find 16 | from solders import instruction as inst # pyre-ignore[21]: module is too hard to find 17 | from solders import transaction as solders_tx # pyre-ignore[21]: module is too hard to find 18 | from solders.hash import Hash 19 | from solders.keypair import Keypair 20 | from solders.message import MessageV0 21 | from solders.pubkey import Pubkey 22 | from solders.system_program import transfer, TransferParams 23 | from solders.compute_budget import set_compute_unit_price 24 | from solders.transaction import VersionedTransaction 25 | from solders import message as msg # pyre-ignore[21]: module is too hard to find 26 | import base64 27 | 28 | 29 | 30 | class Endpoint: 31 | func: Callable[[provider.Provider], Awaitable[bool]] 32 | requires_additional_env_vars: bool 33 | 34 | def __init__(self, func: Callable[[provider.Provider], Awaitable[bool]], requires_additional_env_vars: bool): 35 | self.func = func 36 | self.requires_additional_env_vars = requires_additional_env_vars 37 | 38 | 39 | class EnvironmentVariables: 40 | private_key: str 41 | public_key: str 42 | open_orders_address: str 43 | payer: str 44 | 45 | def __init__(self, private_key, public_key, open_orders_address, payer): 46 | self.private_key = private_key 47 | self.public_key = public_key 48 | self.open_orders_address = open_orders_address 49 | self.payer = payer 50 | 51 | 52 | def initializeEnvironmentVariables() -> EnvironmentVariables: 53 | if not os.getenv("AUTH_HEADER"): 54 | logger.critical("Must specify bloXroute authorization header!") 55 | raise SystemExit("AUTH_HEADER environment variable is required!") 56 | 57 | private_key = os.getenv("PRIVATE_KEY") 58 | if not private_key: 59 | logger.error("PRIVATE_KEY environment variable not set. Cannot run examples requiring transaction submission.") 60 | 61 | public_key = os.getenv("PUBLIC_KEY") 62 | if not public_key: 63 | logger.warning("PUBLIC_KEY environment variable not set. Will skip place/cancel/settle examples.") 64 | 65 | open_orders_address = os.getenv("OPEN_ORDERS") 66 | if not open_orders_address: 67 | logger.error("OPEN_ORDERS environment variable not set. Requests may be slower.") 68 | 69 | payer = os.getenv("PAYER") 70 | if not payer: 71 | if public_key: 72 | logger.warning("PAYER environment variable not set. Defaulting to PUBLIC_KEY as payer.") 73 | payer = public_key 74 | else: 75 | payer = "" 76 | logger.error("PAYER and PUBLIC_KEY environment variables are both unset. PAYER cannot be defaulted.") 77 | 78 | return EnvironmentVariables( 79 | private_key=private_key or "", 80 | public_key=public_key or "", 81 | payer=payer, 82 | open_orders_address="" 83 | ) 84 | 85 | 86 | UserEnvironment = initializeEnvironmentVariables() 87 | 88 | 89 | async def get_markets(p: provider.Provider) -> bool: 90 | resp = await p.get_markets_v2(proto.GetMarketsRequestV2()) 91 | pprint(resp) 92 | 93 | return True if resp.markets is not None else False 94 | 95 | 96 | async def get_pools(p: provider.Provider) -> bool: 97 | resp = await p.get_pools(proto.GetPoolsRequest(projects=[proto.Project.P_RAYDIUM])) 98 | pprint(resp) 99 | return True if resp is not None else False 100 | 101 | 102 | async def get_tickers(p: provider.Provider) -> bool: 103 | resp = await p.get_tickers_v2(proto.GetTickersRequestV2(market="SOLUSDC")) 104 | pprint(resp) 105 | 106 | return True if resp.tickers is not None else False 107 | 108 | 109 | async def get_raydium_clmm_pools(p: provider.Provider) -> bool: 110 | resp = await p.get_raydium_clmm_pools(proto.GetRaydiumClmmPoolsRequest()) 111 | pprint(resp) 112 | 113 | return True if resp.pools is not None else False 114 | 115 | 116 | async def get_orderbook(p: provider.Provider) -> bool: 117 | resp = await p.get_orderbook_v2(proto.GetOrderbookRequestV2("SOL-USDC")) 118 | pprint(resp) 119 | 120 | return True if resp.market is not None else False 121 | 122 | 123 | async def get_raydium_pool_reserves(p: provider.Provider) -> bool: 124 | resp = await p.get_raydium_pool_reserve(proto.GetRaydiumPoolReserveRequest( 125 | pairs_or_addresses=["HZ1znC9XBasm9AMDhGocd9EHSyH8Pyj1EUdiPb4WnZjo", 126 | "D8wAxwpH2aKaEGBKfeGdnQbCc2s54NrRvTDXCK98VAeT"])) 127 | pprint(resp) 128 | 129 | return True if resp.pools is not None else False 130 | 131 | 132 | async def get_market_depth(p: provider.Provider) -> bool: 133 | resp = await p.get_market_depth_v2(proto.GetMarketDepthRequestV2(market="SOLUSDC")) 134 | pprint(resp) 135 | 136 | return True if resp.market is not None else False 137 | 138 | 139 | async def get_open_orders(p: provider.Provider) -> bool: 140 | resp = await p.get_open_orders_v2( 141 | proto.GetOpenOrdersRequestV2(market="SOLUSDC", address="FFqDwRq8B4hhFKRqx7N1M6Dg6vU699hVqeynDeYJdPj5")) 142 | pprint(resp) 143 | 144 | return True if resp.orders is not None else False 145 | 146 | 147 | async def get_transaction(p: provider.Provider) -> bool: 148 | resp = await p.get_transaction( 149 | proto.GetTransactionRequest( 150 | signature="2s48MnhH54GfJbRwwiEK7iWKoEh3uNbS2zDEVBPNu7DaCjPXe3bfqo6RuCg9NgHRFDn3L28sMVfEh65xevf4o5W3")) 151 | pprint(resp) 152 | 153 | return True if resp.slot is not None else False 154 | 155 | 156 | async def get_recent_blockhash(p: provider.Provider) -> bool: 157 | resp = await p.get_recent_block_hash_v2(proto.GetRecentBlockHashRequestV2()) 158 | pprint(resp) 159 | 160 | return True if resp.block_hash is not None else False 161 | 162 | 163 | async def get_recent_blockhash_offset(p: provider.Provider) -> bool: 164 | resp = await p.get_recent_block_hash_v2(proto.GetRecentBlockHashRequestV2(offset=1)) 165 | pprint(resp) 166 | 167 | return True if resp.block_hash is not None else False 168 | 169 | 170 | async def get_rate_limit(p: provider.Provider) -> bool: 171 | resp = await p.get_rate_limit(proto.GetRateLimitRequest()) 172 | pprint(resp) 173 | 174 | return True if resp.limit is not None else False 175 | 176 | 177 | async def get_raydium_pools(p: provider.Provider) -> bool: 178 | resp = await p.get_raydium_pools(proto.GetRaydiumPoolsRequest()) 179 | pprint(resp) 180 | 181 | return True if resp.pools is not None else False 182 | 183 | 184 | async def get_price(p: provider.Provider) -> bool: 185 | resp = await p.get_price(proto.GetPriceRequest(tokens=["So11111111111111111111111111111111111111112", 186 | "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"])) 187 | pprint(resp) 188 | 189 | return True if resp.token_prices is not None else False 190 | 191 | 192 | async def get_raydium_prices(p: provider.Provider) -> bool: 193 | resp = await p.get_raydium_prices( 194 | proto.GetRaydiumPricesRequest(tokens=["So11111111111111111111111111111111111111112", 195 | "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"])) 196 | pprint(resp) 197 | 198 | return True if resp.token_prices is not None else False 199 | 200 | 201 | async def get_jupiter_prices(p: provider.Provider) -> bool: 202 | resp = await p.get_jupiter_prices( 203 | proto.GetJupiterPricesRequest(tokens=["So11111111111111111111111111111111111111112", 204 | "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"])) 205 | pprint(resp) 206 | 207 | return True if resp.token_prices is not None else False 208 | 209 | 210 | async def get_unsettled(p: provider.Provider) -> bool: 211 | resp = await p.get_unsettled_v2( 212 | proto.GetUnsettledRequestV2(market="SOLUSDC", owner_address="HxFLKUAmAMLz1jtT3hbvCMELwH5H9tpM2QugP8sKyfhc")) 213 | pprint(resp) 214 | 215 | return True if resp.market is not None else False 216 | 217 | 218 | async def get_account_balance(p: provider.Provider) -> bool: 219 | if UserEnvironment.public_key != "": 220 | resp = await p.get_account_balance_v2( 221 | proto.GetAccountBalanceRequest(owner_address=UserEnvironment.public_key)) 222 | pprint(resp) 223 | 224 | else: 225 | resp = await p.get_account_balance_v2( 226 | proto.GetAccountBalanceRequest(owner_address="HxFLKUAmAMLz1jtT3hbvCMELwH5H9tpM2QugP8sKyfhc")) 227 | pprint(resp) 228 | 229 | return True if resp.tokens is not None else False 230 | 231 | 232 | async def get_quotes(p: provider.Provider) -> bool: 233 | resp = await p.get_quotes(proto.GetQuotesRequest(in_token="So11111111111111111111111111111111111111112", 234 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 235 | in_amount=0.01, 236 | slippage=5, 237 | limit=5)) 238 | 239 | pprint(resp) 240 | 241 | return True if resp.quotes is not None else False 242 | 243 | 244 | async def get_raydium_quotes(p: provider.Provider) -> bool: 245 | resp = await p.get_raydium_quotes( 246 | proto.GetRaydiumQuotesRequest(in_token="So11111111111111111111111111111111111111112", 247 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 248 | in_amount=0.01, 249 | slippage=5)) 250 | 251 | pprint(resp) 252 | 253 | return True if resp.routes is not None else False 254 | 255 | 256 | async def get_raydium_cpmm_quotes(p: provider.Provider) -> bool: 257 | resp = await p.get_raydium_cpmm_quotes( 258 | proto.GetRaydiumCpmmQuotesRequest(in_token="So11111111111111111111111111111111111111112", 259 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 260 | in_amount=0.01, 261 | slippage=5)) 262 | 263 | pprint(resp) 264 | 265 | return True if resp.out_token is not None else False 266 | 267 | 268 | async def get_raydium_clmm_quotes(p: provider.Provider) -> bool: 269 | resp = await p.get_raydium_clmm_quotes( 270 | proto.GetRaydiumClmmQuotesRequest(in_token="So11111111111111111111111111111111111111112", 271 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 272 | in_amount=0.01, 273 | slippage=5)) 274 | 275 | pprint(resp) 276 | 277 | return True if resp.out_token is not None else False 278 | 279 | 280 | async def get_jupiter_quotes(p: provider.Provider) -> bool: 281 | resp = await p.get_jupiter_quotes( 282 | proto.GetJupiterQuotesRequest(in_token="So11111111111111111111111111111111111111112", 283 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 284 | in_amount=0.01, 285 | slippage=5)) 286 | 287 | pprint(resp) 288 | 289 | return True if resp.out_token is not None else False 290 | 291 | 292 | async def get_pump_fun_quotes(p: provider.Provider) -> bool: 293 | p = provider.http_pump_ny() 294 | 295 | resp = await p.get_pump_fun_quotes( 296 | proto.GetPumpFunQuotesRequest(quote_type="buy", 297 | bonding_curve_address="Dga6eouREJ4kLHMqWWtccGGPsGebexuBYrcepBVd494q", 298 | mint_address="9QG5NHnfqQCyZ9SKhz7BzfjPseTFWaApmAtBTziXLanY", 299 | amount=0.01, slippage=5)) 300 | 301 | pprint(resp) 302 | 303 | return True if resp.out_amount is not None else False 304 | 305 | async def get_priority_fee(p: provider.Provider) -> bool: 306 | resp = await p.get_priority_fee(proto.GetPriorityFeeRequest()) 307 | pprint(resp) 308 | 309 | return True if resp.fee_at_percentile is not None else False 310 | 311 | 312 | async def get_token_accounts(p: provider.Provider) -> bool: 313 | resp = await p.get_token_accounts(proto.GetTokenAccountsRequest(owner_address=UserEnvironment.public_key)) 314 | pprint(resp) 315 | 316 | return True if resp.accounts is not None else False 317 | 318 | 319 | async def orderbook_stream(p: provider.Provider) -> bool: 320 | print("streaming orderbook updates...") 321 | 322 | async for resp in p.get_orderbooks_stream( 323 | get_orderbooks_request=proto.GetOrderbooksRequest( 324 | markets=["SOLUSDC"], project=proto.Project.P_OPENBOOK 325 | ) 326 | ): 327 | pprint(resp) 328 | await p.close() 329 | 330 | return True if resp.orderbook is not None else False 331 | return False 332 | 333 | 334 | async def market_depth_stream(p: provider.Provider) -> bool: 335 | print("streaming market depth updates...") 336 | 337 | async for resp in p.get_market_depths_stream( 338 | get_market_depths_request=proto.GetMarketDepthsRequest( 339 | markets=["SOLUSDC"], limit=5, project=proto.Project.P_OPENBOOK 340 | ), 341 | timeout=10, 342 | ): 343 | pprint(resp) 344 | await p.close() 345 | 346 | return True if resp.data is not None else False 347 | return False 348 | 349 | 350 | async def get_tickers_stream(p: provider.Provider) -> bool: 351 | print("streaming ticker updates...") 352 | 353 | async for resp in p.get_tickers_stream(timeout=10, 354 | get_tickers_stream_request=proto.GetTickersStreamRequest( 355 | markets=[ 356 | "BONK/SOL", 357 | "wSOL/RAY", 358 | "BONK/RAY", 359 | "RAY/USDC", 360 | "SOL/USDC", 361 | "SOL/USDC", 362 | "RAY/USDC", 363 | "USDT/USDC", 364 | ], 365 | project=proto.Project.P_OPENBOOK, 366 | ) 367 | ): 368 | pprint(resp) 369 | await p.close() 370 | 371 | return True if resp.ticker is not None else False 372 | return False 373 | 374 | 375 | async def get_prices_stream(p: provider.Provider) -> bool: 376 | print("streaming price streams...") 377 | async for resp in p.get_prices_stream( 378 | get_prices_stream_request=proto.GetPricesStreamRequest( 379 | projects=[proto.Project.P_RAYDIUM], 380 | tokens=[ 381 | "So11111111111111111111111111111111111111112", 382 | "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 383 | ], 384 | ) 385 | ): 386 | pprint(resp) 387 | 388 | await p.close() 389 | return True if resp.price is not None else False 390 | return False 391 | 392 | 393 | async def get_swaps_stream(p: provider.Provider) -> bool: 394 | print("streaming swap events...") 395 | async for resp in p.get_swaps_stream( 396 | get_swaps_stream_request=proto.GetSwapsStreamRequest( 397 | projects=[proto.Project.P_RAYDIUM], 398 | # RAY-SOL , ETH-SOL, SOL-USDC, SOL-USDT 399 | pools=[ 400 | "AVs9TA4nWDzfPJE9gGVNJMVhcQy3V9PGazuz33BfG2RA", 401 | "9Hm8QX7ZhE9uB8L2arChmmagZZBtBmnzBbpfxzkQp85D", 402 | "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", 403 | "7XawhbbxtsRcQA8KTkHT9f9nc6d69UwqCDh6U5EEbEmX", 404 | ], 405 | include_failed=True, 406 | ) 407 | ): 408 | pprint(resp) 409 | 410 | await p.close() 411 | return True if resp.swap is not None else False 412 | return False 413 | 414 | 415 | async def get_trades_stream(p: provider.Provider) -> bool: 416 | print("streaming trade updates...") 417 | async for resp in p.get_trades_stream( 418 | get_trades_request=proto.GetTradesRequest( 419 | market="SOLUSDC", project=proto.Project.P_OPENBOOK 420 | ) 421 | ): 422 | pprint(resp) 423 | 424 | await p.close() 425 | return True if resp.trades is not None else False 426 | return False 427 | 428 | async def get_pump_fun_new_amm_pool_stream(p: provider.Provider) -> bool: 429 | print("streaming new pump swap amm pools") 430 | 431 | await p.close() 432 | 433 | p = provider.grpc_pump_ny() 434 | await p.connect() 435 | 436 | async for resp in p.get_pump_fun_new_amm_pool_stream( 437 | get_pump_fun_new_amm_pool_stream_request=proto.GetPumpFunNewAmmPoolStreamRequest() 438 | ): 439 | pprint(resp) 440 | await p.close() 441 | 442 | return True if resp.pool is not None else False 443 | 444 | return False 445 | 446 | 447 | async def get_pump_fun_amm_swap_stream(p: provider.Provider) -> bool: 448 | print("streaming new pump swap amm swaps") 449 | 450 | await p.close() 451 | 452 | p = provider.grpc_pump_ny() 453 | await p.connect() 454 | 455 | async for resp in p.get_pump_fun_amm_swap_stream( 456 | get_pump_fun_amm_swap_stream_request=proto.GetPumpFunAmmSwapStreamRequest( 457 | pools=["4w2cysotX6czaUGmmWg13hDpY4QEMG2CzeKYEQyK9Ama"] 458 | ) 459 | ): 460 | pprint(resp) 461 | await p.close() 462 | 463 | return True if resp.tx_hash is not None else False 464 | 465 | return False 466 | 467 | 468 | async def get_new_raydium_pools_stream(p: provider.Provider) -> bool: 469 | print("streaming raydium new pool updates without cpmm pools...") 470 | async for resp in p.get_new_raydium_pools_stream( 471 | get_new_raydium_pools_request=proto.GetNewRaydiumPoolsRequest() 472 | ): 473 | pprint(resp) 474 | 475 | await p.close() 476 | return True if resp.pool is not None else False 477 | return False 478 | 479 | 480 | async def get_new_raydium_pools_stream_cpmm(p: provider.Provider) -> bool: 481 | print("streaming raydium new pool updates without cpmm pools...") 482 | async for resp in p.get_new_raydium_pools_stream( 483 | get_new_raydium_pools_request=proto.GetNewRaydiumPoolsRequest( 484 | include_cpmm=True 485 | ) 486 | ): 487 | pprint(resp) 488 | 489 | await p.close() 490 | return True if resp.pool is not None else False 491 | return False 492 | 493 | 494 | async def get_recent_blockhash_stream(p: provider.Provider) -> bool: 495 | print("streaming raydium new pool updates without cpmm pools...") 496 | async for resp in p.get_recent_block_hash_stream( 497 | get_recent_block_hash_request=proto.GetRecentBlockHashRequest( 498 | ) 499 | ): 500 | pprint(resp) 501 | 502 | await p.close() 503 | return True if resp.block_hash is not None else False 504 | return False 505 | 506 | async def get_recent_pump_fun_token() -> proto.GetPumpFunNewTokensStreamResponse: 507 | print("getting new pump fun token...") 508 | 509 | # Don't close the provider inside this function if it's needed elsewhere 510 | p = provider.grpc_pump_ny() 511 | await p.connect() 512 | try: 513 | request = proto.GetPumpFunNewTokensStreamRequest() 514 | async for resp in p.get_pump_fun_new_tokens_stream(request): 515 | return resp 516 | except Exception as e: 517 | print(f"Error getting pump fun token: {e}") 518 | raise 519 | finally: 520 | await p.close() 521 | 522 | # If we didn't get any responses 523 | return None 524 | 525 | async def get_pool_reserve_stream(p: provider.Provider) -> bool: 526 | print("streaming pool reserves...") 527 | async for resp in p.get_pool_reserves_stream( 528 | get_pool_reserves_stream_request=proto.GetPoolReservesStreamRequest( 529 | projects=[proto.Project.P_RAYDIUM], 530 | pools=[ 531 | "GHGxSHVHsUNcGuf94rqFDsnhzGg3qbN1dD1z6DHZDfeQ", 532 | "HZ1znC9XBasm9AMDhGocd9EHSyH8Pyj1EUdiPb4WnZjo", 533 | "D8wAxwpH2aKaEGBKfeGdnQbCc2s54NrRvTDXCK98VAeT", 534 | "DdpuaJgjB2RptGMnfnCZVmC4vkKsMV6ytRa2gggQtCWt", 535 | ], 536 | ) 537 | ): 538 | pprint(resp) 539 | 540 | await p.close() 541 | return True if resp.reserves is not None else False 542 | return False 543 | 544 | 545 | async def get_block_stream(p: provider.Provider) -> bool: 546 | print("streaming pool reserves...") 547 | async for resp in p.get_block_stream(get_block_stream_request=proto.GetBlockStreamRequest()): 548 | pprint(resp) 549 | 550 | await p.close() 551 | return True if resp.block is not None else False 552 | return False 553 | 554 | 555 | async def get_priority_fee_stream(p: provider.Provider) -> bool: 556 | print("streaming priority fee updates...") 557 | async for resp in p.get_priority_fee_stream( 558 | get_priority_fee_request=proto.GetPriorityFeeRequest() 559 | ): 560 | pprint(resp) 561 | await p.close() 562 | 563 | return True if resp.fee_at_percentile is not None else False 564 | return False 565 | 566 | 567 | async def get_bundle_tip_stream(p: provider.Provider) -> bool: 568 | print("streaming bundle tip updates...") 569 | async for resp in p.get_bundle_tip_stream( 570 | get_bundle_tip_request=proto.GetBundleTipRequest() 571 | ): 572 | pprint(resp) 573 | await p.close() 574 | 575 | return True if resp.timestamp is not None else False 576 | return False 577 | 578 | async def get_priority_fee_by_program_stream(p: provider.Provider) -> bool: 579 | print("streaming priority fee by program updates...") 580 | async for resp in p.get_priority_fee_by_program_stream( 581 | get_priority_fee_by_program_request=proto.GetPriorityFeeByProgramRequest( 582 | programs=[ 583 | "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", 584 | "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", 585 | "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" 586 | ] 587 | ) 588 | ): 589 | pprint(resp) 590 | await p.close() 591 | 592 | return True if resp.data is not None else False 593 | return False 594 | 595 | async def call_trade_swap(p: provider.Provider) -> bool: 596 | print("calling post submit trade swap (using batch submit)...") 597 | 598 | response = await p.submit_post_trade_swap(project=proto.Project.P_RAYDIUM, 599 | owner_address=UserEnvironment.public_key, 600 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 601 | out_token="So11111111111111111111111111111111111111112", 602 | in_amount=0.01, 603 | slippage=0.5, 604 | compute_limit=200000, 605 | compute_price=100000, 606 | tip=1000000, 607 | submit_strategy=proto.SubmitStrategy.P_ABORT_ON_FIRST_ERROR, 608 | skip_pre_flight=True) 609 | 610 | print("signature for trade swap tx", response.transactions[0].signature) 611 | 612 | return True if response.transactions[0].signature else False 613 | 614 | 615 | async def call_route_trade_swap(p: provider.Provider) -> bool: 616 | print("calling post submit route trade swap (using batch submit)...") 617 | 618 | response = await p.submit_post_route_trade_swap(project=proto.Project.P_RAYDIUM, 619 | owner_address=UserEnvironment.public_key, 620 | slippage=0.5, 621 | compute_price=100000, 622 | compute_limit=200000, 623 | tip=1000000, 624 | steps=[proto.RouteStep( 625 | in_token="So11111111111111111111111111111111111111112", 626 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 627 | in_amount=0.01, 628 | out_amount_min=0.007505, 629 | out_amount=0.0074, 630 | project=proto.StepProject(label="Raydium", 631 | id="58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2"))]) 632 | 633 | print("signature for route trade swap tx", response.transactions[0].signature) 634 | 635 | return True if response.transactions[0].signature else False 636 | 637 | 638 | async def call_raydium_trade_swap(p: provider.Provider) -> bool: 639 | print("calling post submit raydium trade swap...") 640 | 641 | response = await p.submit_raydium_swap( 642 | owner_address=UserEnvironment.public_key, 643 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 644 | out_token="So11111111111111111111111111111111111111112", 645 | slippage=0.5, 646 | in_amount=0.01, 647 | compute_price=100000, 648 | compute_limit=200000, 649 | tip=1000000, 650 | ) 651 | 652 | print("signature for raydium swap tx", response) 653 | 654 | return True if response != "" else False 655 | 656 | 657 | async def call_raydium_cpmm_trade_swap(p: provider.Provider) -> bool: 658 | print("calling post submit raydium cpmm trade swap...") 659 | 660 | response = await p.submit_raydium_swap_cpmm( 661 | owner_address=UserEnvironment.public_key, 662 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 663 | out_token="So11111111111111111111111111111111111111112", 664 | slippage=0.5, 665 | in_amount=0.01, 666 | compute_price=100000, 667 | compute_limit=200000, 668 | tip=1000000, 669 | ) 670 | 671 | print("signature for raydium cpmm swap tx", response) 672 | 673 | return True if response != "" else False 674 | 675 | 676 | async def call_raydium_clmm_trade_swap(p: provider.Provider) -> bool: 677 | print("calling post submit raydium clmm trade swap...") 678 | 679 | response = await p.submit_raydium_swap_clmm( 680 | owner_address=UserEnvironment.public_key, 681 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 682 | out_token="So11111111111111111111111111111111111111112", 683 | slippage=0.5, 684 | in_amount=0.01, 685 | compute_price=100000, 686 | compute_limit=200000, 687 | tip=1000000, 688 | ) 689 | 690 | print("signature for raydium clmm swap tx", response) 691 | 692 | return True if response != "" else False 693 | 694 | 695 | async def call_jupiter_trade_swap(p: provider.Provider) -> bool: 696 | print("calling post submit jupiter trade swap...") 697 | 698 | response = await p.submit_jupiter_swap( 699 | owner_address=UserEnvironment.public_key, 700 | in_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 701 | out_token="So11111111111111111111111111111111111111112", 702 | slippage=0.5, 703 | in_amount=0.01, 704 | compute_price=160000, 705 | compute_limit=200000, 706 | tip=1100000, 707 | ) 708 | 709 | print("signature for jupiter swap tx", response) 710 | 711 | return True if response != "" else False 712 | 713 | async def call_pump_fun_trade_swap(p: provider.Provider) -> bool: 714 | print("calling pump fun trade swap...") 715 | 716 | await p.close() 717 | 718 | p = provider.http_pump_ny() 719 | await p.connect() 720 | 721 | new_token = await get_recent_pump_fun_token() 722 | 723 | response = await p.submit_pump_fun_swap( 724 | owner_address=UserEnvironment.public_key, 725 | bonding_curve_address=new_token.bonding_curve, 726 | token_address=new_token.mint, 727 | creator=new_token.creator, 728 | token_amount=10, 729 | sol_threshold=0.0001, 730 | is_buy=True, 731 | compute_price=160000, 732 | compute_limit=200000, 733 | tip=1100000, 734 | ) 735 | 736 | await p.close() 737 | 738 | print("signature for pump fun swap tx", response) 739 | 740 | return True if response != "" else False 741 | 742 | async def get_pump_fun_amm_quotes(p: provider.Provider) -> bool: 743 | print("calling get_pump_fun_amm_quotes...") 744 | 745 | await p.close() 746 | 747 | p = provider.http_pump_ny() 748 | await p.connect() 749 | 750 | response = await p.get_pump_fun_amm_quotes( 751 | get_pump_fun_amm_quotes_request=proto.GetPumpFunAmmQuotesRequest( 752 | in_token="So11111111111111111111111111111111111111112", 753 | in_amount=10, 754 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 755 | pool="Gf7sXMoP8iRw4iiXmJ1nq4vxcRycbGXy5RL8a8LnTd3v", 756 | slippage=0.9 757 | ) 758 | ) 759 | 760 | await p.close() 761 | 762 | print("get_pump_fun_amm_quotes response", response) 763 | 764 | return True if response != "" else False 765 | 766 | async def post_pump_fun_amm_swap(p: provider.Provider) -> bool: 767 | print("calling post_pump_fun_amm_swap...") 768 | 769 | await p.close() 770 | 771 | p = provider.http_pump_ny() 772 | await p.connect() 773 | 774 | response = await p.post_pump_fun_amm_swap( 775 | post_pump_fun_amm_swap_request=proto.PostPumpFunAmmSwapRequest( 776 | owner_address=UserEnvironment.public_key, 777 | in_token="So11111111111111111111111111111111111111112", 778 | in_amount=10, 779 | out_token="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 780 | pool="Gf7sXMoP8iRw4iiXmJ1nq4vxcRycbGXy5RL8a8LnTd3v", 781 | slippage=0.9, 782 | compute_limit=130000, 783 | compute_price=100000, 784 | tip=1000000, 785 | ) 786 | ) 787 | 788 | await p.close() 789 | 790 | print("post_pump_fun_amm_swap response", response) 791 | 792 | return True if response != "" else False 793 | 794 | async def create_personal_tx_and_submit(p: provider.Provider) -> bool: 795 | print("creating own transaction and submitting to trader api... ") 796 | 797 | resp = await p.get_recent_block_hash_v2(proto.GetRecentBlockHashRequestV2()) 798 | 799 | tx = create_trader_api_tip_tx_signed( 800 | tip_amount=1100000, 801 | sender_address=load_private_key_from_env(), 802 | blockhash=Hash.from_string(resp.block_hash) 803 | ) 804 | 805 | response = await p.post_submit(proto.PostSubmitRequest( 806 | transaction=tx, 807 | skip_pre_flight=True) 808 | ) 809 | 810 | 811 | print("signature for custom user tx", response) 812 | 813 | return True if response != "" else False 814 | 815 | 816 | async def call_submit_snipe(p: provider.Provider) -> bool: 817 | resp = await p.get_recent_block_hash_v2(proto.GetRecentBlockHashRequestV2()) 818 | blockhash = Hash.from_string(resp.block_hash) 819 | 820 | fee_payer = Keypair() 821 | small_tip = 100_000 822 | staked_tip_threshold = 1_000_000 823 | tip_wallet = Pubkey.from_string("HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY") 824 | jito_tip_wallet = Pubkey.from_string("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5") 825 | 826 | # First transaction: transfer to both jito and bloxroute 827 | tx1_instructions = [ 828 | transfer(TransferParams( 829 | from_pubkey=fee_payer.pubkey(), 830 | to_pubkey=jito_tip_wallet, 831 | lamports=small_tip 832 | )), 833 | transfer(TransferParams( 834 | from_pubkey=fee_payer.pubkey(), 835 | to_pubkey=tip_wallet, 836 | lamports=small_tip 837 | )) 838 | ] 839 | 840 | tx1_message = MessageV0.try_compile( 841 | payer=fee_payer.pubkey(), 842 | instructions=tx1_instructions, 843 | address_lookup_table_accounts=[], 844 | recent_blockhash=blockhash 845 | ) 846 | tx1 = VersionedTransaction(tx1_message, [fee_payer]) 847 | signature1 = fee_payer.sign_message(msg.to_bytes_versioned(tx1.message)) 848 | tx1 = VersionedTransaction.populate(tx1.message, [signature1]) 849 | 850 | # Second transaction: staked transfer to bloxroute 851 | tx2_instructions = [ 852 | transfer(TransferParams( 853 | from_pubkey=fee_payer.pubkey(), 854 | to_pubkey=tip_wallet, 855 | lamports=staked_tip_threshold 856 | )) 857 | ] 858 | 859 | tx2_message = MessageV0.try_compile( 860 | payer=fee_payer.pubkey(), 861 | instructions=tx2_instructions, 862 | address_lookup_table_accounts=[], 863 | recent_blockhash=blockhash 864 | ) 865 | tx2 = VersionedTransaction(tx2_message, [fee_payer]) 866 | signature2 = fee_payer.sign_message(msg.to_bytes_versioned(tx2.message)) 867 | tx2 = VersionedTransaction.populate(tx2.message, [signature2]) 868 | 869 | transactions = [ 870 | proto.TransactionMessage( 871 | content=base64.b64encode(bytes(tx1)).decode(), 872 | is_cleanup=False 873 | ), 874 | proto.TransactionMessage( 875 | content=base64.b64encode(bytes(tx2)).decode(), 876 | is_cleanup=False 877 | ) 878 | ] 879 | 880 | result = await p.submit_snipe(transactions, use_staked_rpcs=True) 881 | print("Snipe Signatures:", result) 882 | return len(result) > 0 883 | 884 | async def call_place_order_bundle_paladin(p: provider.Provider) -> bool: 885 | print("Starting place order with bundle using Paladin...") 886 | 887 | # Get recent blockhash 888 | resp = await p.get_recent_block_hash_v2(proto.GetRecentBlockHashRequestV2()) 889 | blockhash = Hash.from_string(resp.block_hash) 890 | 891 | # Load private key from environment 892 | private_key = load_private_key_from_env() 893 | 894 | # Create instructions 895 | # 1. Set compute unit price instruction 896 | compute_budget_ix = set_compute_unit_price( 897 | 200000000 898 | ) 899 | 900 | # 2. Transfer instruction 901 | transfer_ix = transfer(TransferParams( 902 | from_pubkey=private_key.pubkey(), 903 | to_pubkey=Pubkey.from_string("HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY"), 904 | lamports=10000000 905 | )) 906 | 907 | # Compile message 908 | tx_message = MessageV0.try_compile( 909 | payer=private_key.pubkey(), 910 | instructions=[compute_budget_ix, transfer_ix], 911 | address_lookup_table_accounts=[], 912 | recent_blockhash=blockhash 913 | ) 914 | 915 | # Create transaction 916 | tx = VersionedTransaction(tx_message, [private_key]) 917 | signature = private_key.sign_message(msg.to_bytes_versioned(tx.message)) 918 | tx = VersionedTransaction.populate(tx.message, [signature]) 919 | 920 | # Encode transaction 921 | tx_base64 = base64.b64encode(bytes(tx)).decode() 922 | 923 | # Submit transaction using paladin 924 | try: 925 | signature = await p.submit_paladin( 926 | signed_tx=tx_base64, 927 | revert_protection=True 928 | ) 929 | 930 | print(f"Submitted order to trader API with signature: {signature}") 931 | return True 932 | except Exception as e: 933 | print(f"Failed to sign and submit order: {e}") 934 | return False 935 | -------------------------------------------------------------------------------- /menu.py: -------------------------------------------------------------------------------- 1 | from prompt_toolkit import Application 2 | from prompt_toolkit.key_binding import KeyBindings 3 | from prompt_toolkit.layout import Layout, ScrollOffsets, FormattedTextControl, Window, HSplit 4 | 5 | 6 | class MenuSelection: 7 | def __init__(self, options, visible_items=10): 8 | self.options = options 9 | self.cursor_index = 0 10 | self.visible_items = visible_items 11 | self.scroll_offset = 0 12 | self.kb = KeyBindings() 13 | 14 | @self.kb.add('up') 15 | def up_key(event): 16 | self.cursor_index = max(0, self.cursor_index - 1) 17 | self._adjust_scroll() 18 | self.update_display() 19 | 20 | @self.kb.add('down') 21 | def down_key(event): 22 | self.cursor_index = min(len(self.options) - 1, self.cursor_index + 1) 23 | self._adjust_scroll() 24 | self.update_display() 25 | 26 | @self.kb.add('pageup') 27 | def pageup_key(event): 28 | self.cursor_index = max(0, self.cursor_index - self.visible_items) 29 | self._adjust_scroll() 30 | self.update_display() 31 | 32 | @self.kb.add('pagedown') 33 | def pagedown_key(event): 34 | self.cursor_index = min(len(self.options) - 1, self.cursor_index + self.visible_items) 35 | self._adjust_scroll() 36 | self.update_display() 37 | 38 | @self.kb.add('enter') 39 | def enter_key(event): 40 | event.app.exit(result=self.options[self.cursor_index]) 41 | 42 | @self.kb.add('q') 43 | def quit_key(event): 44 | event.app.exit(result=None) 45 | 46 | def _adjust_scroll(self): 47 | if self.cursor_index < self.scroll_offset: 48 | self.scroll_offset = self.cursor_index 49 | elif self.cursor_index >= self.scroll_offset + self.visible_items: 50 | self.scroll_offset = self.cursor_index - self.visible_items + 1 51 | 52 | def create_menu_text(self): 53 | lines = [] 54 | visible_options = self.options[self.scroll_offset:self.scroll_offset + self.visible_items] 55 | 56 | for idx, option in enumerate(visible_options, start=self.scroll_offset): 57 | cursor = '>' if idx == self.cursor_index else ' ' 58 | lines.append(f'{cursor} {option}') 59 | 60 | if self.scroll_offset > 0: 61 | lines.insert(0, '▲ More options above') 62 | if self.scroll_offset + self.visible_items < len(self.options): 63 | lines.append('▼ More options below') 64 | 65 | return '\n'.join(lines) 66 | 67 | def update_display(self): 68 | self.menu_window.content.text = self.create_menu_text() 69 | 70 | def run(self): 71 | self.menu_window = Window( 72 | content=FormattedTextControl(''), 73 | height=self.visible_items + 2, 74 | scroll_offsets=ScrollOffsets(top=1, bottom=1) 75 | ) 76 | self.update_display() 77 | 78 | layout = Layout( 79 | HSplit([ 80 | Window( 81 | height=1, 82 | content=FormattedTextControl( 83 | 'Use ↑↓/PgUp/PgDn to move, Enter to select, q to quit' 84 | ) 85 | ), 86 | Window(height=1), # Spacer 87 | self.menu_window, 88 | ]) 89 | ) 90 | 91 | app = Application( 92 | layout=layout, 93 | key_bindings=self.kb, 94 | full_screen=True, 95 | ) 96 | 97 | return app.run() 98 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 80 7 | preview = true 8 | exclude = "proto" 9 | 10 | [tool.pylint.main] 11 | # Ignore protobuf generated files 12 | ignore = ["proto"] 13 | 14 | license = {file = "LICENSE"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | . 2 | build==0.8.0 3 | base58~=2.1.1 4 | solders~=0.19.0 5 | aiohttp~=3.9.0 6 | grpclib~=0.4.3 7 | solana~=0.31.0 8 | aiounittest~=1.4.1 9 | numpy~=1.26.4 10 | pip~=24.2 11 | attrs~=23.1.0 12 | wheel~=0.37.1 13 | docutils~=0.20.1 14 | setuptools~=75.1.0 15 | packaging~=25.0 16 | zipp~=3.11.0 17 | cython~=3.0.7 18 | future~=0.18.3 19 | platformdirs~=3.10.0 20 | filelock~=3.13.1 21 | typeguard~=2.13.3 22 | lxml~=5.0.1 23 | keyring~=24.3.0 24 | pyfiglet~=0.8.post1 25 | colorama~=0.4.6 26 | pyOpenSSL~=24.2.1 27 | cryptography~=43.0.1 28 | more-itertools~=10.5.0 29 | Pygments~=2.18.0 30 | betterproto~=2.0.0b6 31 | jaraco-classes~=3.4.0 32 | nest-asyncio~=1.6.0 33 | termcolor~=2.1.0 34 | bx-jsonrpc-py~=0.2.0 35 | bxsolana-trader-proto==0.1.8 36 | async-timeout~=4.0.3 37 | prompt-toolkit~=3.0.48 38 | 39 | -------------------------------------------------------------------------------- /sdk.py: -------------------------------------------------------------------------------- 1 | from colorama import init 2 | from termcolor import colored 3 | 4 | import bxsolana.provider as provider 5 | 6 | import asyncio 7 | import nest_asyncio 8 | import os 9 | import pyfiglet 10 | 11 | from bxsolana.provider import constants 12 | from menu import MenuSelection 13 | from helpers import Endpoint, get_markets, get_pools, get_tickers, get_raydium_clmm_pools, call_place_order_bundle_paladin, \ 14 | get_orderbook, get_raydium_pool_reserves, get_market_depth, get_open_orders, get_transaction, get_recent_blockhash, \ 15 | get_recent_blockhash_offset, get_rate_limit, get_price, get_raydium_pools, get_raydium_prices, get_jupiter_prices, \ 16 | get_unsettled, get_account_balance, get_quotes, get_raydium_quotes, get_raydium_cpmm_quotes, \ 17 | get_raydium_clmm_quotes, get_jupiter_quotes, get_pump_fun_quotes, orderbook_stream, market_depth_stream, \ 18 | get_tickers_stream, get_prices_stream, get_swaps_stream, get_trades_stream, get_new_raydium_pools_stream, \ 19 | get_new_raydium_pools_stream_cpmm, get_recent_blockhash_stream, get_pool_reserve_stream, \ 20 | get_block_stream, get_priority_fee, get_priority_fee_stream, get_bundle_tip_stream, get_priority_fee_by_program_stream, get_token_accounts, \ 21 | call_trade_swap, call_route_trade_swap, call_raydium_trade_swap, call_raydium_cpmm_trade_swap, \ 22 | call_raydium_clmm_trade_swap, call_jupiter_trade_swap, call_pump_fun_trade_swap, create_personal_tx_and_submit, call_submit_snipe, \ 23 | get_pump_fun_new_amm_pool_stream, get_pump_fun_amm_swap_stream, get_pump_fun_amm_quotes, post_pump_fun_amm_swap 24 | 25 | nest_asyncio.apply() 26 | init(autoreset=True) 27 | 28 | # ANSI color code for yellow 29 | YELLOW = "\033[93m" 30 | RESET = "\033[0m" 31 | 32 | 33 | def print_logo(): 34 | # Generate larger ASCII art for the logo 35 | print(colored("=" * 180)) 36 | 37 | logo = pyfiglet.figlet_format("bloxRoute Trader API", font="block", width=250) # Use 'block' for a bigger font 38 | 39 | # Print the logo in color 40 | print(colored(logo, 'cyan')) 41 | print(colored("Welcome to the bloxRoute Trader API!", 'yellow')) 42 | print(colored("=" * 180)) 43 | 44 | 45 | ExampleEndpoints = { 46 | "get_markets": Endpoint(func=get_markets, requires_additional_env_vars=False), 47 | "get_pools": Endpoint(func=get_pools, requires_additional_env_vars=False), 48 | "get_tickers": Endpoint(func=get_tickers, requires_additional_env_vars=False), 49 | "get_raydium_clmm_pools": Endpoint(func=get_raydium_clmm_pools, requires_additional_env_vars=False), 50 | "get_raydium_pool_reserve": Endpoint(func=get_raydium_pool_reserves, requires_additional_env_vars=False), 51 | "get_raydium_pools": Endpoint(func=get_raydium_pools, requires_additional_env_vars=False), 52 | "get_orderbook": Endpoint(func=get_orderbook, requires_additional_env_vars=False), 53 | "get_market_depth": Endpoint(func=get_market_depth, requires_additional_env_vars=False), 54 | "get_open_orders": Endpoint(func=get_open_orders, requires_additional_env_vars=False), 55 | "get_transaction": Endpoint(func=get_transaction, requires_additional_env_vars=False), 56 | "get_recent_blockhash": Endpoint(func=get_recent_blockhash, requires_additional_env_vars=False), 57 | "get_recent_blockhash_offset": Endpoint(func=get_recent_blockhash_offset, requires_additional_env_vars=False), 58 | "get_rate_limit": Endpoint(func=get_rate_limit, requires_additional_env_vars=False), 59 | "get_priority_fee": Endpoint(func=get_priority_fee, requires_additional_env_vars=False), 60 | "get_token_accounts": Endpoint(func=get_token_accounts, requires_additional_env_vars=True), 61 | "get_price": Endpoint(func=get_price, requires_additional_env_vars=False), 62 | "get_raydium_prices": Endpoint(func=get_raydium_prices, requires_additional_env_vars=False), 63 | "get_jupiter_prices": Endpoint(func=get_jupiter_prices, requires_additional_env_vars=False), 64 | "get_unsettled": Endpoint(func=get_unsettled, requires_additional_env_vars=False), 65 | "get_account_balance": Endpoint(func=get_account_balance, requires_additional_env_vars=False), 66 | "get_quotes": Endpoint(func=get_quotes, requires_additional_env_vars=False), 67 | "get_raydium_quotes": Endpoint(func=get_raydium_quotes, requires_additional_env_vars=False), 68 | "get_raydium_cpmm_quotes": Endpoint(func=get_raydium_cpmm_quotes, requires_additional_env_vars=False), 69 | "get_raydium_clmm_quotes": Endpoint(func=get_raydium_clmm_quotes, requires_additional_env_vars=False), 70 | "get_jupiter_quotes": Endpoint(func=get_jupiter_quotes, requires_additional_env_vars=False), 71 | "get_pump_fun_quotes": Endpoint(func=get_pump_fun_quotes, requires_additional_env_vars=False), 72 | "get_pump_fun_amm_quotes": Endpoint(func=get_pump_fun_amm_quotes, requires_additional_env_vars=False), 73 | 74 | # streaming endpoints 75 | "orderbook_stream": Endpoint(func=orderbook_stream, requires_additional_env_vars=False), 76 | "market_depth_stream": Endpoint(func=market_depth_stream, requires_additional_env_vars=False), 77 | "tickers_stream": Endpoint(func=get_tickers_stream, requires_additional_env_vars=False), 78 | "prices_stream": Endpoint(func=get_prices_stream, requires_additional_env_vars=False), 79 | "swaps_stream": Endpoint(func=get_swaps_stream, requires_additional_env_vars=False), 80 | "trades_stream": Endpoint(func=get_trades_stream, requires_additional_env_vars=False), 81 | "new_raydium_pools_stream": Endpoint(func=get_new_raydium_pools_stream, requires_additional_env_vars=False), 82 | "new_raydium_pools_stream_cpmm": Endpoint(func=get_new_raydium_pools_stream_cpmm, 83 | requires_additional_env_vars=False), 84 | "get_recent_blockhash_stream": Endpoint(func=get_recent_blockhash_stream, requires_additional_env_vars=False), 85 | "get_pool_reserve": Endpoint(func=get_pool_reserve_stream, requires_additional_env_vars=False), 86 | "get_block_stream": Endpoint(func=get_block_stream, requires_additional_env_vars=False), 87 | "get_priority_fee_stream": Endpoint(func=get_priority_fee_stream, requires_additional_env_vars=False), 88 | "get_bundle_tip_stream": Endpoint(func=get_bundle_tip_stream, requires_additional_env_vars=False), 89 | "get_priority_fee_by_program_stream": Endpoint(func=get_priority_fee_by_program_stream, 90 | requires_additional_env_vars=False), 91 | "get_pump_fun_new_amm_pool_stream": Endpoint(func=get_pump_fun_new_amm_pool_stream, 92 | requires_additional_env_vars=False), 93 | "get_pump_fun_amm_swap_stream": Endpoint(func=get_pump_fun_amm_swap_stream, 94 | requires_additional_env_vars=False), 95 | 96 | # transaction endpoints 97 | "trade_swap": Endpoint(func=call_trade_swap, requires_additional_env_vars=True), 98 | "route_trade_swap": Endpoint(func=call_route_trade_swap, requires_additional_env_vars=True), 99 | "raydium_swap": Endpoint(func=call_raydium_trade_swap, requires_additional_env_vars=True), 100 | "raydium_cpmm_swap": Endpoint(func=call_raydium_cpmm_trade_swap, requires_additional_env_vars=True), 101 | "raydium_clmm_swap": Endpoint(func=call_raydium_clmm_trade_swap, requires_additional_env_vars=True), 102 | "jupiter_swap": Endpoint(func=call_jupiter_trade_swap, requires_additional_env_vars=True), 103 | "pump_fun_swap": Endpoint(func=call_pump_fun_trade_swap, requires_additional_env_vars=True), 104 | "pump_fun_amm_swap": Endpoint(func=post_pump_fun_amm_swap, requires_additional_env_vars=True), 105 | "create_custom_bloxroute_transfer": Endpoint(func=create_personal_tx_and_submit, requires_additional_env_vars=True), 106 | "submit_snipe": Endpoint(func=call_submit_snipe, requires_additional_env_vars=True), 107 | "call_place_order_bundle_paladin": Endpoint(func=call_place_order_bundle_paladin, requires_additional_env_vars=True) 108 | } 109 | 110 | 111 | def choose_provider() -> provider.Provider: 112 | print('Choose protocol for provider: ') 113 | provider_options = ["http", "grpc", "ws"] 114 | menu = MenuSelection(provider_options, visible_items=3) 115 | choice = menu.run() 116 | 117 | print('Choose region for provider: (only NY and UK are supported for full sdk examples)') 118 | provider_options_region = ["ny", "uk"] 119 | menu = MenuSelection(provider_options_region, visible_items=2) 120 | region_choice = menu.run() 121 | 122 | selected_region = constants.Region[region_choice.upper()] 123 | 124 | print('Choose environment for provider: ') 125 | provider_options_environment = ["mainnet", "testnet", "local"] 126 | 127 | menu_environment = MenuSelection(provider_options_environment, visible_items=3) 128 | env = menu_environment.run() 129 | 130 | 131 | if env == "mainnet": 132 | if choice == "http": 133 | p = provider.http(region=selected_region) 134 | elif choice == "grpc": 135 | p = provider.grpc(region=selected_region) 136 | else: 137 | p = provider.ws(region=selected_region) 138 | 139 | 140 | elif env == "testnet": 141 | if choice == "http": 142 | p = provider.http_testnet() 143 | elif choice == "grpc": 144 | p = provider.grpc_testnet() 145 | else: 146 | p = provider.ws_testnet() 147 | 148 | else: 149 | if choice == "http": 150 | p = provider.http_local() 151 | elif choice == "grpc": 152 | p = provider.grpc_local() 153 | else: 154 | p = provider.ws_local() 155 | 156 | return p 157 | 158 | 159 | async def sdk_loop(): 160 | menu_options = [] 161 | 162 | while True: 163 | p = choose_provider() 164 | 165 | await p.connect() 166 | menu_text = list() 167 | 168 | for name in ExampleEndpoints.keys(): 169 | menu_options.append(name) 170 | 171 | menu_options.append("quit") 172 | menu = MenuSelection(menu_options, visible_items=10) 173 | choice = menu.run() 174 | 175 | if choice == "quit": 176 | await p.close() 177 | break 178 | 179 | for name, endpoint in ExampleEndpoints.items(): 180 | if choice is name: 181 | example = ExampleEndpoints[name] 182 | result = await example.func(p) 183 | 184 | prompt = "Success" if result is True else "Failure" 185 | 186 | print("-" * 200) 187 | print(f'Function {name} executed with result: {prompt}') 188 | print("-" * 200) 189 | 190 | menu_text.clear() 191 | 192 | await p.close() 193 | 194 | print(f'Press any key to continue...') 195 | input() 196 | 197 | 198 | 199 | async def main(): 200 | print_logo() 201 | 202 | input() 203 | os.system('clear') 204 | 205 | await sdk_loop() 206 | 207 | 208 | if __name__ == "__main__": 209 | try: 210 | loop = asyncio.get_event_loop() 211 | if loop.is_running(): 212 | # Schedule the coroutine with create_task if the loop is already running 213 | loop.create_task(main()) 214 | else: 215 | # Run until complete for standard execution environments 216 | loop.run_until_complete(main()) 217 | except RuntimeError as e: 218 | print(f"Error: {e}") 219 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#declarative-config 2 | # https://packaging.python.org/en/latest/tutorials/packaging-projects/ 3 | 4 | [metadata] 5 | name = bxsolana-trader 6 | version = 2.2.9 7 | description = Python SDK for bloXroute's Solana Trader API 8 | long_description = file: README.md, LICENSE 9 | long_description_content_type = text/markdown 10 | author = bloXroute Labs 11 | author_email = support@bloxroute.com 12 | url = https://github.com/bloXroute-Labs/solana-trader-client-python 13 | keywords = openbook, solana, blockchain, trader, grpc, stream, raydium, jupiter 14 | classifiers = Programming Language :: Python :: 3 15 | 16 | [options] 17 | packages = find: 18 | install_requires = 19 | aiohttp==3.9.0 20 | grpclib==0.4.3 21 | aiounittest==1.4.1 22 | base58==2.1.1 23 | solana==0.31.0 24 | solders==0.19.0 25 | bx-jsonrpc-py==0.2.0 26 | bxsolana-trader-proto==0.1.8 -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloXroute-Labs/solana-trader-client-python/5015d2443e1846fe723b79601b2b03cd9e37a6f4/test/__init__.py -------------------------------------------------------------------------------- /test/integration/test_grpc.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | from bxsolana import provider 4 | from bxsolana_trader_proto import api as proto 5 | 6 | class TestGRPC(unittest.TestCase): 7 | def test_pump_fun_amm_swap_stream(self): 8 | asyncio.run(self._test_impl()) 9 | 10 | async def _test_impl(self): 11 | p = provider.grpc_pump_ny() 12 | await p.connect() 13 | request = proto.GetPumpFunAmmSwapStreamRequest( 14 | pools=["6WwcmiRJFPDNdFmtgVQ8eY1zxMzLKGLrYuUtRy4iZmye"] 15 | ) 16 | 17 | try: 18 | # Get first response only 19 | async for resp in p.get_pump_fun_amm_swap_stream(request): 20 | print(resp) 21 | break 22 | finally: 23 | await p.close() -------------------------------------------------------------------------------- /test/integration/test_ws.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import asyncio 3 | from bxsolana import provider 4 | from bxsolana_trader_proto import api as proto 5 | 6 | class TestGRPC(unittest.TestCase): 7 | def test_pump_fun_amm_swap_stream(self): 8 | asyncio.run(self._test_impl()) 9 | 10 | async def _test_impl(self): 11 | p = provider.ws_pump_ny() 12 | await p.connect() 13 | request = proto.GetPumpFunAmmSwapStreamRequest( 14 | pools=["6WwcmiRJFPDNdFmtgVQ8eY1zxMzLKGLrYuUtRy4iZmye"] 15 | ) 16 | 17 | try: 18 | # Get first response only 19 | async for resp in p.get_pump_fun_amm_swap_stream(request): 20 | print(resp) 21 | break 22 | finally: 23 | await p.close() -------------------------------------------------------------------------------- /test/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloXroute-Labs/solana-trader-client-python/5015d2443e1846fe723b79601b2b03cd9e37a6f4/test/unit/__init__.py -------------------------------------------------------------------------------- /test/unit/transaction/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloXroute-Labs/solana-trader-client-python/5015d2443e1846fe723b79601b2b03cd9e37a6f4/test/unit/transaction/__init__.py -------------------------------------------------------------------------------- /test/unit/transaction/test_memo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | from bxsolana.transaction import memo 5 | 6 | # key generated for this test 7 | RANDOM_PRIVATE_KEY = "3KWC65p6AvMjvpR2r1qLTC4HVSH4jEFr5TMQxagMLo1o3j4yVYzKsfbB3jKtu3yGEHjx2Cc3L5t8wSo91vpjT63t" 8 | EXPECTED_TX = "AAEAAQMmRmIlQZ625bapZEp3haQ7Bu0r1zqVP0wTF9LYtsuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA86EED9glv4WMHdJchvP2ZZvDPQq6PniaYqUsrsmZ94GF+Ae/EKhvjYII9uq1QkZIuRVBHwVbIHdB+Y3tmQI1zgIBAAACAB9Qb3dlcmVkIGJ5IGJsb1hyb3V0ZSBUcmFkZXIgQXBp" 9 | EMPTY_TRANSACTION = "AAEAAQImRmIlQZ625bapZEp3haQ7Bu0r1zqVP0wTF9LYtsuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhfgHvxCob42CCPbqtUJGSLkVQR8FWyB3QfmN7ZkCNc4BAQAA" 10 | 11 | 12 | class TestMemo(unittest.TestCase): 13 | def test_adding_memo_to_serialized_tx(self): 14 | 15 | tx_bytes = memo.add_memo_to_serialized_txn( 16 | EMPTY_TRANSACTION 17 | ) 18 | 19 | self.assertEqual(EXPECTED_TX, tx_bytes) -------------------------------------------------------------------------------- /test/unit/transaction/test_signing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import base58 4 | from solders.keypair import Keypair 5 | 6 | from bxsolana import transaction 7 | 8 | # key generated for this test 9 | RANDOM_PRIVATE_KEY = "3KWC65p6AvMjvpR2r1qLTC4HVSH4jEFr5TMQxagMLo1o3j4yVYzKsfbB3jKtu3yGEHjx2Cc3L5t8wSo91vpjT63t" 10 | RANDOM_PUBLIC_KEY = "3caKCcY8uEcasMixcihjjPWGekwtvo61bxQyhCR88mvr" 11 | 12 | # bytes of a Serum trade 13 | UNSIGNED_TX_1 = "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQD6Amzo3TOF19nBfTBPCjx2oV1dRi5AtFA3kxVbfyOai9/PDbvr0d9iYkVVYCo+5heOy6m60Ua60t+EF+kXEJAgAFD4ls26fgpAnCYufUzDrXMMpDjMYkf2Y2FHuxqKE+2+IrRVKOQVHKKvreZyvh3wca8QpEP1VhjdfPmQtxZk41vr2EwvsYrtYZ9UZjJlPvBgKfAqhkvzgphnGBuyDfHXFcMEoHX5xmEhJgK0YZx3BKh/s3nhpE7IFyBzqsKBqiTDd6jfzI9XsPznt1ZnWa9u9nVKg1KibD5ElrzSfbftYpluJAIIlGU8/d+nt+YMlmaCc2otsPg4VkklsRB3oh4DbXlwD0JuFuuM8DEZF1+YBRQ0SVXONw52WUDzwpQ5VF+0Wppt/RXFB3Bfkzm5U8Gk39vJzBht0vYt9IqVgEXip2UlkfJvXwRhxAEL1cyMpwZt2lhKbucXk0xnet9MJfvRVqLWrj7TJ6D4hJp3KUHZcFDzpujLjdOrzbFHCIfIK1TT82BpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAEGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi7817DQWO+TIR4kugIb5dD6FCxudqoRDfwOC8xhRQk5dqBA0CAAE0AAAAAIB3jgYAAAAApQAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQwEAQoACwEBDgwCAwQFBgcBAAgJDAszAAoAAAABAAAAwAslCgAAAAABAAAAAAAAAACXePYDAAAAAAAAAAAAAAAAAAAAAAAAAP//DAMBAAABCQ==" 14 | EXPECTED_1 = "AjRa6pZdYfbPV1vu4H+dziK+qYoeS4rPJX1B8rHYRo3p7a57JsRkgKe14FC+YWwEgq2i9onrSkTV8FTDph7rhg0QD6Amzo3TOF19nBfTBPCjx2oV1dRi5AtFA3kxVbfyOai9/PDbvr0d9iYkVVYCo+5heOy6m60Ua60t+EF+kXEJAgAFD4ls26fgpAnCYufUzDrXMMpDjMYkf2Y2FHuxqKE+2+IrRVKOQVHKKvreZyvh3wca8QpEP1VhjdfPmQtxZk41vr2EwvsYrtYZ9UZjJlPvBgKfAqhkvzgphnGBuyDfHXFcMEoHX5xmEhJgK0YZx3BKh/s3nhpE7IFyBzqsKBqiTDd6jfzI9XsPznt1ZnWa9u9nVKg1KibD5ElrzSfbftYpluJAIIlGU8/d+nt+YMlmaCc2otsPg4VkklsRB3oh4DbXlwD0JuFuuM8DEZF1+YBRQ0SVXONw52WUDzwpQ5VF+0Wppt/RXFB3Bfkzm5U8Gk39vJzBht0vYt9IqVgEXip2UlkfJvXwRhxAEL1cyMpwZt2lhKbucXk0xnet9MJfvRVqLWrj7TJ6D4hJp3KUHZcFDzpujLjdOrzbFHCIfIK1TT82BpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDwAAAAAAEGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi7817DQWO+TIR4kugIb5dD6FCxudqoRDfwOC8xhRQk5dqBA0CAAE0AAAAAIB3jgYAAAAApQAAAAAAAAAG3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQwEAQoACwEBDgwCAwQFBgcBAAgJDAszAAoAAAABAAAAwAslCgAAAAABAAAAAAAAAACXePYDAAAAAAAAAAAAAAAAAAAAAAAAAP//DAMBAAABCQ==" 15 | 16 | # bytes of a Jupiter swap 17 | UNSIGNED_TX_2 = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAHCibUXsV2mzsJIvnE2gwNr+KfdyhHR5jyjkRbbnsGqAqrbpmrfEFlbO4+KjKolmWHzvZZb9rc1UMgak9Lohf1TbIuGahV4LYOAa+/QpNyN+z24Zpt5aOJKfYLZNemgIMEIwMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnG+nrzvtutOj1l82qryXQxsbvkwtL24OR8pgIDRS9dYQR51S3tv2vF7NCdhFNKNK6ll1BDs2/QKyRlC7WEQ1lcqDfg9KjP9p0wygkxqsEwpYGnVWUr/AMYoE3FpEQfz4YHAwAFAsBcFQAEBgABAAUGBwAGAgABDAIAAACAlpgAAAAAAAcBAQERBAYAAgAIBgcACREHAAIOAA8KAQILDA0OBxANCSLlF8uXeuOtKgABAAAAAguAlpgAAAAAAL3fAwAAAAAAAQAABwMBAAABCQFX3lqkjZImQIEkZIQeRHTRorA35IRt35MRRVgR3QxiLARfYGFiA11eZQ==" 18 | EXPECTED_2 = "ATiKjv0AboF7b5vB2On6f81Fqf4YIXjMjWmPOQUnhlLXsMHt/Zpct9ps9cHGG2xntxxPjRiGSaIanL+wvXUKtAeAAQAHCibUXsV2mzsJIvnE2gwNr+KfdyhHR5jyjkRbbnsGqAqrbpmrfEFlbO4+KjKolmWHzvZZb9rc1UMgak9Lohf1TbIuGahV4LYOAa+/QpNyN+z24Zpt5aOJKfYLZNemgIMEIwMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAjJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FkGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKnG+nrzvtutOj1l82qryXQxsbvkwtL24OR8pgIDRS9dYQR51S3tv2vF7NCdhFNKNK6ll1BDs2/QKyRlC7WEQ1lcqDfg9KjP9p0wygkxqsEwpYGnVWUr/AMYoE3FpEQfz4YHAwAFAsBcFQAEBgABAAUGBwAGAgABDAIAAACAlpgAAAAAAAcBAQERBAYAAgAIBgcACREHAAIOAA8KAQILDA0OBxANCSLlF8uXeuOtKgABAAAAAguAlpgAAAAAAL3fAwAAAAAAAQAABwMBAAABCQFX3lqkjZImQIEkZIQeRHTRorA35IRt35MRRVgR3QxiLARfYGFiA11eZQ==" 19 | 20 | 21 | class TestSigning(unittest.TestCase): 22 | def test_sign_tx(self): 23 | pkey_bytes = bytes(RANDOM_PRIVATE_KEY, encoding="utf-8") 24 | pkey_bytes_base58 = base58.b58decode(pkey_bytes) 25 | kp = Keypair.from_bytes(pkey_bytes_base58) 26 | 27 | signed_tx_base64 = transaction.sign_tx_with_private_key( 28 | UNSIGNED_TX_1, kp 29 | ) 30 | self.assertEqual(EXPECTED_1, signed_tx_base64) 31 | 32 | signed_tx_base64 = transaction.sign_tx_with_private_key( 33 | UNSIGNED_TX_2, kp 34 | ) 35 | self.assertEqual(EXPECTED_2, signed_tx_base64) 36 | -------------------------------------------------------------------------------- /test/unit/ws/test_validation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from bxserum.proto import GetAccountBalanceResponse, GetOpenOrdersResponse, GetServerTimeResponse, \ 4 | TokenBalance 5 | from bxsolana_trader_proto import GetUserResponse 6 | 7 | from bxsolana.provider.ws import _validated_response 8 | 9 | 10 | class TestWSValidation(unittest.TestCase): 11 | def test_response_not_dictionary(self): 12 | response = "abcdef-123" 13 | 14 | try: 15 | _validated_response(response, GetAccountBalanceResponse) 16 | self.fail("_validated_response should have thrown exception") 17 | except Exception as e: 18 | self.assertEqual(str(e), f"response {response} was not a dictionary") 19 | 20 | def test_response_none(self): 21 | response = None 22 | 23 | try: 24 | _validated_response(response, GetAccountBalanceResponse) 25 | self.fail("_validated_response should have thrown exception") 26 | except Exception as e: 27 | self.assertEqual(str(e), f"response {response} was not a dictionary") 28 | 29 | def test_error_message(self): 30 | response = {"code":5, "message":"Not Found", "details":[]} 31 | 32 | try: 33 | _validated_response(response, GetAccountBalanceResponse) 34 | self.fail("_validated_response should have thrown exception") 35 | except Exception as e: 36 | self.assertEqual(str(e), "Not Found") 37 | 38 | def test_incorrect_type(self): 39 | response = GetServerTimeResponse(timestamp="123") 40 | response_dict = response.to_dict() 41 | 42 | try: 43 | _validated_response(response_dict, GetAccountBalanceResponse) 44 | self.fail("_validated_response should have thrown exception") 45 | except Exception as e: 46 | self.assertEqual(str(e), "response {'timestamp': '123'} was not of type ") 47 | 48 | def test_valid_response_1(self): 49 | response = GetOpenOrdersResponse(orders=[]) 50 | response_dict = response.to_dict(include_default_values=True) 51 | 52 | try: 53 | actual_response = _validated_response(response_dict, GetOpenOrdersResponse) 54 | self.assertEqual(actual_response, response) 55 | except Exception: 56 | self.fail("should not have thrown exception") 57 | 58 | def test_valid_response_2(self): 59 | response = GetAccountBalanceResponse(tokens=[TokenBalance(symbol="SOL")]) 60 | response_dict = response.to_dict(include_default_values=True) 61 | 62 | try: 63 | actual_response = _validated_response(response_dict, GetAccountBalanceResponse) 64 | self.assertEqual(actual_response, response) 65 | except Exception: 66 | self.fail("should not have thrown exception") 67 | 68 | def test_valid_response_3(self): 69 | response = GetUserResponse(status="good", account_number=0) 70 | response_dict = response.to_dict(include_default_values=True) 71 | 72 | try: 73 | actual_response = _validated_response(response_dict, GetUserResponse) 74 | self.assertEqual(actual_response, response) 75 | except Exception: 76 | self.fail("should not have thrown exception") 77 | --------------------------------------------------------------------------------