Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions bitsv/network/services/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Copy link
Owner

@AustEcon AustEcon Dec 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ignore my earlier comment. I need to spend the time to actually test this code to understand it better than just reading it...

Sorry. I may have to come back to this next weekend...

What happens if PrivakeKey.get_bitcom_transactions() is called when there is no planaria token? does that case need to return a human-readable error that they require a token (or something like that)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having some trouble replying to you here, I'm sorry. I tried to copy the way API keys were used in a pre-existing part of the system. I haven't verified the no-token case; the error may not be clear.

@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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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
75 changes: 75 additions & 0 deletions bitsv/network/services/planaria.py
Original file line number Diff line number Diff line change
@@ -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))
30 changes: 21 additions & 9 deletions tests/network/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -97,48 +100,57 @@ 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):
with pytest.raises(ConnectionError):
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):
with pytest.raises(ConnectionError):
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):
with pytest.raises(ConnectionError):
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):
with pytest.raises(ConnectionError):
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):
Expand All @@ -149,23 +161,23 @@ 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):
with pytest.raises(ConnectionError):
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):
with pytest.raises(ConnectionError):
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):
Expand Down