diff --git a/bitsv/network/services/network.py b/bitsv/network/services/network.py index cdd4fc6..b79e96e 100644 --- a/bitsv/network/services/network.py +++ b/bitsv/network/services/network.py @@ -10,6 +10,7 @@ from .mattercloud import MatterCloud, MATTERCLOUD_API_KEY_VARNAME from .bchsvexplorer import BCHSVExplorerAPI +from .planaria import Planaria, PLANARIA_TOKEN_VARNAME DEFAULT_TIMEOUT = 30 DEFAULT_RETRY = 3 @@ -96,20 +97,30 @@ def __init__(self, network): self.bitindex3 = MatterCloud(api_key=mattercloud_api_key, network=self.network) self.list_of_apis.appendleft(self.bitindex3) + planaria_token = os.environ.get(PLANARIA_TOKEN_VARNAME, None) + if planaria_token and network == 'main': + self.planaria = Planaria(token=planaria_token) + self.list_of_apis.append(self.planaria) + @retry_annotation(IGNORED_ERRORS, tries=DEFAULT_RETRY) - def retry_wrapper_call(self, api_call, param): - return api_call(param) + def retry_wrapper_call(self, api_call, *params): + return api_call(*params) + + def get_apis_supporting(self, name): + return [api for api in self.list_of_apis if hasattr(api, name)] - def invoke_api_call(self, call_list, param): + def invoke_api_call(self, name, *params): """Tries to invoke all api, raise exception if all fail.""" + call_list = [getattr(api, name) for api in self.get_apis_supporting(name)] + exceptions = [] for api_call in call_list: try: - return self.retry_wrapper_call(api_call, param) + return self.retry_wrapper_call(api_call, *params) except IGNORED_ERRORS as e: # TODO: Write a log here to notify the system has changed the default service. self.list_of_apis.rotate(-1) - if call_list[-1] == api_call: # All api iterated. - raise ConnectionError('All APIs are unreachable, exception:' + str(e)) + exceptions.append(str(e)) + raise ConnectionError('All APIs are unreachable, exceptions:' + ';'.join(exceptions)) def get_balance(self, address): """Gets the balance of an address in satoshis. @@ -119,8 +130,7 @@ def get_balance(self, address): :raises ConnectionError: If all API services fail. :rtype: ``int`` """ - call_list = [api.get_balance for api in self.list_of_apis] - return self.invoke_api_call(call_list, address) + return self.invoke_api_call('get_balance', address) def get_transactions(self, address): """Gets the ID of all transactions related to an address. @@ -130,8 +140,20 @@ def get_transactions(self, address): :raises ConnectionError: If all API services fail. :rtype: ``list`` of ``str`` """ - call_list = [api.get_transactions for api in self.list_of_apis] - return self.invoke_api_call(call_list, address) + return self.invoke_api_call('get_transactions', address) + + def get_bitcom_transactions(self, bitcom, address=None): + """Gets the ID of all transactions with a given bitcom prefix, + optionally only from an address. + + :param bitcom: The bottom prefix in question. + :type bitcom: ``str`` + :param address: An optional address to filter from. + :type address: ``str`` + :raises ConnectionError: If all API services fail. + :rtype: ``list`` of ``str`` + """ + return self.invoke_api_call('get_bitcom_transactions', bitcom, address) def get_transaction(self, txid): """Gets the full transaction details. @@ -141,8 +163,7 @@ def get_transaction(self, txid): :raises ConnectionError: If all API services fail. :rtype: ``Transaction`` """ - call_list = [api.get_transaction for api in self.list_of_apis] - return self.invoke_api_call(call_list, txid) + return self.invoke_api_call('get_transaction', txid) def get_unspents(self, address): """Gets all unspent transaction outputs belonging to an address. @@ -152,8 +173,7 @@ def get_unspents(self, address): :raises ConnectionError: If all API services fail. :rtype: ``list`` of :class:`~bitsv.network.meta.Unspent` """ - call_list = [api.get_unspents for api in self.list_of_apis] - return self.invoke_api_call(call_list, address) + return self.invoke_api_call('get_unspents', address) def broadcast_tx(self, tx_hex): # pragma: no cover """Broadcasts a transaction to the blockchain. @@ -162,6 +182,5 @@ def broadcast_tx(self, tx_hex): # pragma: no cover :type tx_hex: ``str`` :raises ConnectionError: If all API services fail. """ - call_list = [api.send_transaction for api in self.list_of_apis] - tx_id = self.invoke_api_call(call_list, tx_hex) + tx_id = self.invoke_api_call('send_transaction', tx_hex) return tx_id diff --git a/bitsv/network/services/planaria.py b/bitsv/network/services/planaria.py new file mode 100644 index 0000000..116acff --- /dev/null +++ b/bitsv/network/services/planaria.py @@ -0,0 +1,75 @@ +import json +import requests + +PLANARIA_TOKEN_VARNAME = 'PLANARIA_TOKEN' + +class Planaria: + def __init__(self, token): + self.token = token + self.headers = { + 'Content-Type': 'application/json; charset=utf-8', + 'token': self.token + } + + def _query(self, query): + query = json.dumps(query) + r = requests.post( + 'https://txo.bitsocket.network/crawl', + data = query, + headers = self.headers, + stream = True + ) + r.raise_for_status() + + result_hashes = set() + + for line in r.iter_lines(): + yield json.loads(line) + result_hashes.add(hash(line)) + + r = requests.post( + 'https://txo.bitbus.network/block', + data = query, + headers = self.headers, + stream = True + ) + r.raise_for_status() + + for line in r.iter_lines(): + if result_hashes is not None: + if hash(line) in result_hashes: + continue + result_hashes = None + yield json.loads(line) + + # commented out because it doesn't return pre-fork transactions. + #def get_transactions(self, address): + # query = { + # 'v': 3, + # 'q': { + # 'find': { + # '$or': [{ 'in.e.a': address}, { 'out.e.a': address }] + # }, + # 'project': { + # 'tx.h': 1 + # } + # } + # } + # return (result['tx']['h'] for result in self._query(query)) + + + def get_bitcom_transactions(self, bitcom, address=None): + query = { + 'v': 3, + 'q': { + 'find': { + '$or': [{ 'out.s1': bitcom }, { 'out.s2': bitcom }], + }, + 'project': { + 'tx.h': 1 + } + } + } + if address is not None: + query['q']['find']['in.e.a'] = address + return (result['tx']['h'] for result in self._query(query)) diff --git a/tests/network/test_services.py b/tests/network/test_services.py index 9611360..9fa293c 100644 --- a/tests/network/test_services.py +++ b/tests/network/test_services.py @@ -9,6 +9,9 @@ MAIN_ADDRESS_USED1 = '1L2JsXHPMYuAa9ugvHGLwkdstCPUDemNCf' MAIN_ADDRESS_USED2 = '17SkEw2md5avVNyYgj6RiXuQKNwkXaxFyQ' MAIN_ADDRESS_UNUSED = '1DvnoW4vsXA1H9KDgNiMqY7iNkzC187ve1' +MAIN_BITCOM_B = '19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut' +MAIN_BITCOM_B_ADDRESS = '1BiU6C35By6EjMi8mFAKhd7eFxpB7M9opt' +MAIN_BITCOM_B_TXID = '6c784b78cff5ee4f469f783adc0e957265f467d3a0dae2b2b1ecbc84a1bd1fb6' TEST_ADDRESS_USED1 = 'n2eMqTT929pb1RDNuqEnxdaLau1rxy3efi' TEST_ADDRESS_USED2 = 'mmvP3mTe53qxHdPqXEvdu8WdC7GfQ2vmx5' TEST_ADDRESS_USED3 = 'mpnrLMH4m4e6dS8Go84P1r2hWwTiFTXmtW' @@ -97,7 +100,7 @@ def send_transaction(rawtx): class TestNetworkAPI: # Main def test_get_balance_main_equal(self): - results = [api.get_balance(MAIN_ADDRESS_USED2) for api in network_api_main.list_of_apis] + results = [api.get_balance(MAIN_ADDRESS_USED2) for api in network_api_main.get_apis_supporting('get_balance')] assert all(result == results[0] for result in results) def test_get_balance_main_failure(self): @@ -105,7 +108,7 @@ def test_get_balance_main_failure(self): mock_network_api_main.get_balance(MAIN_ADDRESS_USED1) def test_get_transactions_main_equal(self): - results = [api.get_transactions(MAIN_ADDRESS_USED1) for api in network_api_main.list_of_apis] + results = [[*api.get_transactions(MAIN_ADDRESS_USED1)] for api in network_api_main.get_apis_supporting('get_transactions')] assert all_items_common(results[:100]) def test_get_transactions_main_failure(self): @@ -113,16 +116,25 @@ def test_get_transactions_main_failure(self): mock_network_api_main.get_transactions(MAIN_ADDRESS_USED1) def test_get_unspents_main_equal(self): - results = [api.get_unspents(MAIN_ADDRESS_USED2) for api in network_api_main.list_of_apis] + results = [api.get_unspents(MAIN_ADDRESS_USED2) for api in network_api_main.get_apis_supporting('get_unspents')] assert all_items_equal(results) def test_get_unspents_main_failure(self): with pytest.raises(ConnectionError): mock_network_api_main.get_unspents(MAIN_ADDRESS_USED1) + def test_get_bitcom_transactions_address_main_equal(self): + results = [[*api.get_bitcom_transactions(MAIN_BITCOM_B, MAIN_BITCOM_B_ADDRESS)][-1] for api in network_api_main.get_apis_supporting('get_bitcom_transactions')] + results.append(MAIN_BITCOM_B_TXID) + assert all_items_equal(results) + + def test_get_bitcom_transactions_main_failure(self): + with pytest.raises(ConnectionError): + mock_network_api_main.get_bitcom_transactions(MAIN_BITCOM_B, MAIN_BITCOM_B_ADDRESS) + # Test def test_get_balance_test_equal(self): - results = [api.get_balance(TEST_ADDRESS_USED2) for api in network_api_test.list_of_apis] + results = [api.get_balance(TEST_ADDRESS_USED2) for api in network_api_test.get_apis_supporting('get_balance')] assert all(result == results[0] for result in results) def test_get_balance_test_failure(self): @@ -130,7 +142,7 @@ def test_get_balance_test_failure(self): mock_network_api_test.get_balance(TEST_ADDRESS_USED2) def test_get_transactions_test_equal(self): - results = [api.get_transactions(TEST_ADDRESS_USED2)[:100] for api in network_api_test.list_of_apis] + results = [api.get_transactions(TEST_ADDRESS_USED2)[:100] for api in network_api_test.get_apis_supporting('get_transactions')] assert all_items_common(results) def test_get_transactions_test_failure(self): @@ -138,7 +150,7 @@ def test_get_transactions_test_failure(self): mock_network_api_test.get_transactions(TEST_ADDRESS_USED2) def test_get_unspents_test_equal(self): - results = [api.get_unspents(TEST_ADDRESS_USED3) for api in network_api_test.list_of_apis] + results = [api.get_unspents(TEST_ADDRESS_USED3) for api in network_api_test.get_apis_supporting('get_unspents')] assert all_items_equal(results) def test_get_unspents_test_failure(self): @@ -149,7 +161,7 @@ def test_get_unspents_test_failure(self): # Commented out until necessary server upgrades are done on BitIndex # Or until Whatsonchain add number of confirmations to utxo response - otherwise I have it ready to go! """def test_get_balance_stn_equal(self): - results = [api.get_balance(TEST_ADDRESS_USED2) for api in network_api_stn.list_of_apis] + results = [api.get_balance(TEST_ADDRESS_USED2) for api in network_api_stn.get_apis_supporting('get_balance')] assert all(result == results[0] for result in results) def test_get_balance_stn_failure(self): @@ -157,7 +169,7 @@ def test_get_balance_stn_failure(self): mock_network_api_stn.get_balance(TEST_ADDRESS_USED2) def test_get_transactions_stn_equal(self): - results = [api.get_transactions(TEST_ADDRESS_USED2)[:100] for api in network_api_stn.list_of_apis] + results = [api.get_transactions(TEST_ADDRESS_USED2)[:100] for api in network_api_stn.get_apis_supporting('get_transactions')] assert all_items_common(results) def test_get_transactions_stn_failure(self): @@ -165,7 +177,7 @@ def test_get_transactions_stn_failure(self): mock_network_api_stn.get_transactions(TEST_ADDRESS_USED2) def test_get_unspents_stn_equal(self): - results = [api.get_unspents(TEST_ADDRESS_USED3) for api in network_api_stn.list_of_apis] + results = [api.get_unspents(TEST_ADDRESS_USED3) for api in network_api_stn.get_apis_supporting('get_unspents')] assert all_items_equal(results) def test_get_unspents_stn_failure(self):