diff --git a/bit/network/services.py b/bit/network/services.py index f46bf7e..7527aba 100644 --- a/bit/network/services.py +++ b/bit/network/services.py @@ -7,6 +7,8 @@ from bit.network import currency_to_satoshi from bit.network.meta import Unspent from bit.exceptions import BitcoinNodeException +from bit.transaction import address_to_scriptpubkey +from bit.utils import bytes_to_hex DEFAULT_TIMEOUT = 10 @@ -102,6 +104,500 @@ def __call__(self, *args): return responseJSON["result"] +class BTCcomAPI: + ENDPOINT = 'https://chain.api.btc.com/v3/' + ADDRESS_API = ENDPOINT + 'address/{}' + TX_API = ENDPOINT + 'tx/{}' + TX_PUSH_API = ENDPOINT + 'tools/tx-publish' + TX_PUSH_PARAM = 'rawhex' + fake_ua = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 OPR/45.0.2552.882' + } + + @classmethod + def get_balance(cls, address): + r = requests.get(cls.ADDRESS_API.format(address), timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'] + if not response: + return 0 + return response['balance'] + + @classmethod + def get_transactions(cls, address): + endpoint = cls.ADDRESS_API + '/tx' + + transactions = [] + offset = 1 + txs_per_page = 50 + payload = {'page': str(offset), 'pagelimit': str(txs_per_page)} + + r = requests.get(endpoint.format(address), timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + if not response['err_no']: + response = response['data'] + total_txs = response['total_count'] + + while total_txs > 0: + transactions.extend(tx['hash'] for tx in response['list']) + + total_txs -= txs_per_page + offset += 1 + payload['page'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'] + + return transactions + + @classmethod + def get_transaction_by_id(cls, txid): + r = requests.get(cls.TX_API.format(txid) + '/raw', timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + + response = r.json() + if response['err_no']: + return None + return response['data'] + + @classmethod + def get_unspent(cls, address): + # Store scriptPubKey of address + script_pubkey = bytes_to_hex(address_to_scriptpubkey(address)) + + endpoint = cls.ADDRESS_API + '/unspent' + + unspents = [] + offset = 1 + unspents_per_page = 50 + payload = {'page': str(offset), 'pagesize': str(unspents_per_page)} + + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + if not response['err_no']: + response = response['data']['list'] + total_unspents = len(response) + + while total_unspents > 0: + unspents.extend( + Unspent(utxo['value'], utxo['confirmations'], script_pubkey, utxo['tx_hash'], utxo['tx_output_n']) + for utxo in response + ) + + offset += 1 + payload['page'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data']['list'] + total_unspents = len(response) + + return unspents[::-1] + + @classmethod + def broadcast_tx(cls, tx_hex): # pragma: no cover + r = requests.post( + cls.TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT, headers=cls.fake_ua + ) + return True if r.status_code == 200 else False + + +class BlockchairAPI: + MAIN_ENDPOINT = 'https://api.blockchair.com/bitcoin/' + MAIN_ADDRESS_API = MAIN_ENDPOINT + 'dashboards/address/{}' + MAIN_TX_PUSH_API = MAIN_ENDPOINT + 'dashboards/push/transaction' + MAIN_TX_API = MAIN_ENDPOINT + 'raw/transaction/{}' + TEST_ENDPOINT = 'https://api.blockchair.com/bitcoin/testnet/' + TEST_ADDRESS_API = TEST_ENDPOINT + 'dashboards/address/{}' + TEST_TX_PUSH_API = TEST_ENDPOINT + 'dashboards/push/transaction' + TEST_TX_API = TEST_ENDPOINT + 'raw/transaction/{}' + TX_PUSH_PARAM = 'data' + + @classmethod + def get_balance(cls, address): + r = requests.get(cls.MAIN_ADDRESS_API.format(address), timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + return r.json()['data'][address]['address']['balance'] + + @classmethod + def get_balance_testnet(cls, address): + r = requests.get(cls.TEST_ADDRESS_API.format(address), timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + return r.json()['data'][address]['address']['balance'] + + @classmethod + def get_transactions(cls, address): + endpoint = cls.MAIN_ADDRESS_API + + transactions = [] + offset = 0 + txs_per_page = 1000 + payload = {'offset': str(offset), 'limit': str(txs_per_page)} + + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return [] + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + response = response['data'][address] + total_txs = response['address']['transaction_count'] + + while total_txs > 0: + transactions.extend(tx for tx in response['transactions']) + + total_txs -= txs_per_page + offset += txs_per_page + payload['offset'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'][address] + + return transactions + + @classmethod + def get_transactions_testnet(cls, address): + endpoint = cls.TEST_ADDRESS_API + + transactions = [] + offset = 0 + txs_per_page = 1000 + payload = {'offset': str(offset), 'limit': str(txs_per_page)} + + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return [] + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + response = response['data'][address] + total_txs = response['address']['transaction_count'] + + while total_txs > 0: + transactions.extend(tx for tx in response['transactions']) + + total_txs -= txs_per_page + offset += txs_per_page + payload['offset'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'][address] + + return transactions + + @classmethod + def get_transaction_by_id(cls, txid): + r = requests.get(cls.MAIN_TX_API.format(txid), timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + + response = r.json()['data'] + if not response: + return None + return response[txid]['raw_transaction'] + + @classmethod + def get_transaction_by_id_testnet(cls, txid): + r = requests.get(cls.TEST_TX_API.format(txid), timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + + response = r.json()['data'] + if not response: + return None + return response[txid]['raw_transaction'] + + @classmethod + def get_unspent(cls, address): + endpoint = cls.MAIN_ADDRESS_API + + unspents = [] + offset = 0 + unspents_per_page = 1000 + payload = {'offset': str(offset), 'limit': str(unspents_per_page)} + + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + block_height = response['context']['state'] + response = response['data'][address] + script_pubkey = response['address']['script_hex'] + total_unspents = response['address']['unspent_output_count'] + + while total_unspents > 0: + unspents.extend( + Unspent( + utxo['value'], + block_height - utxo['block_id'], + script_pubkey, + utxo['transaction_hash'], + utxo['index'], + ) + for utxo in response['utxo'] + ) + + total_unspents -= unspents_per_page + offset += unspents_per_page + payload['offset'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'][address] + + return unspents + + @classmethod + def get_unspent_testnet(cls, address): + endpoint = cls.TEST_ADDRESS_API + + unspents = [] + offset = 0 + unspents_per_page = 1000 + payload = {'offset': str(offset), 'limit': unspents_per_page} + + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + block_height = response['context']['state'] + response = response['data'][address] + script_pubkey = response['address']['script_hex'] + total_unspents = response['address']['unspent_output_count'] + + while total_unspents > 0: + unspents.extend( + Unspent( + utxo['value'], + block_height - utxo['block_id'], + script_pubkey, + utxo['transaction_hash'], + utxo['index'], + ) + for utxo in response['utxo'] + ) + + total_unspents -= unspents_per_page + offset += unspents_per_page + payload['offset'] = str(offset) + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['data'][address] + + return unspents + + @classmethod + def broadcast_tx( + cls, tx_hex, + ): # pragma: no cover + r = requests.post(cls.MAIN_TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT) + return True if r.status_code == 200 else False + + @classmethod + def broadcast_tx_testnet(cls, tx_hex): # pragma: no cover + r = requests.post(cls.TEST_TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT) + return True if r.status_code == 200 else False + + +class BlockstreamAPI: + MAIN_ENDPOINT = 'https://blockstream.info/api/' + MAIN_ADDRESS_API = MAIN_ENDPOINT + 'address/{}' + MAIN_UNSPENT_API = MAIN_ADDRESS_API + '/utxo' + MAIN_TX_PUSH_API = MAIN_ENDPOINT + 'tx' + MAIN_TX_API = MAIN_ENDPOINT + 'tx/{}/hex' + TEST_ENDPOINT = 'https://blockstream.info/testnet/api/' + TEST_ADDRESS_API = TEST_ENDPOINT + 'address/{}' + TEST_UNSPENT_API = TEST_ADDRESS_API + '/utxo' + TEST_TX_PUSH_API = TEST_ENDPOINT + 'tx' + TEST_TX_API = TEST_ENDPOINT + 'tx/{}/hex' + TX_PUSH_PARAM = 'data' + + @classmethod + def get_balance(cls, address): + r = requests.get(cls.MAIN_ADDRESS_API.format(address), timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + funded = response['chain_stats']['funded_txo_sum'] + response['mempool_stats']['funded_txo_sum'] + spent = response['chain_stats']['spent_txo_sum'] + response['mempool_stats']['spent_txo_sum'] + return funded - spent + + @classmethod + def get_balance_testnet(cls, address): + r = requests.get(cls.TEST_ADDRESS_API.format(address), timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + funded = response['chain_stats']['funded_txo_sum'] + response['mempool_stats']['funded_txo_sum'] + spent = response['chain_stats']['spent_txo_sum'] + response['mempool_stats']['spent_txo_sum'] + return funded - spent + + @classmethod + def get_transactions( + cls, address, + ): + endpoint = cls.MAIN_ADDRESS_API + '/txs/chain/{}' + + transactions = [] + + r = requests.get(endpoint.format(address, ''), timeout=DEFAULT_TIMEOUT) + if r.status_code == 400: + return [] + elif r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + # The first 50 mempool and 25 confirmed transactions are shown with no + # indication of the number of total transactions. + total_txs = len(response) + + while total_txs > 0: + transactions.extend(tx['txid'] for tx in response) + + response = requests.get(endpoint.format(address, transactions[-1]), timeout=DEFAULT_TIMEOUT).json() + total_txs = len(response) + + return transactions + + @classmethod + def get_transactions_testnet(cls, address): + endpoint = cls.TEST_ADDRESS_API + '/txs/chain/{}' + + transactions = [] + + r = requests.get(endpoint.format(address, ''), timeout=DEFAULT_TIMEOUT) + if r.status_code == 400: + return [] + elif r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + + # The first 50 mempool and 25 confirmed transactions are shown with no + # indication of the number of total transactions. + total_txs = len(response) + + while total_txs > 0: + transactions.extend(tx['txid'] for tx in response) + + response = requests.get(endpoint.format(address, transactions[-1]), timeout=DEFAULT_TIMEOUT).json() + total_txs = len(response) + + return transactions + + @classmethod + def get_transaction_by_id(cls, txid): + r = requests.get(cls.MAIN_TX_API.format(txid), timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + return r.text + + @classmethod + def get_transaction_by_id_testnet(cls, txid): + r = requests.get(cls.TEST_TX_API.format(txid), timeout=DEFAULT_TIMEOUT) + if r.status_code == 404: + return None + if r.status_code != 200: # pragma: no cover + raise ConnectionError + return r.text + + @classmethod + def get_unspent(cls, address): + # Get current block height: + r_block = requests.get(cls.MAIN_ENDPOINT + 'blocks/tip/height', timeout=DEFAULT_TIMEOUT) + if r_block.status_code != 200: # pragma: no cover + raise ConnectionError + block_height = int(r_block.text) + + r = requests.get(cls.MAIN_UNSPENT_API.format(address), timeout=DEFAULT_TIMEOUT) + + if r.status_code == 400: + return [] + elif r.status_code != 200: # pragma: no cover + raise ConnectionError + + script_pubkey = bytes_to_hex(address_to_scriptpubkey(address)) + + return sorted( + [ + Unspent( + tx["value"], + block_height - tx["status"]["block_height"] if tx["status"]["confirmed"] else 0, + script_pubkey, + tx["txid"], + tx["vout"], + ) + for tx in r.json() + ], + key=lambda u: u.confirmations, + ) + + @classmethod + def get_unspent_testnet(cls, address): + # Get current block height: + r_block = requests.get(cls.TEST_ENDPOINT + 'blocks/tip/height', timeout=DEFAULT_TIMEOUT) + if r_block.status_code != 200: # pragma: no cover + raise ConnectionError + block_height = int(r_block.text) + + r = requests.get(cls.TEST_UNSPENT_API.format(address), timeout=DEFAULT_TIMEOUT) + + if r.status_code == 400: + return [] + elif r.status_code != 200: # pragma: no cover + raise ConnectionError + + script_pubkey = bytes_to_hex(address_to_scriptpubkey(address)) + + return [ + Unspent( + tx["value"], + block_height - tx["status"]["block_height"] if tx["status"]["confirmed"] else 0, + script_pubkey, + tx["txid"], + tx["vout"], + ) + for tx in r.json() + ] + + @classmethod + def broadcast_tx(cls, tx_hex): # pragma: no cover + r = requests.post(cls.MAIN_TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT) + return True if r.status_code == 200 else False + + @classmethod + def broadcast_tx_testnet(cls, tx_hex): # pragma: no cover + r = requests.post(cls.TEST_TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT) + return True if r.status_code == 200 else False + + class InsightAPI: MAIN_ENDPOINT = '' MAIN_ADDRESS_API = '' @@ -156,70 +652,109 @@ def broadcast_tx(cls, tx_hex): # pragma: no cover return True if r.status_code == 200 else False -class BitpayAPI(InsightAPI): - MAIN_ENDPOINT = 'https://insight.bitpay.com/api/' - MAIN_ADDRESS_API = MAIN_ENDPOINT + 'addr/' - MAIN_BALANCE_API = MAIN_ADDRESS_API + '{}/balance' - MAIN_UNSPENT_API = MAIN_ADDRESS_API + '{}/utxo' +class BitcoreAPI(InsightAPI): + """ Insight API v8 """ + + MAIN_ENDPOINT = 'https://api.bitcore.io/api/BTC/mainnet/' + MAIN_ADDRESS_API = MAIN_ENDPOINT + 'address/{}' + MAIN_BALANCE_API = MAIN_ADDRESS_API + '/balance' + MAIN_UNSPENT_API = MAIN_ADDRESS_API + '/?unspent=true' MAIN_TX_PUSH_API = MAIN_ENDPOINT + 'tx/send' - MAIN_TX_API = MAIN_ENDPOINT + 'rawtx/' - TEST_ENDPOINT = 'https://test-insight.bitpay.com/api/' - TEST_ADDRESS_API = TEST_ENDPOINT + 'addr/' - TEST_BALANCE_API = TEST_ADDRESS_API + '{}/balance' - TEST_UNSPENT_API = TEST_ADDRESS_API + '{}/utxo' + MAIN_TX_API = MAIN_ENDPOINT + 'tx/{}' + MAIN_TX_AMOUNT_API = MAIN_TX_API + TEST_ENDPOINT = 'https://api.bitcore.io/api/BTC/testnet/' + TEST_ADDRESS_API = TEST_ENDPOINT + 'address/{}' + TEST_BALANCE_API = TEST_ADDRESS_API + '/balance' + TEST_UNSPENT_API = TEST_ADDRESS_API + '/?unspent=true' TEST_TX_PUSH_API = TEST_ENDPOINT + 'tx/send' - TEST_TX_API = TEST_ENDPOINT + 'rawtx/' - TX_PUSH_PARAM = 'rawtx' + TEST_TX_API = TEST_ENDPOINT + 'tx/{}' + TEST_TX_AMOUNT_API = TEST_TX_API + TX_PUSH_PARAM = 'rawTx' @classmethod - def get_balance_testnet(cls, address): - r = requests.get(cls.TEST_BALANCE_API.format(address), timeout=DEFAULT_TIMEOUT) + def get_unspent(cls, address): + endpoint = cls.MAIN_UNSPENT_API + "&limit=100" + + unspents = [] + + r = requests.get(endpoint.format(address), timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - return r.json() + + response = r.json() + + while len(response) > 0: + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'satoshi'), + tx['confirmations'], + tx['script'], + tx['mintTxid'], + tx['mintIndex'], + ) + for tx in response + ) + response = requests.get( + endpoint.format(address) + "&since={}".format(response[-1]['_id']), timeout=DEFAULT_TIMEOUT + ).json() + + return unspents @classmethod - def get_transactions_testnet(cls, address): - r = requests.get(cls.TEST_ADDRESS_API + address, timeout=DEFAULT_TIMEOUT) - if r.status_code != 200: # pragma: no cover - raise ConnectionError - return r.json()['transactions'] + def get_balance(cls, address): + r = requests.get(cls.MAIN_BALANCE_API.format(address), timeout=DEFAULT_TIMEOUT) + r.raise_for_status() # pragma: no cover + return r.json()['balance'] @classmethod - def get_transaction_by_id_testnet(cls, txid): - r = requests.get(cls.TEST_TX_API + txid, timeout=DEFAULT_TIMEOUT) - if r.status_code == 404: - return None - if r.status_code != 200: # pragma: no cover - raise ConnectionError - return r.json()["rawtx"] + def get_balance_testnet(cls, address): + r = requests.get(cls.TEST_BALANCE_API.format(address), timeout=DEFAULT_TIMEOUT) + r.raise_for_status() # pragma: no cover + return r.json()['balance'] @classmethod def get_unspent_testnet(cls, address): - r = requests.get(cls.TEST_UNSPENT_API.format(address), timeout=DEFAULT_TIMEOUT) + endpoint = cls.TEST_UNSPENT_API + "&limit=100" + + unspents = [] + + r = requests.get(endpoint.format(address), timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - return [ - Unspent( - currency_to_satoshi(tx['amount'], 'btc'), - tx['confirmations'], - tx['scriptPubKey'], - tx['txid'], - tx['vout'], + + response = r.json() + + while len(response) > 0: + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'satoshi'), + tx['confirmations'], + tx['script'], + tx['mintTxid'], + tx['mintIndex'], + ) + for tx in response ) - for tx in r.json() - ] + response = requests.get( + endpoint.format(address) + "&since={}".format(response[-1]['_id']), timeout=DEFAULT_TIMEOUT + ).json() + + return unspents @classmethod def broadcast_tx_testnet(cls, tx_hex): # pragma: no cover - r = requests.post(cls.TEST_TX_PUSH_API, data={cls.TX_PUSH_PARAM: tx_hex}, timeout=DEFAULT_TIMEOUT) + r = requests.post( + cls.TEST_TX_PUSH_API, + json={cls.TX_PUSH_PARAM: tx_hex, 'network': 'testnet', 'coin': 'BCH'}, + timeout=DEFAULT_TIMEOUT, + ) return True if r.status_code == 200 else False class BlockchainAPI: ENDPOINT = 'https://blockchain.info/' ADDRESS_API = ENDPOINT + 'address/{}?format=json' - UNSPENT_API = ENDPOINT + 'unspent?active=' + UNSPENT_API = ENDPOINT + 'unspent' TX_PUSH_API = ENDPOINT + 'pushtx' TX_API = ENDPOINT + 'rawtx/' TX_PUSH_PARAM = 'tx' @@ -237,10 +772,13 @@ def get_transactions(cls, address): transactions = [] offset = 0 - payload = {'offset': str(offset)} txs_per_page = 50 + payload = {'offset': str(offset)} - response = requests.get(endpoint.format(address), timeout=DEFAULT_TIMEOUT).json() + r = requests.get(endpoint.format(address), params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() total_txs = response['n_tx'] while total_txs > 0: @@ -264,7 +802,14 @@ def get_transaction_by_id(cls, txid): @classmethod def get_unspent(cls, address): - r = requests.get(cls.UNSPENT_API + address, timeout=DEFAULT_TIMEOUT) + # ! BlockchainAPI only supports up to 1000 UTXOs + endpoint = cls.UNSPENT_API + + offset = 0 + utxos_per_page = 1000 + payload = {'active': address, 'offset': str(offset), 'limit': str(utxos_per_page)} + + r = requests.get(endpoint, params=payload, timeout=DEFAULT_TIMEOUT) if r.status_code == 500: return [] @@ -284,43 +829,55 @@ def broadcast_tx(cls, tx_hex): # pragma: no cover class SmartbitAPI: MAIN_ENDPOINT = 'https://api.smartbit.com.au/v1/blockchain/' - MAIN_ADDRESS_API = MAIN_ENDPOINT + 'address/' - MAIN_UNSPENT_API = MAIN_ADDRESS_API + '{}/unspent' + MAIN_ADDRESS_API = MAIN_ENDPOINT + 'address/{}' + MAIN_UNSPENT_API = MAIN_ADDRESS_API + '/unspent' MAIN_TX_PUSH_API = MAIN_ENDPOINT + 'pushtx' MAIN_TX_API = MAIN_ENDPOINT + 'tx/{}/hex' TEST_ENDPOINT = 'https://testnet-api.smartbit.com.au/v1/blockchain/' - TEST_ADDRESS_API = TEST_ENDPOINT + 'address/' - TEST_UNSPENT_API = TEST_ADDRESS_API + '{}/unspent' + TEST_ADDRESS_API = TEST_ENDPOINT + 'address/{}' + TEST_UNSPENT_API = TEST_ADDRESS_API + '/unspent' TEST_TX_PUSH_API = TEST_ENDPOINT + 'pushtx' TEST_TX_API = TEST_ENDPOINT + 'tx/{}/hex' TX_PUSH_PARAM = 'hex' @classmethod def get_balance(cls, address): - r = requests.get(cls.MAIN_ADDRESS_API + address + '?limit=1', timeout=DEFAULT_TIMEOUT) + r = requests.get(cls.MAIN_ADDRESS_API.format(address), params={'limit': '1'}, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError return r.json()['address']['total']['balance_int'] @classmethod def get_balance_testnet(cls, address): - r = requests.get(cls.TEST_ADDRESS_API + address + '?limit=1', timeout=DEFAULT_TIMEOUT) + r = requests.get(cls.TEST_ADDRESS_API.format(address), params={'limit': '1'}, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError return r.json()['address']['total']['balance_int'] @classmethod def get_transactions(cls, address): - r = requests.get(cls.MAIN_ADDRESS_API + address + '?limit=1000', timeout=DEFAULT_TIMEOUT) + txs_per_page = 1000 + payload = {'limit': str(txs_per_page)} + r = requests.get(cls.MAIN_ADDRESS_API.format(address), params=payload, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - data = r.json()['address'] + response = r.json()['address'] transactions = [] + next_link = None - if 'transactions' in data: - transactions.extend(t['txid'] for t in data['transactions']) + if 'transactions' in response: + transactions.extend(t['txid'] for t in response['transactions']) + next_link = response['transaction_paging']['next_link'] + + while next_link: + r = requests.get(next_link, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['address'] + transactions.extend(t['txid'] for t in response['transactions']) + next_link = response['transaction_paging']['next_link'] return transactions @@ -335,16 +892,28 @@ def get_transaction_by_id(cls, txid): @classmethod def get_transactions_testnet(cls, address): - r = requests.get(cls.TEST_ADDRESS_API + address + '?limit=1000', timeout=DEFAULT_TIMEOUT) + txs_per_page = 1000 + payload = {'limit': str(txs_per_page)} + r = requests.get(cls.TEST_ADDRESS_API.format(address), params=payload, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - data = r.json()['address'] + response = r.json()['address'] transactions = [] + next_link = None + + if 'transactions' in response: + transactions.extend(t['txid'] for t in response['transactions']) + next_link = response['transaction_paging']['next_link'] - if 'transactions' in data: - transactions.extend(t['txid'] for t in data['transactions']) + while next_link: + r = requests.get(next_link, params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json()['address'] + transactions.extend(t['txid'] for t in response['transactions']) + next_link = response['transaction_paging']['next_link'] return transactions @@ -359,35 +928,93 @@ def get_transaction_by_id_testnet(cls, txid): @classmethod def get_unspent(cls, address): - r = requests.get(cls.MAIN_UNSPENT_API.format(address) + '?limit=1000', timeout=DEFAULT_TIMEOUT) + txs_per_page = 1000 + payload = {'limit': str(txs_per_page)} + r = requests.get(cls.MAIN_UNSPENT_API.format(address), params=payload, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - return [ - Unspent( - currency_to_satoshi(tx['value'], 'btc'), - tx['confirmations'], - tx['script_pub_key']['hex'], - tx['txid'], - tx['n'], + + response = r.json() + + unspents = [] + next_link = None + + if 'unspent' in response: + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'btc'), + tx['confirmations'], + tx['script_pub_key']['hex'], + tx['txid'], + tx['n'], + ) + for tx in response['unspent'] ) - for tx in r.json()['unspent'] - ] + next_link = response['paging']['next_link'] + + while next_link: + r = requests.get(next_link, params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'btc'), + tx['confirmations'], + tx['script_pub_key']['hex'], + tx['txid'], + tx['n'], + ) + for tx in response['unspent'] + ) + next_link = response['paging']['next_link'] + + return unspents @classmethod def get_unspent_testnet(cls, address): - r = requests.get(cls.TEST_UNSPENT_API.format(address) + '?limit=1000', timeout=DEFAULT_TIMEOUT) + txs_per_page = 1000 + payload = {'limit': str(txs_per_page)} + r = requests.get(cls.TEST_UNSPENT_API.format(address), params=payload, timeout=DEFAULT_TIMEOUT) if r.status_code != 200: # pragma: no cover raise ConnectionError - return [ - Unspent( - currency_to_satoshi(tx['value'], 'btc'), - tx['confirmations'], - tx['script_pub_key']['hex'], - tx['txid'], - tx['n'], + + response = r.json() + + unspents = [] + next_link = None + + if 'unspent' in response: + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'btc'), + tx['confirmations'], + tx['script_pub_key']['hex'], + tx['txid'], + tx['n'], + ) + for tx in response['unspent'] ) - for tx in r.json()['unspent'] - ] + next_link = response['paging']['next_link'] + + while next_link: + r = requests.get(next_link, params=payload, timeout=DEFAULT_TIMEOUT) + if r.status_code != 200: # pragma: no cover + raise ConnectionError + response = r.json() + unspents.extend( + Unspent( + currency_to_satoshi(tx['value'], 'btc'), + tx['confirmations'], + tx['script_pub_key']['hex'], + tx['txid'], + tx['n'], + ) + for tx in response['unspent'] + ) + next_link = response['paging']['next_link'] + + return unspents @classmethod def broadcast_tx(cls, tx_hex): # pragma: no cover @@ -408,32 +1035,73 @@ class NetworkAPI: requests.exceptions.ReadTimeout, ) - GET_BALANCE_MAIN = [BitpayAPI.get_balance, SmartbitAPI.get_balance, BlockchainAPI.get_balance] + GET_BALANCE_MAIN = [ + BlockchairAPI.get_balance, + BlockstreamAPI.get_balance, + BitcoreAPI.get_balance, + SmartbitAPI.get_balance, + BTCcomAPI.get_balance, + BlockchainAPI.get_balance, + ] GET_TRANSACTIONS_MAIN = [ - BitpayAPI.get_transactions, # Limit 1000 + BlockchairAPI.get_transactions, # Limit 1000 + BlockstreamAPI.get_transactions, # Limit 1000 SmartbitAPI.get_transactions, # Limit 1000 - BlockchainAPI.get_transactions, - ] # No limit, requires multiple requests + BTCcomAPI.get_transactions, + BlockchainAPI.get_transactions, # No limit, requires multiple requests + ] GET_TRANSACTION_BY_ID_MAIN = [ - BitpayAPI.get_transaction_by_id, + BlockchairAPI.get_transaction_by_id, + BlockstreamAPI.get_transaction_by_id, SmartbitAPI.get_transaction_by_id, + BTCcomAPI.get_transaction_by_id, BlockchainAPI.get_transaction_by_id, ] GET_UNSPENT_MAIN = [ - BitpayAPI.get_unspent, # No limit + BlockchairAPI.get_unspent, + BlockstreamAPI.get_unspent, + BitcoreAPI.get_unspent, # No limit SmartbitAPI.get_unspent, # Limit 1000 + BTCcomAPI.get_unspent, BlockchainAPI.get_unspent, - ] # Limit 250 - BROADCAST_TX_MAIN = [BitpayAPI.broadcast_tx, SmartbitAPI.broadcast_tx, BlockchainAPI.broadcast_tx] # Limit 5/minute + ] + BROADCAST_TX_MAIN = [ + BlockchairAPI.broadcast_tx, + BlockstreamAPI.broadcast_tx, + BitcoreAPI.broadcast_tx, + SmartbitAPI.broadcast_tx, # Limit 5/minute + BTCcomAPI.broadcast_tx, + BlockchainAPI.broadcast_tx, + ] - GET_BALANCE_TEST = [BitpayAPI.get_balance_testnet, SmartbitAPI.get_balance_testnet] + GET_BALANCE_TEST = [ + BlockchairAPI.get_balance_testnet, + BlockstreamAPI.get_balance_testnet, + BitcoreAPI.get_balance_testnet, + SmartbitAPI.get_balance_testnet, + ] GET_TRANSACTIONS_TEST = [ - BitpayAPI.get_transactions_testnet, # Limit 1000 - SmartbitAPI.get_transactions_testnet, - ] # Limit 1000 - GET_TRANSACTION_BY_ID_TEST = [BitpayAPI.get_transaction_by_id_testnet, SmartbitAPI.get_transaction_by_id_testnet] - GET_UNSPENT_TEST = [BitpayAPI.get_unspent_testnet, SmartbitAPI.get_unspent_testnet] # No limit # Limit 1000 - BROADCAST_TX_TEST = [BitpayAPI.broadcast_tx_testnet, SmartbitAPI.broadcast_tx_testnet] # Limit 5/minute + BlockchairAPI.get_transactions_testnet, # Limit 1000 + BlockstreamAPI.get_transactions_testnet, + SmartbitAPI.get_transactions_testnet, # Limit 1000 + ] + GET_TRANSACTION_BY_ID_TEST = [ + BlockchairAPI.get_transaction_by_id_testnet, + BlockstreamAPI.get_transaction_by_id_testnet, + SmartbitAPI.get_transaction_by_id_testnet, + ] + GET_UNSPENT_TEST = [ + BlockchairAPI.get_unspent_testnet, + BlockstreamAPI.get_unspent_testnet, + BitcoreAPI.get_unspent_testnet, # No limit + SmartbitAPI.get_unspent_testnet, # Limit 1000 + ] + BROADCAST_TX_TEST = [ + BlockchairAPI.broadcast_tx_testnet, + BlockstreamAPI.broadcast_tx_testnet, + BitcoreAPI.broadcast_tx_testnet, + SmartbitAPI.broadcast_tx_testnet, # Limit 5/minute + ] @classmethod def connect_to_node(cls, user, password, host='localhost', port=8332, use_https=False, testnet=False): diff --git a/tests/network/test_services.py b/tests/network/test_services.py index 668ed56..902e3d7 100644 --- a/tests/network/test_services.py +++ b/tests/network/test_services.py @@ -8,18 +8,27 @@ from bit.network.services import ( RPCHost, RPCMethod, - BitpayAPI, - BlockchainAPI, NetworkAPI, + BitcoreAPI, + BlockchainAPI, SmartbitAPI, + BlockstreamAPI, + BlockchairAPI, + BTCcomAPI, set_service_timeout, ) -from tests.utils import catch_errors_raise_warnings, decorate_methods, raise_connection_error +from tests.utils import ( + catch_errors_raise_warnings, + decorate_methods, + raise_connection_error, + check_not_all_raise_errors, +) from bit.transaction import calc_txid MAIN_ADDRESS_USED1 = '1L2JsXHPMYuAa9ugvHGLwkdstCPUDemNCf' MAIN_ADDRESS_USED2 = '17SkEw2md5avVNyYgj6RiXuQKNwkXaxFyQ' +MAIN_ADDRESS_USED3 = '1CounterpartyXXXXXXXXXXXXXXXUWLpVr' MAIN_ADDRESS_UNUSED = '1DvnoW4vsXA1H9KDgNiMqY7iNkzC187ve1' TEST_ADDRESS_USED1 = 'n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi' TEST_ADDRESS_USED2 = 'mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5' @@ -30,6 +39,8 @@ TEST_TX_VALID = 'ff2b4641481f1ee553ba2c9f02f413a86f70240c35b5aee554f84e3efee93292' TX_INVALID = 'ff2b4641481f1ee553ba2c9f02f413a86f70240c35b5aee554f84e3efee93290' +set_service_timeout(30) + def all_items_common(seq): initial_set = set(seq[0]) @@ -188,7 +199,7 @@ def __call__(self, *args): class TestNetworkAPI: def test_get_balance_main_equal(self): - results = [call(MAIN_ADDRESS_USED2) for call in NetworkAPI.GET_BALANCE_MAIN] + results = check_not_all_raise_errors(NetworkAPI.GET_BALANCE_MAIN, NetworkAPI.IGNORED_ERRORS)(MAIN_ADDRESS_USED2) assert all(result == results[0] for result in results) def test_get_balance_main_failure(self): @@ -196,7 +207,7 @@ def test_get_balance_main_failure(self): MockBackend.get_balance(MAIN_ADDRESS_USED2) def test_get_balance_test_equal(self): - results = [call(TEST_ADDRESS_USED2) for call in NetworkAPI.GET_BALANCE_TEST] + results = check_not_all_raise_errors(NetworkAPI.GET_BALANCE_TEST, NetworkAPI.IGNORED_ERRORS)(TEST_ADDRESS_USED2) assert all(result == results[0] for result in results) def test_get_balance_test_failure(self): @@ -204,39 +215,47 @@ def test_get_balance_test_failure(self): MockBackend.get_balance_testnet(TEST_ADDRESS_USED2) def test_get_transactions_main_equal(self): - results = [call(MAIN_ADDRESS_USED1)[:100] for call in NetworkAPI.GET_TRANSACTIONS_MAIN] - assert all_items_common(results) + results = check_not_all_raise_errors(NetworkAPI.GET_TRANSACTIONS_MAIN, NetworkAPI.IGNORED_ERRORS)( + MAIN_ADDRESS_USED1 + ) + assert all_items_common([r[:100] for r in results]) def test_get_transactions_main_failure(self): with pytest.raises(ConnectionError): MockBackend.get_transactions(MAIN_ADDRESS_USED1) def test_get_transactions_test_equal(self): - results = [call(TEST_ADDRESS_USED2)[:100] for call in NetworkAPI.GET_TRANSACTIONS_TEST] - assert all_items_common(results) + results = check_not_all_raise_errors(NetworkAPI.GET_TRANSACTIONS_TEST, NetworkAPI.IGNORED_ERRORS)( + TEST_ADDRESS_USED2 + ) + assert all_items_common([r[:100] for r in results]) def test_get_transactions_test_failure(self): with pytest.raises(ConnectionError): MockBackend.get_transactions_testnet(TEST_ADDRESS_USED2) def test_get_transaction_by_id_main_equal(self): - results = [calc_txid(call(MAIN_TX_VALID)) for call in NetworkAPI.GET_TRANSACTION_BY_ID_MAIN] - assert all_items_equal(results) + results = check_not_all_raise_errors(NetworkAPI.GET_TRANSACTION_BY_ID_MAIN, NetworkAPI.IGNORED_ERRORS)( + MAIN_TX_VALID + ) + assert all_items_equal([calc_txid(r) for r in results]) def test_get_transaction_by_id_main_failure(self): with pytest.raises(ConnectionError): MockBackend.get_transaction_by_id(MAIN_TX_VALID) def test_get_transaction_by_id_test_equal(self): - results = [calc_txid(call(TEST_TX_VALID)) for call in NetworkAPI.GET_TRANSACTION_BY_ID_TEST] - assert all_items_equal(results) + results = check_not_all_raise_errors(NetworkAPI.GET_TRANSACTION_BY_ID_TEST, NetworkAPI.IGNORED_ERRORS)( + TEST_TX_VALID + ) + assert all_items_equal([calc_txid(r) for r in results]) def test_get_transaction_by_id_test_failure(self): with pytest.raises(ConnectionError): MockBackend.get_transaction_by_id_testnet(TEST_TX_VALID) def test_get_unspent_main_equal(self): - results = [call(MAIN_ADDRESS_USED2) for call in NetworkAPI.GET_UNSPENT_MAIN] + results = check_not_all_raise_errors(NetworkAPI.GET_UNSPENT_MAIN, NetworkAPI.IGNORED_ERRORS)(MAIN_ADDRESS_USED2) assert all_items_equal(results) def test_get_unspent_main_failure(self): @@ -244,7 +263,7 @@ def test_get_unspent_main_failure(self): MockBackend.get_unspent(MAIN_ADDRESS_USED1) def test_get_unspent_test_equal(self): - results = [call(TEST_ADDRESS_USED3) for call in NetworkAPI.GET_UNSPENT_TEST] + results = check_not_all_raise_errors(NetworkAPI.GET_UNSPENT_TEST, NetworkAPI.IGNORED_ERRORS)(TEST_ADDRESS_USED3) assert all_items_equal(results) def test_get_unspent_test_failure(self): @@ -359,102 +378,235 @@ class n(NetworkAPI): @decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) -class TestBitpayAPI: +class TestBitcoreAPI: def test_get_balance_return_type(self): - assert isinstance(BitpayAPI.get_balance(MAIN_ADDRESS_USED1), int) + assert isinstance(BitcoreAPI.get_balance(MAIN_ADDRESS_USED1), int) def test_get_balance_main_used(self): - assert BitpayAPI.get_balance(MAIN_ADDRESS_USED1) > 0 + assert BitcoreAPI.get_balance(MAIN_ADDRESS_USED3) > 0 def test_get_balance_main_unused(self): - assert BitpayAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 + assert BitcoreAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 def test_get_balance_test_used(self): - assert BitpayAPI.get_balance_testnet(TEST_ADDRESS_USED2) > 0 + assert BitcoreAPI.get_balance_testnet(TEST_ADDRESS_USED2) > 0 def test_get_balance_test_unused(self): - assert BitpayAPI.get_balance_testnet(TEST_ADDRESS_UNUSED) == 0 + assert BitcoreAPI.get_balance_testnet(TEST_ADDRESS_UNUSED) == 0 + + def test_get_unspent_return_type(self): + assert iter(BitcoreAPI.get_unspent(MAIN_ADDRESS_USED1)) + + def test_get_unspent_main_used(self): + assert len(BitcoreAPI.get_unspent(MAIN_ADDRESS_USED3)) >= 2783 + + def test_get_unspent_main_unused(self): + assert len(BitcoreAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + + def test_get_unspent_test_used(self): + assert len(BitcoreAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 196 + + def test_get_unspent_test_unused(self): + assert len(BitcoreAPI.get_unspent_testnet(TEST_ADDRESS_UNUSED)) == 0 + + +@decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) +class TestBlockchainAPI: + def test_get_balance_return_type(self): + assert isinstance(BlockchainAPI.get_balance(MAIN_ADDRESS_USED1), int) + + def test_get_balance_used(self): + assert BlockchainAPI.get_balance(MAIN_ADDRESS_USED3) > 0 + + def test_get_balance_unused(self): + assert BlockchainAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 def test_get_transactions_return_type(self): - assert iter(BitpayAPI.get_transactions(MAIN_ADDRESS_USED1)) + assert iter(BlockchainAPI.get_transactions(MAIN_ADDRESS_USED1)) + + def test_get_transactions_used(self): + assert len(BlockchainAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 236 + + def test_get_transactions_unused(self): + assert len(BlockchainAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 + + def test_get_transaction_by_id_valid(self): + tx = BlockchainAPI.get_transaction_by_id(MAIN_TX_VALID) + assert calc_txid(tx) == MAIN_TX_VALID + + def test_get_transaction_by_id_invalid(self): + assert BlockchainAPI.get_transaction_by_id(TX_INVALID) == None + + def test_get_unspent_return_type(self): + assert iter(BlockchainAPI.get_unspent(MAIN_ADDRESS_USED1)) + + def test_get_unspent_main_used(self): + # ! BlockchainAPI only supports up to 1000 UTXOs + assert len(BlockchainAPI.get_unspent(MAIN_ADDRESS_USED3)) == 1000 + + def test_get_unspent_main_unused(self): + assert len(BlockchainAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + + +@decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) +class TestBTCcomAPI: + def test_get_balance_return_type(self): + assert isinstance(BTCcomAPI.get_balance(MAIN_ADDRESS_USED1), int) + + def test_get_balance_used(self): + assert BTCcomAPI.get_balance(MAIN_ADDRESS_USED3) > 0 + + def test_get_balance_unused(self): + assert BTCcomAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 + + def test_get_transactions_return_type(self): + assert iter(BTCcomAPI.get_transactions(MAIN_ADDRESS_USED1)) + + def test_get_transactions_used(self): + assert len(BTCcomAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 236 + + def test_get_transactions_unused(self): + assert len(BTCcomAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 + + def test_get_transaction_by_id_valid(self): + tx = BTCcomAPI.get_transaction_by_id(MAIN_TX_VALID) + assert calc_txid(tx) == MAIN_TX_VALID + + def test_get_transaction_by_id_invalid(self): + assert BTCcomAPI.get_transaction_by_id(TX_INVALID) == None + + def test_get_unspent_return_type(self): + assert iter(BTCcomAPI.get_unspent(MAIN_ADDRESS_USED1)) + + def test_get_unspent_used(self): + assert len(BTCcomAPI.get_unspent(MAIN_ADDRESS_USED3)) >= 2783 + + def test_get_unspent_unused(self): + assert len(BTCcomAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + + +@decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) +class TestBlockchairAPI: + def test_get_balance_return_type(self): + assert isinstance(BlockchairAPI.get_balance(MAIN_ADDRESS_USED1), int) + + def test_get_balance_main_used(self): + assert BlockchairAPI.get_balance(MAIN_ADDRESS_USED3) > 0 + + def test_get_balance_main_unused(self): + assert BlockchairAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 + + def test_get_balance_test_used(self): + assert BlockchairAPI.get_balance_testnet(TEST_ADDRESS_USED2) > 0 + + def test_get_balance_test_unused(self): + assert BlockchairAPI.get_balance_testnet(TEST_ADDRESS_UNUSED) == 0 + + def test_get_transactions_return_type(self): + assert iter(BlockchairAPI.get_transactions(MAIN_ADDRESS_USED1)) def test_get_transactions_main_used(self): - assert len(BitpayAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 218 + assert len(BlockchairAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 236 def test_get_transactions_main_unused(self): - assert len(BitpayAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 + assert len(BlockchairAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 def test_get_transactions_test_used(self): - assert len(BitpayAPI.get_transactions_testnet(TEST_ADDRESS_USED2)) >= 444 + assert len(BlockchairAPI.get_transactions_testnet(TEST_ADDRESS_USED2)) >= 444 def test_get_transactions_test_unused(self): - assert len(BitpayAPI.get_transactions_testnet(TEST_ADDRESS_UNUSED)) == 0 + assert len(BlockchairAPI.get_transactions_testnet(TEST_ADDRESS_UNUSED)) == 0 def test_get_transaction_by_id_valid(self): - tx = BitpayAPI.get_transaction_by_id(MAIN_TX_VALID) + tx = BlockchairAPI.get_transaction_by_id(MAIN_TX_VALID) assert calc_txid(tx) == MAIN_TX_VALID def test_get_transaction_by_id_invalid(self): - assert BitpayAPI.get_transaction_by_id(TX_INVALID) == None + assert BlockchairAPI.get_transaction_by_id(TX_INVALID) == None def test_get_transaction_by_id_test_valid(self): - tx = BitpayAPI.get_transaction_by_id_testnet(TEST_TX_VALID) + tx = BlockchairAPI.get_transaction_by_id_testnet(TEST_TX_VALID) assert calc_txid(tx) == TEST_TX_VALID def test_get_transaction_by_id_test_invalid(self): - assert BitpayAPI.get_transaction_by_id_testnet(TX_INVALID) == None + assert BlockchairAPI.get_transaction_by_id_testnet(TX_INVALID) == None def test_get_unspent_return_type(self): - assert iter(BitpayAPI.get_unspent(MAIN_ADDRESS_USED1)) + assert iter(BlockchairAPI.get_unspent(MAIN_ADDRESS_USED1)) def test_get_unspent_main_used(self): - assert len(BitpayAPI.get_unspent(MAIN_ADDRESS_USED2)) >= 1 + assert len(BlockchairAPI.get_unspent(MAIN_ADDRESS_USED3)) >= 2783 def test_get_unspent_main_unused(self): - assert len(BitpayAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + assert len(BlockchairAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 def test_get_unspent_test_used(self): - assert len(BitpayAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 194 + assert len(BlockchairAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 196 def test_get_unspent_test_unused(self): - assert len(BitpayAPI.get_unspent_testnet(TEST_ADDRESS_UNUSED)) == 0 + assert len(BlockchairAPI.get_unspent_testnet(TEST_ADDRESS_UNUSED)) == 0 @decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) -class TestBlockchainAPI: +class TestBlockstreamAPI: def test_get_balance_return_type(self): - assert isinstance(BlockchainAPI.get_balance(MAIN_ADDRESS_USED1), int) + assert isinstance(BlockstreamAPI.get_balance(MAIN_ADDRESS_USED1), int) - def test_get_balance_used(self): - assert BlockchainAPI.get_balance(MAIN_ADDRESS_USED1) > 0 + def test_get_balance_main_used(self): + assert BlockstreamAPI.get_balance(MAIN_ADDRESS_USED3) > 0 - def test_get_balance_unused(self): - assert BlockchainAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 + def test_get_balance_main_unused(self): + assert BlockstreamAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 + + def test_get_balance_test_used(self): + assert BlockstreamAPI.get_balance_testnet(TEST_ADDRESS_USED2) > 0 + + def test_get_balance_test_unused(self): + assert BlockstreamAPI.get_balance_testnet(TEST_ADDRESS_UNUSED) == 0 def test_get_transactions_return_type(self): - assert iter(BlockchainAPI.get_transactions(MAIN_ADDRESS_USED1)) + assert iter(BlockstreamAPI.get_transactions(MAIN_ADDRESS_USED1)) - def test_get_transactions_used(self): - assert len(BlockchainAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 218 + def test_get_transactions_main_used(self): + assert len(BlockstreamAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 236 - def test_get_transactions_unused(self): - assert len(BlockchainAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 + def test_get_transactions_main_unused(self): + assert len(BlockstreamAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 + + def test_get_transactions_test_used(self): + assert len(BlockstreamAPI.get_transactions_testnet(TEST_ADDRESS_USED2)) >= 444 + + def test_get_transactions_test_unused(self): + assert len(BlockstreamAPI.get_transactions_testnet(TEST_ADDRESS_UNUSED)) == 0 def test_get_transaction_by_id_valid(self): - tx = BlockchainAPI.get_transaction_by_id(MAIN_TX_VALID) + tx = BlockstreamAPI.get_transaction_by_id(MAIN_TX_VALID) assert calc_txid(tx) == MAIN_TX_VALID def test_get_transaction_by_id_invalid(self): - assert BlockchainAPI.get_transaction_by_id(TX_INVALID) == None + assert BlockstreamAPI.get_transaction_by_id(TX_INVALID) == None + + def test_get_transaction_by_id_test_valid(self): + tx = BlockstreamAPI.get_transaction_by_id_testnet(TEST_TX_VALID) + assert calc_txid(tx) == TEST_TX_VALID + + def test_get_transaction_by_id_test_invalid(self): + assert BlockstreamAPI.get_transaction_by_id_testnet(TX_INVALID) == None def test_get_unspent_return_type(self): - assert iter(BlockchainAPI.get_unspent(MAIN_ADDRESS_USED1)) + assert iter(BlockstreamAPI.get_unspent(MAIN_ADDRESS_USED1)) def test_get_unspent_main_used(self): - assert len(BlockchainAPI.get_unspent(MAIN_ADDRESS_USED2)) >= 1 + assert len(BlockstreamAPI.get_unspent(MAIN_ADDRESS_USED3)) >= 2783 def test_get_unspent_main_unused(self): - assert len(BlockchainAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + assert len(BlockstreamAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 + + def test_get_unspent_test_used(self): + assert len(BlockstreamAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 196 + + def test_get_unspent_test_unused(self): + assert len(BlockstreamAPI.get_unspent_testnet(TEST_ADDRESS_UNUSED)) == 0 @decorate_methods(catch_errors_raise_warnings, NetworkAPI.IGNORED_ERRORS) @@ -463,7 +615,7 @@ def test_get_balance_return_type(self): assert isinstance(SmartbitAPI.get_balance(MAIN_ADDRESS_USED1), int) def test_get_balance_main_used(self): - assert SmartbitAPI.get_balance(MAIN_ADDRESS_USED1) > 0 + assert SmartbitAPI.get_balance(MAIN_ADDRESS_USED3) > 0 def test_get_balance_main_unused(self): assert SmartbitAPI.get_balance(MAIN_ADDRESS_UNUSED) == 0 @@ -478,7 +630,7 @@ def test_get_transactions_return_type(self): assert iter(SmartbitAPI.get_transactions(MAIN_ADDRESS_USED1)) def test_get_transactions_main_used(self): - assert len(SmartbitAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 218 + assert len(SmartbitAPI.get_transactions(MAIN_ADDRESS_USED1)) >= 236 def test_get_transactions_main_unused(self): assert len(SmartbitAPI.get_transactions(MAIN_ADDRESS_UNUSED)) == 0 @@ -507,13 +659,13 @@ def test_get_unspent_return_type(self): assert iter(SmartbitAPI.get_unspent(MAIN_ADDRESS_USED1)) def test_get_unspent_main_used(self): - assert len(SmartbitAPI.get_unspent(MAIN_ADDRESS_USED2)) >= 1 + assert len(SmartbitAPI.get_unspent(MAIN_ADDRESS_USED3)) >= 2783 def test_get_unspent_main_unused(self): assert len(SmartbitAPI.get_unspent(MAIN_ADDRESS_UNUSED)) == 0 def test_get_unspent_test_used(self): - assert len(SmartbitAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 194 + assert len(SmartbitAPI.get_unspent_testnet(TEST_ADDRESS_USED2)) >= 196 def test_get_unspent_test_unused(self): assert len(SmartbitAPI.get_unspent_testnet(TEST_ADDRESS_UNUSED)) == 0 diff --git a/tests/utils.py b/tests/utils.py index c48e2b7..5b49dca 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -22,7 +22,26 @@ def wrapper(*args, **kwargs): try: f(*args, **kwargs) except ignored_errors: - warnings.warn('Unreachable API from '.format(f.__name__), Warning) + warnings.warn('Unreachable API from {}'.format(f.__name__), Warning) assert True return wrapper + + +def check_not_all_raise_errors(fs, ignored_errors): # pragma: no cover + def wrapper(*args, **kwargs): + success = False + ret = [] + for f in fs: + try: + ret.append(f(*args, **kwargs)) + success = True + except ignored_errors: + continue + + if not success: + raise ConnectionError('All API calls to {} are unreachable'.format(fs[0].__name__)) + + return ret + + return wrapper