├── .readthedocs.yml ├── .travis.yml ├── LICENSE ├── README.rst ├── binance_chain ├── __init__.py ├── constants.py ├── depthcache.py ├── environment.py ├── exceptions.py ├── http.py ├── ledger │ ├── __init__.py │ ├── client.py │ ├── exceptions.py │ └── wallet.py ├── messages.py ├── node_rpc │ ├── __init__.py │ ├── http.py │ ├── pooled_client.py │ ├── request.py │ └── websockets.py ├── protobuf │ ├── __init__.py │ └── dex_pb2.py ├── signing │ ├── __init__.py │ └── http.py ├── utils │ ├── __init__.py │ ├── encode_utils.py │ └── segwit_addr.py ├── wallet.py └── websockets.py ├── docs ├── Makefile ├── binance-chain.rst ├── changelog.rst ├── conf.py ├── index.rst └── requirements.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests ├── .gitkeep ├── __init__.py ├── fixtures │ ├── peers.json │ └── success.json ├── test_client.py ├── test_depthcache.py ├── test_environment.py ├── test_messages.py ├── test_rpc_client.py ├── test_rpc_pooled.py ├── test_rpc_websockets.py ├── test_signing.py ├── test_utils.py ├── test_wallet.py └── test_websockets.py └── tox.ini /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | python: 6 | - "3.6" 7 | - "3.7" 8 | 9 | install: 10 | - pip install -r test-requirements.txt 11 | - pip install -r requirements.txt 12 | - pip install tox-travis 13 | 14 | script: 15 | - tox 16 | 17 | after_success: 18 | - coveralls 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 sammchardy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /binance_chain/__init__.py: -------------------------------------------------------------------------------- 1 | """An unofficial Python3 wrapper for the Binance Chain API 2 | 3 | .. moduleauthor:: Sam McHardy 4 | 5 | """ 6 | 7 | __version__ = '0.1.20' 8 | -------------------------------------------------------------------------------- /binance_chain/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class KlineInterval(str, Enum): 5 | ONE_MINUTE = '1m' 6 | THREE_MINUTES = '3m' 7 | FIVE_MINUTES = '5m' 8 | FIFTEEN_MINUTES = '15m' 9 | THIRTY_MINUTES = '30m' 10 | ONE_HOUR = '1h' 11 | TWO_HOURS = '2h' 12 | FOUR_HOURS = '4h' 13 | SIX_HOURS = '6h' 14 | EIGHT_HOURS = '8h' 15 | TWELVE_HOURS = '12h' 16 | ONE_DAY = '1d' 17 | THREE_DAYS = '3d' 18 | ONE_WEEK = '1w' 19 | ONE_MONTH = '1M' 20 | 21 | 22 | class OrderStatus(str, Enum): 23 | ACK = 'Ack' 24 | PARTIAL_FILL = 'PartialFill' 25 | IOC_NO_FILL = 'IocNoFill' 26 | FULLY_FILL = 'FullyFill' 27 | CANCELED = 'Canceled' 28 | EXPIRED = 'Expired' 29 | FAILED_BLOCKING = 'FailedBlocking' 30 | FAILED_MATCHING = 'FailedMatching' 31 | 32 | 33 | class OrderSide(int, Enum): 34 | BUY = 1 35 | SELL = 2 36 | 37 | 38 | class TimeInForce(int, Enum): 39 | GOOD_TILL_EXPIRE = 1 40 | IMMEDIATE_OR_CANCEL = 3 41 | 42 | 43 | class TransactionSide(str, Enum): 44 | RECEIVE = 'RECEIVE' 45 | SEND = 'SEND' 46 | 47 | 48 | class TransactionType(str, Enum): 49 | NEW_ORDER = 'NEW_ORDER' 50 | ISSUE_TOKEN = 'ISSUE_TOKEN' 51 | BURN_TOKEN = 'BURN_TOKEN' 52 | LIST_TOKEN = 'LIST_TOKEN' 53 | CANCEL_ORDER = 'CANCEL_ORDER' 54 | FREEZE_TOKEN = 'FREEZE_TOKEN' 55 | UN_FREEZE_TOKEN = 'UN_FREEZE_TOKEN' 56 | TRANSFER = 'TRANSFER' 57 | PROPOSAL = 'PROPOSAL' 58 | VOTE = 'VOTE' 59 | 60 | 61 | class OrderType(int, Enum): 62 | LIMIT = 2 63 | 64 | 65 | class PeerType(str, Enum): 66 | NODE = 'node' 67 | WEBSOCKET = 'ws' 68 | 69 | 70 | class RpcBroadcastRequestType(int, Enum): 71 | SYNC = 1 72 | ASYNC = 2 73 | COMMIT = 3 74 | 75 | 76 | class VoteOption(int, Enum): 77 | YES = 1 78 | ABSTAIN = 2 79 | NO = 3 80 | NO_WITH_VETO = 4 81 | -------------------------------------------------------------------------------- /binance_chain/depthcache.py: -------------------------------------------------------------------------------- 1 | import time 2 | from operator import itemgetter 3 | from typing import Optional 4 | 5 | from binance_chain.websockets import BinanceChainSocketManager 6 | from binance_chain.http import HttpApiClient 7 | from binance_chain.environment import BinanceEnvironment 8 | 9 | 10 | class DepthCache(object): 11 | 12 | clear_price = "0.00000000" 13 | 14 | def __init__(self, symbol): 15 | """Initialise the DepthCache 16 | 17 | :param symbol: Symbol to create depth cache for 18 | :type symbol: string 19 | 20 | """ 21 | self.symbol = symbol 22 | self._bids = {} 23 | self._asks = {} 24 | self.update_time = None 25 | 26 | def add_bid(self, bid): 27 | """Add a bid to the cache 28 | 29 | :param bid: 30 | :return: 31 | 32 | """ 33 | if bid[1] == self.clear_price: 34 | del self._bids[bid[0]] 35 | else: 36 | self._bids[bid[0]] = bid[1] 37 | 38 | def add_ask(self, ask): 39 | """Add an ask to the cache 40 | 41 | :param ask: 42 | :return: 43 | 44 | """ 45 | if ask[1] == self.clear_price: 46 | del self._asks[ask[0]] 47 | else: 48 | self._asks[ask[0]] = ask[1] 49 | 50 | def get_bids(self): 51 | """Get the current bids 52 | 53 | :return: list of bids with price and quantity as floats 54 | 55 | .. code-block:: python 56 | 57 | [ 58 | [ 59 | 0.0001946, # Price 60 | 45.0 # Quantity 61 | ], 62 | [ 63 | 0.00019459, 64 | 2384.0 65 | ], 66 | [ 67 | 0.00019158, 68 | 5219.0 69 | ], 70 | [ 71 | 0.00019157, 72 | 1180.0 73 | ], 74 | [ 75 | 0.00019082, 76 | 287.0 77 | ] 78 | ] 79 | 80 | """ 81 | return DepthCache.sort_depth(self._bids, reverse=True) 82 | 83 | def get_asks(self): 84 | """Get the current asks 85 | 86 | :return: list of asks with price and quantity as floats 87 | 88 | .. code-block:: python 89 | 90 | [ 91 | [ 92 | 0.0001955, # Price 93 | 57.0' # Quantity 94 | ], 95 | [ 96 | 0.00019699, 97 | 778.0 98 | ], 99 | [ 100 | 0.000197, 101 | 64.0 102 | ], 103 | [ 104 | 0.00019709, 105 | 1130.0 106 | ], 107 | [ 108 | 0.0001971, 109 | 385.0 110 | ] 111 | ] 112 | 113 | """ 114 | return DepthCache.sort_depth(self._asks, reverse=False) 115 | 116 | @staticmethod 117 | def sort_depth(vals, reverse=False): 118 | """Sort bids or asks by price 119 | """ 120 | lst = [[price, quantity] for price, quantity in vals.items()] 121 | lst = sorted(lst, key=itemgetter(0), reverse=reverse) 122 | return lst 123 | 124 | 125 | class DepthCacheManager(object): 126 | 127 | _default_refresh = 60 * 30 # 30 minutes 128 | 129 | @classmethod 130 | async def create(cls, client: HttpApiClient, loop, symbol: str, coro=None, refresh_interval: int = _default_refresh, 131 | env: Optional[BinanceEnvironment] = None): 132 | """Create a DepthCacheManager instance 133 | 134 | :param client: Binance API client 135 | :param loop: 136 | :param symbol: Symbol to create depth cache for 137 | :param coro: Optional coroutine to receive depth cache updates 138 | :type coro: async coroutine 139 | :param env: Optional coroutine to receive depth cache updates 140 | :param refresh_interval: Optional number of seconds between cache refresh, use 0 or None to disable 141 | 142 | """ 143 | self = DepthCacheManager() 144 | self._client = client 145 | self._loop = loop 146 | self._symbol = symbol 147 | self._coro = coro 148 | self._last_update_id = None 149 | self._depth_message_buffer = [] 150 | self._bm = None 151 | self._depth_cache = DepthCache(self._symbol) 152 | self._refresh_interval = refresh_interval 153 | self._env = env or BinanceEnvironment.get_production_env() 154 | 155 | await self._start_socket() 156 | await self._init_cache() 157 | 158 | return self 159 | 160 | async def _init_cache(self): 161 | """Initialise the depth cache calling REST endpoint 162 | 163 | :return: 164 | """ 165 | self._last_update_time = None 166 | self._depth_message_buffer = [] 167 | 168 | res = self._client.get_order_book(self._symbol) 169 | 170 | # process bid and asks from the order book 171 | for bid in res['bids']: 172 | self._depth_cache.add_bid(bid) 173 | for ask in res['asks']: 174 | self._depth_cache.add_ask(ask) 175 | 176 | # set first update id 177 | self._last_update_time = int(time.time()) 178 | 179 | # set a time to refresh the depth cache 180 | if self._refresh_interval: 181 | self._refresh_time = int(time.time()) + self._refresh_interval 182 | 183 | # Apply any updates from the websocket 184 | for msg in self._depth_message_buffer: 185 | await self._process_depth_message(msg, buffer=True) 186 | 187 | # clear the depth buffer 188 | del self._depth_message_buffer 189 | 190 | # call the callback with the updated depth cache 191 | if self._coro: 192 | await self._coro(self._depth_cache) 193 | 194 | async def _start_socket(self): 195 | """Start the depth cache socket 196 | 197 | :return: 198 | """ 199 | self._bm = await BinanceChainSocketManager.create(self._loop, self._depth_event, self._env) 200 | await self._bm.subscribe_market_diff(symbols=[self._symbol]) 201 | 202 | async def _depth_event(self, msg): 203 | """Handle a depth event 204 | 205 | :param msg: 206 | :return: 207 | 208 | """ 209 | 210 | # TODO: handle errors in message 211 | 212 | if self._last_update_time is None: 213 | # Initial depth snapshot fetch not yet performed, buffer messages 214 | self._depth_message_buffer.append(msg) 215 | else: 216 | await self._process_depth_message(msg) 217 | 218 | async def _process_depth_message(self, msg, buffer=False): 219 | """Process a depth event message. 220 | 221 | :param msg: Depth event message. 222 | :return: 223 | 224 | """ 225 | 226 | if buffer and msg['data']['E'] < self._last_update_time: 227 | # ignore any updates before the initial update id 228 | return 229 | 230 | # add any bid or ask values 231 | for bid in msg['data']['b']: 232 | self._depth_cache.add_bid(bid) 233 | for ask in msg['data']['a']: 234 | self._depth_cache.add_ask(ask) 235 | 236 | # keeping update time 237 | self._depth_cache.update_time = msg['data']['E'] 238 | 239 | # call the callback with the updated depth cache 240 | if self._coro: 241 | await self._coro(self._depth_cache) 242 | 243 | self._last_update_time = msg['data']['E'] 244 | 245 | # after processing event see if we need to refresh the depth cache 246 | if self._refresh_interval and int(time.time()) > self._refresh_time: 247 | await self._init_cache() 248 | 249 | def get_depth_cache(self): 250 | """Get the current depth cache 251 | 252 | :return: DepthCache object 253 | 254 | """ 255 | return self._depth_cache 256 | 257 | async def close(self): 258 | """Close the open socket for this manager 259 | 260 | :return: 261 | """ 262 | await self._bm.close_connection() 263 | self._depth_cache = None 264 | -------------------------------------------------------------------------------- /binance_chain/environment.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BinanceEnvironment: 4 | PROD_ENV = { 5 | 'api_url': 'https://dex.binance.org', 6 | 'wss_url': 'wss://dex.binance.org/api/', 7 | 'hrp': 'bnb' 8 | } 9 | TESTNET_ENV = { 10 | 'api_url': 'https://testnet-dex.binance.org', 11 | 'wss_url': 'wss://testnet-dex.binance.org/api/', 12 | 'hrp': 'tbnb' 13 | } 14 | 15 | def __init__(self, api_url: str = None, wss_url: str = None, hrp: str = None): 16 | """Create custom environment 17 | 18 | """ 19 | 20 | self._api_url = api_url or self.PROD_ENV['api_url'] 21 | self._wss_url = wss_url or self.PROD_ENV['wss_url'] 22 | self._hrp = hrp or self.PROD_ENV['hrp'] 23 | 24 | @classmethod 25 | def get_production_env(cls): 26 | return cls(**cls.PROD_ENV) 27 | 28 | @classmethod 29 | def get_testnet_env(cls): 30 | return cls(**cls.TESTNET_ENV) 31 | 32 | @property 33 | def api_url(self): 34 | return self._api_url 35 | 36 | @property 37 | def wss_url(self): 38 | return self._wss_url 39 | 40 | @property 41 | def hrp(self): 42 | return self._hrp 43 | 44 | def hash(self): 45 | return hash(f"{self.api_url}{self.wss_url}{self.hrp}") 46 | -------------------------------------------------------------------------------- /binance_chain/exceptions.py: -------------------------------------------------------------------------------- 1 | import ujson as json 2 | 3 | 4 | class BinanceChainAPIException(Exception): 5 | 6 | def __init__(self, response, status_code): 7 | self.code = 0 8 | try: 9 | json_res = json.loads(response.content) 10 | except ValueError: 11 | if not response.content: 12 | self.message = status_code 13 | else: 14 | self.message = 'Invalid JSON error message from Binance Chain: {}'.format(response.text) 15 | else: 16 | self.code = json_res.get('code', None) 17 | self.message = json_res['message'] 18 | self.status_code = status_code 19 | self.response = response 20 | self.request = getattr(response, 'request', None) 21 | 22 | def __str__(self): # pragma: no cover 23 | return f'APIError(code={self.code}): {self.message}' 24 | 25 | 26 | class BinanceChainRequestException(Exception): 27 | pass 28 | 29 | 30 | class BinanceChainBroadcastException(Exception): 31 | pass 32 | 33 | 34 | class BinanceChainSigningAuthenticationException(Exception): 35 | pass 36 | 37 | 38 | class BinanceChainRPCException(Exception): 39 | def __init__(self, response): 40 | self.code = 0 41 | try: 42 | json_res = json.loads(response.content) 43 | except ValueError: 44 | self.message = 'Invalid JSON error message from Binance Chain: {}'.format(response.text) 45 | else: 46 | self.code = json_res['error']['code'] 47 | self.message = json_res['error']['message'] 48 | self.status_code = response.status_code 49 | self.response = response 50 | self.request = getattr(response, 'request', None) 51 | 52 | def __str__(self): # pragma: no cover 53 | return f'RPCError(code={self.code}): {self.message}' 54 | -------------------------------------------------------------------------------- /binance_chain/ledger/__init__.py: -------------------------------------------------------------------------------- 1 | from btchip.btchip import getDongle # noqa 2 | 3 | from binance_chain.ledger.client import LedgerApp # noqa 4 | from binance_chain.ledger.wallet import LedgerWallet # noqa 5 | -------------------------------------------------------------------------------- /binance_chain/ledger/client.py: -------------------------------------------------------------------------------- 1 | import re 2 | import binascii 3 | from typing import Optional 4 | 5 | from btchip.btchip import writeUint32LE, BTChipException 6 | 7 | from binance_chain.environment import BinanceEnvironment 8 | from binance_chain.ledger.exceptions import LedgerRequestException 9 | 10 | 11 | class LedgerApp: 12 | 13 | BNC_CLA = 0xbc 14 | BNC_INS_GET_VERSION = 0x00 15 | BNC_INS_PUBLIC_KEY_SECP256K1 = 0x01 16 | BNC_INS_SIGN_SECP256K1 = 0x02 17 | BNC_INS_SHOW_ADDR_SECP256K1 = 0x03 18 | BNC_INS_GET_ADDR_SECP256K1 = 0x04 19 | SUCCESS_CODE = 0x9000 20 | 21 | CHUNK_SIZE = 250 22 | HD_PATH = "44'/714'/0'/0/0" 23 | 24 | def __init__(self, dongle, env: Optional[BinanceEnvironment] = None): 25 | self._dongle = dongle 26 | self._path = LedgerApp.HD_PATH 27 | self._env = env or BinanceEnvironment.get_production_env() 28 | self._hrp = self._env.hrp 29 | 30 | def _exchange(self, apdu): 31 | apdu_data = bytearray(apdu) 32 | try: 33 | response = self._dongle.exchange(apdu_data) 34 | except BTChipException as e: 35 | if e.message.startswith('Invalid status'): 36 | raise LedgerRequestException(e.sw, binascii.hexlify(apdu_data)) 37 | else: 38 | raise e 39 | 40 | return response 41 | 42 | def get_version(self) -> dict: 43 | """Gets the version of the Ledger app that is currently open on the device. 44 | 45 | .. code:: python 46 | 47 | version = client.get_version() 48 | 49 | :return: API Response 50 | 51 | .. code:: python 52 | 53 | {'testMode': False, 'version': '1.1.3', 'locked': False} 54 | 55 | """ 56 | result = {} 57 | apdu = [self.BNC_CLA, self.BNC_INS_GET_VERSION, 0x00, 0x00, 0x00] 58 | response = self._exchange(apdu) 59 | 60 | result['testMode'] = (response[0] == 0xFF) 61 | result['version'] = "%d.%d.%d" % (response[1], response[2], response[3]) 62 | result['locked'] = bool(response[4]) 63 | return result 64 | 65 | def get_public_key(self) -> str: 66 | """Gets the public key from the Ledger app that is currently open on the device. 67 | 68 | .. code:: python 69 | 70 | public_key = client.get_public_key() 71 | 72 | :return: API Response 73 | 74 | .. code:: python 75 | 76 | '' 77 | 78 | """ 79 | dongle_path = self._parse_hd_path(self._path) 80 | apdu = [self.BNC_CLA, self.BNC_INS_PUBLIC_KEY_SECP256K1, 0x00, 0x00, len(dongle_path)] 81 | apdu.extend(dongle_path) 82 | response = self._exchange(apdu) 83 | 84 | return response[0: 1 + 64] 85 | 86 | def show_address(self): 87 | """Shows the user's address for the given HD path on the device display. 88 | 89 | .. code:: python 90 | 91 | client.show_address() 92 | 93 | :return: None 94 | 95 | 96 | """ 97 | dongle_path = self._parse_hrp(self._hrp) + self._parse_hd_path(self._path) 98 | apdu = [self.BNC_CLA, self.BNC_INS_SHOW_ADDR_SECP256K1, 0x00, 0x00, len(dongle_path)] 99 | apdu.extend(dongle_path) 100 | self._exchange(apdu) 101 | 102 | def get_address(self) -> dict: 103 | """Gets the address and public key from the Ledger app that is currently open on the device. 104 | 105 | .. code:: python 106 | 107 | address = client.get_address() 108 | 109 | :return: API Response 110 | 111 | .. code:: python 112 | 113 | {'pk': '', 'address': '
'} 114 | 115 | """ 116 | dongle_path = self._parse_hrp(self._hrp) + self._parse_hd_path(self._path) 117 | apdu = [self.BNC_CLA, self.BNC_INS_GET_ADDR_SECP256K1, 0x00, 0x00, len(dongle_path)] 118 | apdu.extend(dongle_path) 119 | response = self._exchange(apdu) 120 | return { 121 | 'pk': response[0: 1 + 32], 122 | 'address': response[1 + 32:].decode() 123 | } 124 | 125 | def _get_sign_chunks(self, msg: bytes): 126 | chunks = [self._parse_hd_path(self._path)] 127 | chunks += [msg[i:i + self.CHUNK_SIZE] for i in range(0, len(msg), self.CHUNK_SIZE)] 128 | return chunks 129 | 130 | def sign(self, msg: bytes) -> str: 131 | """Sends a transaction sign doc to the Ledger app to be signed. 132 | 133 | .. code:: python 134 | 135 | address = client.get_address() 136 | 137 | :return: str 138 | 139 | .. code:: python 140 | 141 | '' 142 | 143 | """ 144 | chunks = self._get_sign_chunks(msg) 145 | response = '' 146 | for idx, chunk in enumerate(chunks): 147 | apdu = [self.BNC_CLA, self.BNC_INS_SIGN_SECP256K1, idx + 1, len(chunks), len(chunk)] 148 | apdu.extend(chunk) 149 | response = self._exchange(apdu) 150 | 151 | if response[0] != 0x30: 152 | raise Exception("Ledger assertion failed: Expected a signature header of 0x30") 153 | 154 | # decode DER format 155 | r_offset = 4 156 | r_len = response[3] 157 | s_len = response[4 + r_len + 1] 158 | s_offset = len(response) - s_len 159 | 160 | if r_len == 33: 161 | r_offset += 1 162 | r_len -= 1 163 | 164 | if s_len == 3: 165 | s_offset += 1 166 | 167 | sig_r = response[r_offset: r_offset + r_len] 168 | sig_s = response[s_offset:] 169 | 170 | return sig_r + sig_s 171 | 172 | def _parse_hd_path(self, path): 173 | if len(path) == 0: 174 | return bytearray([0]) 175 | result = [] 176 | elements = path.split('/') 177 | if len(elements) > 10: 178 | raise BTChipException("Path too long") 179 | for pathElement in elements: 180 | element = re.split('\'|h|H', pathElement) 181 | if len(element) == 1: 182 | writeUint32LE(int(element[0]), result) 183 | else: 184 | writeUint32LE(0x80000000 | int(element[0]), result) 185 | return bytearray([len(elements)] + result) 186 | 187 | def _parse_hrp(self, hrp): 188 | return bytearray([len(hrp)]) + hrp.encode() 189 | -------------------------------------------------------------------------------- /binance_chain/ledger/exceptions.py: -------------------------------------------------------------------------------- 1 | LEDGER_RESPONSE_CODES = { 2 | 0x6400: 'Execution Error', 3 | 0x6982: 'Empty buffer', 4 | 0x6983: 'Output buffer too small', 5 | 0x6986: 'Command not allowed', 6 | 0x6A80: 'Incorrect tx data', 7 | 0x6D00: 'INS not supported', 8 | 0x6E00: 'CLA not supported', 9 | 0x6F00: 'Unknown', 10 | } 11 | 12 | 13 | class LedgerRequestException(Exception): 14 | 15 | def __init__(self, response_code, request): 16 | self._response_code = response_code 17 | self._response_msg = LEDGER_RESPONSE_CODES.get(response_code, 'Unknown') 18 | 19 | self._request = request 20 | 21 | def __str__(self): # pragma: no cover 22 | return f'LedgerRequestException(code={self._response_code}): {self._response_msg} - request {self._request}' 23 | -------------------------------------------------------------------------------- /binance_chain/ledger/wallet.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from binance_chain.environment import BinanceEnvironment 4 | from binance_chain.wallet import BaseWallet 5 | from binance_chain.ledger.client import LedgerApp 6 | 7 | 8 | class LedgerWallet(BaseWallet): 9 | 10 | def __init__(self, app: LedgerApp, env: Optional[BinanceEnvironment] = None): 11 | super().__init__(env) 12 | self._app = app 13 | pk_address = self._app.get_address() 14 | self._public_key = pk_address['pk'] 15 | self._address = pk_address['address'] 16 | 17 | def sign_message(self, msg_bytes): 18 | return self._app.sign(msg_bytes) 19 | -------------------------------------------------------------------------------- /binance_chain/messages.py: -------------------------------------------------------------------------------- 1 | import ujson as json 2 | import binascii 3 | from typing import List, Dict, Union, Optional, NamedTuple 4 | from decimal import Decimal 5 | from collections import OrderedDict 6 | 7 | from binance_chain.wallet import BaseWallet 8 | from binance_chain.constants import TimeInForce, OrderSide, OrderType, VoteOption 9 | from binance_chain.protobuf.dex_pb2 import ( 10 | NewOrder, CancelOrder, TokenFreeze, TokenUnfreeze, StdTx, StdSignature, Send, Input, Output, Token, Vote 11 | ) 12 | from binance_chain.utils.encode_utils import encode_number, varint_encode 13 | from binance_chain.utils.segwit_addr import decode_address 14 | 15 | # An identifier for tools triggering broadcast transactions, set to zero if unwilling to disclose. 16 | BROADCAST_SOURCE = 0 17 | 18 | 19 | class Transfer(NamedTuple): 20 | amount: Union[int, float, Decimal] 21 | symbol: str 22 | 23 | 24 | class Msg: 25 | 26 | AMINO_MESSAGE_TYPE = "" 27 | INCLUDE_AMINO_LENGTH_PREFIX = False 28 | 29 | def __init__(self, wallet: BaseWallet, memo: str = ''): 30 | self._wallet = wallet 31 | self._memo = memo 32 | 33 | def to_dict(self) -> Dict: 34 | return {} 35 | 36 | def to_sign_dict(self) -> Dict: 37 | return {} 38 | 39 | def to_protobuf(self): 40 | pass 41 | 42 | def to_amino(self): 43 | proto = self.to_protobuf() 44 | if type(proto) != bytes: 45 | proto = proto.SerializeToString() 46 | 47 | # wrap with type 48 | type_bytes = b"" 49 | if self.AMINO_MESSAGE_TYPE: 50 | type_bytes = binascii.unhexlify(self.AMINO_MESSAGE_TYPE) 51 | varint_length = varint_encode(len(proto) + len(type_bytes)) 52 | else: 53 | varint_length = varint_encode(len(proto)) 54 | 55 | msg = b"" 56 | if self.INCLUDE_AMINO_LENGTH_PREFIX: 57 | msg += varint_length 58 | msg += type_bytes + proto 59 | 60 | return msg 61 | 62 | @property 63 | def wallet(self): 64 | return self._wallet 65 | 66 | @property 67 | def memo(self): 68 | return self._memo 69 | 70 | def to_hex_data(self): 71 | """Wrap in a Standard Transaction Message and convert to hex string 72 | 73 | """ 74 | return binascii.hexlify(StdTxMsg(self).to_amino()) 75 | 76 | def increment_sequence(self): 77 | self._wallet.increment_account_sequence() 78 | 79 | 80 | class Signature: 81 | 82 | def __init__(self, msg: Msg, data=None): 83 | self._msg = msg 84 | self._chain_id = msg.wallet.chain_id 85 | self._data = data 86 | self._source = BROADCAST_SOURCE 87 | 88 | def to_json(self): 89 | return json.dumps(OrderedDict([ 90 | ('account_number', str(self._msg.wallet.account_number)), 91 | ('chain_id', self._chain_id), 92 | ('data', self._data), 93 | ('memo', self._msg.memo), 94 | ('msgs', [self._msg.to_dict()]), 95 | ('sequence', str(self._msg.wallet.sequence)), 96 | ('source', str(self._source)) 97 | ]), ensure_ascii=False) 98 | 99 | def to_bytes_json(self): 100 | return self.to_json().encode() 101 | 102 | def sign(self, wallet: Optional[BaseWallet] = None): 103 | wallet = wallet or self._msg.wallet 104 | 105 | # generate string to sign 106 | json_bytes = self.to_bytes_json() 107 | 108 | signed = wallet.sign_message(json_bytes) 109 | return signed[-64:] 110 | 111 | 112 | class NewOrderMsg(Msg): 113 | 114 | AMINO_MESSAGE_TYPE = b"CE6DC043" 115 | 116 | def __init__(self, symbol: str, time_in_force: TimeInForce, order_type: OrderType, side: OrderSide, 117 | price: Union[int, float, Decimal], quantity: Union[int, float, Decimal], 118 | wallet: Optional[BaseWallet] = None): 119 | """NewOrder transaction creates a new order to buy and sell tokens on Binance DEX. 120 | 121 | :param symbol: symbol for trading pair in full name of the tokens e.g. 'ANN-457_BNB' 122 | :param time_in_force: TimeInForce type (GOOD_TILL_EXPIRE, IMMEDIATE_OR_CANCEL) 123 | :param order_type: OrderType (LIMIT, MARKET) 124 | :param side: OrderSide (BUY, SELL) 125 | :param price: price of the order e.g. Decimal(0.000396000) or 0.002384 126 | :param quantity: quantity of the order Decimal(12) or 12 127 | 128 | """ 129 | super().__init__(wallet) 130 | self._symbol = symbol 131 | self._time_in_force = time_in_force.value 132 | self._order_type = order_type.value 133 | self._side = side.value 134 | self._price = price 135 | self._price_encoded = encode_number(price) 136 | self._quantity = quantity 137 | self._quantity_encoded = encode_number(quantity) 138 | 139 | def to_dict(self) -> Dict: 140 | return OrderedDict([ 141 | ('id', self._wallet.generate_order_id()), 142 | ('ordertype', self._order_type), 143 | ('price', self._price_encoded), 144 | ('quantity', self._quantity_encoded), 145 | ('sender', self._wallet.address), 146 | ('side', self._side), 147 | ('symbol', self._symbol), 148 | ('timeinforce', self._time_in_force), 149 | ]) 150 | 151 | def to_sign_dict(self) -> Dict: 152 | return{ 153 | 'order_type': self._order_type, 154 | 'price': self._price, 155 | 'quantity': self._quantity, 156 | 'side': self._side, 157 | 'symbol': self._symbol, 158 | 'time_in_force': self._time_in_force, 159 | } 160 | 161 | def to_protobuf(self) -> NewOrder: 162 | pb = NewOrder() 163 | pb.sender = self._wallet.address_decoded 164 | pb.id = self._wallet.generate_order_id() 165 | pb.symbol = self._symbol.encode() 166 | pb.timeinforce = self._time_in_force 167 | pb.ordertype = self._order_type 168 | pb.side = self._side 169 | pb.price = self._price_encoded 170 | pb.quantity = self._quantity_encoded 171 | return pb 172 | 173 | 174 | class LimitOrderMsg(NewOrderMsg): 175 | 176 | def __init__(self, symbol: str, side: OrderSide, 177 | price: Union[int, float, Decimal], quantity: Union[int, float, Decimal], 178 | time_in_force: TimeInForce = TimeInForce.GOOD_TILL_EXPIRE, 179 | wallet: Optional[BaseWallet] = None): 180 | """NewOrder transaction creates a new order to buy and sell tokens on Binance DEX. 181 | 182 | :param symbol: symbol for trading pair in full name of the tokens e.g. 'ANN-457_BNB' 183 | :param side: OrderSide (BUY, SELL) 184 | :param price: price of the order e.g. Decimal(0.000396000) or 0.002384 185 | :param quantity: quantity of the order Decimal(12) or 12 186 | :param time_in_force: TimeInForce type (GOOD_TILL_EXPIRE, IMMEDIATE_OR_CANCEL) default GOOD_TILL_EXPIRE 187 | 188 | """ 189 | super().__init__( 190 | wallet=wallet, 191 | symbol=symbol, 192 | time_in_force=time_in_force, 193 | order_type=OrderType.LIMIT, 194 | side=side, 195 | price=price, 196 | quantity=quantity 197 | ) 198 | 199 | 200 | class LimitOrderBuyMsg(LimitOrderMsg): 201 | 202 | def __init__(self, symbol: str, price: Union[int, float, Decimal], quantity: Union[int, float, Decimal], 203 | time_in_force: TimeInForce = TimeInForce.GOOD_TILL_EXPIRE, 204 | wallet: Optional[BaseWallet] = None): 205 | """LimitOrderBuyMsg transaction creates a new limit order buy message on Binance DEX. 206 | 207 | :param symbol: symbol for trading pair in full name of the tokens e.g. 'ANN-457_BNB' 208 | :param price: price of the order e.g. Decimal(0.000396000) or 0.002384 209 | :param quantity: quantity of the order Decimal(12) or 12 210 | :param time_in_force: TimeInForce type (GOOD_TILL_EXPIRE, IMMEDIATE_OR_CANCEL) default GOOD_TILL_EXPIRE 211 | 212 | """ 213 | super().__init__( 214 | wallet=wallet, 215 | symbol=symbol, 216 | time_in_force=time_in_force, 217 | side=OrderSide.BUY, 218 | price=price, 219 | quantity=quantity 220 | ) 221 | 222 | 223 | class LimitOrderSellMsg(LimitOrderMsg): 224 | 225 | def __init__(self, symbol: str, price: Union[int, float, Decimal], quantity: Union[int, float, Decimal], 226 | time_in_force: TimeInForce = TimeInForce.GOOD_TILL_EXPIRE, 227 | wallet: Optional[BaseWallet] = None): 228 | """LimitOrderSellMsg transaction creates a new limit order sell message on Binance DEX. 229 | 230 | :param symbol: symbol for trading pair in full name of the tokens e.g. 'ANN-457_BNB' 231 | :param time_in_force: TimeInForce type (GOOD_TILL_EXPIRE, IMMEDIATE_OR_CANCEL) 232 | :param price: price of the order e.g. Decimal(0.000396000) or 0.002384 233 | :param quantity: quantity of the order Decimal(12) or 12 234 | :param time_in_force: TimeInForce type (GOOD_TILL_EXPIRE, IMMEDIATE_OR_CANCEL) default GOOD_TILL_EXPIRE 235 | 236 | """ 237 | super().__init__( 238 | wallet=wallet, 239 | symbol=symbol, 240 | time_in_force=time_in_force, 241 | side=OrderSide.SELL, 242 | price=price, 243 | quantity=quantity 244 | ) 245 | 246 | 247 | class CancelOrderMsg(Msg): 248 | 249 | AMINO_MESSAGE_TYPE = b"166E681B" 250 | 251 | def __init__(self, symbol: str, order_id: str, wallet: Optional[BaseWallet] = None): 252 | """Cancel transactions cancel the outstanding (unfilled) orders from the Binance DEX. After cancel success, 253 | the locked quantity on the orders would return back to the address' balance and become free to use, 254 | i.e. transfer or send new orders. 255 | 256 | :param symbol: symbol for trading pair in full name of the tokens 257 | :param order_id: order id of the one to cancel 258 | """ 259 | super().__init__(wallet) 260 | 261 | self._symbol = symbol 262 | self._order_id = order_id 263 | 264 | def to_dict(self): 265 | return OrderedDict([ 266 | ('refid', self._order_id), 267 | ('sender', self._wallet.address), 268 | ('symbol', self._symbol), 269 | ]) 270 | 271 | def to_sign_dict(self) -> Dict: 272 | return { 273 | 'refid': self._order_id, 274 | 'symbol': self._symbol, 275 | } 276 | 277 | def to_protobuf(self) -> CancelOrder: 278 | pb = CancelOrder() 279 | pb.sender = self._wallet.address_decoded 280 | pb.refid = self._order_id 281 | pb.symbol = self._symbol.encode() 282 | return pb 283 | 284 | 285 | class FreezeMsg(Msg): 286 | 287 | AMINO_MESSAGE_TYPE = b"E774B32D" 288 | 289 | def __init__(self, symbol: str, amount: Union[int, float, Decimal], wallet: Optional[BaseWallet] = None): 290 | """Freeze transaction moves the amount of the tokens into a frozen state, 291 | in which it cannot be used to transfer or send new orders. 292 | 293 | :param symbol: token symbol, in full name with "-" suffix 294 | :param amount: amount of token to freeze 295 | """ 296 | super().__init__(wallet) 297 | self._symbol = symbol 298 | self._amount = amount 299 | self._amount_amino = encode_number(amount) 300 | 301 | def to_dict(self): 302 | return OrderedDict([ 303 | ('amount', self._amount_amino), 304 | ('from', self._wallet.address), 305 | ('symbol', self._symbol), 306 | ]) 307 | 308 | def to_sign_dict(self) -> Dict: 309 | return { 310 | 'amount': self._amount, 311 | 'symbol': self._symbol, 312 | } 313 | 314 | def to_protobuf(self) -> TokenFreeze: 315 | pb = TokenFreeze() 316 | setattr(pb, 'from', self._wallet.address_decoded) 317 | pb.symbol = self._symbol.encode() 318 | pb.amount = self._amount_amino 319 | return pb 320 | 321 | 322 | class UnFreezeMsg(Msg): 323 | 324 | AMINO_MESSAGE_TYPE = b"6515FF0D" 325 | 326 | def __init__(self, symbol: str, amount: Union[int, float, Decimal], wallet: Optional[BaseWallet] = None): 327 | """Turn the amount of frozen tokens back to free state. 328 | 329 | :param symbol: token symbol, in full name with "-" suffix 330 | :param amount: amount of token to unfreeze 331 | """ 332 | super().__init__(wallet) 333 | self._symbol = symbol 334 | self._amount = amount 335 | self._amount_amino = encode_number(amount) 336 | 337 | def to_dict(self): 338 | return OrderedDict([ 339 | ('amount', self._amount_amino), 340 | ('from', self._wallet.address), 341 | ('symbol', self._symbol), 342 | ]) 343 | 344 | def to_sign_dict(self) -> Dict: 345 | return { 346 | 'amount': self._amount, 347 | 'symbol': self._symbol, 348 | } 349 | 350 | def to_protobuf(self) -> TokenUnfreeze: 351 | pb = TokenUnfreeze() 352 | setattr(pb, 'from', self._wallet.address_decoded) 353 | pb.symbol = self._symbol.encode() 354 | pb.amount = self._amount_amino 355 | return pb 356 | 357 | 358 | class SignatureMsg(Msg): 359 | 360 | AMINO_MESSAGE_TYPE = None 361 | 362 | def __init__(self, msg: Msg): 363 | super().__init__(msg.wallet) 364 | self._signature = Signature(msg) 365 | 366 | def to_protobuf(self) -> StdSignature: 367 | pub_key_msg = PubKeyMsg(self._wallet) 368 | std_sig = StdSignature() 369 | std_sig.sequence = self._wallet.sequence 370 | std_sig.account_number = self._wallet.account_number 371 | std_sig.pub_key = pub_key_msg.to_amino() 372 | std_sig.signature = self._signature.sign(self._wallet) 373 | return std_sig 374 | 375 | 376 | class StdTxMsg(Msg): 377 | 378 | AMINO_MESSAGE_TYPE = b"F0625DEE" 379 | INCLUDE_AMINO_LENGTH_PREFIX = True 380 | 381 | def __init__(self, msg: Msg, data=''): 382 | super().__init__(msg.wallet) 383 | 384 | self._msg = msg 385 | self._signature = SignatureMsg(msg) 386 | self._data = data 387 | self._source = BROADCAST_SOURCE 388 | 389 | def to_protobuf(self) -> StdTx: 390 | stdtx = StdTx() 391 | stdtx.msgs.extend([self._msg.to_amino()]) 392 | stdtx.signatures.extend([self._signature.to_amino()]) 393 | stdtx.data = self._data.encode() 394 | stdtx.memo = self._msg.memo 395 | stdtx.source = self._source 396 | return stdtx 397 | 398 | 399 | class PubKeyMsg(Msg): 400 | 401 | AMINO_MESSAGE_TYPE = b"EB5AE987" 402 | 403 | def __init__(self, wallet: BaseWallet): 404 | super().__init__(wallet) 405 | 406 | def to_protobuf(self): 407 | return self._wallet.public_key 408 | 409 | def to_amino(self): 410 | proto = self.to_protobuf() 411 | 412 | type_bytes = binascii.unhexlify(self.AMINO_MESSAGE_TYPE) 413 | 414 | varint_length = varint_encode(len(proto)) 415 | 416 | msg = type_bytes + varint_length + proto 417 | 418 | return msg 419 | 420 | 421 | class TransferMsg(Msg): 422 | 423 | AMINO_MESSAGE_TYPE = b"2A2C87FA" 424 | 425 | def __init__(self, symbol: str, amount: Union[int, float, Decimal], 426 | to_address: str, wallet: Optional[BaseWallet] = None, memo: str = ''): 427 | """Transferring funds between different addresses. 428 | 429 | :param symbol: token symbol, in full name with "-" suffix 430 | :param amount: amount of token to freeze 431 | :param to_address: amount of token to freeze 432 | """ 433 | super().__init__(wallet, memo) 434 | self._symbol = symbol 435 | self._amount = amount 436 | self._amount_amino = encode_number(amount) 437 | self._from_address = wallet.address if wallet else None 438 | self._to_address = to_address 439 | 440 | def to_dict(self): 441 | return OrderedDict([ 442 | ('inputs', [ 443 | OrderedDict([ 444 | ('address', self._from_address), 445 | ('coins', [ 446 | OrderedDict([ 447 | ('amount', self._amount_amino), 448 | ('denom', self._symbol) 449 | ]) 450 | ]) 451 | ]) 452 | ]), 453 | ('outputs', [ 454 | OrderedDict([ 455 | ('address', self._to_address), 456 | ('coins', [ 457 | OrderedDict([ 458 | ('amount', self._amount_amino), 459 | ('denom', self._symbol) 460 | ]) 461 | ]) 462 | ]) 463 | ]) 464 | ]) 465 | 466 | def to_sign_dict(self): 467 | return { 468 | 'to_address': self._to_address, 469 | 'amount': self._amount, 470 | 'denom': self._symbol, 471 | } 472 | 473 | def to_protobuf(self) -> Send: 474 | token = Token() 475 | token.denom = self._symbol 476 | token.amount = self._amount_amino 477 | input_addr = Input() 478 | input_addr.address = decode_address(self._from_address) 479 | input_addr.coins.extend([token]) 480 | output_addr = Output() 481 | output_addr.address = decode_address(self._to_address) 482 | output_addr.coins.extend([token]) 483 | 484 | msg = Send() 485 | msg.inputs.extend([input_addr]) 486 | msg.outputs.extend([output_addr]) 487 | return msg 488 | 489 | 490 | class MultiTransferMsg(Msg): 491 | 492 | AMINO_MESSAGE_TYPE = b"2A2C87FA" 493 | 494 | def __init__(self, transfers: List[Transfer], 495 | to_address: str, wallet: Optional[BaseWallet] = None, memo: str = ''): 496 | """Transferring funds between different addresses. 497 | 498 | :param transfers: List of tokens and amounts to send 499 | :param to_address: amount of token to freeze 500 | """ 501 | super().__init__(wallet, memo) 502 | self._transfers = transfers 503 | self._transfers.sort(key=lambda x: x.symbol) 504 | self._from_address = wallet.address if wallet else None 505 | self._to_address = to_address 506 | 507 | def to_dict(self): 508 | return OrderedDict([ 509 | ('inputs', [ 510 | OrderedDict([ 511 | ('address', self._from_address), 512 | ('coins', [ 513 | OrderedDict([ 514 | ('amount', encode_number(transfer.amount)), 515 | ('denom', transfer.symbol) 516 | ]) for transfer in self._transfers 517 | ]) 518 | ]) 519 | ]), 520 | ('outputs', [ 521 | OrderedDict([ 522 | ('address', self._to_address), 523 | ('coins', [ 524 | OrderedDict([ 525 | ('amount', encode_number(transfer.amount)), 526 | ('denom', transfer.symbol) 527 | ]) for transfer in self._transfers 528 | ]) 529 | ]) 530 | ]) 531 | ]) 532 | 533 | def to_sign_dict(self): 534 | return { 535 | 'to_address': self._to_address, 536 | 'transfers': [ 537 | { 538 | 'amount': transfer.amount, 539 | 'denom': transfer.symbol, 540 | } for transfer in self._transfers 541 | ] 542 | } 543 | 544 | def to_protobuf(self) -> Send: 545 | input_addr = Input() 546 | output_addr = Output() 547 | for transfer in self._transfers: 548 | token = Token() 549 | token.denom = transfer.symbol 550 | token.amount = encode_number(transfer.amount) 551 | input_addr.coins.extend([token]) 552 | output_addr.coins.extend([token]) 553 | input_addr.address = decode_address(self._from_address) 554 | output_addr.address = decode_address(self._to_address) 555 | 556 | msg = Send() 557 | msg.inputs.extend([input_addr]) 558 | msg.outputs.extend([output_addr]) 559 | return msg 560 | 561 | 562 | class VoteMsg(Msg): 563 | 564 | AMINO_MESSAGE_TYPE = b"A1CADD36" 565 | 566 | VOTE_OPTION_STR = { 567 | VoteOption.YES: 'Yes', 568 | VoteOption.ABSTAIN: 'Abstain', 569 | VoteOption.NO: 'No', 570 | VoteOption.NO_WITH_VETO: 'NoWithVeto', 571 | } 572 | 573 | VOTE_OPTION_INT = { 574 | VoteOption.YES: 1, 575 | VoteOption.ABSTAIN: 2, 576 | VoteOption.NO: 3, 577 | VoteOption.NO_WITH_VETO: 4 578 | } 579 | 580 | def __init__(self, proposal_id: int, vote_option: VoteOption, wallet: Optional[BaseWallet] = None): 581 | """Place a vote for a proposal from the given wallet address. 582 | 583 | :param proposal_id: ID of the proposal 584 | :param vote_option: option chosen by the voter 585 | """ 586 | super().__init__(wallet) 587 | self._proposal_id = proposal_id 588 | self._proposal_id_amino = encode_number(proposal_id) 589 | self._voter = wallet.address if wallet else None 590 | self._vote_option = vote_option 591 | 592 | def to_dict(self): 593 | return OrderedDict([ 594 | ('option', self.VOTE_OPTION_STR[self._vote_option]), 595 | ('proposal_id', self._proposal_id_amino), 596 | ('voter', self._voter), 597 | ]) 598 | 599 | def to_sign_dict(self) -> Dict: 600 | return { 601 | 'proposal_id': self._proposal_id, 602 | 'option': self._vote_option, 603 | } 604 | 605 | def to_protobuf(self) -> Vote: 606 | pb = Vote() 607 | pb.voter = self.wallet.address_decoded 608 | pb.proposal_id = self._proposal_id 609 | pb.option = self.VOTE_OPTION_INT[self._vote_option] 610 | return pb 611 | -------------------------------------------------------------------------------- /binance_chain/node_rpc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/binance_chain/node_rpc/__init__.py -------------------------------------------------------------------------------- /binance_chain/node_rpc/http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | from typing import Optional, Dict 4 | 5 | import requests 6 | import aiohttp 7 | import ujson 8 | 9 | from binance_chain.exceptions import BinanceChainRPCException, BinanceChainRequestException 10 | from binance_chain.constants import RpcBroadcastRequestType 11 | from binance_chain.messages import Msg 12 | from binance_chain.node_rpc.request import RpcRequest 13 | 14 | 15 | requests.models.json = ujson 16 | 17 | 18 | class BaseHttpRpcClient: 19 | 20 | id_generator = itertools.count(1) 21 | 22 | def __init__(self, endpoint_url, requests_params: Optional[Dict] = None): 23 | self._endpoint_url = endpoint_url 24 | self._requests_params = requests_params 25 | 26 | self.session = self._init_session() 27 | 28 | def _init_session(self): 29 | 30 | session = requests.session() 31 | session.headers.update(self._get_headers()) 32 | return session 33 | 34 | def _get_rpc_request(self, path, **kwargs) -> str: 35 | 36 | rpc_request = RpcRequest(path, next(self.id_generator), kwargs.get('data', None)) 37 | 38 | return str(rpc_request) 39 | 40 | def _get_headers(self): 41 | return { 42 | 'Accept': 'application/json', 43 | 'Content-Type': 'application/json', 44 | 'User-Agent': 'python-binance-chain', 45 | } 46 | 47 | def request_kwargs(self, method, **kwargs): 48 | 49 | # set default requests timeout 50 | kwargs['timeout'] = 10 51 | 52 | # add our global requests params 53 | if self._requests_params: 54 | kwargs.update(self._requests_params) 55 | 56 | kwargs['data'] = kwargs.get('data', {}) 57 | kwargs['headers'] = kwargs.get('headers', {}) 58 | 59 | if kwargs['data'] and method == 'get': 60 | kwargs['params'] = kwargs['data'] 61 | del(kwargs['data']) 62 | 63 | if method == 'post': 64 | kwargs['headers']['content-type'] = 'text/plain' 65 | 66 | return kwargs 67 | 68 | 69 | class HttpRpcClient(BaseHttpRpcClient): 70 | 71 | def _request(self, path, **kwargs): 72 | 73 | rpc_request = self._get_rpc_request(path, **kwargs) 74 | 75 | response = self.session.post(self._endpoint_url, data=rpc_request.encode(), headers=self._get_headers()) 76 | 77 | return self._handle_response(response) 78 | 79 | def _request_session(self, path, params=None): 80 | 81 | kwargs = { 82 | 'params': params, 83 | 'headers': self._get_headers() 84 | } 85 | 86 | response = self.session.get(f"{self._endpoint_url}/{path}", **kwargs) 87 | 88 | return self._handle_session_response(response) 89 | 90 | @staticmethod 91 | def _handle_response(response): 92 | """Internal helper for handling API responses from the server. 93 | Raises the appropriate exceptions when necessary; otherwise, returns the 94 | response. 95 | """ 96 | 97 | try: 98 | res = response.json() 99 | 100 | if 'error' in res and res['error']: 101 | raise BinanceChainRPCException(response) 102 | 103 | # by default return full response 104 | # if it's a normal response we have a data attribute, return that 105 | if 'result' in res: 106 | res = res['result'] 107 | return res 108 | except ValueError: 109 | raise BinanceChainRequestException('Invalid Response: %s' % response.text) 110 | 111 | @staticmethod 112 | def _handle_session_response(response): 113 | """Internal helper for handling API responses from the server. 114 | Raises the appropriate exceptions when necessary; otherwise, returns the 115 | response. 116 | """ 117 | 118 | if not str(response.status_code).startswith('2'): 119 | raise BinanceChainRPCException(response) 120 | try: 121 | res = response.json() 122 | 123 | if 'code' in res and res['code'] != "200000": 124 | raise BinanceChainRPCException(response) 125 | 126 | if 'success' in res and not res['success']: 127 | raise BinanceChainRPCException(response) 128 | 129 | # by default return full response 130 | # if it's a normal response we have a data attribute, return that 131 | if 'result' in res: 132 | res = res['result'] 133 | return res 134 | except ValueError: 135 | raise BinanceChainRequestException('Invalid Response: %s' % response.text) 136 | 137 | def get_path_list(self): 138 | """Return HTML formatted list of available endpoints 139 | 140 | https://binance-chain.github.io/api-reference/node-rpc.html#get-the-list 141 | 142 | """ 143 | res = self._request(self._endpoint_url, method="get") 144 | return res.content 145 | 146 | def get_abci_info(self): 147 | """Get some info about the application. 148 | 149 | https://binance-chain.github.io/api-reference/node-rpc.html#abciinfo 150 | 151 | """ 152 | return self._request('abci_info') 153 | 154 | def get_consensus_state(self): 155 | """ConsensusState returns a concise summary of the consensus state. UNSTABLE 156 | 157 | https://binance-chain.github.io/api-reference/node-rpc.html#consensusstate 158 | 159 | """ 160 | return self._request('consensus_state') 161 | 162 | def dump_consensus_state(self): 163 | """DumpConsensusState dumps consensus state. UNSTABLE 164 | 165 | https://binance-chain.github.io/api-reference/node-rpc.html#dumpconsensusstate 166 | 167 | """ 168 | return self._request('dump_consensus_state') 169 | 170 | def get_genesis(self): 171 | """Get genesis file. 172 | 173 | https://binance-chain.github.io/api-reference/node-rpc.html#genesis 174 | 175 | """ 176 | return self._request('genesis') 177 | 178 | def get_net_info(self): 179 | """Get network info. 180 | 181 | https://binance-chain.github.io/api-reference/node-rpc.html#netinfo 182 | 183 | """ 184 | return self._request('net_info') 185 | 186 | def get_num_unconfirmed_txs(self): 187 | """Get number of unconfirmed transactions. 188 | 189 | https://binance-chain.github.io/api-reference/node-rpc.html#numunconfirmedtxs 190 | 191 | """ 192 | return self._request('num_unconfirmed_txs') 193 | 194 | def get_status(self): 195 | """Get Tendermint status including node info, pubkey, latest block hash, app hash, block height and time. 196 | 197 | https://binance-chain.github.io/api-reference/node-rpc.html#status 198 | 199 | """ 200 | return self._request('status') 201 | 202 | def get_health(self): 203 | """Get node health. Returns empty result (200 OK) on success, no response - in case of an error. 204 | 205 | https://binance-chain.github.io/api-reference/node-rpc.html#health 206 | 207 | """ 208 | return self._request('health') 209 | 210 | def get_unconfirmed_txs(self): 211 | """Get unconfirmed transactions (maximum ?limit entries) including their number. 212 | 213 | https://binance-chain.github.io/api-reference/node-rpc.html#unconfirmedtxs 214 | 215 | """ 216 | return self._request('unconfirmed_txs') 217 | 218 | def get_validators(self): 219 | """Get the validator set at the given block height. If no height is provided, it will fetch the 220 | current validator set. 221 | 222 | https://binance-chain.github.io/api-reference/node-rpc.html#validators 223 | 224 | """ 225 | return self._request('validators') 226 | 227 | def abci_query(self, data: str, path: Optional[str] = None, 228 | prove: Optional[bool] = None, height: Optional[int] = None): 229 | """Query the application for some information. 230 | 231 | https://binance-chain.github.io/api-reference/node-rpc.html#abciquery 232 | 233 | path string Path to the data ("/a/b/c") 234 | data []byte Data 235 | height int64 Height (0 means latest) 236 | prove bool Includes proof if true 237 | 238 | """ 239 | 240 | data = { 241 | 'data': data 242 | } 243 | if path: 244 | data['path'] = path 245 | if prove: 246 | data['prove'] = str(prove) 247 | if height: 248 | data['height'] = str(height) 249 | 250 | return self._request('abci_query', data=data) 251 | 252 | def get_block(self, height: Optional[int] = None): 253 | """Get block at a given height. If no height is provided, it will fetch the latest block. 254 | 255 | https://binance-chain.github.io/api-reference/node-rpc.html#block 256 | 257 | height int64 258 | 259 | """ 260 | 261 | data = { 262 | 'height': str(height) if height else None 263 | } 264 | 265 | return self._request('block', data=data) 266 | 267 | def get_block_result(self, height: int): 268 | """BlockResults gets ABCIResults at a given height. If no height is provided, it will fetch results for the 269 | latest block. 270 | 271 | https://binance-chain.github.io/api-reference/node-rpc.html#blockresults 272 | 273 | height int64 274 | 275 | """ 276 | 277 | data = { 278 | 'height': str(height) 279 | } 280 | 281 | return self._request('block_result', data=data) 282 | 283 | def get_block_commit(self, height: int): 284 | """Get block commit at a given height. If no height is provided, it will fetch the commit for the latest block. 285 | 286 | https://binance-chain.github.io/api-reference/node-rpc.html#commit 287 | 288 | height int64 0 289 | 290 | """ 291 | 292 | data = { 293 | 'height': str(height) 294 | } 295 | 296 | return self._request('commit', data=data) 297 | 298 | def get_blockchain_info(self, min_height: int, max_height: int): 299 | """Get block headers for minHeight <= height <= maxHeight. Block headers are returned in descending order 300 | (highest first). Returns at most 20 items. 301 | 302 | https://binance-chain.github.io/api-reference/node-rpc.html#blockchaininfo 303 | 304 | min_height int64 0 305 | max_height int64 0 306 | 307 | """ 308 | 309 | assert max_height > min_height 310 | 311 | data = { 312 | 'minHeight': str(min_height), 313 | 'maxHeight': str(max_height) 314 | } 315 | 316 | return self._request('blockchain', data=data) 317 | 318 | def broadcast_msg(self, msg: Msg, request_type: RpcBroadcastRequestType = RpcBroadcastRequestType.SYNC): 319 | """Wrapper function fro broadcasting transactions 320 | 321 | https://binance-chain.github.io/api-reference/node-rpc.html#broadcasttxasync 322 | 323 | RpcBroadcastRequestType 324 | SYNC - Returns with the response from CheckTx. 325 | ASYNC - Returns right away, with no response 326 | COMMIT - only returns error if mempool.CheckTx() errs or if we timeout waiting for tx to commit. 327 | 328 | :param msg: message object to send 329 | :param request_type: type of request to make 330 | :return: 331 | """ 332 | 333 | msg.wallet.initialise_wallet() 334 | data = msg.to_hex_data().decode() 335 | 336 | tx_data = { 337 | 'tx': '0x' + data 338 | } 339 | 340 | if request_type == RpcBroadcastRequestType.ASYNC: 341 | res = self._broadcast_tx_async(tx_data) 342 | elif request_type == RpcBroadcastRequestType.COMMIT: 343 | res = self._broadcast_tx_commit(tx_data) 344 | else: 345 | res = self._broadcast_tx_sync(tx_data) 346 | 347 | msg.wallet.increment_account_sequence() 348 | return res 349 | 350 | def _broadcast_tx_async(self, tx_data: Dict): 351 | """Returns right away, with no response 352 | 353 | https://binance-chain.github.io/api-reference/node-rpc.html#broadcasttxasync 354 | 355 | """ 356 | return self._request_session("broadcast_tx_async", params=tx_data) 357 | 358 | def _broadcast_tx_commit(self, tx_data: Dict): 359 | """CONTRACT: only returns error if mempool.CheckTx() errs or if we timeout waiting for tx to commit. 360 | 361 | https://binance-chain.github.io/api-reference/node-rpc.html#broadcasttxcommit 362 | 363 | """ 364 | return self._request_session("broadcast_tx_commit", params=tx_data) 365 | 366 | def _broadcast_tx_sync(self, tx_data: Dict): 367 | """Returns with the response from CheckTx. 368 | 369 | https://binance-chain.github.io/api-reference/node-rpc.html#broadcasttxsync 370 | 371 | """ 372 | return self._request_session("broadcast_tx_sync", params=tx_data) 373 | 374 | def get_consensus_params(self, height: Optional[int] = None): 375 | """Get the consensus parameters at the given block height. If no height is provided, it will fetch the 376 | current consensus params. 377 | 378 | https://binance-chain.github.io/api-reference/node-rpc.html#consensusparams 379 | 380 | height: int 381 | 382 | """ 383 | data = { 384 | 'height': str(height) if height else None 385 | } 386 | 387 | return self._request('consensus_params', data=data) 388 | 389 | def get_tx(self, tx_hash: str, prove: Optional[bool] = None): 390 | """Tx allows you to query the transaction results. nil could mean the transaction is in the mempool, 391 | invalidated, or was not sent in the first place. 392 | 393 | https://binance-chain.github.io/api-reference/node-rpc.html#tx 394 | 395 | tx_hash string "" true Query 396 | prove bool false false Include proofs of the transactions inclusion in the block 397 | 398 | """ 399 | 400 | data = { 401 | 'hash': tx_hash 402 | } 403 | if prove: 404 | data['prove'] = str(prove) 405 | 406 | return self._request('tx', data=data) 407 | 408 | def tx_search(self, query: str, prove: Optional[bool] = None, 409 | page: Optional[int] = None, limit: Optional[int] = None): 410 | """TxSearch allows you to query for multiple transactions results. It returns a list of transactions 411 | (maximum ?per_page entries) and the total count. 412 | 413 | https://binance-chain.github.io/api-reference/node-rpc.html#txsearch 414 | 415 | query string "" true Query 416 | prove bool false false Include proofs of the transactions inclusion in the block 417 | page int 1 false Page number (1-based) 418 | per_page int 30 false Number of entries per page (max: 100) 419 | 420 | """ 421 | 422 | data = { 423 | 'query': query 424 | } 425 | if prove: 426 | data['prove'] = str(prove) 427 | if page: 428 | data['page'] = str(page) 429 | if limit: 430 | data['limit'] = str(limit) 431 | 432 | return self._request('tx_search', data=data) 433 | 434 | 435 | class AsyncHttpRpcClient(BaseHttpRpcClient): 436 | 437 | DEFAULT_TIMEOUT = 10 438 | 439 | @classmethod 440 | async def create(cls, endpoint_url): 441 | 442 | return AsyncHttpRpcClient(endpoint_url) 443 | 444 | def _init_session(self, **kwargs): 445 | 446 | loop = kwargs.get('loop', asyncio.get_event_loop()) 447 | session = aiohttp.ClientSession( 448 | loop=loop, 449 | headers=self._get_headers(), 450 | json_serialize=ujson.dumps 451 | ) 452 | return session 453 | 454 | async def _request(self, path, **kwargs): 455 | 456 | rpc_request = self._get_rpc_request(path, **kwargs) 457 | 458 | response = await self.session.post(self._endpoint_url, data=rpc_request.encode(), headers=self._get_headers()) 459 | return await self._handle_response(response) 460 | 461 | async def _request_session(self, path, params=None): 462 | 463 | kwargs = { 464 | 'params': params, 465 | 'headers': self._get_headers() 466 | } 467 | 468 | response = await self.session.get(f"{self._endpoint_url}/{path}", **kwargs) 469 | 470 | return await self._handle_session_response(response) 471 | 472 | async def _handle_response(self, response): 473 | """Internal helper for handling API responses from the Binance server. 474 | Raises the appropriate exceptions when necessary; otherwise, returns the 475 | response. 476 | """ 477 | 478 | try: 479 | res = await response.json() 480 | 481 | if 'error' in res and res['error']: 482 | raise BinanceChainRPCException(response) 483 | 484 | # by default return full response 485 | # if it's a normal response we have a data attribute, return that 486 | if 'result' in res: 487 | res = res['result'] 488 | return res 489 | except ValueError: 490 | raise BinanceChainRequestException('Invalid Response: %s' % response.text) 491 | 492 | async def _handle_session_response(self, response): 493 | """Internal helper for handling API responses from the Binance server. 494 | Raises the appropriate exceptions when necessary; otherwise, returns the 495 | response. 496 | """ 497 | if not str(response.status).startswith('2'): 498 | raise BinanceChainRPCException(response) 499 | try: 500 | res = await response.json() 501 | 502 | if 'code' in res and res['code'] != "200000": 503 | raise BinanceChainRPCException(response) 504 | 505 | if 'success' in res and not res['success']: 506 | raise BinanceChainRPCException(response) 507 | 508 | # by default return full response 509 | # if it's a normal response we have a data attribute, return that 510 | if 'result' in res: 511 | res = res['result'] 512 | return res 513 | except ValueError: 514 | raise BinanceChainRequestException('Invalid Response: %s' % await response.text()) 515 | 516 | async def get_path_list(self): 517 | res = await self.client.session.get(self._endpoint_url) 518 | return await res.text() 519 | get_path_list.__doc__ = HttpRpcClient.get_path_list.__doc__ 520 | 521 | async def get_abci_info(self): 522 | return await self._request('abci_info') 523 | get_abci_info.__doc__ = HttpRpcClient.get_abci_info.__doc__ 524 | 525 | async def get_consensus_state(self): 526 | return await self._request('consensus_state') 527 | get_consensus_state.__doc__ = HttpRpcClient.get_consensus_state.__doc__ 528 | 529 | async def dump_consensus_state(self): 530 | return await self._request('dump_consensus_state') 531 | dump_consensus_state.__doc__ = HttpRpcClient.dump_consensus_state.__doc__ 532 | 533 | async def get_genesis(self): 534 | return await self._request('genesis') 535 | get_genesis.__doc__ = HttpRpcClient.get_genesis.__doc__ 536 | 537 | async def get_net_info(self): 538 | return await self._request('net_info') 539 | get_net_info.__doc__ = HttpRpcClient.get_net_info.__doc__ 540 | 541 | async def get_num_unconfirmed_txs(self): 542 | return await self._request('num_unconfirmed_txs') 543 | get_num_unconfirmed_txs.__doc__ = HttpRpcClient.get_num_unconfirmed_txs.__doc__ 544 | 545 | async def get_status(self): 546 | return await self._request('status') 547 | get_status.__doc__ = HttpRpcClient.get_status.__doc__ 548 | 549 | async def get_health(self): 550 | return await self._request('health') 551 | get_health.__doc__ = HttpRpcClient.get_health.__doc__ 552 | 553 | async def get_unconfirmed_txs(self): 554 | return await self._request('unconfirmed_txs') 555 | get_unconfirmed_txs.__doc__ = HttpRpcClient.get_unconfirmed_txs.__doc__ 556 | 557 | async def get_validators(self): 558 | return await self._request('validators') 559 | get_validators.__doc__ = HttpRpcClient.get_validators.__doc__ 560 | 561 | async def abci_query(self, data: str, path: Optional[str] = None, 562 | prove: Optional[bool] = None, height: Optional[int] = None): 563 | data = { 564 | 'data': data 565 | } 566 | if path: 567 | data['path'] = path 568 | if prove: 569 | data['prove'] = str(prove) 570 | if height: 571 | data['height'] = str(height) 572 | 573 | return await self._request('abci_query', data=data) 574 | abci_query.__doc__ = HttpRpcClient.abci_query.__doc__ 575 | 576 | async def get_block(self, height: Optional[int] = None): 577 | data = { 578 | 'height': str(height) if height else None 579 | } 580 | return await self._request('block', data=data) 581 | get_block.__doc__ = HttpRpcClient.get_block.__doc__ 582 | 583 | async def get_block_result(self, height: int): 584 | data = { 585 | 'height': str(height) 586 | } 587 | return await self._request('block_result', data=data) 588 | get_block_result.__doc__ = HttpRpcClient.get_block_result.__doc__ 589 | 590 | async def get_block_commit(self, height: int): 591 | data = { 592 | 'height': str(height) 593 | } 594 | return await self._request('commit', data=data) 595 | get_block_commit.__doc__ = HttpRpcClient.get_block_commit.__doc__ 596 | 597 | async def get_blockchain_info(self, min_height: int, max_height: int): 598 | assert max_height > min_height 599 | 600 | data = { 601 | 'minHeight': str(min_height), 602 | 'maxHeight': str(max_height) 603 | } 604 | 605 | return await self._request('blockchain', data=data) 606 | get_blockchain_info.__doc__ = HttpRpcClient.get_blockchain_info.__doc__ 607 | 608 | async def broadcast_msg(self, msg: Msg, request_type: RpcBroadcastRequestType = RpcBroadcastRequestType.SYNC): 609 | 610 | msg.wallet.initialise_wallet() 611 | data = msg.to_hex_data().decode() 612 | 613 | tx_data = { 614 | 'tx': '0x' + data 615 | } 616 | 617 | if request_type == RpcBroadcastRequestType.ASYNC: 618 | tx_func = self._broadcast_tx_async 619 | elif request_type == RpcBroadcastRequestType.COMMIT: 620 | tx_func = self._broadcast_tx_commit 621 | else: 622 | tx_func = self._broadcast_tx_sync 623 | res = await tx_func(tx_data) 624 | 625 | msg.wallet.increment_account_sequence() 626 | return res 627 | broadcast_msg.__doc__ = HttpRpcClient.broadcast_msg.__doc__ 628 | 629 | async def _broadcast_tx_async(self, tx_data: Dict): 630 | """Returns right away, with no response 631 | 632 | https://binance-chain.github.io/api-reference/node-rpc.html#broadcasttxasync 633 | 634 | """ 635 | return await self._request_session('broadcast_tx_async', params=tx_data) 636 | _broadcast_tx_async.__doc__ = HttpRpcClient._broadcast_tx_async.__doc__ 637 | 638 | async def _broadcast_tx_commit(self, tx_data: Dict): 639 | return await self._request_session('broadcast_tx_commit', params=tx_data) 640 | _broadcast_tx_commit.__doc__ = HttpRpcClient._broadcast_tx_commit.__doc__ 641 | 642 | async def _broadcast_tx_sync(self, tx_data: Dict): 643 | return await self._request_session('broadcast_tx_sync', params=tx_data) 644 | _broadcast_tx_sync.__doc__ = HttpRpcClient._broadcast_tx_sync.__doc__ 645 | 646 | async def get_consensus_params(self, height: Optional[int] = None): 647 | data = { 648 | 'height': str(height) if height else None 649 | } 650 | 651 | return await self._request('consensus_params', data=data) 652 | get_consensus_params.__doc__ = HttpRpcClient.get_consensus_params.__doc__ 653 | 654 | async def get_tx(self, tx_hash: str, prove: Optional[bool] = None): 655 | data = { 656 | 'hash': tx_hash 657 | } 658 | if prove: 659 | data['prove'] = str(prove) 660 | 661 | return await self._request('tx', data=data) 662 | get_tx.__doc__ = HttpRpcClient.get_tx.__doc__ 663 | 664 | async def tx_search(self, query: str, prove: Optional[bool] = None, 665 | page: Optional[int] = None, limit: Optional[int] = None): 666 | data = { 667 | 'query': query 668 | } 669 | if prove: 670 | data['prove'] = str(prove) 671 | if page: 672 | data['page'] = str(page) 673 | if limit: 674 | data['limit'] = str(limit) 675 | 676 | return await self._request('tx_search', data=data) 677 | tx_search.__doc__ = HttpRpcClient.tx_search.__doc__ 678 | -------------------------------------------------------------------------------- /binance_chain/node_rpc/pooled_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Optional 4 | from random import shuffle 5 | 6 | 7 | from binance_chain.http import AsyncHttpApiClient 8 | from binance_chain.environment import BinanceEnvironment 9 | from binance_chain.node_rpc.http import AsyncHttpRpcClient 10 | from binance_chain.constants import RpcBroadcastRequestType 11 | from binance_chain.messages import Msg 12 | 13 | 14 | class PooledRpcClient: 15 | """RPC Node client pooling connections across available peer nodes. 16 | 17 | Each request uses a new node to increase API request limits 18 | 19 | """ 20 | 21 | def __init__(self, env: Optional[BinanceEnvironment] = None): 22 | 23 | self._env = env 24 | self._clients = [AsyncHttpRpcClient] 25 | self._loop = None 26 | self._client_idx = 0 27 | 28 | @classmethod 29 | async def create(cls, loop=None, env: Optional[BinanceEnvironment] = None) -> 'PooledRpcClient': 30 | 31 | self = PooledRpcClient(env=env) 32 | self._loop = loop or asyncio.get_event_loop() 33 | 34 | await self.initialise_clients() 35 | 36 | return self 37 | 38 | async def initialise_clients(self) -> None: 39 | """Initialise the client connections used 40 | 41 | :return: 42 | """ 43 | client = await AsyncHttpApiClient.create(loop=self._loop, env=self._env) 44 | peers = await client.get_node_peers() 45 | shuffle(peers) 46 | 47 | self._clients = [] 48 | for peer in peers: 49 | logging.info(f"Creating client {peer['listen_addr']}") 50 | self._clients.append(await AsyncHttpRpcClient.create(endpoint_url=peer['listen_addr'])) 51 | logging.debug(f"Connected to {self.num_peers} peers") 52 | 53 | @property 54 | def num_peers(self): 55 | return len(self._clients) 56 | 57 | async def _request(self, func_name, params=None): 58 | params = params or {} 59 | if params.get('self'): 60 | del params['self'] 61 | client = self._get_client() 62 | return await getattr(client, func_name)(**params) 63 | 64 | def _get_client(self): 65 | logging.debug(f"using client {self._client_idx}") 66 | client = self._clients[self._client_idx] 67 | self._client_idx = (self._client_idx + 1) % len(self._clients) 68 | return client 69 | 70 | async def get_path_list(self): 71 | return await self._request('get_path_list') 72 | get_path_list.__doc__ = AsyncHttpRpcClient.get_path_list.__doc__ 73 | 74 | async def get_abci_info(self): 75 | return await self._request('get_abci_info') 76 | get_abci_info.__doc__ = AsyncHttpRpcClient.get_abci_info.__doc__ 77 | 78 | async def get_consensus_state(self): 79 | return await self._request('get_consensus_state') 80 | get_consensus_state.__doc__ = AsyncHttpRpcClient.get_consensus_state.__doc__ 81 | 82 | async def dump_consensus_state(self): 83 | return await self._request('dump_consensus_state') 84 | dump_consensus_state.__doc__ = AsyncHttpRpcClient.dump_consensus_state.__doc__ 85 | 86 | async def get_genesis(self): 87 | return await self._request('get_genesis') 88 | get_genesis.__doc__ = AsyncHttpRpcClient.get_genesis.__doc__ 89 | 90 | async def get_net_info(self): 91 | return await self._request('get_net_info') 92 | get_net_info.__doc__ = AsyncHttpRpcClient.get_net_info.__doc__ 93 | 94 | async def get_num_unconfirmed_txs(self): 95 | return await self._request('get_num_unconfirmed_txs') 96 | get_num_unconfirmed_txs.__doc__ = AsyncHttpRpcClient.get_num_unconfirmed_txs.__doc__ 97 | 98 | async def get_status(self): 99 | return await self._request('get_status') 100 | get_status.__doc__ = AsyncHttpRpcClient.get_status.__doc__ 101 | 102 | async def get_health(self): 103 | return await self._request('get_health') 104 | get_health.__doc__ = AsyncHttpRpcClient.get_health.__doc__ 105 | 106 | async def get_unconfirmed_txs(self): 107 | return await self._request('get_unconfirmed_txs') 108 | get_unconfirmed_txs.__doc__ = AsyncHttpRpcClient.get_unconfirmed_txs.__doc__ 109 | 110 | async def get_validators(self): 111 | return await self._request('get_validators') 112 | get_validators.__doc__ = AsyncHttpRpcClient.get_validators.__doc__ 113 | 114 | async def abci_query(self, data: str, path: Optional[str] = None, 115 | prove: Optional[bool] = None, height: Optional[int] = None): 116 | return await self._request('abci_query', locals()) 117 | abci_query.__doc__ = AsyncHttpRpcClient.abci_query.__doc__ 118 | 119 | async def get_block(self, height: Optional[int] = None): 120 | return await self._request('get_block', locals()) 121 | get_block.__doc__ = AsyncHttpRpcClient.get_block.__doc__ 122 | 123 | async def get_block_result(self, height: int): 124 | return await self._request('get_block_result', locals()) 125 | get_block_result.__doc__ = AsyncHttpRpcClient.get_block_result.__doc__ 126 | 127 | async def get_block_commit(self, height: int): 128 | return await self._request('get_block_commit', locals()) 129 | get_block_commit.__doc__ = AsyncHttpRpcClient.get_block_commit.__doc__ 130 | 131 | async def get_blockchain_info(self, min_height: int, max_height: int): 132 | return await self._request('get_blockchain_info', locals()) 133 | get_blockchain_info.__doc__ = AsyncHttpRpcClient.get_blockchain_info.__doc__ 134 | 135 | async def broadcast_msg(self, msg: Msg, request_type: RpcBroadcastRequestType = RpcBroadcastRequestType.SYNC): 136 | return await self._request('broadcast_msg', locals()) 137 | broadcast_msg.__doc__ = AsyncHttpRpcClient.broadcast_msg.__doc__ 138 | 139 | async def get_consensus_params(self, height: Optional[int] = None): 140 | return await self._request('get_consensus_params', locals()) 141 | get_consensus_params.__doc__ = AsyncHttpRpcClient.get_consensus_params.__doc__ 142 | 143 | async def get_tx(self, tx_hash: str, prove: Optional[bool] = None): 144 | return await self._request('get_tx', locals()) 145 | get_tx.__doc__ = AsyncHttpRpcClient.get_tx.__doc__ 146 | 147 | async def tx_search(self, query: str, prove: Optional[bool] = None, 148 | page: Optional[int] = None, limit: Optional[int] = None): 149 | return await self._request('tx_search', locals()) 150 | tx_search.__doc__ = AsyncHttpRpcClient.tx_search.__doc__ 151 | -------------------------------------------------------------------------------- /binance_chain/node_rpc/request.py: -------------------------------------------------------------------------------- 1 | import ujson as json 2 | from collections import OrderedDict 3 | 4 | 5 | class RpcRequest: 6 | 7 | def __init__(self, method, id, params=None): 8 | 9 | self._method = method 10 | self._params = params 11 | self._id = id 12 | 13 | def _sort_request(self, request): 14 | sort_order = ["jsonrpc", "method", "params", "id"] 15 | return OrderedDict(sorted(request.items(), key=lambda k: sort_order.index(k[0]))) 16 | 17 | def __str__(self): 18 | 19 | request = { 20 | 'jsonrpc': '2.0', 21 | 'method': self._method, 22 | 'id': self._id 23 | } 24 | 25 | if self._params: 26 | request['params'] = self._params 27 | 28 | return json.dumps(self._sort_request(request), ensure_ascii=False) 29 | -------------------------------------------------------------------------------- /binance_chain/node_rpc/websockets.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import itertools 3 | from typing import Callable, Awaitable, Optional, Dict 4 | 5 | from binance_chain.node_rpc.http import HttpRpcClient 6 | from binance_chain.websockets import ReconnectingWebsocket, BinanceChainSocketManagerBase 7 | from binance_chain.environment import BinanceEnvironment 8 | from binance_chain.constants import RpcBroadcastRequestType 9 | from binance_chain.messages import Msg 10 | from binance_chain.node_rpc.request import RpcRequest 11 | 12 | 13 | class ReconnectingRpcWebsocket(ReconnectingWebsocket): 14 | 15 | id_generator = itertools.count(1) 16 | 17 | def _get_ws_endpoint_url(self): 18 | return f"{self._env.wss_url}/websocket" 19 | 20 | async def send_keepalive(self): 21 | await self.send_rpc_message('keepAlive') 22 | 23 | async def send_rpc_message(self, method, params=None, retry_count=0): 24 | if not self._socket: 25 | if retry_count < 5: 26 | await asyncio.sleep(1) 27 | await self.send_rpc_message(method, params, retry_count + 1) 28 | else: 29 | req = RpcRequest(method, next(self.id_generator), params) 30 | await self._socket.send(str(req)) 31 | 32 | async def ping(self): 33 | await self.send_rpc_message('ping') 34 | 35 | async def cancel(self): 36 | try: 37 | self._conn.cancel() 38 | except asyncio.CancelledError: 39 | pass 40 | 41 | 42 | class WebsocketRpcClient(BinanceChainSocketManagerBase): 43 | 44 | @classmethod 45 | async def create(cls, loop, callback: Callable[[int], Awaitable[str]], env: Optional[BinanceEnvironment] = None): 46 | """Create a BinanceChainSocketManager instance 47 | 48 | :param loop: asyncio loop 49 | :param callback: async callback function to receive messages 50 | :param env: 51 | :return: 52 | """ 53 | env = env or BinanceEnvironment.get_production_env() 54 | self = WebsocketRpcClient(env=env) 55 | self._loop = loop 56 | self._callback = callback 57 | self._conn = ReconnectingRpcWebsocket(loop, self._recv, env=env) 58 | return self 59 | 60 | async def subscribe(self, query): 61 | """Subscribe for events via WebSocket. 62 | 63 | https://binance-chain.github.io/api-reference/node-rpc.html#subscribe 64 | 65 | To tell which events you want, you need to provide a query. query is a string, which has a form: 66 | "condition AND condition ..." (no OR at the moment). condition has a form: "key operation operand". 67 | key is a string with a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). operation 68 | can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a string (escaped with single quotes), 69 | number, date or time. 70 | 71 | 72 | """ 73 | req_msg = { 74 | "query": query 75 | } 76 | await self._conn.send_rpc_message('subscribe', req_msg) 77 | 78 | async def unsubscribe(self, query): 79 | """Unsubscribe from events via WebSocket. 80 | 81 | https://binance-chain.github.io/api-reference/node-rpc.html#unsubscribe 82 | 83 | """ 84 | req_msg = { 85 | "query": query 86 | } 87 | await self._conn.send_rpc_message('unsubscribe', req_msg) 88 | 89 | async def unsubscribe_all(self): 90 | """Unsubscribe from events via WebSocket. 91 | 92 | https://binance-chain.github.io/api-reference/node-rpc.html#unsubscribeall 93 | 94 | """ 95 | await self._conn.send_rpc_message('unsubscribe_all') 96 | 97 | async def get_abci_info(self): 98 | await self._conn.send_rpc_message('abci_info') 99 | get_abci_info.__doc__ = HttpRpcClient.get_abci_info.__doc__ 100 | 101 | async def get_consensus_state(self): 102 | await self._conn.send_rpc_message('consensus_state') 103 | get_consensus_state.__doc__ = HttpRpcClient.get_consensus_state.__doc__ 104 | 105 | async def dump_consensus_state(self): 106 | await self._conn.send_rpc_message('dump_consensus_state') 107 | dump_consensus_state.__doc__ = HttpRpcClient.dump_consensus_state.__doc__ 108 | 109 | async def get_genesis(self): 110 | await self._conn.send_rpc_message('genesis') 111 | dump_consensus_state.__doc__ = HttpRpcClient.dump_consensus_state.__doc__ 112 | 113 | async def get_net_info(self): 114 | await self._conn.send_rpc_message('net_info') 115 | get_net_info.__doc__ = HttpRpcClient.get_net_info.__doc__ 116 | 117 | async def get_num_unconfirmed_txs(self): 118 | await self._conn.send_rpc_message('num_unconfirmed_txs') 119 | get_num_unconfirmed_txs.__doc__ = HttpRpcClient.get_num_unconfirmed_txs.__doc__ 120 | 121 | async def get_status(self): 122 | await self._conn.send_rpc_message('status') 123 | get_status.__doc__ = HttpRpcClient.get_status.__doc__ 124 | 125 | async def get_health(self): 126 | await self._conn.send_rpc_message('health') 127 | get_health.__doc__ = HttpRpcClient.get_health.__doc__ 128 | 129 | async def get_unconfirmed_txs(self): 130 | await self._conn.send_rpc_message('unconfirmed_txs') 131 | get_unconfirmed_txs.__doc__ = HttpRpcClient.get_unconfirmed_txs.__doc__ 132 | 133 | async def get_validators(self): 134 | await self._conn.send_rpc_message('validators') 135 | get_validators.__doc__ = HttpRpcClient.get_validators.__doc__ 136 | 137 | async def abci_query(self, data: str, path: Optional[str] = None, 138 | prove: Optional[bool] = None, height: Optional[int] = None): 139 | data = { 140 | 'data': data 141 | } 142 | if path: 143 | data['path'] = path 144 | if prove: 145 | data['prove'] = str(prove) 146 | if height: 147 | data['height'] = str(height) 148 | 149 | await self._conn.send_rpc_message('abci_query', data) 150 | abci_query.__doc__ = HttpRpcClient.abci_query.__doc__ 151 | 152 | async def get_block(self, height: Optional[int] = None): 153 | data = { 154 | 'height': str(height) if height else None 155 | } 156 | await self._conn.send_rpc_message('block', data) 157 | get_block.__doc__ = HttpRpcClient.get_block.__doc__ 158 | 159 | async def get_block_result(self, height: int): 160 | data = { 161 | 'height': str(height) 162 | } 163 | await self._conn.send_rpc_message('block_result', data) 164 | get_block_result.__doc__ = HttpRpcClient.get_block_result.__doc__ 165 | 166 | async def get_block_commit(self, height: int): 167 | data = { 168 | 'height': str(height) 169 | } 170 | await self._conn.send_rpc_message('commit', data) 171 | get_block_commit.__doc__ = HttpRpcClient.get_block_commit.__doc__ 172 | 173 | async def get_blockchain_info(self, min_height: int, max_height: int): 174 | assert max_height > min_height 175 | 176 | data = { 177 | 'minHeight': str(min_height), 178 | 'maxHeight': str(max_height) 179 | } 180 | await self._conn.send_rpc_message('blockchain', data) 181 | get_blockchain_info.__doc__ = HttpRpcClient.get_blockchain_info.__doc__ 182 | 183 | async def broadcast_msg(self, msg: Msg, request_type: RpcBroadcastRequestType = RpcBroadcastRequestType.SYNC): 184 | 185 | msg.wallet.initialise_wallet() 186 | data = msg.to_hex_data().decode() 187 | 188 | tx_data = { 189 | 'tx': '0x' + data 190 | } 191 | 192 | if request_type == RpcBroadcastRequestType.ASYNC: 193 | tx_func = self._broadcast_tx_async 194 | elif request_type == RpcBroadcastRequestType.COMMIT: 195 | tx_func = self._broadcast_tx_commit 196 | else: 197 | tx_func = self._broadcast_tx_sync 198 | res = await tx_func(tx_data) 199 | 200 | msg.wallet.increment_account_sequence() 201 | return res 202 | broadcast_msg.__doc__ = HttpRpcClient.broadcast_msg.__doc__ 203 | 204 | async def _broadcast_tx_async(self, tx_data: Dict): 205 | await self._conn.send_rpc_message('broadcast_tx_async', tx_data) 206 | _broadcast_tx_async.__doc__ = HttpRpcClient._broadcast_tx_async.__doc__ 207 | 208 | async def _broadcast_tx_commit(self, tx_data: Dict): 209 | await self._conn.send_rpc_message('broadcast_tx_commit', tx_data) 210 | _broadcast_tx_commit.__doc__ = HttpRpcClient._broadcast_tx_commit.__doc__ 211 | 212 | async def _broadcast_tx_sync(self, tx_data: Dict): 213 | await self._conn.send_rpc_message('broadcast_tx_sync', tx_data) 214 | _broadcast_tx_sync.__doc__ = HttpRpcClient._broadcast_tx_sync.__doc__ 215 | 216 | async def get_consensus_params(self, height: Optional[int] = None): 217 | data = { 218 | 'height': str(height) if height else None 219 | } 220 | await self._conn.send_rpc_message('consensus_params', data) 221 | get_consensus_params.__doc__ = HttpRpcClient.get_consensus_params.__doc__ 222 | 223 | async def get_tx(self, tx_hash: str, prove: Optional[bool] = None): 224 | data = { 225 | 'hash': tx_hash 226 | } 227 | if prove: 228 | data['prove'] = str(prove) 229 | 230 | await self._conn.send_rpc_message('tx', data) 231 | get_tx.__doc__ = HttpRpcClient.get_tx.__doc__ 232 | 233 | async def tx_search(self, query: str, prove: Optional[bool] = None, 234 | page: Optional[int] = None, limit: Optional[int] = None): 235 | data = { 236 | 'query': query 237 | } 238 | if prove: 239 | data['prove'] = str(prove) 240 | if page: 241 | data['page'] = str(page) 242 | if limit: 243 | data['limit'] = str(limit) 244 | 245 | await self._conn.send_rpc_message('tx_search', data) 246 | tx_search.__doc__ = HttpRpcClient.tx_search.__doc__ 247 | -------------------------------------------------------------------------------- /binance_chain/protobuf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/binance_chain/protobuf/__init__.py -------------------------------------------------------------------------------- /binance_chain/protobuf/dex_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: dex.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='dex.proto', 19 | package='transaction', 20 | syntax='proto3', 21 | serialized_options=_b('\n\031com.binance.dex.api.protoB\013TransactionP\001'), 22 | serialized_pb=_b('\n\tdex.proto\x12\x0btransaction\"U\n\x05StdTx\x12\x0c\n\x04msgs\x18\x01 \x03(\x0c\x12\x12\n\nsignatures\x18\x02 \x03(\x0c\x12\x0c\n\x04memo\x18\x03 \x01(\t\x12\x0e\n\x06source\x18\x04 \x01(\x03\x12\x0c\n\x04\x64\x61ta\x18\x05 \x01(\x0c\"f\n\x0cStdSignature\x12\x0f\n\x07pub_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x16\n\x0e\x61\x63\x63ount_number\x18\x03 \x01(\x03\x12\x10\n\x08sequence\x18\x04 \x01(\x03\x1a\x08\n\x06PubKey\"\x8d\x01\n\x08NewOrder\x12\x0e\n\x06sender\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\x12\x0e\n\x06symbol\x18\x03 \x01(\t\x12\x11\n\tordertype\x18\x04 \x01(\x03\x12\x0c\n\x04side\x18\x05 \x01(\x03\x12\r\n\x05price\x18\x06 \x01(\x03\x12\x10\n\x08quantity\x18\x07 \x01(\x03\x12\x13\n\x0btimeinforce\x18\x08 \x01(\x03\"<\n\x0b\x43\x61ncelOrder\x12\x0e\n\x06sender\x18\x01 \x01(\x0c\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\r\n\x05refid\x18\x03 \x01(\t\";\n\x0bTokenFreeze\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x0c\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x03\"=\n\rTokenUnfreeze\x12\x0c\n\x04\x66rom\x18\x01 \x01(\x0c\x12\x0e\n\x06symbol\x18\x02 \x01(\t\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x03\"&\n\x05Token\x12\r\n\x05\x64\x65nom\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x03\";\n\x05Input\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12!\n\x05\x63oins\x18\x02 \x03(\x0b\x32\x12.transaction.Token\"<\n\x06Output\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12!\n\x05\x63oins\x18\x02 \x03(\x0b\x32\x12.transaction.Token\"P\n\x04Send\x12\"\n\x06inputs\x18\x01 \x03(\x0b\x32\x12.transaction.Input\x12$\n\x07outputs\x18\x02 \x03(\x0b\x32\x13.transaction.Output\":\n\x04Vote\x12\x13\n\x0bproposal_id\x18\x01 \x01(\x03\x12\r\n\x05voter\x18\x02 \x01(\x0c\x12\x0e\n\x06option\x18\x03 \x01(\x03\x42*\n\x19\x63om.binance.dex.api.protoB\x0bTransactionP\x01\x62\x06proto3') 23 | ) 24 | 25 | 26 | 27 | 28 | _STDTX = _descriptor.Descriptor( 29 | name='StdTx', 30 | full_name='transaction.StdTx', 31 | filename=None, 32 | file=DESCRIPTOR, 33 | containing_type=None, 34 | fields=[ 35 | _descriptor.FieldDescriptor( 36 | name='msgs', full_name='transaction.StdTx.msgs', index=0, 37 | number=1, type=12, cpp_type=9, label=3, 38 | has_default_value=False, default_value=[], 39 | message_type=None, enum_type=None, containing_type=None, 40 | is_extension=False, extension_scope=None, 41 | serialized_options=None, file=DESCRIPTOR), 42 | _descriptor.FieldDescriptor( 43 | name='signatures', full_name='transaction.StdTx.signatures', index=1, 44 | number=2, type=12, cpp_type=9, label=3, 45 | has_default_value=False, default_value=[], 46 | message_type=None, enum_type=None, containing_type=None, 47 | is_extension=False, extension_scope=None, 48 | serialized_options=None, file=DESCRIPTOR), 49 | _descriptor.FieldDescriptor( 50 | name='memo', full_name='transaction.StdTx.memo', index=2, 51 | number=3, type=9, cpp_type=9, label=1, 52 | has_default_value=False, default_value=_b("").decode('utf-8'), 53 | message_type=None, enum_type=None, containing_type=None, 54 | is_extension=False, extension_scope=None, 55 | serialized_options=None, file=DESCRIPTOR), 56 | _descriptor.FieldDescriptor( 57 | name='source', full_name='transaction.StdTx.source', index=3, 58 | number=4, type=3, cpp_type=2, label=1, 59 | has_default_value=False, default_value=0, 60 | message_type=None, enum_type=None, containing_type=None, 61 | is_extension=False, extension_scope=None, 62 | serialized_options=None, file=DESCRIPTOR), 63 | _descriptor.FieldDescriptor( 64 | name='data', full_name='transaction.StdTx.data', index=4, 65 | number=5, type=12, cpp_type=9, label=1, 66 | has_default_value=False, default_value=_b(""), 67 | message_type=None, enum_type=None, containing_type=None, 68 | is_extension=False, extension_scope=None, 69 | serialized_options=None, file=DESCRIPTOR), 70 | ], 71 | extensions=[ 72 | ], 73 | nested_types=[], 74 | enum_types=[ 75 | ], 76 | serialized_options=None, 77 | is_extendable=False, 78 | syntax='proto3', 79 | extension_ranges=[], 80 | oneofs=[ 81 | ], 82 | serialized_start=26, 83 | serialized_end=111, 84 | ) 85 | 86 | 87 | _STDSIGNATURE_PUBKEY = _descriptor.Descriptor( 88 | name='PubKey', 89 | full_name='transaction.StdSignature.PubKey', 90 | filename=None, 91 | file=DESCRIPTOR, 92 | containing_type=None, 93 | fields=[ 94 | ], 95 | extensions=[ 96 | ], 97 | nested_types=[], 98 | enum_types=[ 99 | ], 100 | serialized_options=None, 101 | is_extendable=False, 102 | syntax='proto3', 103 | extension_ranges=[], 104 | oneofs=[ 105 | ], 106 | serialized_start=207, 107 | serialized_end=215, 108 | ) 109 | 110 | _STDSIGNATURE = _descriptor.Descriptor( 111 | name='StdSignature', 112 | full_name='transaction.StdSignature', 113 | filename=None, 114 | file=DESCRIPTOR, 115 | containing_type=None, 116 | fields=[ 117 | _descriptor.FieldDescriptor( 118 | name='pub_key', full_name='transaction.StdSignature.pub_key', index=0, 119 | number=1, type=12, cpp_type=9, label=1, 120 | has_default_value=False, default_value=_b(""), 121 | message_type=None, enum_type=None, containing_type=None, 122 | is_extension=False, extension_scope=None, 123 | serialized_options=None, file=DESCRIPTOR), 124 | _descriptor.FieldDescriptor( 125 | name='signature', full_name='transaction.StdSignature.signature', index=1, 126 | number=2, type=12, cpp_type=9, label=1, 127 | has_default_value=False, default_value=_b(""), 128 | message_type=None, enum_type=None, containing_type=None, 129 | is_extension=False, extension_scope=None, 130 | serialized_options=None, file=DESCRIPTOR), 131 | _descriptor.FieldDescriptor( 132 | name='account_number', full_name='transaction.StdSignature.account_number', index=2, 133 | number=3, type=3, cpp_type=2, label=1, 134 | has_default_value=False, default_value=0, 135 | message_type=None, enum_type=None, containing_type=None, 136 | is_extension=False, extension_scope=None, 137 | serialized_options=None, file=DESCRIPTOR), 138 | _descriptor.FieldDescriptor( 139 | name='sequence', full_name='transaction.StdSignature.sequence', index=3, 140 | number=4, type=3, cpp_type=2, label=1, 141 | has_default_value=False, default_value=0, 142 | message_type=None, enum_type=None, containing_type=None, 143 | is_extension=False, extension_scope=None, 144 | serialized_options=None, file=DESCRIPTOR), 145 | ], 146 | extensions=[ 147 | ], 148 | nested_types=[_STDSIGNATURE_PUBKEY, ], 149 | enum_types=[ 150 | ], 151 | serialized_options=None, 152 | is_extendable=False, 153 | syntax='proto3', 154 | extension_ranges=[], 155 | oneofs=[ 156 | ], 157 | serialized_start=113, 158 | serialized_end=215, 159 | ) 160 | 161 | 162 | _NEWORDER = _descriptor.Descriptor( 163 | name='NewOrder', 164 | full_name='transaction.NewOrder', 165 | filename=None, 166 | file=DESCRIPTOR, 167 | containing_type=None, 168 | fields=[ 169 | _descriptor.FieldDescriptor( 170 | name='sender', full_name='transaction.NewOrder.sender', index=0, 171 | number=1, type=12, cpp_type=9, label=1, 172 | has_default_value=False, default_value=_b(""), 173 | message_type=None, enum_type=None, containing_type=None, 174 | is_extension=False, extension_scope=None, 175 | serialized_options=None, file=DESCRIPTOR), 176 | _descriptor.FieldDescriptor( 177 | name='id', full_name='transaction.NewOrder.id', index=1, 178 | number=2, type=9, cpp_type=9, label=1, 179 | has_default_value=False, default_value=_b("").decode('utf-8'), 180 | message_type=None, enum_type=None, containing_type=None, 181 | is_extension=False, extension_scope=None, 182 | serialized_options=None, file=DESCRIPTOR), 183 | _descriptor.FieldDescriptor( 184 | name='symbol', full_name='transaction.NewOrder.symbol', index=2, 185 | number=3, type=9, cpp_type=9, label=1, 186 | has_default_value=False, default_value=_b("").decode('utf-8'), 187 | message_type=None, enum_type=None, containing_type=None, 188 | is_extension=False, extension_scope=None, 189 | serialized_options=None, file=DESCRIPTOR), 190 | _descriptor.FieldDescriptor( 191 | name='ordertype', full_name='transaction.NewOrder.ordertype', index=3, 192 | number=4, type=3, cpp_type=2, label=1, 193 | has_default_value=False, default_value=0, 194 | message_type=None, enum_type=None, containing_type=None, 195 | is_extension=False, extension_scope=None, 196 | serialized_options=None, file=DESCRIPTOR), 197 | _descriptor.FieldDescriptor( 198 | name='side', full_name='transaction.NewOrder.side', index=4, 199 | number=5, type=3, cpp_type=2, label=1, 200 | has_default_value=False, default_value=0, 201 | message_type=None, enum_type=None, containing_type=None, 202 | is_extension=False, extension_scope=None, 203 | serialized_options=None, file=DESCRIPTOR), 204 | _descriptor.FieldDescriptor( 205 | name='price', full_name='transaction.NewOrder.price', index=5, 206 | number=6, type=3, cpp_type=2, label=1, 207 | has_default_value=False, default_value=0, 208 | message_type=None, enum_type=None, containing_type=None, 209 | is_extension=False, extension_scope=None, 210 | serialized_options=None, file=DESCRIPTOR), 211 | _descriptor.FieldDescriptor( 212 | name='quantity', full_name='transaction.NewOrder.quantity', index=6, 213 | number=7, type=3, cpp_type=2, label=1, 214 | has_default_value=False, default_value=0, 215 | message_type=None, enum_type=None, containing_type=None, 216 | is_extension=False, extension_scope=None, 217 | serialized_options=None, file=DESCRIPTOR), 218 | _descriptor.FieldDescriptor( 219 | name='timeinforce', full_name='transaction.NewOrder.timeinforce', index=7, 220 | number=8, type=3, cpp_type=2, label=1, 221 | has_default_value=False, default_value=0, 222 | message_type=None, enum_type=None, containing_type=None, 223 | is_extension=False, extension_scope=None, 224 | serialized_options=None, file=DESCRIPTOR), 225 | ], 226 | extensions=[ 227 | ], 228 | nested_types=[], 229 | enum_types=[ 230 | ], 231 | serialized_options=None, 232 | is_extendable=False, 233 | syntax='proto3', 234 | extension_ranges=[], 235 | oneofs=[ 236 | ], 237 | serialized_start=218, 238 | serialized_end=359, 239 | ) 240 | 241 | 242 | _CANCELORDER = _descriptor.Descriptor( 243 | name='CancelOrder', 244 | full_name='transaction.CancelOrder', 245 | filename=None, 246 | file=DESCRIPTOR, 247 | containing_type=None, 248 | fields=[ 249 | _descriptor.FieldDescriptor( 250 | name='sender', full_name='transaction.CancelOrder.sender', index=0, 251 | number=1, type=12, cpp_type=9, label=1, 252 | has_default_value=False, default_value=_b(""), 253 | message_type=None, enum_type=None, containing_type=None, 254 | is_extension=False, extension_scope=None, 255 | serialized_options=None, file=DESCRIPTOR), 256 | _descriptor.FieldDescriptor( 257 | name='symbol', full_name='transaction.CancelOrder.symbol', index=1, 258 | number=2, type=9, cpp_type=9, label=1, 259 | has_default_value=False, default_value=_b("").decode('utf-8'), 260 | message_type=None, enum_type=None, containing_type=None, 261 | is_extension=False, extension_scope=None, 262 | serialized_options=None, file=DESCRIPTOR), 263 | _descriptor.FieldDescriptor( 264 | name='refid', full_name='transaction.CancelOrder.refid', index=2, 265 | number=3, type=9, cpp_type=9, label=1, 266 | has_default_value=False, default_value=_b("").decode('utf-8'), 267 | message_type=None, enum_type=None, containing_type=None, 268 | is_extension=False, extension_scope=None, 269 | serialized_options=None, file=DESCRIPTOR), 270 | ], 271 | extensions=[ 272 | ], 273 | nested_types=[], 274 | enum_types=[ 275 | ], 276 | serialized_options=None, 277 | is_extendable=False, 278 | syntax='proto3', 279 | extension_ranges=[], 280 | oneofs=[ 281 | ], 282 | serialized_start=361, 283 | serialized_end=421, 284 | ) 285 | 286 | 287 | _TOKENFREEZE = _descriptor.Descriptor( 288 | name='TokenFreeze', 289 | full_name='transaction.TokenFreeze', 290 | filename=None, 291 | file=DESCRIPTOR, 292 | containing_type=None, 293 | fields=[ 294 | _descriptor.FieldDescriptor( 295 | name='from', full_name='transaction.TokenFreeze.from', index=0, 296 | number=1, type=12, cpp_type=9, label=1, 297 | has_default_value=False, default_value=_b(""), 298 | message_type=None, enum_type=None, containing_type=None, 299 | is_extension=False, extension_scope=None, 300 | serialized_options=None, file=DESCRIPTOR), 301 | _descriptor.FieldDescriptor( 302 | name='symbol', full_name='transaction.TokenFreeze.symbol', index=1, 303 | number=2, type=9, cpp_type=9, label=1, 304 | has_default_value=False, default_value=_b("").decode('utf-8'), 305 | message_type=None, enum_type=None, containing_type=None, 306 | is_extension=False, extension_scope=None, 307 | serialized_options=None, file=DESCRIPTOR), 308 | _descriptor.FieldDescriptor( 309 | name='amount', full_name='transaction.TokenFreeze.amount', index=2, 310 | number=3, type=3, cpp_type=2, label=1, 311 | has_default_value=False, default_value=0, 312 | message_type=None, enum_type=None, containing_type=None, 313 | is_extension=False, extension_scope=None, 314 | serialized_options=None, file=DESCRIPTOR), 315 | ], 316 | extensions=[ 317 | ], 318 | nested_types=[], 319 | enum_types=[ 320 | ], 321 | serialized_options=None, 322 | is_extendable=False, 323 | syntax='proto3', 324 | extension_ranges=[], 325 | oneofs=[ 326 | ], 327 | serialized_start=423, 328 | serialized_end=482, 329 | ) 330 | 331 | 332 | _TOKENUNFREEZE = _descriptor.Descriptor( 333 | name='TokenUnfreeze', 334 | full_name='transaction.TokenUnfreeze', 335 | filename=None, 336 | file=DESCRIPTOR, 337 | containing_type=None, 338 | fields=[ 339 | _descriptor.FieldDescriptor( 340 | name='from', full_name='transaction.TokenUnfreeze.from', index=0, 341 | number=1, type=12, cpp_type=9, label=1, 342 | has_default_value=False, default_value=_b(""), 343 | message_type=None, enum_type=None, containing_type=None, 344 | is_extension=False, extension_scope=None, 345 | serialized_options=None, file=DESCRIPTOR), 346 | _descriptor.FieldDescriptor( 347 | name='symbol', full_name='transaction.TokenUnfreeze.symbol', index=1, 348 | number=2, type=9, cpp_type=9, label=1, 349 | has_default_value=False, default_value=_b("").decode('utf-8'), 350 | message_type=None, enum_type=None, containing_type=None, 351 | is_extension=False, extension_scope=None, 352 | serialized_options=None, file=DESCRIPTOR), 353 | _descriptor.FieldDescriptor( 354 | name='amount', full_name='transaction.TokenUnfreeze.amount', index=2, 355 | number=3, type=3, cpp_type=2, label=1, 356 | has_default_value=False, default_value=0, 357 | message_type=None, enum_type=None, containing_type=None, 358 | is_extension=False, extension_scope=None, 359 | serialized_options=None, file=DESCRIPTOR), 360 | ], 361 | extensions=[ 362 | ], 363 | nested_types=[], 364 | enum_types=[ 365 | ], 366 | serialized_options=None, 367 | is_extendable=False, 368 | syntax='proto3', 369 | extension_ranges=[], 370 | oneofs=[ 371 | ], 372 | serialized_start=484, 373 | serialized_end=545, 374 | ) 375 | 376 | 377 | _TOKEN = _descriptor.Descriptor( 378 | name='Token', 379 | full_name='transaction.Token', 380 | filename=None, 381 | file=DESCRIPTOR, 382 | containing_type=None, 383 | fields=[ 384 | _descriptor.FieldDescriptor( 385 | name='denom', full_name='transaction.Token.denom', index=0, 386 | number=1, type=9, cpp_type=9, label=1, 387 | has_default_value=False, default_value=_b("").decode('utf-8'), 388 | message_type=None, enum_type=None, containing_type=None, 389 | is_extension=False, extension_scope=None, 390 | serialized_options=None, file=DESCRIPTOR), 391 | _descriptor.FieldDescriptor( 392 | name='amount', full_name='transaction.Token.amount', index=1, 393 | number=2, type=3, cpp_type=2, label=1, 394 | has_default_value=False, default_value=0, 395 | message_type=None, enum_type=None, containing_type=None, 396 | is_extension=False, extension_scope=None, 397 | serialized_options=None, file=DESCRIPTOR), 398 | ], 399 | extensions=[ 400 | ], 401 | nested_types=[], 402 | enum_types=[ 403 | ], 404 | serialized_options=None, 405 | is_extendable=False, 406 | syntax='proto3', 407 | extension_ranges=[], 408 | oneofs=[ 409 | ], 410 | serialized_start=547, 411 | serialized_end=585, 412 | ) 413 | 414 | 415 | _INPUT = _descriptor.Descriptor( 416 | name='Input', 417 | full_name='transaction.Input', 418 | filename=None, 419 | file=DESCRIPTOR, 420 | containing_type=None, 421 | fields=[ 422 | _descriptor.FieldDescriptor( 423 | name='address', full_name='transaction.Input.address', index=0, 424 | number=1, type=12, cpp_type=9, label=1, 425 | has_default_value=False, default_value=_b(""), 426 | message_type=None, enum_type=None, containing_type=None, 427 | is_extension=False, extension_scope=None, 428 | serialized_options=None, file=DESCRIPTOR), 429 | _descriptor.FieldDescriptor( 430 | name='coins', full_name='transaction.Input.coins', index=1, 431 | number=2, type=11, cpp_type=10, label=3, 432 | has_default_value=False, default_value=[], 433 | message_type=None, enum_type=None, containing_type=None, 434 | is_extension=False, extension_scope=None, 435 | serialized_options=None, file=DESCRIPTOR), 436 | ], 437 | extensions=[ 438 | ], 439 | nested_types=[], 440 | enum_types=[ 441 | ], 442 | serialized_options=None, 443 | is_extendable=False, 444 | syntax='proto3', 445 | extension_ranges=[], 446 | oneofs=[ 447 | ], 448 | serialized_start=587, 449 | serialized_end=646, 450 | ) 451 | 452 | 453 | _OUTPUT = _descriptor.Descriptor( 454 | name='Output', 455 | full_name='transaction.Output', 456 | filename=None, 457 | file=DESCRIPTOR, 458 | containing_type=None, 459 | fields=[ 460 | _descriptor.FieldDescriptor( 461 | name='address', full_name='transaction.Output.address', index=0, 462 | number=1, type=12, cpp_type=9, label=1, 463 | has_default_value=False, default_value=_b(""), 464 | message_type=None, enum_type=None, containing_type=None, 465 | is_extension=False, extension_scope=None, 466 | serialized_options=None, file=DESCRIPTOR), 467 | _descriptor.FieldDescriptor( 468 | name='coins', full_name='transaction.Output.coins', index=1, 469 | number=2, type=11, cpp_type=10, label=3, 470 | has_default_value=False, default_value=[], 471 | message_type=None, enum_type=None, containing_type=None, 472 | is_extension=False, extension_scope=None, 473 | serialized_options=None, file=DESCRIPTOR), 474 | ], 475 | extensions=[ 476 | ], 477 | nested_types=[], 478 | enum_types=[ 479 | ], 480 | serialized_options=None, 481 | is_extendable=False, 482 | syntax='proto3', 483 | extension_ranges=[], 484 | oneofs=[ 485 | ], 486 | serialized_start=648, 487 | serialized_end=708, 488 | ) 489 | 490 | 491 | _SEND = _descriptor.Descriptor( 492 | name='Send', 493 | full_name='transaction.Send', 494 | filename=None, 495 | file=DESCRIPTOR, 496 | containing_type=None, 497 | fields=[ 498 | _descriptor.FieldDescriptor( 499 | name='inputs', full_name='transaction.Send.inputs', index=0, 500 | number=1, type=11, cpp_type=10, label=3, 501 | has_default_value=False, default_value=[], 502 | message_type=None, enum_type=None, containing_type=None, 503 | is_extension=False, extension_scope=None, 504 | serialized_options=None, file=DESCRIPTOR), 505 | _descriptor.FieldDescriptor( 506 | name='outputs', full_name='transaction.Send.outputs', index=1, 507 | number=2, type=11, cpp_type=10, label=3, 508 | has_default_value=False, default_value=[], 509 | message_type=None, enum_type=None, containing_type=None, 510 | is_extension=False, extension_scope=None, 511 | serialized_options=None, file=DESCRIPTOR), 512 | ], 513 | extensions=[ 514 | ], 515 | nested_types=[], 516 | enum_types=[ 517 | ], 518 | serialized_options=None, 519 | is_extendable=False, 520 | syntax='proto3', 521 | extension_ranges=[], 522 | oneofs=[ 523 | ], 524 | serialized_start=710, 525 | serialized_end=790, 526 | ) 527 | 528 | 529 | _VOTE = _descriptor.Descriptor( 530 | name='Vote', 531 | full_name='transaction.Vote', 532 | filename=None, 533 | file=DESCRIPTOR, 534 | containing_type=None, 535 | fields=[ 536 | _descriptor.FieldDescriptor( 537 | name='proposal_id', full_name='transaction.Vote.proposal_id', index=0, 538 | number=1, type=3, cpp_type=2, label=1, 539 | has_default_value=False, default_value=0, 540 | message_type=None, enum_type=None, containing_type=None, 541 | is_extension=False, extension_scope=None, 542 | serialized_options=None, file=DESCRIPTOR), 543 | _descriptor.FieldDescriptor( 544 | name='voter', full_name='transaction.Vote.voter', index=1, 545 | number=2, type=12, cpp_type=9, label=1, 546 | has_default_value=False, default_value=_b(""), 547 | message_type=None, enum_type=None, containing_type=None, 548 | is_extension=False, extension_scope=None, 549 | serialized_options=None, file=DESCRIPTOR), 550 | _descriptor.FieldDescriptor( 551 | name='option', full_name='transaction.Vote.option', index=2, 552 | number=3, type=3, cpp_type=2, label=1, 553 | has_default_value=False, default_value=0, 554 | message_type=None, enum_type=None, containing_type=None, 555 | is_extension=False, extension_scope=None, 556 | serialized_options=None, file=DESCRIPTOR), 557 | ], 558 | extensions=[ 559 | ], 560 | nested_types=[], 561 | enum_types=[ 562 | ], 563 | serialized_options=None, 564 | is_extendable=False, 565 | syntax='proto3', 566 | extension_ranges=[], 567 | oneofs=[ 568 | ], 569 | serialized_start=792, 570 | serialized_end=850, 571 | ) 572 | 573 | _STDSIGNATURE_PUBKEY.containing_type = _STDSIGNATURE 574 | _INPUT.fields_by_name['coins'].message_type = _TOKEN 575 | _OUTPUT.fields_by_name['coins'].message_type = _TOKEN 576 | _SEND.fields_by_name['inputs'].message_type = _INPUT 577 | _SEND.fields_by_name['outputs'].message_type = _OUTPUT 578 | DESCRIPTOR.message_types_by_name['StdTx'] = _STDTX 579 | DESCRIPTOR.message_types_by_name['StdSignature'] = _STDSIGNATURE 580 | DESCRIPTOR.message_types_by_name['NewOrder'] = _NEWORDER 581 | DESCRIPTOR.message_types_by_name['CancelOrder'] = _CANCELORDER 582 | DESCRIPTOR.message_types_by_name['TokenFreeze'] = _TOKENFREEZE 583 | DESCRIPTOR.message_types_by_name['TokenUnfreeze'] = _TOKENUNFREEZE 584 | DESCRIPTOR.message_types_by_name['Token'] = _TOKEN 585 | DESCRIPTOR.message_types_by_name['Input'] = _INPUT 586 | DESCRIPTOR.message_types_by_name['Output'] = _OUTPUT 587 | DESCRIPTOR.message_types_by_name['Send'] = _SEND 588 | DESCRIPTOR.message_types_by_name['Vote'] = _VOTE 589 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 590 | 591 | StdTx = _reflection.GeneratedProtocolMessageType('StdTx', (_message.Message,), dict( 592 | DESCRIPTOR = _STDTX, 593 | __module__ = 'dex_pb2' 594 | # @@protoc_insertion_point(class_scope:transaction.StdTx) 595 | )) 596 | _sym_db.RegisterMessage(StdTx) 597 | 598 | StdSignature = _reflection.GeneratedProtocolMessageType('StdSignature', (_message.Message,), dict( 599 | 600 | PubKey = _reflection.GeneratedProtocolMessageType('PubKey', (_message.Message,), dict( 601 | DESCRIPTOR = _STDSIGNATURE_PUBKEY, 602 | __module__ = 'dex_pb2' 603 | # @@protoc_insertion_point(class_scope:transaction.StdSignature.PubKey) 604 | )) 605 | , 606 | DESCRIPTOR = _STDSIGNATURE, 607 | __module__ = 'dex_pb2' 608 | # @@protoc_insertion_point(class_scope:transaction.StdSignature) 609 | )) 610 | _sym_db.RegisterMessage(StdSignature) 611 | _sym_db.RegisterMessage(StdSignature.PubKey) 612 | 613 | NewOrder = _reflection.GeneratedProtocolMessageType('NewOrder', (_message.Message,), dict( 614 | DESCRIPTOR = _NEWORDER, 615 | __module__ = 'dex_pb2' 616 | # @@protoc_insertion_point(class_scope:transaction.NewOrder) 617 | )) 618 | _sym_db.RegisterMessage(NewOrder) 619 | 620 | CancelOrder = _reflection.GeneratedProtocolMessageType('CancelOrder', (_message.Message,), dict( 621 | DESCRIPTOR = _CANCELORDER, 622 | __module__ = 'dex_pb2' 623 | # @@protoc_insertion_point(class_scope:transaction.CancelOrder) 624 | )) 625 | _sym_db.RegisterMessage(CancelOrder) 626 | 627 | TokenFreeze = _reflection.GeneratedProtocolMessageType('TokenFreeze', (_message.Message,), dict( 628 | DESCRIPTOR = _TOKENFREEZE, 629 | __module__ = 'dex_pb2' 630 | # @@protoc_insertion_point(class_scope:transaction.TokenFreeze) 631 | )) 632 | _sym_db.RegisterMessage(TokenFreeze) 633 | 634 | TokenUnfreeze = _reflection.GeneratedProtocolMessageType('TokenUnfreeze', (_message.Message,), dict( 635 | DESCRIPTOR = _TOKENUNFREEZE, 636 | __module__ = 'dex_pb2' 637 | # @@protoc_insertion_point(class_scope:transaction.TokenUnfreeze) 638 | )) 639 | _sym_db.RegisterMessage(TokenUnfreeze) 640 | 641 | Token = _reflection.GeneratedProtocolMessageType('Token', (_message.Message,), dict( 642 | DESCRIPTOR = _TOKEN, 643 | __module__ = 'dex_pb2' 644 | # @@protoc_insertion_point(class_scope:transaction.Token) 645 | )) 646 | _sym_db.RegisterMessage(Token) 647 | 648 | Input = _reflection.GeneratedProtocolMessageType('Input', (_message.Message,), dict( 649 | DESCRIPTOR = _INPUT, 650 | __module__ = 'dex_pb2' 651 | # @@protoc_insertion_point(class_scope:transaction.Input) 652 | )) 653 | _sym_db.RegisterMessage(Input) 654 | 655 | Output = _reflection.GeneratedProtocolMessageType('Output', (_message.Message,), dict( 656 | DESCRIPTOR = _OUTPUT, 657 | __module__ = 'dex_pb2' 658 | # @@protoc_insertion_point(class_scope:transaction.Output) 659 | )) 660 | _sym_db.RegisterMessage(Output) 661 | 662 | Send = _reflection.GeneratedProtocolMessageType('Send', (_message.Message,), dict( 663 | DESCRIPTOR = _SEND, 664 | __module__ = 'dex_pb2' 665 | # @@protoc_insertion_point(class_scope:transaction.Send) 666 | )) 667 | _sym_db.RegisterMessage(Send) 668 | 669 | Vote = _reflection.GeneratedProtocolMessageType('Vote', (_message.Message,), dict( 670 | DESCRIPTOR = _VOTE, 671 | __module__ = 'dex_pb2' 672 | # @@protoc_insertion_point(class_scope:transaction.Vote) 673 | )) 674 | _sym_db.RegisterMessage(Vote) 675 | 676 | 677 | DESCRIPTOR._options = None 678 | # @@protoc_insertion_point(module_scope) 679 | -------------------------------------------------------------------------------- /binance_chain/signing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/binance_chain/signing/__init__.py -------------------------------------------------------------------------------- /binance_chain/signing/http.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | from typing import Optional, Dict 3 | 4 | import asyncio 5 | import aiohttp 6 | import requests 7 | 8 | import binance_chain.messages 9 | from binance_chain.exceptions import ( 10 | BinanceChainAPIException, BinanceChainRequestException, 11 | BinanceChainSigningAuthenticationException 12 | ) 13 | 14 | 15 | requests.models.json = ujson 16 | 17 | 18 | class BaseApiSigningClient: 19 | 20 | def __init__(self, endpoint: str, username: str, password: str, requests_params: Optional[Dict] = None): 21 | """Binance Chain Signing API Client constructor 22 | 23 | :param endpoint: URL of signing service 24 | :param username: username for auth 25 | :param password: password for auth 26 | :param requests_params: 27 | 28 | """ 29 | 30 | self._token = None 31 | self._endpoint = endpoint 32 | self._username = username 33 | self._password = password 34 | self._headers = { 35 | 'Accept': 'application/json', 36 | 'User-Agent': 'python-binance-chain', 37 | } 38 | self._requests_params = requests_params 39 | self.session = self._init_session() 40 | 41 | def _init_session(self): 42 | 43 | session = requests.session() 44 | session.headers.update(self._headers) 45 | return session 46 | 47 | def _create_uri(self, path): 48 | return f'{self._endpoint}/api/{path}' 49 | 50 | def authenticate(self): 51 | raise NotImplementedError() 52 | 53 | def _get_request_kwargs(self, method, **kwargs): 54 | 55 | # set default requests timeout 56 | kwargs['timeout'] = 10 57 | 58 | # add our global requests params 59 | if self._requests_params: 60 | kwargs.update(self._requests_params) 61 | 62 | if self._token: 63 | kwargs['headers'] = { 64 | 'Authorization': f'Bearer {self._token}' 65 | } 66 | 67 | return kwargs 68 | 69 | 70 | class HttpApiSigningClient(BaseApiSigningClient): 71 | 72 | def __init__(self, endpoint: str, username: str, password: str, requests_params: Optional[Dict] = None): 73 | """Binance Chain Signing API Client constructor 74 | 75 | :param endpoint: URL of signing service 76 | :param username: username for auth 77 | :param password: password for auth 78 | :param requests_params: 79 | 80 | """ 81 | 82 | super().__init__(endpoint, username, password, requests_params) 83 | 84 | self.authenticate() 85 | 86 | def _request(self, method, path, **kwargs): 87 | 88 | uri = self._create_uri(path) 89 | 90 | kwargs = self._get_request_kwargs(method, **kwargs) 91 | 92 | response = getattr(self.session, method)(uri, **kwargs) 93 | return self._handle_response(response) 94 | 95 | @staticmethod 96 | def _handle_response(response): 97 | """Internal helper for handling API responses from the server. 98 | Raises the appropriate exceptions when necessary; otherwise, returns the 99 | response. 100 | 101 | """ 102 | 103 | if not str(response.status_code).startswith('2'): 104 | raise BinanceChainAPIException(response, response.status_code) 105 | try: 106 | res = response.json() 107 | 108 | if 'code' in res and res['code'] != "200000": 109 | raise BinanceChainAPIException(response, response.status_code) 110 | 111 | if 'message' in res: 112 | raise BinanceChainAPIException(response, response.status_code) 113 | 114 | return res 115 | except ValueError: 116 | raise BinanceChainRequestException('Invalid Response: %s' % response.text) 117 | 118 | def _get(self, path, **kwargs): 119 | return self._request('get', path, **kwargs) 120 | 121 | def _post(self, path, **kwargs): 122 | return self._request('post', path, **kwargs) 123 | 124 | def authenticate(self): 125 | data = { 126 | "username": self._username, 127 | "password": self._password 128 | } 129 | res = self._post("auth/login", json=data) 130 | 131 | self._token = res.get('access_token') 132 | 133 | if not self._token: 134 | raise BinanceChainSigningAuthenticationException("Invalid username and password") 135 | 136 | def sign_order(self, msg: binance_chain.messages.NewOrderMsg, wallet_name: str): 137 | """Sign a message using a signing service 138 | 139 | :param msg: Type of NewOrderMsg 140 | :param wallet_name: Name of the wallet 141 | 142 | .. code:: python 143 | 144 | # new order example 145 | # construct the message 146 | new_order_msg = NewOrderMsg( 147 | symbol="ANN-457_BNB", 148 | time_in_force=TimeInForce.GTE, 149 | order_type=OrderType.LIMIT, 150 | side=OrderSide.BUY, 151 | price=Decimal(0.000396000), 152 | quantity=Decimal(12) 153 | ) 154 | # then broadcast it 155 | res = client.sign_order(new_order_msg, wallet_name='mywallet') 156 | 157 | :return: API Response 158 | 159 | """ 160 | data = { 161 | 'msg': msg.to_sign_dict(), 162 | 'wallet_name': wallet_name 163 | } 164 | return self._post('order/sign', json=data) 165 | 166 | def broadcast_order(self, msg: binance_chain.messages.NewOrderMsg, wallet_name: str): 167 | """Sign and broadcast a message using a signing service 168 | 169 | :param msg: Type of NewOrderMsg 170 | :param wallet_name: Name of the wallet 171 | 172 | .. code:: python 173 | 174 | # new order example 175 | # construct the message 176 | new_order_msg = NewOrderMsg( 177 | symbol="ANN-457_BNB", 178 | time_in_force=TimeInForce.GTE, 179 | order_type=OrderType.LIMIT, 180 | side=OrderSide.BUY, 181 | price=Decimal(0.000396000), 182 | quantity=Decimal(12) 183 | ) 184 | # then broadcast it 185 | res = client.broadcast_order(new_order_msg, wallet_name='mywallet') 186 | 187 | :return: API Response 188 | 189 | """ 190 | data = { 191 | 'msg': msg.to_sign_dict(), 192 | 'wallet_name': wallet_name 193 | } 194 | return self._post('order/broadcast', json=data) 195 | 196 | def sign_cancel_order(self, msg: binance_chain.messages.CancelOrderMsg, wallet_name: str): 197 | """Sign a message using a signing service 198 | 199 | :param msg: Type of NewOrderMsg 200 | :param wallet_name: Name of the wallet 201 | 202 | .. code:: python 203 | 204 | # cancel order example 205 | cancel_order_msg = CancelOrderMsg( 206 | order_id="09F8B32D33CBE2B546088620CBEBC1FF80F9BE001ACF42762B0BBFF0A729CE3", 207 | symbol='ANN-457_BNB', 208 | ) 209 | res = client.sign_cancel_order(cancel_order_msg, wallet_name='mywallet') 210 | 211 | :return: API Response 212 | 213 | """ 214 | data = { 215 | 'msg': msg.to_sign_dict(), 216 | 'wallet_name': wallet_name 217 | } 218 | return self._post('order/cancel/sign', json=data) 219 | 220 | def broadcast_cancel_order(self, msg: binance_chain.messages.CancelOrderMsg, wallet_name: str): 221 | """Sign and broadcast a message using a signing service 222 | 223 | :param msg: Type of NewOrderMsg 224 | :param wallet_name: Name of the wallet 225 | 226 | .. code:: python 227 | 228 | # cancel order example 229 | cancel_order_msg = CancelOrderMsg( 230 | order_id="09F8B32D33CBE2B546088620CBEBC1FF80F9BE001ACF42762B0BBFF0A729CE3", 231 | symbol='ANN-457_BNB', 232 | ) 233 | res = client.broadcast_cancel_order(cancel_order_msg, wallet_name='mywallet') 234 | 235 | :return: API Response 236 | 237 | """ 238 | data = { 239 | 'msg': msg.to_sign_dict(), 240 | 'wallet_name': wallet_name 241 | } 242 | return self._post('order/cancel/broadcast', json=data) 243 | 244 | def sign_transfer(self, msg: binance_chain.messages.TransferMsg, wallet_name: str): 245 | """Sign a message using a signing service 246 | 247 | :param msg: Type of TransferMsg 248 | :param wallet_name: Name of the wallet 249 | 250 | .. code:: python 251 | 252 | transfer_msg = TransferMsg( 253 | symbol='BNB', 254 | amount=1, 255 | to_address='' 256 | ) 257 | res = client.sign_transfer(transfer_msg, wallet_name='mywallet') 258 | 259 | :return: API Response 260 | 261 | """ 262 | data = { 263 | 'msg': msg.to_sign_dict(), 264 | 'wallet_name': wallet_name 265 | } 266 | return self._post('transfer/sign', json=data) 267 | 268 | def broadcast_transfer(self, msg: binance_chain.messages.TransferMsg, wallet_name: str): 269 | """Sign and broadcast a message using a signing service 270 | 271 | :param msg: Type of TransferMsg 272 | :param wallet_name: Name of the wallet 273 | 274 | .. code:: python 275 | 276 | transfer_msg = TransferMsg( 277 | symbol='BNB', 278 | amount=1, 279 | to_address='' 280 | ) 281 | res = client.sign_transfer(transfer_msg, wallet_name='mywallet') 282 | 283 | :return: API Response 284 | 285 | """ 286 | data = { 287 | 'msg': msg.to_sign_dict(), 288 | 'wallet_name': wallet_name 289 | } 290 | return self._post('transfer/broadcast', json=data) 291 | 292 | def sign_freeze(self, msg: binance_chain.messages.FreezeMsg, wallet_name: str): 293 | """Sign a message using a signing service 294 | 295 | :param msg: Type of FreezeMsg 296 | :param wallet_name: Name of the wallet 297 | 298 | .. code:: python 299 | 300 | freeze_msg = FreezeMsg( 301 | symbol='BNB', 302 | amount=Decimal(10) 303 | ) 304 | res = client.sign_freeze(freeze_msg, wallet_name='mywallet') 305 | 306 | :return: API Response 307 | 308 | """ 309 | data = { 310 | 'msg': msg.to_sign_dict(), 311 | 'wallet_name': wallet_name 312 | } 313 | return self._post('freeze/sign', json=data) 314 | 315 | def broadcast_freeze(self, msg: binance_chain.messages.FreezeMsg, wallet_name: str): 316 | """Sign and broadcast a message using a signing service 317 | 318 | :param msg: Type of FreezeMsg 319 | :param wallet_name: Name of the wallet 320 | 321 | .. code:: python 322 | 323 | freeze_msg = FreezeMsg( 324 | symbol='BNB', 325 | amount=Decimal(10) 326 | ) 327 | res = client.sign_transfer(freeze_msg, wallet_name='mywallet') 328 | 329 | :return: API Response 330 | 331 | """ 332 | data = { 333 | 'msg': msg.to_sign_dict(), 334 | 'wallet_name': wallet_name 335 | } 336 | return self._post('freeze/broadcast', json=data) 337 | 338 | def sign_unfreeze(self, msg: binance_chain.messages.UnFreezeMsg, wallet_name: str): 339 | """Sign a message using a signing service 340 | 341 | :param msg: Type of UnFreezeMsg 342 | :param wallet_name: Name of the wallet 343 | 344 | .. code:: python 345 | 346 | unfreeze_msg = UnFreezeMsg( 347 | symbol='BNB', 348 | amount=Decimal(10) 349 | ) 350 | res = client.sign_unfreeze(unfreeze_msg, wallet_name='mywallet') 351 | 352 | :return: API Response 353 | 354 | """ 355 | data = { 356 | 'msg': msg.to_sign_dict(), 357 | 'wallet_name': wallet_name 358 | } 359 | return self._post('unfreeze/sign', json=data) 360 | 361 | def broadcast_unfreeze(self, msg: binance_chain.messages.UnFreezeMsg, wallet_name: str): 362 | """Sign and broadcast a message using a signing service 363 | 364 | :param msg: Type of UnFreezeMsg 365 | :param wallet_name: Name of the wallet 366 | 367 | .. code:: python 368 | 369 | unfreeze_msg = UnFreezeMsg( 370 | symbol='BNB', 371 | amount=Decimal(10) 372 | ) 373 | res = client.sign_unfreeze(unfreeze_msg, wallet_name='mywallet') 374 | 375 | :return: API Response 376 | 377 | """ 378 | data = { 379 | 'msg': msg.to_sign_dict(), 380 | 'wallet_name': wallet_name 381 | } 382 | return self._post('unfreeze/broadcast', json=data) 383 | 384 | def sign_vote(self, msg: binance_chain.messages.VoteMsg, wallet_name: str): 385 | """Sign a message using a signing service 386 | 387 | :param msg: Type of VoteMsg 388 | :param wallet_name: Name of the wallet 389 | 390 | .. code:: python 391 | 392 | vote_msg = VoteMsg( 393 | proposal_id=1, 394 | vote_option=VoteOption.YES 395 | ) 396 | res = client.sign_vote(vote_msg, wallet_name='mywallet') 397 | 398 | :return: API Response 399 | 400 | """ 401 | data = { 402 | 'msg': msg.to_sign_dict(), 403 | 'wallet_name': wallet_name 404 | } 405 | return self._post('vote/sign', json=data) 406 | 407 | def broadcast_vote(self, msg: binance_chain.messages.VoteMsg, wallet_name: str): 408 | """Sign and broadcast a message using a signing service 409 | 410 | :param msg: Type of VoteMsg 411 | :param wallet_name: Name of the wallet 412 | 413 | .. code:: python 414 | 415 | vote_msg = VoteMsg( 416 | proposal_id=1, 417 | vote_option=VoteOption.YES 418 | ) 419 | res = client.broadcast_vote(vote_msg, wallet_name='mywallet') 420 | 421 | :return: API Response 422 | 423 | """ 424 | data = { 425 | 'msg': msg.to_sign_dict(), 426 | 'wallet_name': wallet_name 427 | } 428 | return self._post('vote/broadcast', json=data) 429 | 430 | def wallet_resync(self, wallet_name: str): 431 | """Resynchronise the wallet to the chain 432 | 433 | :param wallet_name: 434 | 435 | .. code:: python 436 | 437 | res = client.wallet_resync(wallet_name='mywallet') 438 | 439 | :return: API Response 440 | 441 | """ 442 | data = { 443 | 'wallet_name': wallet_name 444 | } 445 | return self._post('wallet/resync', json=data) 446 | 447 | def wallet_info(self, wallet_name: Optional[str] = None): 448 | """Get wallet information for the currently authenticated user 449 | 450 | :param wallet_name: Optional- if not passed returns all walets 451 | 452 | .. code:: python 453 | 454 | # info about all wallets 455 | res_all = client.wallet_info() 456 | 457 | # info about particular wallets 458 | res_mywallet = client.wallet_info(wallet_name='mywallet') 459 | 460 | :return: API Response 461 | 462 | """ 463 | req_path = 'wallet' 464 | if wallet_name: 465 | req_path = f'wallet/{wallet_name}' 466 | return self._get(req_path) 467 | 468 | 469 | class AsyncHttpApiSigningClient(BaseApiSigningClient): 470 | 471 | def __init__(self, endpoint: str, username: str, password: str, 472 | loop: Optional[asyncio.AbstractEventLoop] = None, 473 | requests_params: Optional[Dict] = None): 474 | 475 | super().__init__(endpoint, username, password, requests_params=requests_params) 476 | 477 | self._loop = loop 478 | 479 | @classmethod 480 | async def create(cls, 481 | endpoint: str, username: str, password: str, 482 | loop: Optional[asyncio.AbstractEventLoop] = None, 483 | requests_params: Optional[Dict] = None): 484 | 485 | return AsyncHttpApiSigningClient(endpoint, username, password, requests_params=requests_params, loop=loop) 486 | 487 | def _init_session(self, **kwargs): 488 | 489 | loop = kwargs.get('loop', asyncio.get_event_loop()) 490 | session = aiohttp.ClientSession( 491 | loop=loop, 492 | headers=self._headers, 493 | json_serialize=ujson.dumps 494 | ) 495 | return session 496 | 497 | async def _request(self, method, path, **kwargs): 498 | 499 | uri = self._create_uri(path) 500 | 501 | kwargs = self._get_request_kwargs(method, **kwargs) 502 | 503 | async with getattr(self.session, method)(uri, **kwargs) as response: 504 | return await self._handle_response(response) 505 | 506 | async def _handle_response(self, response): 507 | """Internal helper for handling API responses from the Binance server. 508 | Raises the appropriate exceptions when necessary; otherwise, returns the 509 | response. 510 | """ 511 | if not str(response.status).startswith('2'): 512 | raise BinanceChainAPIException(response, response.status) 513 | try: 514 | res = await response.json() 515 | 516 | if 'code' in res and res['code'] != "200000": 517 | raise BinanceChainAPIException(response, response.status) 518 | 519 | if 'success' in res and not res['success']: 520 | raise BinanceChainAPIException(response, response.status) 521 | 522 | # by default return full response 523 | # if it's a normal response we have a data attribute, return that 524 | if 'data' in res: 525 | res = res['data'] 526 | return res 527 | except ValueError: 528 | raise BinanceChainRequestException('Invalid Response: %s' % await response.text()) 529 | 530 | async def _get(self, path, **kwargs): 531 | return await self._request('get', path, **kwargs) 532 | 533 | async def _post(self, path, **kwargs): 534 | return await self._request('post', path, **kwargs) 535 | 536 | async def authenticate(self): 537 | data = { 538 | "username": self._username, 539 | "password": self._password 540 | } 541 | res = await self._post("auth/login", json=data) 542 | 543 | self._token = res.get('access_token') 544 | 545 | if not self._token: 546 | raise BinanceChainSigningAuthenticationException("Invalid username and password") 547 | 548 | async def sign_order(self, msg: binance_chain.messages.NewOrderMsg, wallet_name: str): 549 | data = { 550 | 'msg': msg.to_sign_dict(), 551 | 'wallet_name': wallet_name 552 | } 553 | return await self._post('order/sign', json=data) 554 | sign_order.__doc__ = HttpApiSigningClient.sign_order.__doc__ 555 | 556 | async def broadcast_order(self, msg: binance_chain.messages.NewOrderMsg, wallet_name: str): 557 | data = { 558 | 'msg': msg.to_sign_dict(), 559 | 'wallet_name': wallet_name 560 | } 561 | return await self._post('order/broadcast', json=data) 562 | broadcast_order.__doc__ = HttpApiSigningClient.broadcast_order.__doc__ 563 | 564 | async def sign_cancel_order(self, msg: binance_chain.messages.CancelOrderMsg, wallet_name: str): 565 | data = { 566 | 'msg': msg.to_sign_dict(), 567 | 'wallet_name': wallet_name 568 | } 569 | return await self._post('order/cancel/sign', json=data) 570 | sign_cancel_order.__doc__ = HttpApiSigningClient.sign_cancel_order.__doc__ 571 | 572 | async def broadcast_cancel_order(self, msg: binance_chain.messages.CancelOrderMsg, wallet_name: str): 573 | data = { 574 | 'msg': msg.to_sign_dict(), 575 | 'wallet_name': wallet_name 576 | } 577 | return await self._post('order/cancel/broadcast', json=data) 578 | broadcast_cancel_order.__doc__ = HttpApiSigningClient.broadcast_cancel_order.__doc__ 579 | 580 | async def sign_transfer(self, msg: binance_chain.messages.TransferMsg, wallet_name: str): 581 | data = { 582 | 'msg': msg.to_sign_dict(), 583 | 'wallet_name': wallet_name 584 | } 585 | return await self._post('transfer/sign', json=data) 586 | sign_transfer.__doc__ = HttpApiSigningClient.sign_transfer.__doc__ 587 | 588 | async def broadcast_transfer(self, msg: binance_chain.messages.TransferMsg, wallet_name: str): 589 | data = { 590 | 'msg': msg.to_sign_dict(), 591 | 'wallet_name': wallet_name 592 | } 593 | return await self._post('transfer/broadcast', json=data) 594 | broadcast_transfer.__doc__ = HttpApiSigningClient.broadcast_transfer.__doc__ 595 | 596 | async def sign_freeze(self, msg: binance_chain.messages.FreezeMsg, wallet_name: str): 597 | data = { 598 | 'msg': msg.to_sign_dict(), 599 | 'wallet_name': wallet_name 600 | } 601 | return await self._post('freeze/sign', json=data) 602 | sign_freeze.__doc__ = HttpApiSigningClient.sign_freeze.__doc__ 603 | 604 | async def broadcast_freeze(self, msg: binance_chain.messages.FreezeMsg, wallet_name: str): 605 | data = { 606 | 'msg': msg.to_sign_dict(), 607 | 'wallet_name': wallet_name 608 | } 609 | return await self._post('freeze/broadcast', json=data) 610 | broadcast_freeze.__doc__ = HttpApiSigningClient.broadcast_freeze.__doc__ 611 | 612 | async def sign_unfreeze(self, msg: binance_chain.messages.UnFreezeMsg, wallet_name: str): 613 | data = { 614 | 'msg': msg.to_sign_dict(), 615 | 'wallet_name': wallet_name 616 | } 617 | return await self._post('unfreeze/sign', json=data) 618 | sign_unfreeze.__doc__ = HttpApiSigningClient.sign_unfreeze.__doc__ 619 | 620 | async def broadcast_unfreeze(self, msg: binance_chain.messages.UnFreezeMsg, wallet_name: str): 621 | data = { 622 | 'msg': msg.to_sign_dict(), 623 | 'wallet_name': wallet_name 624 | } 625 | return await self._post('unfreeze/broadcast', json=data) 626 | broadcast_unfreeze.__doc__ = HttpApiSigningClient.broadcast_unfreeze.__doc__ 627 | 628 | async def sign_vote(self, msg: binance_chain.messages.VoteMsg, wallet_name: str): 629 | data = { 630 | 'msg': msg.to_sign_dict(), 631 | 'wallet_name': wallet_name 632 | } 633 | return await self._post('vote/sign', json=data) 634 | sign_vote.__doc__ = HttpApiSigningClient.sign_vote.__doc__ 635 | 636 | async def broadcast_vote(self, msg: binance_chain.messages.VoteMsg, wallet_name: str): 637 | data = { 638 | 'msg': msg.to_sign_dict(), 639 | 'wallet_name': wallet_name 640 | } 641 | return await self._post('vote/broadcast', json=data) 642 | broadcast_vote.__doc__ = HttpApiSigningClient.broadcast_vote.__doc__ 643 | 644 | async def wallet_resync(self, wallet_name: str): 645 | data = { 646 | 'wallet_name': wallet_name 647 | } 648 | return await self._post('wallet/resync', json=data) 649 | wallet_resync.__doc__ = HttpApiSigningClient.wallet_resync.__doc__ 650 | 651 | async def wallet_info(self, wallet_name: Optional[str] = None): 652 | req_path = 'wallet' 653 | if wallet_name: 654 | req_path = f'wallet/{wallet_name}' 655 | return await self._get(req_path) 656 | wallet_info.__doc__ = HttpApiSigningClient.wallet_info.__doc__ 657 | -------------------------------------------------------------------------------- /binance_chain/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/binance_chain/utils/__init__.py -------------------------------------------------------------------------------- /binance_chain/utils/encode_utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | from decimal import Decimal 3 | from typing import Union 4 | 5 | 6 | def encode_number(num: Union[float, Decimal]) -> int: 7 | """Encode number multiply by 1e8 (10^8) and round to int 8 | 9 | :param num: number to encode 10 | 11 | """ 12 | if type(num) == Decimal: 13 | return int(num * (Decimal(10) ** 8)) 14 | else: 15 | return int(num * math.pow(10, 8)) 16 | 17 | 18 | def varint_encode(num): 19 | """Convert number into varint bytes 20 | 21 | :param num: number to encode 22 | 23 | """ 24 | buf = b'' 25 | while True: 26 | towrite = num & 0x7f 27 | num >>= 7 28 | if num: 29 | buf += bytes(((towrite | 0x80), )) 30 | else: 31 | buf += bytes((towrite, )) 32 | break 33 | return buf 34 | -------------------------------------------------------------------------------- /binance_chain/utils/segwit_addr.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 Pieter Wuille 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Reference implementation for Bech32 and segwit addresses. 22 | 23 | From https://github.com/sipa/bech32 24 | 25 | """ 26 | 27 | import array 28 | import hashlib 29 | 30 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 31 | 32 | 33 | def bech32_polymod(values): 34 | """Internal function that computes the Bech32 checksum.""" 35 | generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] 36 | chk = 1 37 | for value in values: 38 | top = chk >> 25 39 | chk = (chk & 0x1ffffff) << 5 ^ value 40 | for i in range(5): 41 | chk ^= generator[i] if ((top >> i) & 1) else 0 42 | return chk 43 | 44 | 45 | def bech32_hrp_expand(hrp): 46 | """Expand the HRP into values for checksum computation.""" 47 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 48 | 49 | 50 | def bech32_verify_checksum(hrp, data): 51 | """Verify a checksum given HRP and converted data characters.""" 52 | return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 53 | 54 | 55 | def bech32_create_checksum(hrp, data): 56 | """Compute the checksum values given HRP and data.""" 57 | values = bech32_hrp_expand(hrp) + data 58 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 59 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 60 | 61 | 62 | def bech32_encode(hrp, data): 63 | """Compute a Bech32 string given HRP and data values.""" 64 | combined = data + bech32_create_checksum(hrp, data) 65 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 66 | 67 | 68 | def bech32_decode(bech): 69 | """Validate a Bech32 string, and determine HRP and data.""" 70 | if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech): 71 | return None, None 72 | bech = bech.lower() 73 | pos = bech.rfind('1') 74 | if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: 75 | return None, None 76 | if not all(x in CHARSET for x in bech[pos + 1:]): 77 | return None, None 78 | hrp = bech[:pos] 79 | data = [CHARSET.find(x) for x in bech[pos + 1:]] 80 | if not bech32_verify_checksum(hrp, data): 81 | return None, None 82 | return hrp, data[:-6] 83 | 84 | 85 | def convertbits(data, frombits, tobits, pad=True): 86 | """General power-of-2 base conversion.""" 87 | acc = 0 88 | bits = 0 89 | ret = [] 90 | maxv = (1 << tobits) - 1 91 | max_acc = (1 << (frombits + tobits - 1)) - 1 92 | for value in data: 93 | if value < 0 or (value >> frombits): 94 | return None 95 | acc = ((acc << frombits) | value) & max_acc 96 | bits += frombits 97 | while bits >= tobits: 98 | bits -= tobits 99 | ret.append((acc >> bits) & maxv) 100 | if pad: 101 | if bits: 102 | ret.append((acc << (tobits - bits)) & maxv) 103 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 104 | return None 105 | return ret 106 | 107 | 108 | def encode(hrp, witprog): 109 | """Encode a segwit address.""" 110 | return bech32_encode(hrp, convertbits(witprog, 8, 5)) 111 | 112 | 113 | def decode_address(address): 114 | hrp, data = bech32_decode(address) 115 | if hrp is None: 116 | return None 117 | 118 | bits = convertbits(data, 5, 8, False) 119 | return array.array('B', bits).tobytes() 120 | 121 | 122 | def address_from_public_key(public_key, hrp='tbnb'): 123 | s = hashlib.new('sha256', public_key).digest() 124 | r = hashlib.new('ripemd160', s).digest() 125 | 126 | return encode(hrp, r) 127 | -------------------------------------------------------------------------------- /binance_chain/wallet.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from enum import Enum 3 | from typing import Optional 4 | 5 | from secp256k1 import PrivateKey 6 | from mnemonic import Mnemonic 7 | from pywallet.utils.bip32 import Wallet as Bip32Wallet 8 | 9 | from binance_chain.utils.segwit_addr import address_from_public_key, decode_address 10 | from binance_chain.environment import BinanceEnvironment 11 | 12 | 13 | class MnemonicLanguage(str, Enum): 14 | ENGLISH = 'english' 15 | FRENCH = 'french' 16 | ITALIAN = 'italian' 17 | JAPANESE = 'japanese' 18 | KOREAN = 'korean' 19 | SPANISH = 'spanish' 20 | CHINESE = 'chinese_traditional' 21 | CHINESE_SIMPLIFIED = 'chinese_simplified' 22 | 23 | 24 | class BaseWallet: 25 | 26 | HD_PATH = "44'/714'/0'/0/0" 27 | 28 | def __init__(self, env: Optional[BinanceEnvironment] = None): 29 | self._env = env or BinanceEnvironment.get_production_env() 30 | self._public_key = None 31 | self._address = None 32 | self._account_number = None 33 | self._sequence = None 34 | self._chain_id = None 35 | self._http_client = None 36 | 37 | def initialise_wallet(self): 38 | if self._account_number: 39 | return 40 | account = self._get_http_client().get_account(self._address) 41 | 42 | self._account_number = account['account_number'] 43 | self._sequence = account['sequence'] 44 | 45 | node_info = self._get_http_client().get_node_info() 46 | self._chain_id = node_info['node_info']['network'] 47 | 48 | def increment_account_sequence(self): 49 | if self._sequence: 50 | self._sequence += 1 51 | 52 | def decrement_account_sequence(self): 53 | if self._sequence: 54 | self._sequence -= 1 55 | 56 | def reload_account_sequence(self): 57 | sequence_res = self._get_http_client().get_account_sequence(self._address) 58 | self._sequence = sequence_res['sequence'] 59 | 60 | def generate_order_id(self): 61 | return f"{binascii.hexlify(self.address_decoded).decode().upper()}-{(self._sequence + 1)}" 62 | 63 | def _get_http_client(self): 64 | if not self._http_client: 65 | from binance_chain.http import HttpApiClient 66 | self._http_client = HttpApiClient(self._env) 67 | return self._http_client 68 | 69 | @property 70 | def env(self): 71 | return self._env 72 | 73 | @property 74 | def address(self): 75 | return self._address 76 | 77 | @property 78 | def address_decoded(self): 79 | return decode_address(self._address) 80 | 81 | @property 82 | def public_key(self): 83 | return self._public_key 84 | 85 | @property 86 | def public_key_hex(self): 87 | return binascii.hexlify(self._public_key) 88 | 89 | @property 90 | def account_number(self): 91 | return self._account_number 92 | 93 | @property 94 | def sequence(self): 95 | return self._sequence 96 | 97 | @property 98 | def chain_id(self): 99 | return self._chain_id 100 | 101 | def sign_message(self, msg_bytes): 102 | raise NotImplementedError 103 | 104 | 105 | class Wallet(BaseWallet): 106 | 107 | HD_PATH = "44'/714'/0'/0/0" 108 | 109 | def __init__(self, private_key, env: Optional[BinanceEnvironment] = None): 110 | super().__init__(env) 111 | self._private_key = private_key 112 | self._pk = PrivateKey(bytes(bytearray.fromhex(self._private_key))) 113 | self._public_key = self._pk.pubkey.serialize(compressed=True) 114 | self._address = address_from_public_key(self._public_key, self._env.hrp) 115 | 116 | @classmethod 117 | def create_random_wallet(cls, language: MnemonicLanguage = MnemonicLanguage.ENGLISH, 118 | env: Optional[BinanceEnvironment] = None): 119 | """Create wallet with random mnemonic code 120 | 121 | :return: 122 | """ 123 | m = Mnemonic(language.value) 124 | phrase = m.generate() 125 | return cls.create_wallet_from_mnemonic(phrase, env=env) 126 | 127 | @classmethod 128 | def create_wallet_from_mnemonic(cls, mnemonic: str, env: Optional[BinanceEnvironment] = None): 129 | """Create wallet with random mnemonic code 130 | 131 | :return: 132 | """ 133 | seed = Mnemonic.to_seed(mnemonic) 134 | new_wallet = Bip32Wallet.from_master_secret(seed=seed, network='BTC') 135 | child = new_wallet.get_child_for_path(Wallet.HD_PATH) 136 | return cls(child.get_private_key_hex().decode(), env=env) 137 | 138 | @property 139 | def private_key(self): 140 | return self._private_key 141 | 142 | def sign_message(self, msg_bytes): 143 | # check if ledger wallet 144 | sig = self._pk.ecdsa_sign(msg_bytes) 145 | return self._pk.ecdsa_serialize_compact(sig) 146 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = python-binance-chain 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | rst: 23 | sphinx-apidoc -f -o ./ ../ 24 | -------------------------------------------------------------------------------- /docs/binance-chain.rst: -------------------------------------------------------------------------------- 1 | Binance Chain API 2 | ================= 3 | 4 | client module 5 | ---------------------- 6 | 7 | .. automodule:: binance_chain.http 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | :member-order: bysource 12 | 13 | environment module 14 | ---------------------- 15 | 16 | .. automodule:: binance_chain.environment 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | :member-order: bysource 21 | 22 | wallet module 23 | ---------------------- 24 | 25 | .. automodule:: binance_chain.wallet 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | :member-order: bysource 30 | 31 | messages module 32 | ---------------------- 33 | 34 | .. automodule:: binance_chain.messages 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | :member-order: bysource 39 | 40 | websockets module 41 | -------------------------- 42 | 43 | .. automodule:: binance_chain.websockets 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | :member-order: bysource 48 | 49 | node rpc http module 50 | -------------------------- 51 | 52 | .. automodule:: binance_chain.node_rpc.http 53 | :members: 54 | :undoc-members: 55 | :show-inheritance: 56 | :member-order: bysource 57 | 58 | node rpc websockets module 59 | -------------------------- 60 | 61 | .. automodule:: binance_chain.node_rpc.websockets 62 | :members: 63 | :undoc-members: 64 | :show-inheritance: 65 | :member-order: bysource 66 | 67 | signing service http module 68 | --------------------------- 69 | 70 | .. automodule:: binance_chain.signing.http 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | :member-order: bysource 75 | 76 | deptch cache module 77 | -------------------------- 78 | 79 | .. automodule:: binance_chain.depthcache 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | :member-order: bysource 84 | 85 | constants module 86 | ---------------------- 87 | 88 | .. automodule:: binance_chain.constants 89 | :members: 90 | :undoc-members: 91 | :show-inheritance: 92 | :member-order: bysource 93 | 94 | 95 | exceptions module 96 | -------------------------- 97 | 98 | .. automodule:: binance_chain.exceptions 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | :member-order: bysource 103 | 104 | utils.encode_utils module 105 | -------------------------- 106 | 107 | .. automodule:: binance_chain.utils.encode_utils 108 | :members: 109 | :undoc-members: 110 | :show-inheritance: 111 | :member-order: bysource 112 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v0.1.20 - 2019-06-29 5 | ^^^^^^^^^^^^^^^^^^^^ 6 | 7 | **Added** 8 | 9 | - Multi Transfer Msg option 10 | 11 | **Fixed** 12 | 13 | - Response code of 0 for http requests 14 | 15 | v0.1.19 - 2019-04-30 16 | ^^^^^^^^^^^^^^^^^^^^ 17 | 18 | **Added** 19 | 20 | - Shuffling of peer nodes in the Pooled HTTP RPC Node client 21 | 22 | v0.1.18 - 2019-04-29 23 | ^^^^^^^^^^^^^^^^^^^^ 24 | 25 | **Added** 26 | 27 | - Advanced async Pooled HTTP RPC Node client spreading requests over available peers 28 | 29 | **Updated** 30 | 31 | - RPC request id behaviour to work across multiple connections 32 | 33 | v0.1.17 - 2019-04-29 34 | ^^^^^^^^^^^^^^^^^^^^ 35 | 36 | **Added** 37 | 38 | - UltraJson ultra fast JSON parser library 39 | - Docs for requests/aiohttp params and proxy support 40 | - more docs and tests 41 | 42 | **Fixed** 43 | 44 | - DepthCache close method 45 | 46 | v0.1.16 - 2019-04-28 47 | ^^^^^^^^^^^^^^^^^^^^ 48 | 49 | **Added** 50 | 51 | - Vote transaction type 52 | - Simple transaction signing example 53 | 54 | **Fixed** 55 | 56 | - Depth Cache doesn't wait for websocket messages before outputting 57 | 58 | v0.1.13 - 2019-04-28 59 | ^^^^^^^^^^^^^^^^^^^^ 60 | 61 | **Added** 62 | 63 | - `get_address` function for Ledger hardware wallet 64 | - better error handling and parsing of Ledger hardware wallet responses 65 | 66 | v0.1.12 - 2019-04-27 67 | ^^^^^^^^^^^^^^^^^^^^ 68 | 69 | **Added** 70 | 71 | - Ledger Hardware wallet support for signing transactions 72 | 73 | v0.1.10 - 2019-04-24 74 | ^^^^^^^^^^^^^^^^^^^^ 75 | 76 | **Fixed** 77 | 78 | - Connecting to secure and insecure websocket connections 79 | 80 | v0.1.9 - 2019-04-23 81 | ^^^^^^^^^^^^^^^^^^^ 82 | 83 | **Added** 84 | 85 | - More test coverage including for python 3.7 86 | 87 | **Fixed** 88 | 89 | - Params in async http client for `get_klines` 90 | - coveralls report 91 | - small fixes 92 | 93 | v0.1.8 - 2019-04-21 94 | ^^^^^^^^^^^^^^^^^^^ 95 | 96 | **Added** 97 | 98 | - `get_block_exchange_fee` function to Http API Clients 99 | - memo param to Transfer message 100 | - more tests 101 | 102 | **Updated** 103 | 104 | - Remove jsonrpcclient dependency 105 | 106 | **Fixed** 107 | 108 | - Use of enums in request params 109 | - Some deprecation warnings 110 | 111 | v0.1.7 - 2019-04-18 112 | ^^^^^^^^^^^^^^^^^^^ 113 | 114 | **Updated** 115 | 116 | - fix package 117 | 118 | 119 | v0.1.6 - 2019-04-17 120 | ^^^^^^^^^^^^^^^^^^^ 121 | 122 | **Updated** 123 | 124 | - fix package requirement versions 125 | 126 | v0.1.5 - 2019-04-17 127 | ^^^^^^^^^^^^^^^^^^^ 128 | 129 | **Fixed** 130 | 131 | - signing module imported 132 | 133 | v0.1.4 - 2019-04-16 134 | ^^^^^^^^^^^^^^^^^^^ 135 | 136 | **Fixed** 137 | 138 | - Issue with protobuf file 139 | 140 | v0.1.3 - 2019-04-16 141 | ^^^^^^^^^^^^^^^^^^^ 142 | 143 | **Added** 144 | 145 | - Wallet methods for Binance Signing Service v0.0.2 146 | 147 | v0.1.2 - 2019-04-14 148 | ^^^^^^^^^^^^^^^^^^^ 149 | 150 | **Added** 151 | 152 | - Binance Chain Signing Service Interfaces v0.0.1 153 | 154 | **Updated** 155 | 156 | - Cleaned up TransferMsg as from_address is found from wallet instance 157 | 158 | v0.1.1 - 2019-04-13 159 | ^^^^^^^^^^^^^^^^^^^ 160 | 161 | **Added** 162 | 163 | - Broadcast message taking signed hex data 164 | 165 | v0.1.0 - 2019-04-11 166 | ^^^^^^^^^^^^^^^^^^^ 167 | 168 | **Added** 169 | 170 | - Async versions of HTTP Client 171 | - Async version of Node RPC Client 172 | - Node RPC Websocket client 173 | - Async Depth Cache 174 | - Transfer message implementation 175 | - Message broadcast over Node RPC 176 | 177 | v0.0.5 - 2019-04-08 178 | ^^^^^^^^^^^^^^^^^^^ 179 | 180 | **Added** 181 | 182 | - All websocket stream endpoints 183 | - Wallet functions to read account and keep track of transaction sequence 184 | - Support for Testnet and Production environments, along with user defined environment 185 | - Helper classes to create limit buy and sell messages 186 | 187 | **Updated** 188 | 189 | - Refactored modules and tidied up message creation and wallets 190 | 191 | v0.0.4 - 2019-04-07 192 | ^^^^^^^^^^^^^^^^^^^ 193 | 194 | **Added** 195 | 196 | - Wallet initialise from private key or mnemonic string 197 | - Create wallet by generating a mnemonic 198 | 199 | v0.0.3 - 2019-04-06 200 | ^^^^^^^^^^^^^^^^^^^ 201 | 202 | **Added** 203 | 204 | - Transaction Broadcasts 205 | - Generated Docs 206 | 207 | v0.0.2 - 2019-04-04 208 | ^^^^^^^^^^^^^^^^^^^ 209 | 210 | **Added** 211 | 212 | - NodeRPC implementation 213 | - Websockets 214 | 215 | v0.0.1 - 2019-02-24 216 | ^^^^^^^^^^^^^^^^^^^ 217 | 218 | - HTTP API Implementation 219 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # python-binance-chain documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Sep 21 20:24:54 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 35 | 'sphinx.ext.imgmath', 36 | 'sphinx.ext.viewcode', 37 | 'sphinx.ext.githubpages'] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The master toctree document. 49 | master_doc = 'index' 50 | 51 | # General information about the project. 52 | project = 'python-binance-chain' 53 | copyright = '2019, Sam McHardy' 54 | author = 'Sam McHardy' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '0.2.0' 62 | # The full version, including alpha/beta/rc tags. 63 | release = '0.2.0' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # 68 | # This is also used if you do content translation via gettext catalogs. 69 | # Usually you set "language" from the command line for these cases. 70 | language = None 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | # This patterns also effect to html_static_path and html_extra_path 75 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 76 | 77 | # The name of the Pygments (syntax highlighting) style to use. 78 | pygments_style = 'sphinx' 79 | 80 | # If true, `todo` and `todoList` produce output, else they produce nothing. 81 | todo_include_todos = False 82 | 83 | 84 | # -- Options for HTML output ---------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | #html_theme = 'alabaster' 90 | html_theme = 'sphinx_rtd_theme' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | # 96 | # html_theme_options = {} 97 | 98 | # Add any paths that contain custom static files (such as style sheets) here, 99 | # relative to this directory. They are copied after the builtin static files, 100 | # so a file named "default.css" will overwrite the builtin "default.css". 101 | html_static_path = ['_static'] 102 | 103 | # Custom sidebar templates, must be a dictionary that maps document names 104 | # to template names. 105 | # 106 | # This is required for the alabaster theme 107 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 108 | html_sidebars = { 109 | '**': [ 110 | 'about.html', 111 | 'navigation.html', 112 | 'relations.html', # needs 'show_related': True theme option to display 113 | 'searchbox.html', 114 | 'donate.html', 115 | ] 116 | } 117 | 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'python-binance-chaindoc' 123 | 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | 132 | # The font size ('10pt', '11pt' or '12pt'). 133 | # 134 | # 'pointsize': '10pt', 135 | 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'python-binance-chain.tex', 'python-binance-chain Documentation', 150 | 'Sam McHardy', 'manual'), 151 | ] 152 | 153 | 154 | # -- Options for manual page output --------------------------------------- 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [ 159 | (master_doc, 'python-binance-chain', 'python-binance-chain Documentation', 160 | [author], 1) 161 | ] 162 | 163 | 164 | # -- Options for Texinfo output ------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | (master_doc, 'python-binance-chain', 'python-binance-chain Documentation', 171 | author, 'python-binance-chain', 'One line description of project.', 172 | 'Miscellaneous'), 173 | ] 174 | 175 | 176 | def skip(app, what, name, obj, skip, options): 177 | # Ensure that the __init__ method gets documented. 178 | if name == "__init__": 179 | return False 180 | return skip 181 | 182 | 183 | def setup(app): 184 | app.connect("autodoc-skip-member", skip) 185 | 186 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-binance-chain documentation master file 2 | 3 | .. include:: ../README.rst 4 | 5 | Contents 6 | ======== 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | changelog 12 | 13 | binance-chain 14 | 15 | Index 16 | ================== 17 | 18 | * :ref:`genindex` 19 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/docs/requirements.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pywallet>=0.1.0 2 | requests>=2.21.0 3 | aiohttp>=3.5.4 4 | websockets>=7.0 5 | secp256k1>=0.13.2 6 | mnemonic>=0.18 7 | protobuf>=3.6.1 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [pep8] 5 | ignore = E501 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import codecs 3 | import os 4 | import re 5 | from setuptools import setup, find_packages 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | 10 | def read(*parts): 11 | with codecs.open(os.path.join(here, *parts), 'r') as fp: 12 | return fp.read() 13 | 14 | 15 | def find_version(*file_paths): 16 | version_file = read(*file_paths) 17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 18 | version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError("Unable to find version string.") 22 | 23 | 24 | def install_requires(): 25 | 26 | requires = [ 27 | 'pywallet>=0.1.0', 'requests>=2.21.0', 'websockets>=7.0', 'aiohttp>=3.5.4', 28 | 'secp256k1>=0.13.2', 'protobuf>=3.6.1', 'mnemonic>=0.18', 'ujson>=1.35' 29 | ] 30 | return requires 31 | 32 | 33 | setup( 34 | name='python-binance-chain', 35 | version=find_version("binance_chain", "__init__.py"), 36 | packages=find_packages(), 37 | description='Binance Chain HTTP API python implementation', 38 | url='https://github.com/sammchardy/python-binance-chain', 39 | author='Sam McHardy', 40 | license='MIT', 41 | author_email='', 42 | install_requires=install_requires(), 43 | extras_require={ 44 | 'ledger': ['btchip-python>=0.1.28', ], 45 | }, 46 | keywords='binance dex exchange rest api bitcoin ethereum btc eth bnb ledger', 47 | classifiers=[ 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: MIT License', 50 | 'Operating System :: OS Independent', 51 | 'Programming Language :: Python :: 3', 52 | 'Programming Language :: Python :: 3.6', 53 | 'Programming Language :: Python :: 3.7', 54 | 'Programming Language :: Python', 55 | 'Topic :: Software Development :: Libraries :: Python Modules', 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | flake8 3 | mock 4 | pytest 5 | pytest-cov 6 | pytest-pep8 7 | pytest-asyncio 8 | python-coveralls 9 | requests-mock 10 | tox 11 | setuptools -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/tests/.gitkeep -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnb-chain/python-sdk/0f6b8a6077f486b26eda3e448f3e84ef35bfff75/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/peers.json: -------------------------------------------------------------------------------- 1 | [{"accelerated": true, "access_addr": "https://testnet-dex.binance.org:443", "capabilities": ["qs", "ap", "ws"], "id": "gateway-ingress", "listen_addr": "https://testnet-dex.binance.org:443", "moniker": "gateway-ingress", "network": "gateway", "stream_addr": "wss://testnet-dex.binance.org", "version": "1.0.0"}, {"access_addr": "http://seed-pre-s1.binance.org:80", "capabilities": ["node"], "id": "aea74b16d28d06cbfbb1179c177e8cd71315cce4", "listen_addr": "http://seed-pre-s1.binance.org:80", "moniker": "seed", "network": "Binance-Chain-Nile", "original_listen_addr": "ac6d84c3f243a11e98ced0ac108d49f7-704ea117aa391bbe.elb.ap-northeast-1.amazonaws.com:27146", "version": "0.30.1"}, {"access_addr": "https://data-seed-pre-0-s1.binance.org:443", "capabilities": ["node"], "id": "9612b570bffebecca4776cb4512d08e252119005", "listen_addr": "https://data-seed-pre-0-s1.binance.org:443", "moniker": "data-seed-0", "network": "Binance-Chain-Nile", "original_listen_addr": "a0b88b324243a11e994280efee3352a7-96b6996626c6481d.elb.ap-northeast-1.amazonaws.com:27146", "version": "0.30.1"}, {"access_addr": "https://data-seed-pre-1-s1.binance.org:443", "capabilities": ["node"], "id": "8c379d4d3b9995c712665dc9a9414dbde5b30483", "listen_addr": "https://data-seed-pre-1-s1.binance.org:443", "moniker": "data-seed-1", "network": "Binance-Chain-Nile", "original_listen_addr": "aa1e4d0d1243a11e9a951063f6065739-7a82be90a58744b6.elb.ap-northeast-1.amazonaws.com:27146", "version": "0.30.1"}, {"access_addr": "https://data-seed-pre-2-s1.binance.org:443", "capabilities": ["node"], "id": "7156d461742e2a1e569fd68426009c4194830c93", "listen_addr": "https://data-seed-pre-2-s1.binance.org:443", "moniker": "data-seed-2", "network": "Binance-Chain-Nile", "original_listen_addr": "aa841c226243a11e9a951063f6065739-eee556e439dc6a3b.elb.ap-northeast-1.amazonaws.com:27146", "version": "0.30.1"}] -------------------------------------------------------------------------------- /tests/fixtures/success.json: -------------------------------------------------------------------------------- 1 | {"success": "true", "data": {"message": "success"}} -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | import requests_mock 6 | from urllib.parse import urlencode 7 | 8 | from binance_chain.http import HttpApiClient, AsyncHttpApiClient 9 | from binance_chain.constants import PeerType, OrderSide, OrderType, TransactionSide, TransactionType, KlineInterval 10 | from binance_chain.environment import BinanceEnvironment 11 | 12 | 13 | class TestClient: 14 | 15 | @pytest.fixture 16 | def env(self): 17 | return BinanceEnvironment.get_testnet_env() 18 | 19 | @pytest.fixture 20 | def httpclient(self, env): 21 | return HttpApiClient(env=env, requests_params={'timeout': 1}) 22 | 23 | def load_fixture(self, file_name): 24 | this_dir = os.path.dirname(os.path.realpath(__file__)) 25 | with open(f'{this_dir}/fixtures/{file_name}', 'r') as f: 26 | return json.load(f) 27 | 28 | def test_get_time(self, httpclient): 29 | assert httpclient.get_time() 30 | 31 | def test_get_node_info(self, httpclient): 32 | assert httpclient.get_node_info() 33 | 34 | def test_get_validators(self, httpclient): 35 | assert httpclient.get_validators() 36 | 37 | def test_get_peers(self, httpclient): 38 | assert httpclient.get_peers() 39 | 40 | def test_get_transaction(self, httpclient): 41 | assert httpclient.get_transaction('B17DB550FCE00268C07D11F312E86F72813481124831B798FDC491E363D17989') 42 | 43 | @pytest.mark.parametrize("peer_type", [ 44 | PeerType.NODE, 45 | PeerType.WEBSOCKET 46 | ]) 47 | def test_get_peers_capability_node(self, peer_type, httpclient): 48 | with requests_mock.mock() as m: 49 | m.get(httpclient._create_uri('peers'), json=self.load_fixture('peers.json')) 50 | peers = httpclient.get_peers(peer_type=peer_type) 51 | for p in peers: 52 | assert peer_type in p['capabilities'] 53 | 54 | def test_get_node_peers(self, httpclient): 55 | with requests_mock.mock() as m: 56 | m.get(httpclient._create_uri('peers'), json=self.load_fixture('peers.json')) 57 | peers = httpclient.get_node_peers() 58 | for p in peers: 59 | assert PeerType.NODE in p['capabilities'] 60 | assert httpclient.get_peers() 61 | 62 | def test_get_websocket_peers(self, httpclient): 63 | with requests_mock.mock() as m: 64 | m.get(httpclient._create_uri('peers'), json=self.load_fixture('peers.json')) 65 | peers = httpclient.get_websocket_peers() 66 | for p in peers: 67 | assert PeerType.WEBSOCKET in p['capabilities'] 68 | assert httpclient.get_peers() 69 | 70 | def test_get_tokens(self, httpclient): 71 | assert httpclient.get_tokens() 72 | 73 | def test_get_markets(self, httpclient): 74 | assert httpclient.get_markets() 75 | 76 | def test_get_fees(self, httpclient): 77 | assert httpclient.get_fees() 78 | 79 | def test_get_trades_url(self, httpclient): 80 | params = { 81 | 'symbol': 'BNB', 82 | 'side': OrderSide.BUY.value, 83 | 'quoteAsset': 'B.BTC', 84 | 'buyerOrderId': 'buyer_id', 85 | 'sellerOrderId': 'seller_id', 86 | 'height': 3000, 87 | 'offset': 1, 88 | 'limit': 100, 89 | 'start': 2000, 90 | 'end': 5000, 91 | 'total': 5 92 | } 93 | qs = urlencode(params) 94 | with requests_mock.mock() as m: 95 | m.get(httpclient._create_uri('trades') + '?' + qs, json=self.load_fixture('success.json')) 96 | res = httpclient.get_trades( 97 | symbol='BNB', 98 | side=OrderSide.BUY, 99 | quote_asset='B.BTC', 100 | buyer_order_id='buyer_id', 101 | seller_order_id='seller_id', 102 | height='3000', 103 | offset=1, 104 | limit=100, 105 | start_time=2000, 106 | end_time=5000, 107 | total=5 108 | ) 109 | assert res == {"message": "success"} 110 | 111 | def test_get_transactions_url(self, httpclient): 112 | addr = 'tbnb2jadf8u2' 113 | params = { 114 | 'address': addr, 115 | 'symbol': 'BNB', 116 | 'side': TransactionSide.RECEIVE.value, 117 | 'txAsset': 'B.BTC', 118 | 'txType': TransactionType.BURN_TOKEN.value, 119 | 'blockHeight': 3000, 120 | 'offset': 1, 121 | 'limit': 100, 122 | 'startTime': 2000, 123 | 'endTime': 5000 124 | } 125 | qs = urlencode(params) 126 | with requests_mock.mock() as m: 127 | m.get(httpclient._create_uri('transactions') + '?' + qs, json=self.load_fixture('success.json')) 128 | res = httpclient.get_transactions( 129 | address=addr, 130 | symbol='BNB', 131 | side=TransactionSide.RECEIVE, 132 | tx_asset='B.BTC', 133 | tx_type=TransactionType.BURN_TOKEN, 134 | height='3000', 135 | offset=1, 136 | limit=100, 137 | start_time=2000, 138 | end_time=5000 139 | ) 140 | assert res == {"message": "success"} 141 | 142 | def test_get_klines_url(self, httpclient): 143 | params = { 144 | 'symbol': 'BNB', 145 | 'interval': KlineInterval.ONE_DAY.value, 146 | 'limit': 100, 147 | 'startTime': 2000, 148 | 'endTime': 5000 149 | } 150 | qs = urlencode(params) 151 | with requests_mock.mock() as m: 152 | m.get(httpclient._create_uri('klines') + '?' + qs, json=self.load_fixture('success.json')) 153 | res = httpclient.get_klines( 154 | symbol='BNB', 155 | interval=KlineInterval.ONE_DAY, 156 | limit=100, 157 | start_time=2000, 158 | end_time=5000 159 | ) 160 | assert res == {"message": "success"} 161 | 162 | 163 | class TestAsyncClient: 164 | 165 | @pytest.fixture 166 | def env(self): 167 | return BinanceEnvironment.get_testnet_env() 168 | 169 | @pytest.fixture 170 | @pytest.mark.asyncio 171 | async def httpclient(self, env, event_loop): 172 | return await AsyncHttpApiClient.create(loop=event_loop, env=env, requests_params={'timeout': 1}) 173 | 174 | def load_fixture(self, file_name): 175 | this_dir = os.path.dirname(os.path.realpath(__file__)) 176 | with open(f'{this_dir}/fixtures/{file_name}', 'r') as f: 177 | return json.load(f) 178 | 179 | @pytest.mark.asyncio 180 | async def test_get_time(self, httpclient): 181 | assert await httpclient.get_time() 182 | -------------------------------------------------------------------------------- /tests/test_depthcache.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from binance_chain.depthcache import DepthCache, DepthCacheManager 4 | from binance_chain.environment import BinanceEnvironment 5 | from binance_chain.http import HttpApiClient 6 | 7 | 8 | class TestDepthCache: 9 | 10 | clear_price = "0.00000000" 11 | 12 | def test_init_depth_cache(self): 13 | 14 | symbol = 'BNB_ETH' 15 | dc = DepthCache(symbol='BNB_ETH') 16 | 17 | assert dc.symbol == symbol 18 | assert len(dc.get_asks()) == 0 19 | assert len(dc.get_bids()) == 0 20 | 21 | def test_add_bid(self): 22 | 23 | dc = DepthCache('BNB_ETH') 24 | bid = [1.0, 2.0] 25 | 26 | dc.add_bid(bid) 27 | 28 | assert dc.get_bids() == [bid] 29 | assert len(dc.get_asks()) == 0 30 | 31 | def test_remove_bid(self): 32 | 33 | dc = DepthCache('BNB_ETH') 34 | bid = [1.0, 2.0] 35 | 36 | dc.add_bid(bid) 37 | 38 | bid = [1.0, self.clear_price] 39 | 40 | dc.add_bid(bid) 41 | 42 | assert len(dc.get_bids()) == 0 43 | assert len(dc.get_asks()) == 0 44 | 45 | def test_add_ask(self): 46 | 47 | dc = DepthCache('BNB_ETH') 48 | ask = [1.0, 2.0] 49 | 50 | dc.add_ask(ask) 51 | 52 | assert dc.get_asks() == [ask] 53 | assert len(dc.get_bids()) == 0 54 | 55 | def test_remove_ask(self): 56 | 57 | dc = DepthCache('BNB_ETH') 58 | ask = [1.0, 2.0] 59 | 60 | dc.add_ask(ask) 61 | 62 | ask = [1.0, self.clear_price] 63 | 64 | dc.add_ask(ask) 65 | 66 | assert len(dc.get_bids()) == 0 67 | assert len(dc.get_asks()) == 0 68 | 69 | def test_sorted_bids(self): 70 | 71 | dc = DepthCache('BNB_ETH') 72 | bid = [1.0, 2.0] 73 | 74 | dc.add_bid(bid) 75 | 76 | bid2 = [2.0, 3.0] 77 | 78 | dc.add_bid(bid2) 79 | 80 | assert dc.get_bids() == [bid2, bid] 81 | assert len(dc.get_asks()) == 0 82 | 83 | def test_sorted_asks(self): 84 | 85 | dc = DepthCache('BNB_ETH') 86 | ask = [1.0, 2.0] 87 | 88 | dc.add_ask(ask) 89 | 90 | ask2 = [2.0, 3.0] 91 | 92 | dc.add_ask(ask2) 93 | 94 | assert dc.get_asks() == [ask, ask2] 95 | assert len(dc.get_bids()) == 0 96 | 97 | 98 | class TestDepthCacheConnection: 99 | 100 | @pytest.fixture() 101 | def env(self): 102 | return BinanceEnvironment.get_testnet_env() 103 | 104 | @pytest.fixture 105 | def httpclient(self, env): 106 | return HttpApiClient(env=env) 107 | 108 | @pytest.mark.asyncio 109 | async def test_depthcache_create(self, event_loop, env): 110 | 111 | async def callback(_depth_cache): 112 | pass 113 | 114 | client = HttpApiClient(env=env) 115 | 116 | dcm1 = await DepthCacheManager.create(client, event_loop, "MITH-C76_BNB", callback, env=env) 117 | 118 | assert dcm1 119 | 120 | assert dcm1.get_depth_cache() 121 | 122 | await dcm1.close() 123 | -------------------------------------------------------------------------------- /tests/test_environment.py: -------------------------------------------------------------------------------- 1 | from binance_chain.environment import BinanceEnvironment 2 | 3 | 4 | class TestEnvironment: 5 | 6 | def test_prod_environment(self): 7 | 8 | env = BinanceEnvironment() 9 | 10 | assert env.hrp == BinanceEnvironment.PROD_ENV['hrp'] 11 | assert env.wss_url == BinanceEnvironment.PROD_ENV['wss_url'] 12 | assert env.api_url == BinanceEnvironment.PROD_ENV['api_url'] 13 | 14 | prod_env = BinanceEnvironment.get_production_env() 15 | 16 | assert prod_env.hrp == BinanceEnvironment.PROD_ENV['hrp'] 17 | assert prod_env.wss_url == BinanceEnvironment.PROD_ENV['wss_url'] 18 | assert prod_env.api_url == BinanceEnvironment.PROD_ENV['api_url'] 19 | 20 | def test_testnet_environment(self): 21 | env = BinanceEnvironment.get_testnet_env() 22 | 23 | assert env.hrp == BinanceEnvironment.TESTNET_ENV['hrp'] 24 | assert env.wss_url == BinanceEnvironment.TESTNET_ENV['wss_url'] 25 | assert env.api_url == BinanceEnvironment.TESTNET_ENV['api_url'] 26 | 27 | def test_custom_environment(self): 28 | 29 | api_url = 'my_api_url' 30 | wss_url = 'my_wss_url' 31 | hrp = 'my_hrp' 32 | env = BinanceEnvironment(api_url, wss_url, hrp) 33 | 34 | assert env.hrp == hrp 35 | assert env.wss_url == wss_url 36 | assert env.api_url == api_url 37 | -------------------------------------------------------------------------------- /tests/test_messages.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import binascii 3 | from collections import OrderedDict 4 | 5 | from binance_chain.messages import PubKeyMsg, NewOrderMsg, CancelOrderMsg, TransferMsg 6 | from binance_chain.environment import BinanceEnvironment 7 | from binance_chain.wallet import Wallet 8 | from binance_chain.utils.encode_utils import varint_encode 9 | from binance_chain.constants import OrderType, OrderSide, TimeInForce 10 | 11 | 12 | class TestMessages: 13 | 14 | @pytest.fixture() 15 | def private_key(self): 16 | return '3dcc267e1f7edca86e03f0963b2d0b7804552d3014caddcfc435a4d7bc240cf5' 17 | 18 | @pytest.fixture() 19 | def mnemonic(self): 20 | return ('smart depend recycle toward already roof country frost field dose joke zero start notable vote ' 21 | 'eight symptom suffer camp milk dream swear wrap accident') 22 | 23 | @pytest.fixture() 24 | def env(self): 25 | return BinanceEnvironment.get_testnet_env() 26 | 27 | @pytest.fixture() 28 | def wallet(self, private_key, env): 29 | wallet = Wallet(private_key=private_key, env=env) 30 | wallet._address = 'tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5rr' 31 | return wallet 32 | 33 | def test_public_key_message_protobuf(self, wallet): 34 | 35 | pkm = PubKeyMsg(wallet) 36 | 37 | assert pkm.to_protobuf() == wallet.public_key 38 | 39 | def test_public_key_message_amino(self, wallet): 40 | 41 | pkm = PubKeyMsg(wallet) 42 | 43 | type_bytes = binascii.unhexlify(PubKeyMsg.AMINO_MESSAGE_TYPE) 44 | len_bytes = varint_encode(len(wallet.public_key)) 45 | 46 | assert pkm.to_amino() == type_bytes + len_bytes + wallet.public_key 47 | 48 | def test_new_order_message_dict(self, wallet): 49 | 50 | wallet._account_number = 23452 51 | wallet._sequence = 2 52 | 53 | symbol = 'ANN-457_BNB' 54 | order_type = OrderType.LIMIT 55 | order_type_expected = 2 56 | order_side = OrderSide.BUY 57 | order_side_expected = 1 58 | price = 0.000396000 59 | expected_price = 39600 60 | quantity = 10 61 | expected_quantity = 1000000000 62 | time_in_force = TimeInForce.GOOD_TILL_EXPIRE 63 | time_in_force_expected = 1 64 | 65 | msg = NewOrderMsg( 66 | wallet=wallet, 67 | symbol=symbol, 68 | order_type=order_type, 69 | side=order_side, 70 | price=price, 71 | quantity=quantity, 72 | time_in_force=time_in_force 73 | ) 74 | 75 | expected = OrderedDict([ 76 | ('id', wallet.generate_order_id()), 77 | ('ordertype', order_type_expected), 78 | ('price', expected_price), 79 | ('quantity', expected_quantity), 80 | ('sender', wallet.address), 81 | ('side', order_side_expected), 82 | ('symbol', symbol), 83 | ('timeinforce', time_in_force_expected), 84 | ]) 85 | 86 | assert msg.to_dict() == expected 87 | 88 | def test_new_order_message_hex(self, wallet): 89 | 90 | wallet._account_number = 23452 91 | wallet._sequence = 2 92 | 93 | symbol = 'ANN-457_BNB' 94 | order_type = OrderType.LIMIT 95 | order_side = OrderSide.BUY 96 | price = 0.000396000 97 | quantity = 10 98 | time_in_force = TimeInForce.GOOD_TILL_EXPIRE 99 | 100 | msg = NewOrderMsg( 101 | wallet=wallet, 102 | symbol=symbol, 103 | order_type=order_type, 104 | side=order_side, 105 | price=price, 106 | quantity=quantity, 107 | time_in_force=time_in_force 108 | ) 109 | 110 | expected = (b'db01f0625dee0a63ce6dc0430a147f756b1be93aa2e2fdc3d7cb713abc206f877802122a3746373536423142453' 111 | b'93341413245324644433344374342373133414243323036463837373830322d331a0b414e4e2d3435375f424e42' 112 | b'2002280130b0b502388094ebdc03400112700a26eb5ae9872102cce2ee4e37dc8c65d6445c966faf31ebfe578a9' 113 | b'0695138947ee7cab8ae9a2c081240ad219de59c60637d642a684a9d56e4a2d189a2bb2a9028d0f4664206540bee' 114 | b'763a62c3ce0ea0069f6e81f0733791c9b2a0e883c66aa6cc732f8ce92829e0b278189cb7012002') 115 | 116 | print(msg.to_hex_data()) 117 | 118 | assert msg.to_hex_data() == expected 119 | 120 | def test_cancel_order_message_dict(self, wallet): 121 | 122 | wallet._account_number = 23452 123 | wallet._sequence = 2 124 | 125 | symbol = 'ANN-457_BNB' 126 | order_id = '7F756B1BE93AA2E2FDC3D7CB713ABC206F877802-3' 127 | 128 | msg = CancelOrderMsg( 129 | wallet=wallet, 130 | symbol=symbol, 131 | order_id=order_id 132 | ) 133 | 134 | expected = OrderedDict([ 135 | ('refid', order_id), 136 | ('sender', wallet.address), 137 | ('symbol', symbol), 138 | ]) 139 | 140 | assert msg.to_dict() == expected 141 | 142 | def test_cancel_order_message_hex(self, wallet): 143 | 144 | wallet._account_number = 23452 145 | wallet._sequence = 2 146 | 147 | symbol = 'ANN-457_BNB' 148 | order_id = '7F756B1BE93AA2E2FDC3D7CB713ABC206F877802-3' 149 | 150 | msg = CancelOrderMsg( 151 | wallet=wallet, 152 | symbol=symbol, 153 | order_id=order_id 154 | ) 155 | 156 | print(msg.to_hex_data()) 157 | expected = (b'cb01f0625dee0a53166e681b0a147f756b1be93aa2e2fdc3d7cb713abc206f877802120b414e4e2d3435375f424e' 158 | b'421a2a374637353642314245393341413245324644433344374342373133414243323036463837373830322d3312' 159 | b'700a26eb5ae9872102cce2ee4e37dc8c65d6445c966faf31ebfe578a90695138947ee7cab8ae9a2c08124031640a' 160 | b'1f18daef6a36aeef358ebe559ae34b97c73947915abad91b5c244426ce1130b0425c73d6ebdc7720e2c48613d8e8' 161 | b'26a7434e3899e317543f25bf6f63c1189cb7012002') 162 | 163 | assert msg.to_hex_data() == expected 164 | 165 | def test_transfer_message_hex(self, wallet): 166 | 167 | wallet._account_number = 23452 168 | wallet._sequence = 2 169 | 170 | symbol = 'BNB' 171 | to_address = 'tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5rr' 172 | amount = 1 173 | 174 | msg = TransferMsg( 175 | wallet=wallet, 176 | symbol=symbol, 177 | to_address=to_address, 178 | amount=amount 179 | ) 180 | 181 | expected = (b'c401f0625dee0a4c2a2c87fa0a220a147f756b1be93aa2e2fdc3d7cb713abc206f877802120a0a03424e421080c2d7' 182 | b'2f12220a147f756b1be93aa2e2fdc3d7cb713abc206f877802120a0a03424e421080c2d72f12700a26eb5ae9872102' 183 | b'cce2ee4e37dc8c65d6445c966faf31ebfe578a90695138947ee7cab8ae9a2c081240d3dab13ba287b21467e735f2db' 184 | b'5385479ada835d22cb59721682e53e696d807c6401ae48c51a898582baec46a1906d9448b5150f7c0322181101257d' 185 | b'cc917fd2189cb7012002') 186 | 187 | assert msg.to_hex_data() == expected 188 | -------------------------------------------------------------------------------- /tests/test_rpc_client.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import pytest 4 | 5 | from binance_chain.http import HttpApiClient 6 | from binance_chain.node_rpc.http import HttpRpcClient, AsyncHttpRpcClient 7 | from binance_chain.node_rpc.request import RpcRequest 8 | from binance_chain.environment import BinanceEnvironment 9 | from binance_chain.wallet import Wallet 10 | from binance_chain.constants import PeerType 11 | 12 | 13 | class TestHttpRpcClient: 14 | 15 | peers = None 16 | 17 | @pytest.fixture 18 | def env(self): 19 | return BinanceEnvironment.get_testnet_env() 20 | 21 | @pytest.fixture 22 | def httpclient(self, env): 23 | return HttpApiClient(env=env) 24 | 25 | @pytest.fixture 26 | def private_key(self): 27 | return '3dcc267e1f7edca86e03f0963b2d0b7804552d3014caddcfc435a4d7bc240cf5' 28 | 29 | @pytest.fixture 30 | def wallet(self, env, private_key): 31 | return Wallet(private_key=private_key, env=env) 32 | 33 | @pytest.fixture 34 | def listen_address(self, httpclient): 35 | if not self.peers: 36 | self.peers = httpclient.get_peers(peer_type=PeerType.NODE) 37 | return self.peers[0]['listen_addr'] 38 | 39 | @pytest.fixture 40 | def rpcclient(self, listen_address): 41 | return HttpRpcClient(endpoint_url=listen_address) 42 | 43 | def test_get_abci_info(self, rpcclient): 44 | assert rpcclient.get_abci_info() 45 | 46 | def test_get_block(self, rpcclient): 47 | assert rpcclient.get_block() 48 | 49 | assert rpcclient.get_block(10000) 50 | 51 | 52 | class TestAsyncHttpRpcClient: 53 | 54 | peers = None 55 | 56 | @pytest.fixture 57 | def env(self): 58 | return BinanceEnvironment.get_testnet_env() 59 | 60 | @pytest.fixture 61 | def httpclient(self, env): 62 | return HttpApiClient(env=env) 63 | 64 | @pytest.fixture 65 | def private_key(self): 66 | return '3dcc267e1f7edca86e03f0963b2d0b7804552d3014caddcfc435a4d7bc240cf5' 67 | 68 | @pytest.fixture 69 | def wallet(self, env, private_key): 70 | return Wallet(private_key=private_key, env=env) 71 | 72 | @pytest.fixture 73 | def listen_address(self, httpclient): 74 | if not self.peers: 75 | self.peers = httpclient.get_peers(peer_type=PeerType.NODE) 76 | return self.peers[0]['listen_addr'] 77 | 78 | @pytest.mark.asyncio 79 | @pytest.fixture 80 | async def rpcclient(self, listen_address): 81 | return await AsyncHttpRpcClient.create(endpoint_url=listen_address) 82 | 83 | @pytest.mark.asyncio 84 | async def test_get_abci_info(self, rpcclient): 85 | assert await rpcclient.get_abci_info() 86 | 87 | @pytest.mark.asyncio 88 | async def test_get_block(self, rpcclient): 89 | assert await rpcclient.get_block() 90 | 91 | assert await rpcclient.get_block(10000) 92 | 93 | 94 | class TestRpcRequest: 95 | 96 | def test_json_string_building(self): 97 | # reset id count 98 | RpcRequest.id_generator = itertools.count(1) 99 | req = RpcRequest("mymethod", 1, {'param1': 'this', 'otherparam': 'that'}) 100 | 101 | assert str(req) == '{"jsonrpc":"2.0","method":"mymethod","params":{"param1":"this","otherparam":"that"},"id":1}' 102 | 103 | req2 = RpcRequest("mymethod", 2, {'param1': 'this', 'otherparam': 'that'}) 104 | assert str(req2) == \ 105 | '{"jsonrpc":"2.0","method":"mymethod","params":{"param1":"this","otherparam":"that"},"id":2}' 106 | -------------------------------------------------------------------------------- /tests/test_rpc_pooled.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from binance_chain.node_rpc.pooled_client import PooledRpcClient 4 | from binance_chain.environment import BinanceEnvironment 5 | 6 | 7 | class TestRpcPooled: 8 | 9 | @pytest.fixture() 10 | def env(self): 11 | return BinanceEnvironment.get_testnet_env() 12 | 13 | @pytest.mark.asyncio 14 | async def test_rpc_pooled_create(self, event_loop, env): 15 | 16 | prc = await PooledRpcClient.create(loop=event_loop, env=env) 17 | 18 | assert prc 19 | 20 | await prc.get_consensus_state() 21 | await prc.get_blockchain_info(1, 1000) 22 | await prc.get_abci_info() 23 | -------------------------------------------------------------------------------- /tests/test_rpc_websockets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | 4 | from binance_chain.http import HttpApiClient 5 | from binance_chain.node_rpc.websockets import ReconnectingRpcWebsocket, WebsocketRpcClient 6 | from binance_chain.environment import BinanceEnvironment 7 | 8 | 9 | class TestRpcWebsockets: 10 | 11 | peers = None 12 | 13 | @pytest.fixture() 14 | def env(self): 15 | return BinanceEnvironment.get_testnet_env() 16 | 17 | @pytest.fixture 18 | def httpclient(self, env): 19 | return HttpApiClient(env=env) 20 | 21 | @pytest.fixture 22 | def listen_address(self, httpclient): 23 | if not self.peers: 24 | self.peers = httpclient.get_node_peers() 25 | return self.peers[0]['listen_addr'] 26 | 27 | @pytest.fixture() 28 | def ws_env(self, env, listen_address): 29 | listen_address = listen_address.replace('http://', 'ws://') 30 | listen_address = listen_address.replace('https://', 'wss://') 31 | return BinanceEnvironment(env.api_url, listen_address, env.hrp) 32 | 33 | @pytest.mark.asyncio 34 | async def test_rpc_websocket_connects(self, event_loop, ws_env, listen_address): 35 | 36 | async def callback(): 37 | pass 38 | 39 | socket = ReconnectingRpcWebsocket(event_loop, callback, ws_env) 40 | 41 | await asyncio.sleep(2) 42 | 43 | assert socket._socket is not None 44 | 45 | @pytest.mark.asyncio 46 | async def test_rpc_websocket_create(self, event_loop, env): 47 | 48 | async def callback(): 49 | pass 50 | 51 | wrc = await WebsocketRpcClient.create(event_loop, callback, env=env) 52 | 53 | assert wrc 54 | 55 | await wrc.tx_search("query", True, 1, 10) 56 | await wrc.get_blockchain_info(1, 1000) 57 | await wrc.abci_query("data_str", "path_str", True, 1000) 58 | -------------------------------------------------------------------------------- /tests/test_signing.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | 4 | from binance_chain.signing.http import HttpApiSigningClient, AsyncHttpApiSigningClient 5 | 6 | 7 | class TestHttpSigningClient: 8 | 9 | @mock.patch('binance_chain.signing.http.HttpApiSigningClient.authenticate') 10 | def test_initialise(self, _mocker): 11 | 12 | assert HttpApiSigningClient('https://binance-signing-service.com', 'sam', 'mypass') 13 | 14 | 15 | class TestAsyncHttpSigningClient: 16 | 17 | @mock.patch('binance_chain.signing.http.AsyncHttpApiSigningClient.authenticate') 18 | @pytest.mark.asyncio 19 | async def test_initialise(self, _mocker, event_loop): 20 | 21 | assert await AsyncHttpApiSigningClient.create('https://binance-signing-service.com', 'sam', 'mypass') 22 | 23 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | import binascii 3 | 4 | import pytest 5 | 6 | from binance_chain.utils.encode_utils import encode_number, varint_encode 7 | from binance_chain.utils.segwit_addr import decode_address, address_from_public_key 8 | 9 | 10 | @pytest.mark.parametrize("num, expected", [ 11 | (10, 1000000000), 12 | (0.08992342, 8992342), 13 | (Decimal('0.08992342'), 8992342), 14 | (Decimal('0.99999999'), 99999999), 15 | (Decimal(10), 1000000000), 16 | ]) 17 | def test_encode_correct(num, expected): 18 | assert encode_number(num) == expected 19 | 20 | 21 | @pytest.mark.parametrize("num, expected", [ 22 | (10, b'\n'), 23 | (100, b'd'), 24 | (64, b'@'), 25 | (3542, b'\xd6\x1b'), 26 | ]) 27 | def test_varint_encode_correct(num, expected): 28 | assert varint_encode(num) == expected 29 | 30 | 31 | def test_decode_address(): 32 | expected = binascii.unhexlify(b'7f756b1be93aa2e2fdc3d7cb713abc206f877802') 33 | assert decode_address('tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5rr') == expected 34 | 35 | 36 | def test_decode_invalid_address(): 37 | assert decode_address('tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5') is None 38 | 39 | 40 | def test_address_from_public_key(): 41 | public_key_hex = b'02cce2ee4e37dc8c65d6445c966faf31ebfe578a90695138947ee7cab8ae9a2c08' 42 | assert address_from_public_key(public_key_hex) == 'tbnb1csdyysz0xqas7dlq754flfsmey8jwaxwgwaqdx' 43 | -------------------------------------------------------------------------------- /tests/test_wallet.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from binance_chain.wallet import Wallet 4 | from binance_chain.environment import BinanceEnvironment 5 | from binance_chain.http import HttpApiClient 6 | 7 | 8 | class TestWallet: 9 | 10 | @pytest.fixture() 11 | def private_key(self): 12 | return '3dcc267e1f7edca86e03f0963b2d0b7804552d3014caddcfc435a4d7bc240cf5' 13 | 14 | @pytest.fixture() 15 | def mnemonic(self): 16 | return ('smart depend recycle toward already roof country frost field dose joke zero start notable vote ' 17 | 'eight symptom suffer camp milk dream swear wrap accident') 18 | 19 | @pytest.fixture() 20 | def env(self): 21 | return BinanceEnvironment.get_testnet_env() 22 | 23 | def test_wallet_create_from_private_key(self, private_key, env): 24 | wallet = Wallet(private_key=private_key, env=env) 25 | 26 | assert wallet 27 | assert wallet.public_key_hex == b'02cce2ee4e37dc8c65d6445c966faf31ebfe578a90695138947ee7cab8ae9a2c08' 28 | assert wallet.address == 'tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5rr' 29 | 30 | def test_wallet_initialise(self, private_key, env): 31 | 32 | wallet = Wallet(private_key=private_key, env=env) 33 | 34 | wallet.initialise_wallet() 35 | 36 | assert wallet.sequence is not None 37 | assert wallet.account_number is not None 38 | assert wallet.chain_id is not None 39 | 40 | def test_initialise_from_mnemonic(self, private_key, mnemonic, env): 41 | 42 | wallet = Wallet.create_wallet_from_mnemonic(mnemonic, env=env) 43 | 44 | assert wallet.private_key == private_key 45 | assert wallet.public_key_hex == b'02cce2ee4e37dc8c65d6445c966faf31ebfe578a90695138947ee7cab8ae9a2c08' 46 | assert wallet.address == 'tbnb10a6kkxlf823w9lwr6l9hzw4uyphcw7qzrud5rr' 47 | 48 | def test_initialise_from_random_mnemonic(self, env): 49 | wallet = Wallet.create_random_wallet(env=env) 50 | 51 | assert wallet 52 | assert wallet.private_key is not None 53 | assert wallet.public_key is not None 54 | assert wallet.public_key_hex is not None 55 | assert wallet.address is not None 56 | 57 | def test_wallet_sequence_increment(self, private_key, env): 58 | 59 | wallet = Wallet(private_key=private_key, env=env) 60 | 61 | wallet._sequence = 100 62 | 63 | wallet.increment_account_sequence() 64 | 65 | assert wallet.sequence == 101 66 | 67 | def test_wallet_sequence_decrement(self, private_key, env): 68 | wallet = Wallet(private_key=private_key, env=env) 69 | 70 | wallet._sequence = 100 71 | 72 | wallet.decrement_account_sequence() 73 | 74 | assert wallet.sequence == 99 75 | 76 | def test_wallet_reload_sequence(self, private_key, env): 77 | wallet = Wallet(private_key=private_key, env=env) 78 | 79 | wallet.initialise_wallet() 80 | account_sequence = wallet.sequence 81 | 82 | wallet.increment_account_sequence() 83 | 84 | wallet.reload_account_sequence() 85 | 86 | assert wallet.sequence == account_sequence 87 | 88 | def test_generate_order_id(self, private_key, env): 89 | wallet = Wallet(private_key=private_key, env=env) 90 | 91 | wallet.initialise_wallet() 92 | 93 | order_id = wallet.generate_order_id() 94 | 95 | assert order_id == f"7F756B1BE93AA2E2FDC3D7CB713ABC206F877802-{wallet.sequence + 1}" 96 | 97 | def test_sign_message(self, private_key, env): 98 | wallet = Wallet(private_key=private_key, env=env) 99 | 100 | expected = (b'\xd9\x01\x02\xab\x13\xfd^4Ge\x82\xa9\xee\x82\xb5\x8c\xa9\x97}\xf9t\xa9\xc7\nC\xee\xfd\x8bG' 101 | b'\x95N\xe84\xfc\x17\xc0JE\x9a.\xe2\xbb\xa3\x14\xde$\x07\t\xbbB\xeb\xe2\xfb\x1e\xa1dc\x9d\xba' 102 | b'\xd2\xfa\xe3\xb6\xc1') 103 | assert wallet.sign_message(b"testmessage") == expected 104 | -------------------------------------------------------------------------------- /tests/test_websockets.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import asyncio 3 | 4 | from binance_chain.websockets import BinanceChainSocketManager, ReconnectingWebsocket 5 | from binance_chain.environment import BinanceEnvironment 6 | 7 | 8 | class TestWebsockets: 9 | 10 | @pytest.fixture() 11 | def env(self): 12 | return BinanceEnvironment.get_testnet_env() 13 | 14 | @pytest.mark.asyncio 15 | async def test_websocket_connects(self, event_loop, env): 16 | 17 | async def callback(): 18 | pass 19 | 20 | socket = ReconnectingWebsocket(event_loop, callback, env) 21 | 22 | await asyncio.sleep(2) 23 | 24 | assert socket._socket is not None 25 | 26 | @pytest.mark.asyncio 27 | async def test_websocket_create(self, event_loop, env): 28 | 29 | async def callback(): 30 | pass 31 | 32 | bcsm = await BinanceChainSocketManager.create(event_loop, callback, env=env) 33 | 34 | assert bcsm 35 | 36 | await bcsm.close_connection() 37 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37 3 | 4 | [testenv] 5 | deps = 6 | -rtest-requirements.txt 7 | -rrequirements.txt 8 | commands = py.test -v tests/ --doctest-modules --cov binance_chain --cov-report term-missing 9 | passenv = 10 | TRAVIS 11 | TRAVIS_BRANCH 12 | TRAVIS_JOB_ID 13 | 14 | [testenv:flake8] 15 | commands = flake8 --exclude binance_chain/protobuf/dex_pb2.py binance_chain/ 16 | deps = flake8 17 | 18 | [travis] 19 | python = 20 | 3.6: py36, flake8 21 | 22 | [flake8] 23 | exclude = 24 | .git, 25 | .tox, 26 | build, 27 | dist 28 | ignore = E501 29 | 30 | [pep8] 31 | ignore = E501 32 | --------------------------------------------------------------------------------