diff --git a/.github/workflows/publish_to_pypy.yml b/.github/workflows/publish_to_pypy.yml new file mode 100644 index 0000000..d1b9ab1 --- /dev/null +++ b/.github/workflows/publish_to_pypy.yml @@ -0,0 +1,31 @@ +--- +name: publish_to_pypi +on: + push: + branches: + - quant-852 + pull_request: + branches: + - main + types: + - closed +jobs: + build-n-publish: + #if: github.event.pull_request.merged == true + name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install pypa/build + run: python -m pip install build --user + - name: Build a binary wheel and a source tarball + run: python -m build --sdist --wheel --outdir dist/ . + - name: Publish distribution 📦 to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_KEY }} + skip_existing: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c795b05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e2590f2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + } +} \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..94211d9 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,78 @@ +# Changes w.r.t old library + +## Initialisation change + +1. The way we initialise is still the same, the difference is that NOW we give `SEED_PHRASE` as `TEST_ACCT_KEY` +2. Currently we support test phrase only and we sign it using ED25519 + +## Sockets + +1. Nothing is changes in sockets except for the url link + +## Signing (internal) + +1. We do not sign our transaction using ETH library anymore. We now use `nacl` and `hashlib` in order to sign the messages. +2. For signing orders we sign it using the following scheme + + 1. Assuming we have the order with values in sui format. + 2. We first get the `orderhash` of the order using the following method + + ```python flags = self.get_order_flags(order) + flags = hexToByteArray(numberToHex(flags,2)) + + buffer=bytearray() + orderPriceHex=hexToByteArray(numberToHex(int(order["price"]))) + orderQuantityHex=hexToByteArray(numberToHex(int(order['quantity']))) + orderLeverageHex=hexToByteArray (numberToHex(int(order['leverage']))) + orderSalt=hexToByteArray(numberToHex(int(order['salt']))) + orderExpiration=hexToByteArray(numberToHex(int(order['expiration']),16)) + orderMaker=hexToByteArray(numberToHex(int(order['maker'],16),64)) + orderMarket=hexToByteArray(numberToHex(int(order['market'],16),64)) + bluefin=bytearray("Bluefin", encoding="utf-8") + + buffer=orderPriceHex+orderQuantityHex+orderLeverageHex+orderSalt+orderExpiration+orderMaker+orderMarket+flags+bluefin + ``` + + 3. We then get the sha256 of the buffer.hex() + 4. We then sign it and append '1' to it, specifying that we signed it using ed25519 + +3. For signing onboarding signer we follow a different approach. + + 1. What is onboarding signer: When we are calling a Bluefin init function we sign a string using our private key and send it to bluefin exchange along with our public key, bluefin exchange verifies it and returns us a token. + 2. For signing it we first convert our message to bytes and then add [3,0,0, len(message)] to the start of our bytearray and then our message. if our message length is greater than 256 then it wont fit in a byte in this case we follow BCS methodology to send our message. + +4. For signing cancel order, there are two ways. + 1. We first sign the order and send it to bluefin. Now we have the hash of the order. We first change our encode our hash to BCS format. + ```python + sigDict={} + sigDict['orderHashes']=order_hash + encodedMessage=self.encode_message(sigDict) + ``` + 2. Please have a look at signer.py to see the implementation or encode_message. + 3. Then we sign the encodedMessage and send the signature to bluefin for cancelling order +5. For signing cancel order second method. + 1. We sign the order and send it to bluefin, now imagine we do not have the hash of our order. + 2. We resign our order and get the hash and then we follow the similar approach as above. Please have a look at `7.cancelling_orders.py` in our examples file. + +## A detailed Guide on Onboarding: + +1. Basically as explained earlier when sign the onboarding url and send it to bluefin, bluefin returns us the TOKEN. +2. The change in this repo is following. +3. ```python + # imagine msg="https://testnet.bluefin.io" + msgDict={} + msgDict['onboardingUrl']=msg + msg=json.dumps(msgDict,separators=(',', ':')) + # we first create a json something like this '{"onboardingURL":"https://testnet.bluefin.io"} + + # we then convert this json to a bytearray + msg_bytearray=bytearray(msg.encode("utf-8")) + intent=bytearray() + #we then append [3,0,0,length of our json object] to our intent bytearray + intent.extend([3,0,0, len(msg_bytearray)]) + intent=intent+msg_bytearray + + # we then take a blake2b hash of intent bytearray we created + hash=hashlib.blake2b(intent,digest_size=32) + #then we finally sign the hash + ``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f419762 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2022] [Seed Labs] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 65ce590..20c0ead 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,253 @@ -# bluefin-v2-client-python -Python client v2 for Bluefin Exchange API and Smart Contracts for SUI +
+ + +

Bluefin Python Client Library

+ +
+ +Python Client for the Bluefin Exchange API and SUI Contracts. +​ + +### Install + +The package can be installed from [PyPi](https://pypi.org/project/bluefin-v2-client-python/) using pip: + +``` +pip install bluefin_client_sui +``` + +Alternatively, you could run: + +``` +pip install . +``` + +The package currently supports python `>=3.8`. Find complete documentation on the library at https://docs.bluefin.io/. + +### Getting Started + +When initializing the client, users must accept [terms and conditions](https://bluefin.io/terms-of-use) and define network object containing the following values: + +```json +{ + "apiGateway": "https://dapi.api.sui-prod.bluefin.io", + "socketURL": "wss://dapi.api.sui-prod.bluefin.io", + "dmsURL": "https://dapi.api.sui-prod.bluefin.io", + "webSocketURL": "wss://notifications.api.sui-prod.bluefin.io", + "onboardingUrl": "https://trade.bluefin.io" +} +``` + +Users can import predefined networks from [constants](https://github.com/fireflyprotocol/bluefin-v2-client-python/blob/main/src/bluefin_client_sui/constants.py): + +```python +from bluefin_client_sui import Networks +``` + +For testing purposes use `Networks[SUI_STAGING]` and for production use `Networks[SUI_PROD]`. + +## Initialization example​ + +```python +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks +from pprint import pprint +import asyncio + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # on boards user on bluefin. Must be set to true for firs time use + await client.init(True) + + print('Account address:', client.get_public_address()) + + # gets user account on-chain data + data = await client.get_user_account_data() + + # close aio http connection + await client.apis.close_session() + + pprint(data) + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() +``` + +**Read-only Initialization:** +Bluefin-client can also be initialized in `read-only` mode, below is the example: + +```python +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks +from pprint import pprint +import asyncio + +async def main(): + # initialize client without providing private_key + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + ) + + # Initializing client for the private key provided. The second argument api_token is optional + await client.init(True,"54b0bfafc9a48728f76e52848a716e96d490263392e3959c2d44f05dea960761") + + # close aio http connection + await client.apis.close_session() + await client.dmsApi.close_session() + + pprint(data) + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() +``` + +​Here is the [list](https://docs.bluefin.io/8/2.readonly-access-data) of APIs that can be accessed in `read-only` mode. + +**Placing Orders:** + +```python +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS, ORDER_SIDE, ORDER_TYPE, OrderSignatureRequest +import asyncio + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + await client.init(True) + + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + # creates a LIMIT order to be signed + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1900, # price at which you want to place order + quantity=0.01, # quantity + side=ORDER_SIDE.SELL, + orderType=ORDER_TYPE.LIMIT, + leverage=user_leverage + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a limit order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + await client.apis.close_session() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() +``` + +​ +**Listening To Events Using Socket.io:** + +```python +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, SOCKET_EVENTS +import asyncio +import time + +def callback(event): + print("Event data:", event) + +async def main(): + # initialize + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + await client.init(True) + # make connection with bluefin exchange + await client.socket.open() + + # subscribe to local user events + await client.socket.subscribe_user_update_by_token() + + # listen to exchange health updates and trigger callback + await client.socket.listen(SOCKET_EVENTS.EXCHANGE_HEALTH.value, callback) + time.sleep(10) + # unsubscribe from user events + await client.socket.unsubscribe_user_update_by_token() + # close socket connection + await client.socket.close() + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close()​ +``` + +Look at the [example](https://github.com/fireflyprotocol/bluefin-v2-client-python/tree/main/examples) directory to see more examples on how to use this library. + +**Listening To Events Using Web Sockets:** + +```python +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, SOCKET_EVENTS, MARKET_SYMBOLS +import time +import asyncio + +def callback(event): + print("Event data:", event) + +async def main(): + # initialize + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + await client.init(True) + + def on_open(ws): + # subscribe to global events + resp = client.webSocketClient.subscribe_global_updates_by_symbol(symbol=MARKET_SYMBOLS.ETH) + if resp: + print("Subscribed to global updates") + resp = client.webSocketClient.subscribe_user_update_by_token() + if resp: + print("Subscribed to user updates") + + # make connection with bluefin exchange + client.webSocketClient.initialize_socket(on_open=on_open) + # listen to user order updates and trigger callback + client.webSocketClient.listen(SOCKET_EVENTS.EXCHANGE_HEALTH.value, callback) + + time.sleep(60) + + # unsubscribe from global events + client.webSocketClient.unsubscribe_global_updates_by_symbol(symbol=MARKET_SYMBOLS.ETH) + client.webSocketClient.stop() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() +``` diff --git a/bluefin-client-python-sui/src/bluefin_client_sui/constants.py b/bluefin-client-python-sui/src/bluefin_client_sui/constants.py new file mode 100644 index 0000000..af29f9b --- /dev/null +++ b/bluefin-client-python-sui/src/bluefin_client_sui/constants.py @@ -0,0 +1,67 @@ +Networks = { + "SUI_STAGING": { + "url": "https://fullnode.testnet.sui.io:443", + "apiGateway": "https://dapi.api.sui-staging.bluefin.io", + "socketURL": "wss://dapi.api.sui-staging.bluefin.io", + "dmsURL": "https://dapi.api.sui-staging.bluefin.io", + "webSocketURL": "wss://notifications.api.sui-staging.bluefin.io", + "onboardingUrl": "https://testnet.bluefin.io", + }, + "SUI_PROD": { + "url": "https://fullnode.testnet.sui.io:443", + "apiGateway": "https://dapi.api.sui-prod.bluefin.io", + "socketURL": "wss://dapi.api.sui-prod.bluefin.io", + "dmsURL": "https://dapi.api.sui-prod.bluefin.io", + "webSocketURL": "wss://notifications.api.sui-prod.bluefin.io", + "onboardingUrl": "https://trade.bluefin.io", + }, +} + +ORDER_FLAGS = {"IS_BUY": 1, "IS_DECREASE_ONLY": 2} + +TIME = { + "SECONDS_IN_A_MINUTE": 60, + "SECONDS_IN_A_DAY": 86400, + "SECONDS_IN_A_MONTH": 2592000, +} + +ADDRESSES = { + "ZERO": "0x0000000000000000000000000000000000000000", +} + +SERVICE_URLS = { + "MARKET": { + "ORDER_BOOK": "/orderbook", + "RECENT_TRADE": "/recentTrades", + "CANDLE_STICK_DATA": "/candlestickData", + "EXCHANGE_INFO": "/exchangeInfo", + "MARKET_DATA": "/marketData", + "META": "/meta", + "STATUS": "/status", + "SYMBOLS": "/marketData/symbols", + "CONTRACT_ADDRESSES": "/marketData/contractAddresses", + "TICKER": "/ticker", + "MASTER_INFO": "/masterInfo", + "FUNDING_RATE": "/fundingRate", + }, + "USER": { + "USER_POSITIONS": "/userPosition", + "USER_TRADES": "/userTrades", + "ORDERS": "/orders", + "GENERATE_READONLY_TOKEN": "/generateReadOnlyToken", + "ACCOUNT": "/account", + "USER_TRANSACTION_HISTORY": "/userTransactionHistory", + "AUTHORIZE": "/authorize", + "ADJUST_LEVERAGE": "/account/adjustLeverage", + "FUND_GAS": "/account/fundGas", + "TRANSFER_HISTORY": "/userTransferHistory", + "FUNDING_HISTORY": "/userFundingHistory", + "CANCEL_ON_DISCONNECT": "/dms-countdown", + }, + "ORDERS": { + "ORDERS": "/orders", + "ORDERS_HASH": "/orders/hash", + }, +} + +DAPI_BASE_NUM = 1000000000000000000 diff --git a/examples/1.initialization.py b/examples/1.initialization.py new file mode 100644 index 0000000..ebf65d3 --- /dev/null +++ b/examples/1.initialization.py @@ -0,0 +1,38 @@ +import os +import sys + +print(os.getcwd()) +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK + +from bluefin_client_sui import BluefinClient, Networks +from pprint import pprint +import asyncio + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # Initializing client for the private key provided. The second argument api_token is optional + await client.init(True) + + print("Account Address:", client.get_public_address()) + + # # gets user account data on-chain + data = await client.get_user_account_data() + + await client.close_connections() + + pprint(data) + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/10.1.socket_readonly.py b/examples/10.1.socket_readonly.py new file mode 100644 index 0000000..ea6cd88 --- /dev/null +++ b/examples/10.1.socket_readonly.py @@ -0,0 +1,85 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS, SOCKET_EVENTS +import asyncio + +TEST_NETWORK = "SUI_STAGING" + +event_received = False + + +def callback(event): + global event_received + print("Event data:", event) + event_received = True + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + response = await client.generate_readonly_token() + readOnlyclient = BluefinClient(True, Networks[TEST_NETWORK]) + await readOnlyclient.init(True, response) + + async def my_callback(): + print("Subscribing To Rooms") + # subscribe to global event updates for BTC market + status = await readOnlyclient.socket.subscribe_global_updates_by_symbol( + MARKET_SYMBOLS.BTC + ) + print("Subscribed to global BTC events: {}".format(status)) + + # subscribe to local user events + status = await readOnlyclient.socket.subscribe_user_update_by_token() + print("Subscribed to user events: {}".format(status)) + + # triggered when order book updates + print("Listening to exchange health updates") + await readOnlyclient.socket.listen( + SOCKET_EVENTS.EXCHANGE_HEALTH.value, callback + ) + + # triggered when status of any user order updates + print("Listening to user order updates") + await readOnlyclient.socket.listen(SOCKET_EVENTS.ORDER_UPDATE.value, callback) + + await readOnlyclient.socket.listen("connect", my_callback) + + # must open socket before subscribing + print("Making socket connection to Bluefin exchange") + await readOnlyclient.socket.open() + + # SOCKET_EVENTS contains all events that can be listened to + + # logs event name and data for all markets and users that are subscribed. + # helpful for debugging + # client.socket.listen("default",callback) + timeout = 30 + end_time = time.time() + timeout + while not event_received and time.time() < end_time: + time.sleep(1) + + # # unsubscribe from global events + status = await readOnlyclient.socket.unsubscribe_global_updates_by_symbol( + MARKET_SYMBOLS.BTC + ) + print("Unsubscribed from global BTC events: {}".format(status)) + + status = await readOnlyclient.socket.unsubscribe_user_update_by_token() + print("Unsubscribed from user events: {}".format(status)) + + # # close socket connection + print("Closing sockets!") + await readOnlyclient.socket.close() + + await readOnlyclient.apis.close_session() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/10.sockets.py b/examples/10.sockets.py new file mode 100644 index 0000000..eb855a0 --- /dev/null +++ b/examples/10.sockets.py @@ -0,0 +1,79 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS, SOCKET_EVENTS +import asyncio + +TEST_NETWORK = "SUI_STAGING" +event_received = False + + +def callback(event): + global event_received + print("Event data:", event) + event_received = True + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + async def my_callback(): + print("Subscribing To Rooms") + # subscribe to global event updates for BTC market + status = await client.socket.subscribe_global_updates_by_symbol( + MARKET_SYMBOLS.BTC + ) + print("Subscribed to global BTC events: {}".format(status)) + + # subscribe to local user events + status = await client.socket.subscribe_user_update_by_token() + print("Subscribed to user events: {}".format(status)) + + # triggered when order book updates + print("Listening to exchange health updates") + await client.socket.listen(SOCKET_EVENTS.EXCHANGE_HEALTH.value, callback) + + # triggered when status of any user order updates + print("Listening to user order updates") + await client.socket.listen(SOCKET_EVENTS.ORDER_UPDATE.value, callback) + + await client.socket.listen("connect", my_callback) + + # must open socket before subscribing + print("Making socket connection to Bluefin exchange") + await client.socket.open() + + # SOCKET_EVENTS contains all events that can be listened to + + # logs event name and data for all markets and users that are subscribed. + # helpful for debugging + # client.socket.listen("default",callback) + timeout = 30 + end_time = time.time() + timeout + while not event_received and time.time() < end_time: + time.sleep(1) + + # # unsubscribe from global events + status = await client.socket.unsubscribe_global_updates_by_symbol( + MARKET_SYMBOLS.BTC + ) + print("Unsubscribed from global BTC events: {}".format(status)) + + status = await client.socket.unsubscribe_user_update_by_token() + print("Unsubscribed from user events: {}".format(status)) + + # # close socket connection + print("Closing sockets!") + await client.socket.close() + + await client.apis.close_session() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/11.sub_accounts.py b/examples/11.sub_accounts.py new file mode 100644 index 0000000..a7b11b0 --- /dev/null +++ b/examples/11.sub_accounts.py @@ -0,0 +1,61 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_SUB_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + Networks, + OrderSignatureRequest, +) +import asyncio +from bluefin_client_sui.utilities import * + + +async def main(): + clientParent = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await clientParent.init(True) + + clientChild = BluefinClient(True, Networks[TEST_NETWORK], TEST_SUB_ACCT_KEY) + await clientChild.init(True) + + print("Parent: ", clientParent.get_public_address()) + + print("Child: ", clientChild.get_public_address()) + + # # whitelist sub account + status = await clientParent.update_sub_account( + clientChild.get_public_address(), True + ) + print(f"Sub account created: {status}") + + parent_leverage = await clientParent.get_user_leverage(MARKET_SYMBOLS.ETH) + await clientParent.adjust_leverage(MARKET_SYMBOLS.ETH, 1) + parent_leverage = await clientParent.get_user_leverage(MARKET_SYMBOLS.ETH) + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # sub account is only whitelisted for ETH market + maker=clientParent.get_public_address(), # maker of the order is the parent account + price=0, + quantity=0.02, + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.MARKET, + leverage=parent_leverage, + ) + + # order is signed using sub account's private key + signed_order = clientChild.create_signed_order(signature_request) + + resp = await clientChild.post_signed_order(signed_order) + + print(resp) + + await clientChild.close_connections() + await clientParent.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/12.open_order_event.py b/examples/12.open_order_event.py new file mode 100644 index 0000000..a65c53f --- /dev/null +++ b/examples/12.open_order_event.py @@ -0,0 +1,96 @@ +""" +The code example opens socket connection and listens to user order update events +It places a limit order and as soon as its OPENED on order book, we receive +an event, log its data and terminate connection +""" +import sys, os + +sys.path.append(os.getcwd() + "/src/") +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + SOCKET_EVENTS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) +import asyncio + +TEST_NETWORK = "SUI_STAGING" + +event_received = False + + +async def place_limit_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + await client.adjust_leverage(MARKET_SYMBOLS.ETH, 1) + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + # creates a LIMIT order to be signed + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1300000000000, # price at which you want to place order + quantity=10000000, # quantity + side=ORDER_SIDE.SELL, + orderType=ORDER_TYPE.LIMIT, + leverage=1000000000, + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a limit order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + return + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + def callback(event): + global event_received + print("Event data:", event) + event_received = True + + # must open socket before subscribing + print("Making socket connection to Bluefin exchange") + await client.socket.open() + + # subscribe to user events + await client.socket.subscribe_user_update_by_token() + print("Subscribed to user events") + + print("Listening to user order updates") + await client.socket.listen(SOCKET_EVENTS.ORDER_UPDATE.value, callback) + + # place a limit order + await place_limit_order(client) + + timeout = 30 + end_time = time.time() + timeout + while not event_received and time.time() < end_time: + time.sleep(1) + + status = await client.socket.unsubscribe_user_update_by_token() + print("Unsubscribed from user events: {}".format(status)) + + # close socket connection + print("Closing sockets!") + await client.socket.close() + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/13.orderbook_updates.py b/examples/13.orderbook_updates.py new file mode 100644 index 0000000..49531be --- /dev/null +++ b/examples/13.orderbook_updates.py @@ -0,0 +1,92 @@ +""" +When ever the state of orderbook changes, an event is emitted by exchange. +In this code example we open a socket connection and listen to orderbook update event +""" +import sys, os + +sys.path.append(os.getcwd() + "/src/") +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + SOCKET_EVENTS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) +import asyncio + +TEST_NETWORK = "SUI_STAGING" + +event_received = False + + +async def place_limit_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + # creates a LIMIT order to be signed + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1300, # price at which you want to place order + quantity=0.01, # quantity + side=ORDER_SIDE.SELL, + orderType=ORDER_TYPE.LIMIT, + leverage=user_leverage, + ) + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a limit order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + return + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + def callback(event): + global event_received + print("Event data:", event) + event_received = True + + # must open socket before subscribing + print("Making socket connection to Bluefin exchange") + await client.socket.open() + + # subscribe to global event updates for ETH market + await client.socket.subscribe_global_updates_by_symbol(MARKET_SYMBOLS.ETH) + print("Subscribed to ETH Market events") + + print("Listening to ETH Orderbook update event") + await client.socket.listen(SOCKET_EVENTS.ORDERBOOK_UPDATE.value, callback) + + await place_limit_order(client) + + timeout = 30 + end_time = time.time() + timeout + while not event_received and time.time() < end_time: + time.sleep(1) + status = await client.socket.unsubscribe_global_updates_by_symbol( + MARKET_SYMBOLS.ETH + ) + print("Unsubscribed from orderbook update events for ETH Market: {}".format(status)) + + # close socket connection + print("Closing sockets!") + await client.socket.close() + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/14.web_sockets.py b/examples/14.web_sockets.py new file mode 100644 index 0000000..50885ce --- /dev/null +++ b/examples/14.web_sockets.py @@ -0,0 +1,75 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + SOCKET_EVENTS, + config_logging, +) +import asyncio +import logging + +config_logging(logging, logging.DEBUG) + +event_received = False + + +def callback(event): + global event_received + print("Event data:", event) + event_received = True + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + def on_error(ws, error): + print(error) + + def on_close(ws): + # unsubscribe from global events + status = client.webSocketClient.unsubscribe_global_updates_by_symbol( + MARKET_SYMBOLS.ETH + ) + print("Unsubscribed from global ETH events: {}".format(status)) + # close socket connection + print("### closed ###") + + def on_open(ws): + # subscribe to global event updates for ETH market + status = client.webSocketClient.subscribe_global_updates_by_symbol( + MARKET_SYMBOLS.ETH + ) + print("Subscribed to global ETH events: {}".format(status)) + + # SOCKET_EVENTS contains all events that can be listened to + print("Listening to Exchange Health updates") + client.webSocketClient.listen(SOCKET_EVENTS.EXCHANGE_HEALTH.value, callback) + + # logs event name and data for all markets and users that are subscribed. + # helpful for debugging + # client.socket.listen("default",callback) + + print("Making socket connection to Bluefin exchange") + client.webSocketClient.initialize_socket( + on_open=on_open, on_error=on_error, on_close=on_close + ) + + timeout = 30 + end_time = time.time() + timeout + while not event_received and time.time() < end_time: + time.sleep(1) + + client.webSocketClient.stop() + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/15.get_funding_history.py b/examples/15.get_funding_history.py new file mode 100644 index 0000000..dd45644 --- /dev/null +++ b/examples/15.get_funding_history.py @@ -0,0 +1,38 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + GetFundingHistoryRequest, +) +from pprint import pprint +import asyncio + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + # create a funding history request + funding_history_request = GetFundingHistoryRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + pageSize=50, # gets provided number of payments <= 50 + cursor=0, # fetch a particular page. A single page contains upto 50 records + ) + + # submit request for funding history + funding_history_response = await client.get_funding_history(funding_history_request) + + # returns funding history response + pprint(funding_history_response) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/16.listening_events_using_sub_account.py b/examples/16.listening_events_using_sub_account.py new file mode 100644 index 0000000..c13ceba --- /dev/null +++ b/examples/16.listening_events_using_sub_account.py @@ -0,0 +1,93 @@ +""" + This example shows how you can listen to user events using sub account + On our exchange, a sub account is trading on its parent's position and thus + has no position of its own. So when placing orders or listening to position updates + the sub account must specify the parent address whose position its listening. +""" +import time, sys +from config import TEST_ACCT_KEY, TEST_NETWORK, TEST_SUB_ACCT_KEY +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + OrderSignatureRequest, + ORDER_SIDE, + ORDER_TYPE, + SOCKET_EVENTS, +) +import asyncio + + +def callback(event): + print("Event data: {}\n".format(event)) + + +async def main(): + clientParent = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await clientParent.init(True) + + clientChild = BluefinClient(True, Networks[TEST_NETWORK], TEST_SUB_ACCT_KEY) + await clientChild.init(True) + + print("Parent: ", clientParent.get_public_address()) + + print("Child: ", clientChild.get_public_address()) + + # # whitelist sub account + status = await clientParent.update_sub_account( + MARKET_SYMBOLS.ETH, clientChild.get_public_address(), True + ) + print("Sub account created: {}\n".format(status)) + + # must open socket before subscribing + print("Making socket connection to Bluefin exchange") + await clientChild.socket.open() + + # subscribe to parent's events + resp = await clientChild.socket.subscribe_user_update_by_token( + clientParent.get_public_address() + ) + print("Subscribed to parent's events:", resp) + + # triggered when status of any user order updates + print("Listening to parents position updates") + await clientChild.socket.listen(SOCKET_EVENTS.POSITION_UPDATE.value, callback) + + parent_leverage = await clientParent.get_user_leverage(MARKET_SYMBOLS.ETH) + + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # sub account is only whitelisted for ETH market + maker=clientParent.get_public_address(), # maker of the order is the parent account + price=0, + quantity=0.02, + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.MARKET, + leverage=parent_leverage, + ) + + # order is signed using sub account's private key + signed_order = clientChild.create_signed_order(signature_request) + + resp = await clientChild.post_signed_order(signed_order) + + print(resp) + + time.sleep(10) + + status = await clientChild.socket.unsubscribe_user_update_by_token( + clientParent.get_public_address() + ) + print("Unsubscribed from user events: {}".format(status)) + + # close socket connection + print("Closing sockets!") + await clientChild.socket.close() + + await clientChild.close_connections() + await clientParent.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/17.1.get_orders_readonly.py b/examples/17.1.get_orders_readonly.py new file mode 100644 index 0000000..38a1b4e --- /dev/null +++ b/examples/17.1.get_orders_readonly.py @@ -0,0 +1,76 @@ +""" + This example shows how users can get their orders information. + The get_orders route provides a number of optional params that can be + mixed together to fetch the exact data that user needs. +""" + +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_STATUS, + ORDER_TYPE, +) +import asyncio + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + response = await client.generate_readonly_token() + readOnlyclient = BluefinClient(True, Networks[TEST_NETWORK]) + await readOnlyclient.init(True, response) + + print("Get all ETH market orders regardless of their type/status") + orders = await readOnlyclient.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + } + ) + print("Received orders: ", len(orders)) + + print("Get orders based on status (OPEN and PENDING)") + orders = await readOnlyclient.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + "statuses": [ORDER_STATUS.OPEN, ORDER_STATUS.PENDING], + } + ) + print("Received orders: ", len(orders)) + + print("Get an order 180318 using id (possible this order is not available anymore)") + orders = await readOnlyclient.get_orders( + {"symbol": MARKET_SYMBOLS.ETH, "orderId": 180318} + ) + print("Received orders: ", len(orders)) + + print("Get orders using hashes (possible these orders are not available anymore)") + orders = await readOnlyclient.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + "orderHashes": [ + "0x21eeb24b0af6832989484e61294db70e8cf8ce0e030c6cfbbb23f3b3d85f9374", + "0xd61fe390f6e6d89a884c73927741ba7d2d024e01f65af61f13363403e805e2c0", + ], + } + ) + print("Received orders: ", len(orders)) + + print("Get only MARKET orders for ETH market") + orders = await readOnlyclient.get_orders( + {"symbol": MARKET_SYMBOLS.ETH, "orderType": [ORDER_TYPE.MARKET]} + ) + print("Received orders: ", len(orders)) + + await readOnlyclient.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/17.get_orders.py b/examples/17.get_orders.py new file mode 100644 index 0000000..a97a82c --- /dev/null +++ b/examples/17.get_orders.py @@ -0,0 +1,69 @@ +""" + This example shows how users can get their orders information. + The get_orders route provides a number of optional params that can be + mixed together to fetch the exact data that user needs. +""" +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_STATUS, + ORDER_TYPE, +) +import asyncio + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + print("Get all ETH market orders regardless of their type/status") + orders = await client.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + } + ) + print("Received orders: ", len(orders)) + + print("Get orders based on status (OPEN and PENDING)") + orders = await client.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + "statuses": [ORDER_STATUS.OPEN, ORDER_STATUS.PENDING], + } + ) + print("Received orders: ", len(orders)) + + print("Get an order 180318 using id (possible this order is not available anymore)") + orders = await client.get_orders({"symbol": MARKET_SYMBOLS.ETH, "orderId": 180318}) + print("Received orders: ", len(orders)) + + print("Get orders using hashes (possible these orders are not available anymore)") + orders = await client.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + "orderHashes": [ + "0x21eeb24b0af6832989484e61294db70e8cf8ce0e030c6cfbbb23f3b3d85f9374", + "0xd61fe390f6e6d89a884c73927741ba7d2d024e01f65af61f13363403e805e2c0", + ], + } + ) + print("Received orders: ", len(orders)) + + print("Get only MARKET orders for ETH market") + orders = await client.get_orders( + {"symbol": MARKET_SYMBOLS.ETH, "orderType": [ORDER_TYPE.MARKET]} + ) + print("Received orders: ", len(orders)) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/18.dms_api.py b/examples/18.dms_api.py new file mode 100644 index 0000000..ab6a95a --- /dev/null +++ b/examples/18.dms_api.py @@ -0,0 +1,50 @@ +import json + +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_SUB_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + Networks, + OrderSignatureRequest, +) +import asyncio + +from bluefin_client_sui.interfaces import PostTimerAttributes + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + print("User: ", client.get_public_address()) + countDownsObject: PostTimerAttributes = dict() + countDowns = list() + countDowns.append({"symbol": MARKET_SYMBOLS.BTC.value, "countDown": 3 * 1000}) + + countDowns.append({"symbol": MARKET_SYMBOLS.ETH.value, "countDown": 3 * 1000}) + + countDownsObject["countDowns"] = countDowns + try: + # sending post request to reset user's count down timer with MARKET_SYMBOL for auto cancellation of order + postResponse = await client.reset_cancel_on_disconnect_timer(countDownsObject) + print(postResponse) + # get request to get user's count down timer for MARKET_SYMBOL + getResponse = await client.get_cancel_on_disconnect_timer( + {"symbol": MARKET_SYMBOLS.ETH} + ) + print(getResponse) + + except Exception as e: + print(e) + + await client.close_connections() + + +if __name__ == "__main__": + event_loop = asyncio.get_event_loop() + event_loop.run_until_complete(main()) diff --git a/examples/19.Generate_readonly_token.py b/examples/19.Generate_readonly_token.py new file mode 100644 index 0000000..5d66925 --- /dev/null +++ b/examples/19.Generate_readonly_token.py @@ -0,0 +1,36 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks +from pprint import pprint +import asyncio + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # initialize the client + # on boards user on Bluefin. Must be set to true for first time use + # + await client.init(True) + + print("Account Address:", client.get_public_address()) + + # # generates read-only token for user + data = await client.generate_readonly_token() + + print("Read-only Token:", str(data)) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/2.user_info.py b/examples/2.user_info.py new file mode 100644 index 0000000..1b6b65c --- /dev/null +++ b/examples/2.user_info.py @@ -0,0 +1,43 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS +from pprint import pprint +import asyncio + + +async def main(): + # create client instance + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # initialize the client + # on boards user on Bluefin. Must be set to true for first time use + await client.init(True) + + # gets user account data on Bluefin exchange + data = await client.get_user_account_data() + + pprint(data) + + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.ETH}) + + # returns {} when user has no position + pprint(position) + + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.BTC}) + + # returns user position if exists + pprint(position) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/20.contract_call.py b/examples/20.contract_call.py new file mode 100644 index 0000000..2e01f4e --- /dev/null +++ b/examples/20.contract_call.py @@ -0,0 +1,57 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +import base64 +from bluefin_client_sui.utilities import * +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) +import asyncio +from bluefin_client_sui import signer +from bluefin_client_sui import * + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + await client.init(True) + ### you need to have usdc coins to deposit it to margin bank. + ### the below functions just gets usdc coins that you have. + usdc_coins = client.get_usdc_coins() + + coin_obj_id = usdc_coins["data"][1]["coinObjectId"] + await client.deposit_margin_to_bank(1000, coin_obj_id) + + # await client.withdraw_margin_from_bank(100) + + # await client.withdraw_all_margin_from_bank() + + print("Printing Margin Bank balance") + print(await client.get_margin_bank_balance()) + + print("Printing usdc balance") + print(await client.get_usdc_balance()) + + print("Printing SUI balance") + print(await client.get_native_chain_token_balance()) + + print("getting usdc coins") + print(client.get_usdc_coins()) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/3.balance.py b/examples/3.balance.py new file mode 100644 index 0000000..ea204ec --- /dev/null +++ b/examples/3.balance.py @@ -0,0 +1,53 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks +import asyncio + + +async def main(): + # create client instance + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # initialize the client + # on boards user on Bluefin. Must be set to true for first time use + await client.init(True) + + # checks chain native token balance. + # A user must have native tokens to perform contract calls + + print("Chain token balance:", await client.get_native_chain_token_balance()) + + # check margin bank balance on-chain + print("Margin bank balance:", await client.get_margin_bank_balance()) + + # check usdc balance user has on-chain + print("USDC balance:", await client.get_usdc_balance()) + + # deposit usdc to margin bank + # must have native chain tokens to pay for gas fee + print("USDC deposited:", await client.deposit_margin_to_bank(10)) + + # check margin bank balance + resp = await client.get_margin_bank_balance() + print("Margin bank balance:", resp) + + # withdraw margin bank balance + print("USDC Withdrawn:", await client.withdraw_margin_from_bank(resp)) + + # check margin bank balance + print("Margin bank balance:", await client.get_margin_bank_balance()) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/4.placing_orders.py b/examples/4.placing_orders.py new file mode 100644 index 0000000..1d17f97 --- /dev/null +++ b/examples/4.placing_orders.py @@ -0,0 +1,96 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) +import asyncio + + +async def place_limit_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + print("User Leverage", user_leverage) + + # creates a LIMIT order to be signed + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1636.8, # price at which you want to place order + quantity=0.01, # quantity + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.LIMIT, + leverage=user_leverage, + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a limit order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + return + + +async def place_market_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, + price=0, + quantity=1, + leverage=1, + side=ORDER_SIDE.BUY, + reduceOnly=False, + postOnly=False, + orderbookOnly=True, + maker="0xa3c3504d90c428274beaa89f1238a769ea1d1c3516c31c0f4157f33787367af0", + expiration=1700530261000, + salt=1668690862116, + orderType=ORDER_TYPE.MARKET, + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a market order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + return + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + await client.init(True) + + # await place_limit_order(client) + await client + await place_limit_order(client) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/5.adjusting_leverage.py b/examples/5.adjusting_leverage.py new file mode 100644 index 0000000..1b5638b --- /dev/null +++ b/examples/5.adjusting_leverage.py @@ -0,0 +1,48 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS +import asyncio + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + await client.init(True, symbol=MARKET_SYMBOLS.BTC) + + print("Leverage on BTC market:", await client.get_user_leverage(MARKET_SYMBOLS.BTC)) + # we have a position on BTC so this will perform on-chain leverage update + # must have native chain tokens to pay for gas fee + await client.adjust_leverage(MARKET_SYMBOLS.BTC, 6) + + print("Leverage on BTC market:", await client.get_user_leverage(MARKET_SYMBOLS.BTC)) + + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + await client.init(True, symbol=MARKET_SYMBOLS.ETH) + + print("Leverage on ETH market:", await client.get_user_leverage(MARKET_SYMBOLS.ETH)) + # since we don't have a position on-chain, it will perform off-chain leverage adjustment + await client.adjust_leverage(MARKET_SYMBOLS.ETH, 7) + + print("Leverage on ETH market:", await client.get_user_leverage(MARKET_SYMBOLS.ETH)) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/6.adjusting_margin.py b/examples/6.adjusting_margin.py new file mode 100644 index 0000000..d9ce6d5 --- /dev/null +++ b/examples/6.adjusting_margin.py @@ -0,0 +1,120 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS, ADJUST_MARGIN +import asyncio +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) + +TEST_NETWORK = "SUI_STAGING" + + +async def place_limit_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + await client.adjust_leverage(MARKET_SYMBOLS.ETH, 3) + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + print("User Leverage", user_leverage) + + # creates a LIMIT order to be signed + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1636.8, # price at which you want to place order + quantity=0.01, # quantity + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.LIMIT, + leverage=user_leverage, + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a limit order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + return + + +async def place_market_order(client: BluefinClient): + # default leverage of account is set to 3 on Bluefin + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.ETH) + + signature_request = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, + price=0, + quantity=1, + leverage=user_leverage, + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.MARKET, + ) + + # create signed order + signed_order = client.create_signed_order(signature_request) + + print("Placing a market order") + # place signed order on orderbook + resp = await client.post_signed_order(signed_order) + + # returned order with PENDING state + print(resp) + + return + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + print(await client.get_usdc_balance()) + + # usdc_coins=client.get_usdc_coins() + # coin_obj_id=usdc_coins["data"][1]["coinObjectId"] + # await client.deposit_margin_to_bank(1000000000000, coin_obj_id) + + print(await client.get_margin_bank_balance()) + await place_market_order(client) + + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.ETH}) + print("Current margin in position:", position) + + # adding 100$ from our margin bank into our BTC position on-chain + # must have native chain tokens to pay for gas fee + await client.adjust_margin(MARKET_SYMBOLS.ETH, ADJUST_MARGIN.ADD, 100) + + # get updated position margin. Note it can take a few seconds to show updates + # to on-chain positions on exchange as off-chain infrastructure waits for blockchain + # to emit position update event + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.ETH}) + print("Current margin in position:", position["margin"]) + + # removing 100$ from margin + await client.adjust_margin(MARKET_SYMBOLS.ETH, ADJUST_MARGIN.REMOVE, 100) + + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.ETH}) + print("Current margin in position:", int(position["margin"])) + + try: + # will throw as user does not have any open position on BTC to adjust margin on + await client.adjust_margin(MARKET_SYMBOLS.BTC, ADJUST_MARGIN.ADD, 100) + except Exception as e: + print("Error:", e) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/7.cancelling_orders.py b/examples/7.cancelling_orders.py new file mode 100644 index 0000000..b47f07c --- /dev/null +++ b/examples/7.cancelling_orders.py @@ -0,0 +1,75 @@ +import sys, os, random + +sys.path.append(os.getcwd() + "/src/") + +import time +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + ORDER_STATUS, + OrderSignatureRequest, +) +from pprint import pprint +import asyncio + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + # client.create_order_to_sign() + await client.adjust_leverage(MARKET_SYMBOLS.ETH, 1) + + # creates a LIMIT order to be signed + order = OrderSignatureRequest( + symbol=MARKET_SYMBOLS.ETH, # market symbol + price=1636.8, # price at which you want to place order + quantity=0.01, # quantity + side=ORDER_SIDE.BUY, + orderType=ORDER_TYPE.LIMIT, + leverage=1, + salt=random.randint(0, 100000000), + expiration=int(time.time() + (30 * 24 * 60 * 60)) * 1000, + ) + + signed_order = client.create_signed_order(order) + resp = await client.post_signed_order(signed_order) + print("sleeping for two seconds") + + # sign order for cancellation using order hash + # you can pass a list of hashes to be signed for cancellation, good to be used when multiple orders are to be cancelled + cancellation_request = client.create_signed_cancel_orders( + MARKET_SYMBOLS.ETH, order_hash=[resp["hash"]] + ) + pprint(cancellation_request) + + # # or sign the order for cancellation using order data + cancellation_request = client.create_signed_cancel_order(order) + pprint(cancellation_request) # same as above cancellation request + + # post order to exchange for cancellation + resp = await client.post_cancel_order(cancellation_request) + + pprint(resp) + + # cancels all open orders, returns false if there is no open order to cancel + resp = await client.cancel_all_orders( + MARKET_SYMBOLS.ETH, [ORDER_STATUS.OPEN, ORDER_STATUS.PARTIAL_FILLED] + ) + + if resp == False: + print("No open order to cancel") + else: + pprint(resp) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/8.exchange_data.py b/examples/8.exchange_data.py new file mode 100644 index 0000000..da56722 --- /dev/null +++ b/examples/8.exchange_data.py @@ -0,0 +1,72 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + TRADE_TYPE, + Interval, +) +from pprint import pprint +import asyncio + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + # returns status/health of exchange + status = await client.get_exchange_status() + pprint(status) + + # gets state of order book. Gets first 10 asks and bids + orderbook = await client.get_orderbook({"symbol": MARKET_SYMBOLS.ETH, "limit": 10}) + pprint(orderbook) + + # returns available market for trading + market_symbols = await client.get_market_symbols() + print(market_symbols) + + # gets current funding rate on market + funding_rate = await client.get_funding_rate(MARKET_SYMBOLS.ETH) + pprint(funding_rate) + + # gets markets meta data about contracts, blockchain, exchange url + meta = await client.get_market_meta_info() # (optional param MARKET_SYMBOL) + # should log meta for all markets + pprint(meta) + + # gets market's current state + market_data = await client.get_market_data(MARKET_SYMBOLS.ETH) + pprint(market_data) + + # gets market data about min/max order size, oracle price, fee etc.. + exchange_info = await client.get_exchange_info(MARKET_SYMBOLS.ETH) + pprint(exchange_info) + + # gets market candle info + candle_data = await client.get_market_candle_stick_data( + {"symbol": MARKET_SYMBOLS.ETH, "interval": Interval._1M} + ) + pprint(candle_data) + + # gets recent isolated/normal trades on ETH market + recent_trades = await client.get_market_recent_trades( + {"symbol": MARKET_SYMBOLS.ETH, "traders": TRADE_TYPE.ISOLATED} + ) + pprint(recent_trades) + + # gets addresses of on-chain contracts + contract_address = await client.get_contract_addresses() + pprint(contract_address) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/9.user_data.py b/examples/9.user_data.py new file mode 100644 index 0000000..ea9df5d --- /dev/null +++ b/examples/9.user_data.py @@ -0,0 +1,70 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") + +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import BluefinClient, Networks, MARKET_SYMBOLS, ORDER_STATUS +from pprint import pprint +import asyncio + +TEST_NETWORK = "SUI_STAGING" + + +async def main(): + client = BluefinClient(True, Networks[TEST_NETWORK], TEST_ACCT_KEY) + await client.init(True) + + # returns user account (having pvt key and pub address) + user_account = client.get_account() + print("account:", user_account) + + # returns user public address + pub_address = client.get_public_address() + print("pub_address:", pub_address) + + # used to fetch user orders. Pass in statuses of orders to get + orders = await client.get_orders( + { + "symbol": MARKET_SYMBOLS.ETH, + "statuses": [ORDER_STATUS.OPEN, ORDER_STATUS.PENDING], + } + ) + + print("User open and pending orders:") + pprint(orders) + + # fetches user transaction history. Pass page number and size as the route is paginated + tx_history = await client.get_transaction_history( + { + "symbol": MARKET_SYMBOLS.ETH, + } + ) + print("User transaction history:") + pprint(tx_history) + + # gets user current position + position = await client.get_user_position({"symbol": MARKET_SYMBOLS.ETH}) + + print("User position:") + pprint(position) + + # fetches user trades + trades = await client.get_user_trades({"symbol": MARKET_SYMBOLS.BTC}) + print("User trades:") + pprint(trades) + + # fetches user account's general data like leverage, pnl etc. + account_data = await client.get_user_account_data() + print("Account data:") + pprint(account_data) + + user_leverage = await client.get_user_leverage(MARKET_SYMBOLS.BTC) + print("Account leverage:", user_leverage) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/examples/SocketClient.py b/examples/SocketClient.py new file mode 100644 index 0000000..fd1f627 --- /dev/null +++ b/examples/SocketClient.py @@ -0,0 +1,25 @@ +import socketio + +# Create a Socket.IO client instance +sio = socketio.Client() + + +# Define an event handler for the 'connect' event +@sio.event +def connect(): + print("Connected to server") + # Subscribe to the 'message' event + # sio.emit('subscribeToRoom', "room1") + + +# Define an event handler for the 'message' event +@sio.event +def roomMessage(data): + print("Received message:", data) + + +# Connect to the Socket.IO server +sio.connect("http://localhost:3000") + +# Wait for events and keep the connection alive +sio.wait() diff --git a/examples/SocketServer.py b/examples/SocketServer.py new file mode 100644 index 0000000..2c9c805 --- /dev/null +++ b/examples/SocketServer.py @@ -0,0 +1,44 @@ +import socketio +import eventlet +import time + +# Create a Socket.IO server instance +sio = socketio.Server() + + +# Define an event handler for the 'connect' event +@sio.on("connect") +def handle_connect(sid, environ): + print("Client connected:", sid) + background_task() + + +# Define an event handler for the 'message' event +@sio.on("message") +def handle_message(sid, data): + print("Received message:", data) + # Broadcast the received message to all connected clients + sio.emit("message", data) + + +# Define the broadcast task +def broadcast_message(): + message = "Hello from the server!" + print(message) + sio.emit("message", message, namespace="/") + + +# Background task to broadcast messages at an interval +def background_task(): + print("hello") + while True: + time.sleep(5) # Adjust the interval as needed + broadcast_message() + + +# Run the Socket.IO server +if __name__ == "__main__": + app = socketio.WSGIApp(sio) + socketio.Middleware(sio, app) + eventlet.wsgi.server(eventlet.listen(("", 3051)), app) + print("hello after") diff --git a/examples/config.py b/examples/config.py new file mode 100644 index 0000000..77f4011 --- /dev/null +++ b/examples/config.py @@ -0,0 +1,6 @@ +TEST_ACCT_KEY = ( + "negative repeat fold noodle symptom spirit spend trophy merge ethics math erupt" +) +# TEST_ACCT_KEY = "milk fit tape notable input seek circle define deny rally camera sorry" +TEST_SUB_ACCT_KEY = "7540d48032c731b3a17947b63a04763492d84aef854246d355a703adc9b54ce9" +TEST_NETWORK = "SUI_STAGING" diff --git a/examples/contract_call.py b/examples/contract_call.py new file mode 100644 index 0000000..2a632ee --- /dev/null +++ b/examples/contract_call.py @@ -0,0 +1,52 @@ +import sys, os + +sys.path.append(os.getcwd() + "/src/") +import base64 +from bluefin_client_sui.utilities import * +from config import TEST_ACCT_KEY, TEST_NETWORK +from bluefin_client_sui import ( + BluefinClient, + Networks, + MARKET_SYMBOLS, + ORDER_SIDE, + ORDER_TYPE, + OrderSignatureRequest, +) +import asyncio +from bluefin_client_sui import signer +from bluefin_client_sui import * + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + await client.init(True) + + await client.withdraw_margin_from_bank(1000) + + txBytes = "AAADAQGPzuAZV5krLKJWD1WUXOjk7Guz2vki2pBMZJ6CXkRf1XLGgQAAAAAAAQAgH/qFdX+Vzs7ShiLTDkGkUsiFFt9TRQyxkTgS3YKNWWgAEOgDAAAAAAAAAAAAAAAAAAABAF82iNaL7Cly31h0P767ErFoKbQb8bxQeNNRAJDvbPWOC21hcmdpbl9iYW5rEndpdGhkcmF3X2Zyb21fYmFuawADAQAAAQEAAQIAH/qFdX+Vzs7ShiLTDkGkUsiFFt9TRQyxkTgS3YKNWWgBWiybg/9fRf7zXUmsdBU3umIFeucEZNWeMGzlLttPf86tHg8AAAAAACA3qZfzxP1+yVILtVkJ6LdfZvkF7gK877AJco9Xook9Th/6hXV/lc7O0oYi0w5BpFLIhRbfU0UMsZE4Et2CjVlo6AMAAAAAAAAA4fUFAAAAAAA=" + dec_msg = base64.b64decode(txBytes) + mysigner = signer.Signer() + + seed = "negative repeat fold noodle symptom spirit spend trophy merge ethics math erupt" + sui_wallet = SuiWallet(seed=seed) + + # private_key=mnemonicToPrivateKey(seed) + # privateKeyBytes=private_key.ToBytes() + # publicKey=privateKeyToPublicKey(private_key) + # publicKeyBytes=publicKey.ToBytes() + + result = mysigner.sign_tx(dec_msg, sui_wallet) + print(result) + + await client.close_connections() + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..cdd348e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[project] +name = "bluefin_client_sui" +version = "2.0.0" +description = "Library to interact with firefly exchange protocol including its off-chain api-gateway and on-chain contracts" +readme = "README.md" +requires-python = ">=3.8" +keywords = ["bluefin", "exchange", "decentralized", "perpetuals", "blockchain"] +dependencies = [ + 'web3 ~= 5.31.3', + 'requests ~= 2.28.1', + 'python-socketio ~= 5.7.2', + 'websocket-client ~= 1.5.1' +] + +[project.urls] +"Homepage" = "https://github.com/fireflyprotocol/bluefin-v2-client-python" +"Bug Reports" = "https://github.com/fireflyprotocol/bluefin-v2-client-python/issues" +"Source" = "https://github.com/fireflyprotocol/bluefin-v2-client-python" + +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..47dfbbe --- /dev/null +++ b/requirements.in @@ -0,0 +1,33 @@ +aiohttp==3.8.5 +aiosignal==1.3.1 +asn1crypto==1.5.1 +async-timeout==4.0.3 +attrs==23.1.0 +bidict==0.22.1 +bip-utils==2.7.1 +cbor2==5.4.6 +cffi==1.15.1 +charset-normalizer==3.2.0 +coincurve==17.0.0 +crcmod==1.7 +ecdsa==0.18.0 +ed25519-blake2b==1.4 +frozenlist==1.4.0 +gevent==23.7.0 +greenlet==2.0.2 +idna==3.4 +multidict==6.0.4 +netifaces==0.10.6 +py-sr25519-bindings==0.2.0 +pycparser==2.21 +pycryptodome==3.18.0 +PyNaCl==1.5.0 +python-engineio==4.6.0 +python-socketio==5.8.0 +six==1.16.0 +socketio==0.2.1 +websocket==0.2.1 +websocket-client==1.6.2 +yarl==1.9.2 +zope.event==5.0 +zope.interface==6.0 diff --git a/res/banner.png b/res/banner.png new file mode 100644 index 0000000..e109051 Binary files /dev/null and b/res/banner.png differ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bluefin_client_sui/__init__.py b/src/bluefin_client_sui/__init__.py new file mode 100644 index 0000000..9b2492c --- /dev/null +++ b/src/bluefin_client_sui/__init__.py @@ -0,0 +1,6 @@ +from .api_service import * +from .client import * +from .constants import * +from .enumerations import * +from .interfaces import * +from .utilities import * diff --git a/src/bluefin_client_sui/account.py b/src/bluefin_client_sui/account.py new file mode 100644 index 0000000..b4a8ac5 --- /dev/null +++ b/src/bluefin_client_sui/account.py @@ -0,0 +1,36 @@ +from .utilities import * +import base64 + + +class SuiWallet: + def __init__(self, seed="", privateKey=""): + if seed == "" and privateKey == "": + return "Error" + if seed != "": + self.privateKey = mnemonicToPrivateKey(seed) + self.publicKey = privateKeyToPublicKey(self.privateKey) + self.key = self.getPrivateKey() + elif privateKey != "": + self.privateKey = privateKey + self.publicKey = privateKeyToPublicKey(self.privateKey) + self.key = self.getPrivateKey() + + else: + return "error" + + self.publicKeyBase64 = base64.b64encode(self.publicKey.ToBytes()[1:]) + self.privateKeyBase64 = base64.b64encode(self.privateKey.ToBytes()[1:]) + + self.privateKeyBytes = self.privateKey.ToBytes() + self.publicKeyBytes = self.publicKey.ToBytes() + + self.address = getAddressFromPublicKey(self.publicKey) + + def getPublicKey(self): + return self.publicKey.ToHex() + + def getPrivateKey(self): + return self.privateKey.ToHex() + + def getUserAddress(self): + return self.address diff --git a/src/bluefin_client_sui/api_service.py b/src/bluefin_client_sui/api_service.py new file mode 100644 index 0000000..35b5a10 --- /dev/null +++ b/src/bluefin_client_sui/api_service.py @@ -0,0 +1,119 @@ +import json +import aiohttp +from .interfaces import * + + +class APIService: + def __init__(self, url, UUID=""): + self.server_url = url + self.auth_token = None + self.api_token = None + self.uuid = UUID + self.client = aiohttp.ClientSession() + + async def close_session(self): + if self.client is not None: + return await self.client.close() + + async def get(self, service_url, query={}, auth_required=False): + """ + Makes a GET request and returns the results + Inputs: + - service_url(str): the url to make the request to. + - query(dict): the get query. + - auth_required(bool): indicates whether authorization is required for the call or not. + """ + url = self._create_url(service_url) + + response = None + if auth_required: + response = await self.client.get( + url, + params=query, + headers={ + "Authorization": "Bearer {}".format(self.auth_token) + if self.auth_token + else "", + "x-api-token": self.api_token or "", + "x-mm-id": self.uuid or "", + }, + ) + else: + response = await self.client.get(url, params=query) + + try: + if response.status != 503: # checking for service unavailitbility + return await response.json() + else: + return response + except: + raise Exception("Error while getting {}: {}".format(url, response)) + + async def post(self, service_url, data, auth_required=False, contentType=""): + """ + Makes a POST request and returns the results + Inputs: + - service_url(str): the url to make the request to. + - data(dict): the data to post with POST request. + - auth_required(bool): indicates whether authorization is required for the call or not. + """ + url = self._create_url(service_url) + response = None + if auth_required: + headers = { + "Authorization": "Bearer {}".format(self.auth_token), + "x-mm-id": self.uuid or "", + } + + if contentType is not "": + headers["Content-type"] = contentType + + response = await self.client.post(url=url, data=data, headers=headers) + else: + response = await self.client.post(url=url, data=data) + + try: + if response.status != 503: # checking for service unavailitbility + return await response.json() + else: + return response + except: + raise Exception("Error while posting to {}: {}".format(url, response)) + + async def delete(self, service_url, data, auth_required=False): + """ + Makes a DELETE request and returns the results + Inputs: + - service_url(str): the url to make the request to. + - data(dict): the data to post with POST request. + - auth_required(bool): indicates whether authorization is required for the call or not. + """ + url = self._create_url(service_url) + + response = None + if auth_required: + response = await self.client.delete( + url=url, + data=data, + headers={ + "Authorization": "Bearer {}".format(self.auth_token), + "x-mm-id": self.uuid or "", + }, + ) + else: + response = await self.client.delete(url=url, data=data) + + try: + return await response.json() + except: + raise Exception("Error while posting to {}: {}".format(url, response)) + + """ + Private methods + """ + + def _create_url(self, path): + """ + Appends namespace to server url + """ + return "{}{}".format(self.server_url, path) diff --git a/src/bluefin_client_sui/client.py b/src/bluefin_client_sui/client.py new file mode 100644 index 0000000..4b11c4a --- /dev/null +++ b/src/bluefin_client_sui/client.py @@ -0,0 +1,924 @@ +import json +from copy import deepcopy + +from .api_service import APIService +from .contracts import Contracts +from .order_signer import OrderSigner +from .onboarding_signer import OnboardingSigner +from .constants import TIME, SERVICE_URLS +from .sockets_lib import Sockets +from .websocket_client import WebsocketClient +from .signer import Signer +from .utilities import default_value +from .rpc import * +from .account import * +from .interfaces import * +from .enumerations import * + +_SUI_BASE_NUM = 1000000000 + + +class BluefinClient: + def __init__(self, are_terms_accepted, network, private_key=""): + self.are_terms_accepted = are_terms_accepted + self.network = network + if private_key != "": + # currently we only support seed phrase + self.account = SuiWallet(seed=private_key) + # self.account = Account.from_key(private_key) + self.apis = APIService( + self.network["apiGateway"], default_value(self.network, "UUID", "") + ) + self.dms_api = APIService(self.network["dmsURL"]) + self.socket = Sockets(self.network["socketURL"]) + self.ws_client = WebsocketClient(self.network["webSocketURL"]) + self.contracts = Contracts() + self.order_signer = OrderSigner() + self.onboarding_signer = OnboardingSigner() + self.contract_signer = Signer() + self.url = self.network["url"] + + async def init(self, user_onboarding=True, api_token=""): + """ + Initialize the client. + Inputs: + user_onboarding (bool, optional): If set to true onboards the user address to exchange and gets authToken. Defaults to True. + api_token(string, optional): API token to initialize client in read-only mode + """ + contract_info = await self.get_contract_addresses() + self.contracts.set_contract_addresses(contract_info) + + if api_token: + self.apis.api_token = api_token + # for socket + self.socket.set_api_token(api_token) + self.ws_client.set_api_token(api_token) + # In case of api_token received, user onboarding is not done + elif user_onboarding: + self.apis.auth_token = await self.onboard_user() + self.dms_api.auth_token = self.apis.auth_token + self.socket.set_token(self.apis.auth_token) + self.ws_client.set_token(self.apis.auth_token) + + async def onboard_user(self, token: str = None): + """ + On boards the user address and returns user authentication token. + Inputs: + token: user access token, if you possess one. + Returns: + str: user authorization token + """ + user_auth_token = token + + # if no auth token provided create on + if not user_auth_token: + onboarding_signature = self.onboarding_signer.create_signature( + self.network["onboardingUrl"], self.account.privateKeyBytes + ) + onboarding_signature = ( + onboarding_signature + self.account.publicKeyBase64.decode() + ) + response = await self.authorize_signed_hash(onboarding_signature) + + if "error" in response: + raise SystemError( + f"Authorization error: {response['error']['message']}" + ) + + user_auth_token = response["token"] + + return user_auth_token + + async def authorize_signed_hash(self, signed_hash: str): + """ + Registers user as an authorized user on server and returns authorization token. + Inputs: + signed_hash: signed onboarding hash + Returns: + dict: response from user authorization API Bluefin + """ + return await self.apis.post( + SERVICE_URLS["USER"]["AUTHORIZE"], + { + "signature": signed_hash, + "userAddress": self.account.address, + "isTermAccepted": self.are_terms_accepted, + }, + ) + + def create_order_to_sign(self, params: OrderSignatureRequest): + """ + Creates order signature request for an order. + Inputs: + params (OrderSignatureRequest): parameters to create order with, refer OrderSignatureRequest + + Returns: + Order: order raw info + """ + expiration = current_unix_timestamp() + # MARKET ORDER set expiration of 1 minute + if params["orderType"] == ORDER_TYPE.MARKET: + expiration += TIME["SECONDS_IN_A_MINUTE"] + expiration *= 1000 + # LIMIT ORDER set expiration of 30 days + else: + expiration += TIME["SECONDS_IN_A_MONTH"] + expiration *= 1000 + + return Order( + market=default_value( + params, "market", self.contracts.get_perpetual_id(params["symbol"]) + ), + isBuy=params["side"] == ORDER_SIDE.BUY, + price=params["price"], + quantity=params["quantity"], + leverage=default_value(params, "leverage", 1), + maker=params["maker"].lower() + if "maker" in params + else self.account.address.lower(), + reduceOnly=default_value(params, "reduceOnly", False), + postOnly=default_value(params, "postOnly", False), + orderbookOnly=default_value(params, "orderbookOnly", True), + expiration=default_value(params, "expiration", expiration), + salt=default_value(params, "salt", random_number(1000000)), + ioc=default_value(params, "ioc", False), + ) + + def create_signed_order(self, req: OrderSignatureRequest) -> OrderSignatureResponse: + """ + Create an order from provided params and signs it using the private key of the account + Inputs: + params (OrderSignatureRequest): parameters to create order with + Returns: + OrderSignatureResponse: order raw info and generated signature + """ + sui_params = deepcopy(req) + sui_params["price"] = self._to_sui_base(req["price"]) + sui_params["quantity"] = self._to_sui_base(req["quantity"]) + sui_params["leverage"] = self._to_sui_base(req["leverage"]) + + order = self.create_order_to_sign(sui_params) + symbol = sui_params["symbol"].value + order_signature = self.order_signer.sign_order( + order, self.account.privateKeyBytes + ) + order_signature = order_signature + self.account.publicKeyBase64.decode() + return OrderSignatureResponse( + symbol=symbol, + price=sui_params["price"], + quantity=sui_params["quantity"], + side=sui_params["side"], + leverage=default_value(sui_params, "leverage", self._to_sui_base(1)), + reduceOnly=default_value(sui_params, "reduceOnly", False), + salt=order["salt"], + expiration=order["expiration"], + orderSignature=order_signature, + orderType=sui_params["orderType"], + maker=order["maker"], + orderbookOnly=default_value(sui_params, "orderbookOnly", True), + ) + + def create_signed_cancel_order( + self, params: OrderSignatureRequest, parentAddress: str = "" + ): + """ + Creates a cancel order request from provided params and signs it using the private + key of the account + + Inputs: + params (OrderSignatureRequest): parameters to create cancel order with + parentAddress (str): Only provided by a sub account + + Returns: + OrderSignatureResponse: generated cancel signature + """ + order_to_sign = self.create_order_to_sign(params) + hash_val = self.order_signer.get_order_hash(order_to_sign, withBufferHex=False) + return self.create_signed_cancel_orders( + params["symbol"], hash_val.hex(), parentAddress + ) + + def create_signed_cancel_orders( + self, symbol: MARKET_SYMBOLS, order_hash: list, parentAddress: str = "" + ): + """ + Creates a cancel order from provided params and sign it using the private + key of the account + + Inputs: + params (list): a list of order hashes + parentAddress (str): only provided by a sub account + Returns: + OrderCancellationRequest: containing symbol, hashes and signature + """ + if isinstance(order_hash, list) is False: + order_hash = [order_hash] + cancel_hash = self.order_signer.encode_message({"orderHashes": order_hash}) + hash_sig = ( + self.order_signer.sign_hash(cancel_hash, self.account.privateKeyBytes, "") + + self.account.publicKeyBase64.decode() + ) + return OrderCancellationRequest( + symbol=symbol.value, + hashes=order_hash, + signature=hash_sig, + parentAddress=parentAddress, + ) + + async def post_cancel_order(self, params: OrderCancellationRequest): + """ + POST cancel order request to Bluefin + Inputs: + params(dict): a dictionary with OrderCancellationRequest required params + Returns: + dict: response from orders delete API Bluefin + """ + + return await self.apis.delete( + SERVICE_URLS["ORDERS"]["ORDERS_HASH"], + { + "symbol": params["symbol"], + "orderHashes": params["hashes"], + "cancelSignature": params["signature"], + "parentAddress": params["parentAddress"], + }, + auth_required=True, + ) + + async def cancel_all_orders( + self, + symbol: MARKET_SYMBOLS, + status: List[ORDER_STATUS], + parentAddress: str = "", + ): + """ + GETs all orders of specified status for the specified symbol, + and creates a cancellation request for all orders and + POSTs the cancel order request to Bluefin + Inputs: + symbol (MARKET_SYMBOLS): Market for which orders are to be cancelled + status (List[ORDER_STATUS]): status of orders that need to be cancelled + parentAddress (str): address of parent account, only provided by sub account + Returns: + dict: response from orders delete API Bluefin + """ + orders = await self.get_orders( + {"symbol": symbol, "parentAddress": parentAddress, "statuses": status} + ) + + hashes = [] + for i in orders: + hashes.append(i["hash"]) + + if len(hashes) > 0: + req = self.create_signed_cancel_orders(symbol, hashes, parentAddress) + return await self.post_cancel_order(req) + + return False + + async def post_signed_order(self, params: PlaceOrderRequest): + """ + Creates an order from provided params and signs it using the private + key of the account + + Inputs: + params (OrderSignatureRequest): parameters to create order with + + Returns: + OrderSignatureResponse: order raw info and generated signature + """ + + return await self.apis.post( + SERVICE_URLS["ORDERS"]["ORDERS"], + { + "orderbookOnly": params["orderbookOnly"], + "symbol": params["symbol"], + "price": params["price"], + "quantity": params["quantity"], + "leverage": params["leverage"], + "userAddress": params["maker"], + "orderType": params["orderType"].value, + "side": params["side"].value, + "reduceOnly": params["reduceOnly"], + "salt": params["salt"], + "expiration": params["expiration"], + "orderSignature": params["orderSignature"], + "timeInForce": default_enum_value( + params, "timeInForce", TIME_IN_FORCE.GOOD_TILL_TIME + ), + "postOnly": default_value(params, "postOnly", False), + "cancelOnRevert": default_value(params, "cancelOnRevert", False), + "clientId": "bluefin-python-client: {}".format( + default_value(params, "clientId", "bluefin-python-client") + ), + }, + auth_required=True, + ) + + ## Contract calls + async def deposit_margin_to_bank(self, amount: int, coin_id: str) -> bool: + """ + Deposits given amount of USDC from user's account to margin bank + + Inputs: + amount (number): quantity of usdc to be deposited to bank in base decimals (1,2 etc) + + Returns: + Boolean: true if amount is successfully deposited, false otherwise + """ + package_id = self.contracts.get_package_id() + user_address = self.account.getUserAddress() + callArgs = [] + callArgs.append(self.contracts.get_bank_id()) + callArgs.append(self.account.getUserAddress()) + callArgs.append(str(self._to_sui_base(amount))) + callArgs.append(coin_id) + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "deposit_to_bank", + "margin_bank", + user_address, + package_id, + ) + signature = self.contract_signer.sign_tx(txBytes, self.account) + res = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + + return res + + async def withdraw_margin_from_bank(self, amount): + """ + Withdraws given amount of usdc from margin bank if possible + + Inputs: + amount (number): quantity of usdc to be withdrawn from bank in base decimals (1,2 etc) + + Returns: + Boolean: true if amount is successfully withdrawn, false otherwise + """ + + bank_id = self.contracts.get_bank_id() + account_address = self.account.getUserAddress() + + callArgs = [bank_id, account_address, str(amount)] + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "withdraw_from_bank", + "margin_bank", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + signature = self.contract_signer.sign_tx(txBytes, self.account) + res = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + + return res + + async def withdraw_all_margin_from_bank(self): + bank_id = self.contracts.get_bank_id() + account_address = self.account.getUserAddress() + + callArgs = [bank_id, account_address] + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "withdraw_all_margin_from_bank", + "margin_bank", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + signature = self.contract_signer.sign_tx(txBytes, self.account) + res = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + return res + + async def adjust_leverage(self, symbol, leverage, parentAddress: str = ""): + """ + Adjusts user leverage to the provided one for their current position on-chain and off-chain. + If a user has no position for the provided symbol, leverage only recorded off-chain + + Inputs: + symbol (MARKET_SYMBOLS): market for which to adjust user leverage + leverage (number): new leverage to be set. Must be in base decimals (1,2 etc.) + parentAddress (str): optional, if provided, the leverage of parent is + being adjusted (for sub accounts only) + Returns: + Boolean: true if the leverage is successfully adjusted + """ + + user_position = await self.get_user_position( + {"symbol": symbol, "parentAddress": parentAddress} + ) + + account_address = self.account.address if parentAddress == "" else parentAddress + # implies user has an open position on-chain, perform on-chain leverage update + if user_position != {}: + callArgs = [] + callArgs.append(self.contracts.get_perpetual_id(symbol)) + callArgs.append(self.contracts.get_bank_id()) + callArgs.append(self.contracts.get_sub_account_id()) + callArgs.append(account_address) + callArgs.append(str(self._to_sui_base(leverage))) + callArgs.append(self.contracts.get_price_oracle_object_id(symbol.value)) + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "adjust_leverage", + "exchange", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + signature = self.contract_signer.sign_tx(txBytes, self.account) + result = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + return result + else: + await self.apis.post( + SERVICE_URLS["USER"]["ADJUST_LEVERAGE"], + { + "symbol": symbol.value, + "address": account_address, + "leverage": toDapiBase(leverage), + "marginType": MARGIN_TYPE.ISOLATED.value, + }, + auth_required=True, + ) + return True + + async def adjust_margin( + self, + symbol: MARKET_SYMBOLS, + operation: ADJUST_MARGIN, + amount: str, + parentAddress: str = "", + ): + """ + Adjusts user's on-chain position by adding or removing the specified amount of margin. + Performs on-chain contract call, the user must have gas tokens + Inputs: + symbol (MARKET_SYMBOLS): market for which to adjust user leverage + operation (ADJUST_MARGIN): ADD/REMOVE adding or removing margin to position + amount (number): amount of margin to be adjusted + parentAddress (str): optional, if provided, the margin of parent is + being adjusted (for sub accounts only) + Returns: + Boolean: true if the margin is adjusted + """ + + user_position = await self.get_user_position( + {"symbol": symbol, "parentAddress": parentAddress} + ) + + if user_position == {}: + raise (Exception(f"User has no open position on market: {symbol}")) + + callArgs = [] + callArgs.append(self.contracts.get_perpetual_id(symbol)) + callArgs.append(self.contracts.get_bank_id()) + + callArgs.append(self.contracts.get_sub_account_id()) + callArgs.append(self.account.getUserAddress()) + callArgs.append(str(amount)) + callArgs.append(self.contracts.get_price_oracle_object_id(symbol)) + if operation == ADJUST_MARGIN.ADD: + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "add_margin", + "exchange", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + + else: + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "remove_margin", + "exchange", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + + signature = self.contract_signer.sign_tx(txBytes, self.account) + result = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + return True + + async def update_sub_account(self, sub_account_address: str, status: bool) -> bool: + """ + Used to whitelist and account as a sub account or revoke sub account status from an account. + Inputs: + sub_account_address (str): address of the sub account + status (bool): new status of the sub account + + Returns: + Boolean: true if the sub account status is update + """ + callArgs = [] + callArgs.append(self.contracts.get_sub_account_id()) + callArgs.append(sub_account_address) + callArgs.append(status) + txBytes = rpc_unsafe_moveCall( + self.url, + callArgs, + "set_sub_account", + "roles", + self.account.getUserAddress(), + self.contracts.get_package_id(), + ) + + signature = self.contract_signer.sign_tx(txBytes, self.account) + result = rpc_sui_executeTransactionBlock(self.url, txBytes, signature) + if result["result"]["effects"]["status"]["status"] == "success": + return True + else: + return False + + async def get_native_chain_token_balance(self) -> float: + """ + Returns user's native chain token (SUI) balance + """ + try: + callArgs = [] + callArgs.append(self.account.getUserAddress()) + callArgs.append("0x2::sui::SUI") + + result = rpc_call_sui_function( + self.url, callArgs, method="suix_getBalance" + )["totalBalance"] + return self._from_sui_base(result) + except Exception as e: + raise (Exception(f"Failed to get balance, error: {e}")) + + def get_usdc_coins(self): + try: + callArgs = [] + callArgs.append(self.account.getUserAddress()) + callArgs.append(self.contracts.get_currency_type()) + result = rpc_call_sui_function(self.url, callArgs, method="suix_getCoins") + return result + except Exception as e: + raise (Exception("Failed to get USDC coins, Exception: {}".format(e))) + + async def get_usdc_balance(self) -> float: + """ + Returns user's USDC token balance on Bluefin. + """ + try: + callArgs = [] + callArgs.append(self.account.getUserAddress()) + callArgs.append(self.contracts.get_currency_type()) + result = rpc_call_sui_function( + self.url, callArgs, method="suix_getBalance" + )["totalBalance"] + return self._from_sui_base(result) + + except Exception as e: + raise (Exception("Failed to get balance, Exception: {}".format(e))) + + async def get_margin_bank_balance(self) -> float: + """ + Returns user's Margin Bank balance. + """ + try: + call_args = [] + call_args.append(self.contracts.get_bank_table_id()) + call_args.append( + {"type": "address", "value": self.account.getUserAddress()} + ) + result = rpc_call_sui_function( + self.url, call_args, method="suix_getDynamicFieldObject" + ) + + balance = self._from_sui_base( + result["data"]["content"]["fields"]["value"]["fields"]["balance"] + ) + return balance + except Exception as e: + raise (Exception("Failed to get balance, Exception: {}".format(e))) + + ## Market endpoints + async def get_orderbook(self, params: GetOrderbookRequest): + """ + Returns a dictionary containing the orderbook snapshot. + Inputs: + params(GetOrderbookRequest): the order symbol and limit(orderbook depth) + Returns: + dict: Orderbook snapshot + """ + params = extract_enums(params, ["symbol"]) + return await self.apis.get(SERVICE_URLS["MARKET"]["ORDER_BOOK"], params) + + async def get_exchange_status(self): + """ + Returns a dictionary containing the exchange status. + Returns: + dict: exchange status + """ + return await self.apis.get(SERVICE_URLS["MARKET"]["STATUS"], {}) + + async def get_market_symbols(self): + """ + Returns a list of active market symbols. + Returns: + list: active market symbols + """ + return await self.apis.get(SERVICE_URLS["MARKET"]["SYMBOLS"], {}) + + async def get_funding_rate(self, symbol: MARKET_SYMBOLS): + """ + Returns a dictionary containing the current funding rate on market. + Inputs: + symbol(MARKET_SYMBOLS): symbol of market + Returns: + dict: Funding rate into + """ + return await self.apis.get( + SERVICE_URLS["MARKET"]["FUNDING_RATE"], {"symbol": symbol.value} + ) + + async def get_transfer_history(self, params: GetTransferHistoryRequest): + """ + Returns a list of the user's transfer history records, a boolean indicating if there is/are more page(s), + and the next page number + Inputs: + params(GetTransferHistoryRequest): params required to fetch transfer history + Returns: + GetUserTransferHistoryResponse: + isMoreDataAvailable: boolean indicating if there is/are more page(s) + nextCursor: the next page number + data: a list of the user's transfer history record + """ + + return await self.apis.get( + SERVICE_URLS["USER"]["TRANSFER_HISTORY"], params, auth_required=True + ) + + async def get_funding_history(self, params: GetFundingHistoryRequest): + """ + Returns a list of the user's funding payments, a boolean indicating if there is/are more page(s), + and the next page number + Inputs: + params(GetFundingHistoryRequest): params required to fetch funding history + Returns: + GetFundingHistoryResponse: + isMoreDataAvailable: boolean indicating if there is/are more page(s) + nextCursor: the next page number + data: a list of the user's funding payments + """ + + params = extract_enums(params, ["symbol"]) + + return await self.apis.get( + SERVICE_URLS["USER"]["FUNDING_HISTORY"], params, auth_required=True + ) + + async def get_market_meta_info(self, symbol: MARKET_SYMBOLS = None): + """ + Returns a dictionary containing market meta info. + Inputs: + symbol(MARKET_SYMBOLS): the market symbol + Returns: + dict: meta info + """ + query = {"symbol": symbol.value} if symbol else {} + + return await self.apis.get(SERVICE_URLS["MARKET"]["META"], query) + + async def get_market_data(self, symbol: MARKET_SYMBOLS = None): + """ + Returns a dictionary containing market's current data about best ask/bid, 24 hour volume, market price etc.. + Inputs: + symbol(MARKET_SYMBOLS): the market symbol + Returns: + dict: meta info + """ + query = {"symbol": symbol.value} if symbol else {} + + return await self.apis.get(SERVICE_URLS["MARKET"]["MARKET_DATA"], query) + + async def get_exchange_info(self, symbol: MARKET_SYMBOLS = None): + """ + Returns a dictionary containing exchange info for market(s). The min/max trade size, max allowed oi open + min/max trade price, step size, tick size etc... + Inputs: + symbol(MARKET_SYMBOLS): the market symbol + Returns: + dict: exchange info + """ + query = {"symbol": symbol.value} if symbol else {} + return await self.apis.get(SERVICE_URLS["MARKET"]["EXCHANGE_INFO"], query) + + async def get_master_info(self, symbol: MARKET_SYMBOLS = None): + """ + Returns a dictionary containing master info for market(s). + It contains all market data, exchange info and meta data of market(s) + Inputs: + symbol(MARKET_SYMBOLS): the market symbol + Returns: + dict: master info + """ + query = {"symbol": symbol.value} if symbol else {} + return await self.apis.get(SERVICE_URLS["MARKET"]["MASTER_INFO"], query) + + async def get_ticker_data(self, symbol: MARKET_SYMBOLS = None): + """ + Returns a dictionary containing ticker data for market(s). + Inputs: + symbol(MARKET_SYMBOLS): the market symbol + Returns: + dict: ticker info + """ + query = {"symbol": symbol.value} if symbol else {} + return await self.apis.get(SERVICE_URLS["MARKET"]["TICKER"], query) + + async def get_market_candle_stick_data(self, params: GetCandleStickRequest): + """ + Returns a list containing the candle stick data. + Inputs: + params(GetCandleStickRequest): params required to fetch candle stick data + Returns: + list: the candle stick data + """ + params = extract_enums(params, ["symbol", "interval"]) + + return await self.apis.get(SERVICE_URLS["MARKET"]["CANDLE_STICK_DATA"], params) + + async def get_market_recent_trades(self, params: GetMarketRecentTradesRequest): + """ + Returns a list containing the recent trades data. + Inputs: + params(GetCandleStickRequest): params required to fetch candle stick data + Returns: + ist: the recent trades + """ + params = extract_enums(params, ["symbol", "traders"]) + + return await self.apis.get(SERVICE_URLS["MARKET"]["RECENT_TRADE"], params) + + async def get_contract_addresses(self): + """ + Returns: + dict: all contract addresses for the all markets. + """ + return await self.apis.get(SERVICE_URLS["MARKET"]["CONTRACT_ADDRESSES"]) + + ## User endpoints + + def get_account(self): + """ + Returns the user account object + """ + return self.account + + def get_public_address(self): + """ + Returns the user account public address + """ + return self.account.address + + async def generate_readonly_token(self): + """ + Returns a read-only token generated for authenticated user. + """ + return await self.apis.post( + SERVICE_URLS["USER"]["GENERATE_READONLY_TOKEN"], {}, True + ) + + async def get_orders(self, params: GetOrderRequest): + """ + Returns a list of orders. + Inputs: + params(GetOrderRequest): params required to query orders (e.g. symbol,statuses) + Returns: + list: a list of orders + """ + params = extract_enums(params, ["symbol", "statuses", "orderType"]) + + return await self.apis.get(SERVICE_URLS["USER"]["ORDERS"], params, True) + + async def get_transaction_history(self, params: GetTransactionHistoryRequest): + """ + Returns a list of transaction. + Inputs: + params(GetTransactionHistoryRequest): params to query transactions (e.g. symbol) + Returns: + list: a list of transactions + """ + params = extract_enums(params, ["symbol"]) + return await self.apis.get( + SERVICE_URLS["USER"]["USER_TRANSACTION_HISTORY"], params, True + ) + + async def get_user_position(self, params: GetPositionRequest): + """ + Returns a list of positions. + Inputs: + params(GetPositionRequest): params required to query positions (e.g. symbol) + Returns: + list: a list of positions + """ + params = extract_enums(params, ["symbol"]) + return await self.apis.get(SERVICE_URLS["USER"]["USER_POSITIONS"], params, True) + + async def get_user_trades(self, params: GetUserTradesRequest): + """ + Returns a list of user trades. + Inputs: + params(GetUserTradesRequest): params to query trades (e.g. symbol) + Returns: + list: a list of positions + """ + params = extract_enums(params, ["symbol", "type"]) + return await self.apis.get(SERVICE_URLS["USER"]["USER_TRADES"], params, True) + + async def get_user_account_data(self, parentAddress: str = ""): + """ + Returns user account data. + Inputs: + parentAddress: an optional field, used by sub accounts to fetch parent account state + """ + return await self.apis.get( + service_url=SERVICE_URLS["USER"]["ACCOUNT"], + query={"parentAddress": parentAddress}, + auth_required=True, + ) + + async def get_user_leverage(self, symbol: MARKET_SYMBOLS, parentAddress: str = ""): + """ + Returns user market default leverage. + Inputs: + symbol(MARKET_SYMBOLS): market symbol to get user market default leverage for. + parentAddress(str): an optional field, used by sub accounts to fetch parent account state + Returns: + str: user default leverage + """ + account_data_by_market = (await self.get_user_account_data(parentAddress))[ + "accountDataByMarket" + ] + + for i in account_data_by_market: + if symbol.value == i["symbol"]: + return fromDapiBase(int(i["selectedLeverage"])) + # default leverage on system is 3 + # todo fetch from exchange info route + return 3 + + async def get_cancel_on_disconnect_timer( + self, params: GetCancelOnDisconnectTimerRequest = None + ): + """ + Returns a list of the user's countDowns for provided market symbol, + Inputs: + - symbol(MARKET_SYMBOLS): (Optional) market symbol to get user market cancel_on_disconnect timer for, not providing it would return all the active countDown timers for each market. + - parentAddress (str):(Optional) Only provided by a sub account + Returns: + - GetCountDownsResponse: + - countDowns: object with provided market symbol and respective countDown timer + - timestamp + """ + + params = extract_enums(params, ["symbol"]) + response = await self.dms_api.get( + SERVICE_URLS["USER"]["CANCEL_ON_DISCONNECT"], params, auth_required=True + ) + # check for service unavailibility + if hasattr(response, "status") and response.status == 503: + raise Exception( + "Cancel on Disconnect (dead-mans-switch) feature is currently unavailable" + ) + + return response + + async def reset_cancel_on_disconnect_timer(self, params: PostTimerAttributes): + """ + Returns PostTimerResponse containing accepted and failed countdowns, and the next page number + Inputs: + - params(PostTimerAttributes): params required to fetch funding history + Returns: + - PostTimerResponse: + - acceptedToReset: array with symbols for which timer was reset successfully + - failedReset: aray with symbols for whcih timer failed to reset + """ + response = await self.dms_api.post( + SERVICE_URLS["USER"]["CANCEL_ON_DISCONNECT"], + json.dumps(params), + auth_required=True, + contentType="application/json", + ) + # check for service unavailibility + if hasattr(response, "status") and response.status == 503: + raise Exception( + "Cancel on Disconnect (dead-mans-switch) feature is currently unavailable" + ) + return response + + async def close_connections(self): + # close aio http connection + await self.apis.close_session() + await self.dms_api.close_session() + + def _from_sui_base(self, number: Union[str, int]) -> float: + number = float(number) + return number / float(_SUI_BASE_NUM) + + def _to_sui_base(self, number: Union[int, float]) -> int: + return int(number * _SUI_BASE_NUM) diff --git a/src/bluefin_client_sui/constants.py b/src/bluefin_client_sui/constants.py new file mode 100644 index 0000000..af29f9b --- /dev/null +++ b/src/bluefin_client_sui/constants.py @@ -0,0 +1,67 @@ +Networks = { + "SUI_STAGING": { + "url": "https://fullnode.testnet.sui.io:443", + "apiGateway": "https://dapi.api.sui-staging.bluefin.io", + "socketURL": "wss://dapi.api.sui-staging.bluefin.io", + "dmsURL": "https://dapi.api.sui-staging.bluefin.io", + "webSocketURL": "wss://notifications.api.sui-staging.bluefin.io", + "onboardingUrl": "https://testnet.bluefin.io", + }, + "SUI_PROD": { + "url": "https://fullnode.testnet.sui.io:443", + "apiGateway": "https://dapi.api.sui-prod.bluefin.io", + "socketURL": "wss://dapi.api.sui-prod.bluefin.io", + "dmsURL": "https://dapi.api.sui-prod.bluefin.io", + "webSocketURL": "wss://notifications.api.sui-prod.bluefin.io", + "onboardingUrl": "https://trade.bluefin.io", + }, +} + +ORDER_FLAGS = {"IS_BUY": 1, "IS_DECREASE_ONLY": 2} + +TIME = { + "SECONDS_IN_A_MINUTE": 60, + "SECONDS_IN_A_DAY": 86400, + "SECONDS_IN_A_MONTH": 2592000, +} + +ADDRESSES = { + "ZERO": "0x0000000000000000000000000000000000000000", +} + +SERVICE_URLS = { + "MARKET": { + "ORDER_BOOK": "/orderbook", + "RECENT_TRADE": "/recentTrades", + "CANDLE_STICK_DATA": "/candlestickData", + "EXCHANGE_INFO": "/exchangeInfo", + "MARKET_DATA": "/marketData", + "META": "/meta", + "STATUS": "/status", + "SYMBOLS": "/marketData/symbols", + "CONTRACT_ADDRESSES": "/marketData/contractAddresses", + "TICKER": "/ticker", + "MASTER_INFO": "/masterInfo", + "FUNDING_RATE": "/fundingRate", + }, + "USER": { + "USER_POSITIONS": "/userPosition", + "USER_TRADES": "/userTrades", + "ORDERS": "/orders", + "GENERATE_READONLY_TOKEN": "/generateReadOnlyToken", + "ACCOUNT": "/account", + "USER_TRANSACTION_HISTORY": "/userTransactionHistory", + "AUTHORIZE": "/authorize", + "ADJUST_LEVERAGE": "/account/adjustLeverage", + "FUND_GAS": "/account/fundGas", + "TRANSFER_HISTORY": "/userTransferHistory", + "FUNDING_HISTORY": "/userFundingHistory", + "CANCEL_ON_DISCONNECT": "/dms-countdown", + }, + "ORDERS": { + "ORDERS": "/orders", + "ORDERS_HASH": "/orders/hash", + }, +} + +DAPI_BASE_NUM = 1000000000000000000 diff --git a/src/bluefin_client_sui/contracts.py b/src/bluefin_client_sui/contracts.py new file mode 100644 index 0000000..0ddc310 --- /dev/null +++ b/src/bluefin_client_sui/contracts.py @@ -0,0 +1,35 @@ +from .enumerations import MARKET_SYMBOLS + + +_DEFAULT_MARKET = "BTC-PERP" + + +class Contracts: + def __init__(self): + self.contracts_global_info = {} + self.contract_info = {} + + def set_contract_addresses(self, contracts_info): + self.contract_info = contracts_info + self.contracts_global_info = contracts_info[_DEFAULT_MARKET]["Deployment"] + + def get_sub_account_id(self): + return self.contracts_global_info["SubAccounts"]["id"] + + def get_bank_table_id(self): + return self.contracts_global_info["BankTable"]["id"] + + def get_package_id(self): + return self.contracts_global_info["package"]["id"] + + def get_bank_id(self): + return self.contracts_global_info["Bank"]["id"] + + def get_currency_type(self): + return self.contracts_global_info["Currency"]["dataType"] + + def get_price_oracle_object_id(self, market: MARKET_SYMBOLS): + return self.contract_info[market]["objects"]["PriceOracle"]["id"] + + def get_perpetual_id(self, market: MARKET_SYMBOLS): + return self.contract_info[market]["objects"]["Perpetual"]["id"] diff --git a/src/bluefin_client_sui/enumerations.py b/src/bluefin_client_sui/enumerations.py new file mode 100644 index 0000000..2c512fb --- /dev/null +++ b/src/bluefin_client_sui/enumerations.py @@ -0,0 +1,108 @@ +from enum import Enum + + +class ORDER_TYPE(Enum): + LIMIT = "LIMIT" + MARKET = "MARKET" + + +class ORDER_SIDE(Enum): + BUY = "BUY" + SELL = "SELL" + + +class MARKET_SYMBOLS(Enum): + BTC = "BTC-PERP" + ETH = "ETH-PERP" + + +class TIME_IN_FORCE(Enum): + FILL_OR_KILL = "FOK" + IMMEDIATE_OR_CANCEL = "IOC" + GOOD_TILL_TIME = "GTT" + + +class ONBOARDING_MESSAGES(Enum): + ONBOARDING = "Firefly Onboarding" + KEY_DERIVATION = "Firefly Access Key" + + +class ORDER_STATUS(Enum): + PENDING = "PENDING" + OPEN = "OPEN" + PARTIAL_FILLED = "PARTIAL_FILLED" + FILLED = "FILLED" + CANCELLING = "CANCELLING" + CANCELLED = "CANCELLED" + REJECTED = "REJECTED" + EXPIRED = "EXPIRED" + + +class CANCEL_REASON(Enum): + UNDERCOLLATERALIZED = "UNDERCOLLATERALIZED" + INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE" + USER_CANCELLED = "USER_CANCELLED" + EXCEEDS_MARKET_BOUND = "EXCEEDS_MARKET_BOUND" + COULD_NOT_FILL = "COULD_NOT_FILL" + EXPIRED = "EXPIRED" + USER_CANCELLED_ON_CHAIN = "USER_CANCELLED_ON_CHAIN" + SYSTEM_CANCELLED = "SYSTEM_CANCELLED" + SELF_TRADE = "SELF_TRADE" + POST_ONLY_FAIL = "POST_ONLY_FAIL" + FAILED = "FAILED" + NETWORK_DOWN = "NETWORK_DOWN" + + +class Interval(Enum): + _1m = "1m" + _3m = "3m" + _5m = "5m" + _15m = "15m" + _30m = "30m" + _1h = "1h" + _2h = "2h" + _4h = "4h" + _6h = "6h" + _8h = "8h" + _12h = "12h" + _1d = "1d" + _3d = "3d" + _1w = "1w" + _1M = "1M" + + +class SOCKET_EVENTS(Enum): + GET_LAST_KLINE_WITH_INTERVAL = "{symbol}@kline@{interval}" + # rooms that can be joined + GLOBAL_UPDATES_ROOM = "globalUpdates" + USER_UPDATES_ROOM = "userUpdates" + # events that can be listened + MARKET_DATA_UPDATE = "MarketDataUpdate" + RECENT_TRADES = "RecentTrades" + ORDERBOOK_UPDATE = "OrderbookUpdate" + ADJUST_MARGIN = "AdjustMargin" + MARKET_HEALTH = "MarketHealth" + EXCHANGE_HEALTH = "ExchangeHealth" + ORDER_UPDATE = "OrderUpdate" + ORDER_SENT_FOR_SETTLEMENT = "OrderSettlementUpdate" + ORDER_REQUEUE_UPDATE = "OrderRequeueUpdate" + ORDER_CANCELLATION = "OrderCancelled" + POSITION_UPDATE = "PositionUpdate" + USER_TRADE = "UserTrade" + USER_TRANSACTION = "UserTransaction" + ACCOUNT_DATA = "AccountDataUpdate" + + +class MARGIN_TYPE(Enum): + ISOLATED = "ISOLATED" + CROSS = "CROSS" + + +class ADJUST_MARGIN(Enum): + ADD = "ADD" + REMOVE = "REMOVE" + + +class TRADE_TYPE(Enum): + ISOLATED = "IsolatedTrader" + LIQUIDATION = "IsolatedLiquidation" diff --git a/src/bluefin_client_sui/interfaces.py b/src/bluefin_client_sui/interfaces.py new file mode 100644 index 0000000..35036ad --- /dev/null +++ b/src/bluefin_client_sui/interfaces.py @@ -0,0 +1,243 @@ +from typing import TypedDict, List +from .enumerations import * + + +class Order(TypedDict): + market: str + price: int + isBuy: bool + reduceOnly: bool + quantity: int + postOnly: bool + orderbookOnly: bool + leverage: int + expiration: int + salt: int + maker: str + ioc: bool + + +class SignedOrder(Order): + typedSignature: str + + +class RequiredOrderFields(TypedDict): + # symbol: MARKET_SYMBOLS # market for which to create order + market: str + price: int # price at which to place order. Will be zero for a market order + quantity: int # quantity/size of order + side: ORDER_SIDE # BUY/SELL + orderType: ORDER_TYPE # MARKET/LIMIT + + +class OrderSignatureRequest(RequiredOrderFields): + leverage: int # (optional) leverage to take, default is 1 + reduceOnly: bool # (optional) is order to be reduce only true/false, default its false + salt: int # (optional) random number for uniqueness of order. Generated randomly if not provided + expiration: int # (optional) time at which order will expire. Will be set to 1 month if not provided + maker: str # (optional) maker of the order, if not provided the account used to initialize the client will be default maker + # isBuy: bool + postOnly: bool + orderBookOnly: bool + ioc: bool + + +class OrderSignatureResponse(RequiredOrderFields): + maker: str + orderSignature: str + + +class PlaceOrderRequest(OrderSignatureResponse): + timeInForce: TIME_IN_FORCE # FOK/IOC/GTT by default all orders are GTT + postOnly: bool # true/false, default is true + cancelOnRevert: bool # if true, the order will be cancelled in case of on-chain settlement error, default is false + clientId: str # id of the client + + +class GetOrderbookRequest(TypedDict): + symbol: MARKET_SYMBOLS + limit: int # number of bids/asks to retrieve, should be <= 50 + + +class OnboardingMessage(TypedDict): + action: str + onlySignOn: str + + +class OrderResponse(TypedDict): + id: int + clientId: str + requestTime: int + cancelReason: CANCEL_REASON + orderStatus: ORDER_STATUS + hash: str + symbol: MARKET_SYMBOLS + orderType: ORDER_TYPE + timeInForce: TIME_IN_FORCE + userAddress: str + side: ORDER_SIDE + price: str + quantity: str + leverage: str + reduceOnly: bool + expiration: int + salt: int + orderSignature: str + filledQty: str + avgFillPrice: str + createdAt: int + updatedAt: int + makerFee: str + takerFee: str + openQty: str + cancelOnRevert: bool + + +class GetOrderResponse(OrderResponse): + fee: str + postOnly: bool + triggerPrice: str + + +class GetCandleStickRequest(TypedDict): + symbol: MARKET_SYMBOLS + interval: Interval + startTime: float + endTime: float + limit: int + + +class GetMarketRecentTradesRequest(TypedDict): + symbol: MARKET_SYMBOLS + pageSize: int + pageNumber: int + traders: TRADE_TYPE + + +class OrderCancelSignatureRequest(TypedDict): + symbol: MARKET_SYMBOLS + hashes: list + parentAddress: str # (optional) should only be provided by a sub account + + +class OrderCancellationRequest(OrderCancelSignatureRequest): + signature: str + + +class CancelOrder(TypedDict): + hash: str + reason: str + + +class CancelOrderResponse(TypedDict): + message: str + data: dict + + +class GetTransactionHistoryRequest(TypedDict): + symbol: MARKET_SYMBOLS # will fetch orders of provided market + pageSize: int # will get only provided number of orders must be <= 50 + pageNumber: int # will fetch particular page records. A single page contains 50 records. + + +class GetPositionRequest(GetTransactionHistoryRequest): + parentAddress: str # (optional) should be provided by sub accounts + + +class GetUserTradesRequest(TypedDict): + symbol: MARKET_SYMBOLS + maker: bool + fromId: int + startTime: int + endTime: int + pageSize: int + pageNumber: int + type: ORDER_TYPE + parentAddress: str # (optional) should be provided by sub account + + +class GetOrderRequest(GetTransactionHistoryRequest): + statuses: List[ORDER_STATUS] # (optional) status of orders to be fetched + orderId: int # (optional) the id of order to be fetched + orderType: List[ORDER_TYPE] # (optional) type of order Limit/Market + orderHashes: List[str] # (optional) hashes of order to be fetched + parentAddress: str # (optional) should be provided by sub accounts + + +class GetFundingHistoryRequest(TypedDict): + symbol: MARKET_SYMBOLS # will fetch orders of provided market + pageSize: int # will get only provided number of orders must be <= 50 + cursor: int # will fetch particular page records. A single page contains 50 records. + parentAddress: str # (optional) should be provided by a sub account + + +class FundingHistoryResponse(TypedDict): + id: int # unique id + symbol: MARKET_SYMBOLS # market for which to create order + userAddress: str # user public address + quantity: int # size of position + time: int # created time + appliedFundingRate: str # funding rate percent applied + isFundingRatePositive: bool # was funding rate +ve or -ve + payment: str # amount + isPaymentPositive: bool # whether payment was deducted or added + oraclePrice: str # price from oracle + side: ORDER_SIDE # BUY/SELL + blockNumber: int # transaction block number + isPositionPositive: bool # is position LONG or SHORT + + +class GetFundingHistoryResponse(TypedDict): + isMoreDataAvailable: bool # boolean indicating if there is more data available + nextCursor: int # next page number + data: List[FundingHistoryResponse] + + +class GetTransferHistoryRequest(TypedDict): + pageSize: int # will get only provided number of orders must be <= 50 + cursor: int # will fetch particular page records. A single page contains 50 records. + action: str # (optional) Deposit / Withdraw + + +class UserTransferHistoryResponse(TypedDict): + id: int # unique id + status: str # status of transaction + action: str # Deposit / Withdraw + amount: str # amount withdrawn/deposited + userAddress: str # user public address + blockNumber: int # transaction block number + latestTxHash: str # transaction hash + time: int # created time + createdAt: int + updatedAt: int + + +class GetUserTransferHistoryResponse(TypedDict): + isMoreDataAvailable: bool # boolean indicating if there is more data available + nextCursor: int # next page number + data: List[UserTransferHistoryResponse] + + +class CountDown(TypedDict): + symbol: str + countDown: int + + +class GetCancelOnDisconnectTimerRequest(TypedDict): + symbol: MARKET_SYMBOLS # will fetch Cancel On Disconnect Timer of provided market + parentAddress: str # (optional) should be provided by a sub account + + +class PostTimerAttributes(TypedDict): + countDowns: List[CountDown] + parentAddress: str + + +class FailedCountDownResetResponse(TypedDict): + symbol: str + reason: str + + +class PostTimerResponse(TypedDict): + acceptedToReset: List[str] + failedReset: List[FailedCountDownResetResponse] diff --git a/src/bluefin_client_sui/onboarding_signer.py b/src/bluefin_client_sui/onboarding_signer.py new file mode 100644 index 0000000..4b280bc --- /dev/null +++ b/src/bluefin_client_sui/onboarding_signer.py @@ -0,0 +1,29 @@ +from .interfaces import * +from .signer import Signer +import hashlib +import json + + +class OnboardingSigner(Signer): + def __init__(self): + super().__init__() + + def create_signature(self, msg, private_key, encoding="utf-8"): + """ + Signs the message. + Inputs: + - msg: the message to be signed + - private_key: the signer's private key + Returns: + - str: signed msg hash + """ + msgDict = {} + msgDict["onboardingUrl"] = msg + msg = json.dumps(msgDict, separators=(",", ":")) + msg_bytearray = bytearray(msg.encode("utf-8")) + intent = bytearray() + intent.extend([3, 0, 0, len(msg_bytearray)]) + intent = intent + msg_bytearray + + hash = hashlib.blake2b(intent, digest_size=32) + return self.sign_hash(hash.digest(), private_key) diff --git a/src/bluefin_client_sui/order_signer.py b/src/bluefin_client_sui/order_signer.py new file mode 100644 index 0000000..0b2c5c3 --- /dev/null +++ b/src/bluefin_client_sui/order_signer.py @@ -0,0 +1,89 @@ +from .utilities import numberToHex, hexToByteArray +from .signer import Signer +from .interfaces import Order +import hashlib + + +class OrderSigner(Signer): + def __init__(self, version="1.0"): + super().__init__() + self.version = version + + def get_order_flags(self, order): + """0th bit = ioc + 1st bit = postOnly + 2nd bit = reduceOnly + 3rd bit = isBuy + 4th bit = orderbookOnly + e.g. 00000000 // all flags false + e.g. 00000001 // ioc order, sell side, can be executed by taker + e.e. 00010001 // same as above but can only be executed by settlement operator + """ + flag = 0 + if order["ioc"]: + flag += 1 + if order["postOnly"]: + flag += 2 + if order["reduceOnly"]: + flag += 4 + if order["isBuy"]: + flag += 8 + if order["orderbookOnly"]: + flag += 16 + return flag + + def get_order_hash(self, order: Order, withBufferHex=True): + """ + Returns order hash. + Inputs: + - order: the order to be signed + Returns: + - str: order hash + """ + flags = self.get_order_flags(order) + flags = hexToByteArray(numberToHex(flags, 2)) + + buffer = bytearray() + orderPriceHex = hexToByteArray(numberToHex(int(order["price"]))) + orderQuantityHex = hexToByteArray(numberToHex(int(order["quantity"]))) + orderLeverageHex = hexToByteArray(numberToHex(int(order["leverage"]))) + orderSalt = hexToByteArray(numberToHex(int(order["salt"]))) + orderExpiration = hexToByteArray(numberToHex(int(order["expiration"]), 16)) + orderMaker = hexToByteArray(numberToHex(int(order["maker"], 16), 64)) + orderMarket = hexToByteArray(numberToHex(int(order["market"], 16), 64)) + bluefin = bytearray("Bluefin", encoding="utf-8") + + buffer = ( + orderPriceHex + + orderQuantityHex + + orderLeverageHex + + orderSalt + + orderExpiration + + orderMaker + + orderMarket + + flags + + bluefin + ) + + # for cancel order signature verification we use buffer directly + # for placing order we use buffer.hex().encode("utf-8") + if withBufferHex: + order_hash = hashlib.sha256(buffer.hex().encode("utf-8")).digest() + else: + order_hash = hashlib.sha256(buffer).digest() + return order_hash + + def sign_order(self, order: Order, private_key): + """ + Used to create an order signature. The method will use the provided key + in params to sign the order. + + Args: + order (Order): an order containing order fields (look at Order interface) + private_key (str): private key of the account to be used for signing + + Returns: + str: generated signature + """ + order_hash = self.get_order_hash(order) + return self.sign_hash(order_hash, private_key, "") diff --git a/src/bluefin_client_sui/rpc.py b/src/bluefin_client_sui/rpc.py new file mode 100644 index 0000000..7ff4752 --- /dev/null +++ b/src/bluefin_client_sui/rpc.py @@ -0,0 +1,136 @@ +import requests +import json + + +def rpc_unsafe_moveCall( + url, + params, + function_name: str, + function_library: str, + userAddress, + packageId, + gasBudget=100000000, +): + base_dict = {} + base_dict["jsonrpc"] = "2.0" + base_dict["id"] = 1689764924887 + base_dict["method"] = "unsafe_moveCall" + base_dict["params"] = [] + base_dict["params"].extend( + [userAddress, packageId, function_library, function_name] + ) + base_dict["params"].append([]) + base_dict["params"].append(params) + + base_dict["params"].append(None) + base_dict["params"].append(str(gasBudget)) + + payload = json.dumps(base_dict) + + headers = {"Content-Type": "application/json"} + response = requests.request("POST", url, headers=headers, data=payload) + result = json.loads(response.text) + return result["result"]["txBytes"] + + +def rpc_sui_executeTransactionBlock(url, txBytes, signature): + base_dict = {} + base_dict["jsonrpc"] = "2.0" + base_dict["id"] = 5 + base_dict["method"] = "sui_executeTransactionBlock" + base_dict["params"] = [] + base_dict["params"].append(txBytes) + base_dict["params"].append([signature]) + + outputTypeDict = { + "showInput": True, + "showEffects": True, + "showEvents": True, + "showObjectChanges": True, + } + base_dict["params"].append(outputTypeDict) + base_dict["params"].append("WaitForLocalExecution") + payload = json.dumps(base_dict) + + headers = {"Content-Type": "application/json"} + response = requests.request("POST", url, headers=headers, data=payload) + result = json.loads(response.text) + return result + + +def rpc_call_sui_function(url, params, method="suix_getCoins"): + base_dict = {} + base_dict["jsonrpc"] = "2.0" + base_dict["id"] = 1 + base_dict["method"] = method + base_dict["params"] = params + payload = json.dumps(base_dict) + + headers = {"Content-Type": "application/json"} + response = requests.request("POST", url, headers=headers, data=payload) + result = json.loads(response.text) + return result["result"] + + +""" + +payload = json.dumps({ + "jsonrpc": "2.0", + "id": 5, + "method": "sui_executeTransactionBlock", + "params": [ + "AAADAQGPzuAZV5krLKJWD1WUXOjk7Guz2vki2pBMZJ6CXkRf1XLGgQAAAAAAAQAgH/qFdX+Vzs7ShiLTDkGkUsiFFt9TRQyxkTgS3YKNWWgAEOgDAAAAAAAAAAAAAAAAAAABAF82iNaL7Cly31h0P767ErFoKbQb8bxQeNNRAJDvbPWOC21hcmdpbl9iYW5rEndpdGhkcmF3X2Zyb21fYmFuawADAQAAAQEAAQIAH/qFdX+Vzs7ShiLTDkGkUsiFFt9TRQyxkTgS3YKNWWgBWiybg/9fRf7zXUmsdBU3umIFeucEZNWeMGzlLttPf86tHg8AAAAAACA3qZfzxP1+yVILtVkJ6LdfZvkF7gK877AJco9Xook9Th/6hXV/lc7O0oYi0w5BpFLIhRbfU0UMsZE4Et2CjVlo6AMAAAAAAAAA4fUFAAAAAAA=", + [ + "ANyIBWjL6U9T6qBoWWTc18qzVViytirDmwX+dOEqd77dibe0tgLcziDZpe3XoTRVbBJGUV9TIHCN2C21aNvUTA/JFlIyQTT87zRFBPBubLG+G22kP5UDgk3kIg8JPeUiBw==" + ], + { + "showInput": True, + "showEffects": True, + "showEvents": True, + "showObjectChanges": True + }, + "WaitForLocalExecution" + ] +}) +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, data=payload) + +print(response.text) + + +""" + + +""" +url = "https://fullnode.testnet.sui.io:443" + +payload = json.dumps({ + "jsonrpc": "2.0", + "id": 1689764924887, + "method": "unsafe_moveCall", + "params": [ + "0x1ffa85757f95ceced28622d30e41a452c88516df53450cb1913812dd828d5968", + "0x5f3688d68bec2972df58743fbebb12b16829b41bf1bc5078d3510090ef6cf58e", + "margin_bank", + "withdraw_from_bank", + [], + [ + "0x8fcee01957992b2ca2560f55945ce8e4ec6bb3daf922da904c649e825e445fd5", + "0x1ffa85757f95ceced28622d30e41a452c88516df53450cb1913812dd828d5968", + "1000" + ], + "0x5a2c9b83ff5f45fef35d49ac741537ba62057ae70464d59e306ce52edb4f7fce", + "100000000" + ] +}) +headers = { + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, data=payload) + +print(response.text) +""" diff --git a/src/bluefin_client_sui/signer.py b/src/bluefin_client_sui/signer.py new file mode 100644 index 0000000..a378749 --- /dev/null +++ b/src/bluefin_client_sui/signer.py @@ -0,0 +1,70 @@ +import nacl +import hashlib +import json +import base64 +from .utilities import * +from .account import * + + +class Signer: + def __init__(self): + pass + + def sign_tx(self, tx_bytes_str: str, sui_wallet: SuiWallet) -> str: + """ + expects the msg in str + expects the suiwallet object + Signs the msg and returns the signature. + Returns the value in b64 encoded format + """ + tx_bytes = base64.b64decode(tx_bytes_str) + + intent = bytearray() + intent.extend([0, 0, 0]) + intent = intent + tx_bytes + hash = hashlib.blake2b(intent, digest_size=32).digest() + + result = nacl.signing.SigningKey(sui_wallet.privateKeyBytes).sign(hash)[:64] + temp = bytearray() + temp.append(0) + temp.extend(result) + temp.extend(sui_wallet.publicKeyBytes[1:]) + res = base64.b64encode(temp) + return res.decode() + + def sign_hash(self, hash, private_key, append=""): + """ + Signs the hash and returns the signature. + """ + result = nacl.signing.SigningKey(private_key).sign(hash)[:64] + return result.hex() + "1" + append + + def encode_message(self, msg: dict): + msg = json.dumps(msg, separators=(",", ":")) + msg_bytearray = bytearray(msg.encode("utf-8")) + intent = bytearray() + encodeLengthBCS = self.decimal_to_bcs(len(msg_bytearray)) + intent.extend([3, 0, 0]) + intent.extend(encodeLengthBCS) + intent = intent + msg_bytearray + hash = hashlib.blake2b(intent, digest_size=32) + return hash.digest() + + def decimal_to_bcs(self, num): + # Initialize an empty list to store the BCS bytes + bcs_bytes = [] + while num > 0: + # Take the last 7 bits of the number + bcs_byte = num & 0x7F + + # Set the most significant bit (MSB) to 1 if there are more bytes to follow + if num > 0x7F: + bcs_byte |= 0x80 + + # Append the BCS byte to the list + bcs_bytes.append(bcs_byte) + + # Right-shift the number by 7 bits to process the next portion + num >>= 7 + + return bcs_bytes diff --git a/src/bluefin_client_sui/socket_manager.py b/src/bluefin_client_sui/socket_manager.py new file mode 100644 index 0000000..b834bb7 --- /dev/null +++ b/src/bluefin_client_sui/socket_manager.py @@ -0,0 +1,103 @@ +import logging +import threading +from websocket import ( + ABNF, + create_connection, + WebSocketException, + WebSocketConnectionClosedException, +) + + +class SocketManager(threading.Thread): + def __init__( + self, + stream_url, + on_message=None, + on_open=None, + on_close=None, + on_error=None, + on_ping=None, + on_pong=None, + logger=None, + ): + threading.Thread.__init__(self) + if not logger: + logger = logging.getLogger(__name__) + self.logger = logger + self.stream_url = stream_url + self.on_message = on_message + self.on_open = on_open + self.on_close = on_close + self.on_ping = on_ping + self.on_pong = on_pong + self.on_error = on_error + + def create_ws_connection(self): + self.logger.debug( + "Creating connection with WebSocket Server: %s", self.stream_url + ) + self.ws = create_connection(self.stream_url) + self.logger.debug( + "WebSocket connection has been established: %s", self.stream_url + ) + self._callback(self.on_open) + + def run(self): + self.read_data() + + def send_message(self, message): + self.logger.debug("Sending message to WebSocket Server: %s", message) + self.ws.send(message) + + def ping(self): + self.ws.ping() + + def read_data(self): + data = "" + while True: + try: + op_code, frame = self.ws.recv_data_frame(True) + except WebSocketException as e: + if isinstance(e, WebSocketConnectionClosedException): + self.logger.error("Lost websocket connection") + else: + self.logger.error("Websocket exception: {}".format(e)) + raise e + except Exception as e: + self.logger.error("Exception in read_data: {}".format(e)) + raise e + + if op_code == ABNF.OPCODE_CLOSE: + self.logger.warning( + "CLOSE frame received, closing websocket connection" + ) + self._callback(self.on_close) + break + elif op_code == ABNF.OPCODE_PING: + self._callback(self.on_ping, frame.data) + self.ws.pong("") + self.logger.debug("Received Ping; PONG frame sent back") + elif op_code == ABNF.OPCODE_PONG: + self.logger.debug("Received PONG frame") + self._callback(self.on_pong) + else: + data = frame.data + if op_code == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + self._callback(self.on_message, data) + + def close(self): + if not self.ws.connected: + self.logger.warn("Websocket already closed") + else: + self.ws.send_close() + return + + def _callback(self, callback, *args): + if callback: + try: + callback(self, *args) + except Exception as e: + self.logger.error("Error from callback {}: {}".format(callback, e)) + if self.on_error: + self.on_error(self, e) diff --git a/src/bluefin_client_sui/sockets_lib.py b/src/bluefin_client_sui/sockets_lib.py new file mode 100644 index 0000000..3a4f2f0 --- /dev/null +++ b/src/bluefin_client_sui/sockets_lib.py @@ -0,0 +1,209 @@ +import socketio +import time +from .enumerations import MARKET_SYMBOLS, SOCKET_EVENTS +import asyncio + +sio = socketio.Client() + + +class Sockets: + callbacks = {} + + def __init__(self, url, timeout=10, token=None) -> None: + self.url = url + self.timeout = timeout + self.token = token + self.api_token = "" + return + + def _establish_connection(self): + """ + Connects to the desired url + """ + try: + sio.connect(self.url, wait_timeout=self.timeout, transports=["websocket"]) + return True + except: + return False + + def set_token(self, token): + """ + Sets default user token + Inputs: + - token (user auth token): Bluefin onboarding token. + """ + self.token = token + + def set_api_token(self, token): + """ + Sets default user token + Inputs: + - token (user auth token): Bluefin onboarding token. + """ + self.api_token = token + + async def open(self): + """ + opens socket instance connection + """ + self.connection_established = self._establish_connection() + if not self.connection_established: + await self.close() + raise (Exception("Failed to connect to Host: {}".format(self.url))) + return + + async def close(self): + """ + closes the socket instance connection + """ + sio.disconnect() + return + + @sio.on("*") + def listener(event, data): + """ + Listens to all events emitted by the server + """ + try: + if event in Sockets.callbacks.keys(): + Sockets.callbacks[event](data) + elif "default" in Sockets.callbacks.keys(): + Sockets.callbacks["default"]({"event": event, "data": data}) + else: + pass + except: + pass + return + + @sio.event + def connect(): + print("Connected To Socket Server") + # add 10 seconds sleep to allow connection to be established before callbacks for connections are executed + if "connect" in Sockets.callbacks: + # Execute the callback using asyncio.run() if available + time.sleep(10) + asyncio.run(Sockets.callbacks["connect"]()) + + @sio.event + def disconnect(): + print("Disconnected From Socket Server") + if "disconnect" in Sockets.callbacks: + # Execute the callback using asyncio.run() if available + asyncio.run(Sockets.callbacks["disconnect"]()) + + async def listen(self, event, callback): + """ + Assigns callbacks to desired events + """ + Sockets.callbacks[event] = callback + return + + async def subscribe_global_updates_by_symbol(self, symbol: MARKET_SYMBOLS): + """ + Allows user to subscribe to global updates for the desired symbol. + Inputs: + - symbol: market symbol of market user wants global updates for. (e.g. DOT-PERP) + """ + try: + if not self.connection_established: + raise Exception( + "Socket connection is established, invoke socket.open()" + ) + + resp = sio.call( + "SUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.GLOBAL_UPDATES_ROOM.value, + "p": symbol.value, + }, + ], + ) + return resp["success"] + except Exception as e: + print("Error: ", e) + return False + + async def unsubscribe_global_updates_by_symbol(self, symbol: MARKET_SYMBOLS): + """ + Allows user to unsubscribe to global updates for the desired symbol. + Inputs: + - symbol: market symbol of market user wants to remove global updates for. (e.g. DOT-PERP) + """ + try: + if not self.connection_established: + return False + + resp = sio.call( + "UNSUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.GLOBAL_UPDATES_ROOM.value, + "p": symbol.value, + }, + ], + ) + + return resp["success"] + except Exception as e: + print(e) + return False + + async def subscribe_user_update_by_token( + self, parent_account: str = None, user_token: str = None + ) -> bool: + """ + Allows user to subscribe to their account updates. + Inputs: + - parent_account(str): address of parent account. Only whitelisted + sub-account can listen to its parent account position updates + - token(str): auth token generated when onboarding on Bluefin + """ + try: + if not self.connection_established: + return False + + resp = sio.call( + "SUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.USER_UPDATES_ROOM.value, + "pa": parent_account, + "t": self.token if user_token == None else user_token, + "rt": self.api_token, + }, + ], + ) + + return resp["success"] + except Exception as e: + print(e) + return False + + async def unsubscribe_user_update_by_token( + self, parent_account: str = None, user_token: str = None + ): + """ + Allows user to unsubscribe to their account updates. + Inputs: + - parent_account(str): address of parent account. Only for sub-accounts + - token: auth token generated when onboarding on Bluefin + """ + try: + if not self.connection_established: + return False + + resp = sio.call( + "UNSUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.USER_UPDATES_ROOM.value, + "pa": parent_account, + "t": self.token if user_token == None else user_token, + "rt": self.api_token, + }, + ], + ) + return resp["success"] + except: + return False diff --git a/src/bluefin_client_sui/utilities.py b/src/bluefin_client_sui/utilities.py new file mode 100644 index 0000000..2f89d7f --- /dev/null +++ b/src/bluefin_client_sui/utilities.py @@ -0,0 +1,127 @@ +from datetime import datetime +from random import randint + +# from web3 import Web3 +import time +import bip_utils +import hashlib +from typing import Union +from .constants import SUI_BASE_NUM, DAPI_BASE_NUM + + +def toDapiBase(number: Union[int, float]) -> int: + return int(number * DAPI_BASE_NUM) + + +def fromDapiBase(number: Union[int, float], dtype=int) -> int: + return dtype(number / DAPI_BASE_NUM) + + +def numberToHex(num, pad=32): + # converting number to Hexadecimal format + hexNum = hex(num) + + # padding it with zero to make the size 32 bytes + padHex = hexNum[2:].zfill(pad) + return padHex + + +def hexToByteArray(hexStr): + return bytearray.fromhex(hexStr) + + +def mnemonicToPrivateKey(seedPhrase: str) -> str: + bip39_seed = bip_utils.Bip39SeedGenerator(seedPhrase).Generate() + bip32_ctx = bip_utils.Bip32Slip10Ed25519.FromSeed(bip39_seed) + derivation_path = "m/44'/784'/0'/0'/0'" + bip32_der_ctx = bip32_ctx.DerivePath(derivation_path) + private_key: str = bip32_der_ctx.PrivateKey().Raw() + return private_key + + +def privateKeyToPublicKey(privateKey: str) -> str: + privateKeyBytes = bytes(privateKey) + bip32_ctx = bip_utils.Bip32Slip10Ed25519.FromPrivateKey(privateKeyBytes) + public_key: str = bip32_ctx.PublicKey().RawCompressed() + return public_key + + +def getAddressFromPublicKey(publicKey: str) -> str: + address: str = ( + "0x" + hashlib.blake2b(publicKey.ToBytes(), digest_size=32).digest().hex()[:] + ) + return address + + +def strip_hex_prefix(input): + if input[0:2] == "0x": + return input[2:] + else: + return input + + +def address_to_bytes32(addr): + return "0x000000000000000000000000" + strip_hex_prefix(addr) + + +def bn_to_bytes8(value: int): + return str("0x" + "0" * 16 + hex(value)[2:]).encode("utf-8") + + +def default_value(dict, key, default_value): + if key in dict: + return dict[key] + else: + return default_value + + +def default_enum_value(dict, key, default_value): + if key in dict: + return dict[key].value + else: + return default_value.value + + +def current_unix_timestamp(): + return int(datetime.now().timestamp()) + + +def random_number(max_range): + return current_unix_timestamp() + randint(0, max_range) + randint(0, max_range) + + +def extract_query(value: dict): + query = "" + for i, j in value.items(): + query += "&{}={}".format(i, j) + return query[1:] + + +def extract_enums(params: dict, enums: list): + for i in enums: + if i in params.keys(): + if type(params[i]) == list: + params[i] = [x.value for x in params[i]] + else: + params[i] = params[i].value + return params + + +def config_logging(logging, logging_level, log_file: str = None): + """Configures logging to provide a more detailed log format, which includes date time in UTC + Example: 2021-11-02 19:42:04.849 UTC : + Args: + logging: python logging + logging_level (int/str): For logging to include all messages with log levels >= logging_level. Ex: 10 or "DEBUG" + logging level should be based on https://docs.python.org/3/library/logging.html#logging-levels + Keyword Args: + log_file (str, optional): The filename to pass the logging to a file, instead of using console. Default filemode: "a" + """ + + logging.Formatter.converter = time.gmtime # date time in GMT/UTC + logging.basicConfig( + level=logging_level, + filename=log_file, + format="%(asctime)s.%(msecs)03d UTC %(levelname)s %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) diff --git a/src/bluefin_client_sui/websocket_client.py b/src/bluefin_client_sui/websocket_client.py new file mode 100644 index 0000000..3544232 --- /dev/null +++ b/src/bluefin_client_sui/websocket_client.py @@ -0,0 +1,220 @@ +import json +import logging +from .socket_manager import SocketManager +from .enumerations import MARKET_SYMBOLS, SOCKET_EVENTS + + +class WebsocketClient: + def __init__( + self, + stream_url, + token=None, + api_token=None, + logger=None, + ): + if not logger: + logger = logging.getLogger(__name__) + self.logger = logger + self.token = token + self.api_token = api_token + self.stream_url = stream_url + self.callbacks = {} + + def initialize_socket( + self, + on_open, + on_close=None, + on_error=None, + on_ping=None, + on_pong=None, + logger=None, + ): + self.socket_manager = SocketManager( + self.stream_url, + on_message=self.listener, + on_open=on_open, + on_close=on_close, + on_error=on_error, + on_ping=on_ping, + on_pong=on_pong, + logger=logger, + ) + + # start the thread + self.socket_manager.create_ws_connection() + self.logger.debug("WebSocket Client started.") + self.socket_manager.start() + + def set_token(self, token): + """ + Sets default user token + Inputs: + - token (user auth token): Bluefin onboarding token. + """ + self.token = token + + def set_api_token(self, token): + """ + Sets default user token + Inputs: + - token (user auth token): Bluefin onboarding token. + """ + self.api_token = token + + def listen(self, event, callback): + """ + Assigns callbacks to desired events + """ + self.callbacks[event] = callback + return + + def send(self, message: dict): + self.socket_manager.send_message(json.dumps(message)) + + def subscribe_global_updates_by_symbol(self, symbol: MARKET_SYMBOLS): + """ + Allows user to subscribe to global updates for the desired symbol. + Inputs: + - symbol: market symbol of market user wants global updates for. (e.g. DOT-PERP) + """ + try: + if not self.socket_manager.ws.connected: + raise Exception( + "Socket connection is established, invoke socket.open()" + ) + + self.socket_manager.send_message( + json.dumps( + [ + "SUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.GLOBAL_UPDATES_ROOM.value, + "p": symbol.value, + }, + ], + ] + ) + ) + return True + except Exception: + return False + + def unsubscribe_global_updates_by_symbol(self, symbol: MARKET_SYMBOLS): + """ + Allows user to unsubscribe to global updates for the desired symbol. + Inputs: + - symbol: market symbol of market user wants to remove global updates for. (e.g. DOT-PERP) + """ + try: + if not self.socket_manager.ws.connected: + return False + + self.socket_manager.send_message( + json.dumps( + ( + [ + "UNSUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.GLOBAL_UPDATES_ROOM.value, + "p": symbol.value, + }, + ], + ] + ) + ) + ) + return True + except: + return False + + def subscribe_user_update_by_token(self, user_token: str = None): + """ + Allows user to subscribe to their account updates. + Inputs: + - token(str): auth token generated when onboarding on Bluefin + """ + try: + if not self.socket_manager.ws.connected: + return False + + self.socket_manager.send_message( + json.dumps( + ( + [ + "SUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.USER_UPDATES_ROOM.value, + "t": self.token + if user_token == None + else user_token, + "rt": self.api_token, + }, + ], + ] + ) + ) + ) + return True + except: + return False + + def unsubscribe_user_update_by_token(self, user_token: str = None): + """ + Allows user to unsubscribe to their account updates. + Inputs: + - token: auth token generated when onboarding on Bluefin + """ + try: + if not self.socket_manager.ws.connected: + return False + + self.socket_manager.send_message( + json.dumps( + ( + [ + "UNSUBSCRIBE", + [ + { + "e": SOCKET_EVENTS.USER_UPDATES_ROOM.value, + "t": self.token + if user_token == None + else user_token, + "rt": self.api_token, + }, + ], + ] + ) + ) + ) + return True + except: + return False + + def ping(self): + self.logger.debug("Sending ping to WebSocket Server") + self.socket_manager.ping() + + def stop(self, id=None): + self.socket_manager.close() + # self.socket_manager.join() + + def listener(self, _, message): + """ + Listens to all events emitted by the server + """ + data = json.loads(message) + event_name = data["eventName"] + try: + if event_name in self.callbacks: + callback = self.callbacks[event_name] + callback(data["data"]) + elif "default" in self.callbacks.keys(): + self.callbacks["default"]({"event": event_name, "data": data["data"]}) + else: + pass + except: + pass + return diff --git a/src/check.py b/src/check.py new file mode 100644 index 0000000..0c62fff --- /dev/null +++ b/src/check.py @@ -0,0 +1,33 @@ +from bluefin_exchange_client_sui import BluefinClient, Networks +from pprint import pprint +import asyncio + +TEST_ACCT_KEY = "!#12" +TEST_NETWORK = "SUI_STAGING" + + +async def main(): + # initialize client + client = BluefinClient( + True, # agree to terms and conditions + Networks[TEST_NETWORK], # network to connect with + TEST_ACCT_KEY, # private key of wallet + ) + + # Initializing client for the private key provided. The second argument api_token is optional + await client.init(True) + + print("Account Address:", client.get_public_address()) + + # # gets user account data on-chain + data = await client.get_user_account_data() + + await client.close_connections() + + pprint(data) + + +if __name__ == "__main__": + loop = asyncio.new_event_loop() + loop.run_until_complete(main()) + loop.close()