Skip to content

Commit

Permalink
Adds test for blockheight for all endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
yashasvi-ranawat committed May 5, 2024
1 parent a525e23 commit eea963f
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 8 deletions.
7 changes: 7 additions & 0 deletions bitcash/network/APIs/BitcoinDotComAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(self, network_endpoint: str):
"address": "address/details/{}",
"raw-tx": "rawtransactions/sendRawTransaction",
"tx-details": "transaction/details/{}",
"block-height": "blockchain/getBlockCount",
}

@classmethod
Expand All @@ -52,6 +53,12 @@ def get_default_endpoints(cls, network):
def make_endpoint_url(self, path):
return self.network_endpoint + self.PATHS[path]

def get_blockheight(self, *args, **kwargs):
api_url = self.make_endpoint_url("block-height")
r = session.get(api_url)
r.raise_for_status()
return r.json()

def get_balance(self, address, *args, **kwargs):
address = cashtokenaddress_to_address(address)
api_url = self.make_endpoint_url("address").format(address)
Expand Down
21 changes: 21 additions & 0 deletions bitcash/network/APIs/ChaingraphAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,27 @@ def send_request(self, json_request, *args, **kwargs):
def get_default_endpoints(cls, network):
return cls.DEFAULT_ENDPOINTS[network]

def get_blockheight(self, *args, **kwargs):
json_request = {
"query": """
query GetBlockheight($node: String!) {
block(
limit: 1
order_by: { height: desc }
where: { accepted_by: { node: { name: { _like: $node } } } }
) {
height
}
}
""",
"variables": {
"node": self.node_like,
},
}
json = self.send_request(json_request, *args, **kwargs)
blockheight = int(json["data"]["block"][0]["height"])
return blockheight

def get_balance(self, address, *args, **kwargs):
json_request = {
"query": """
Expand Down
9 changes: 9 additions & 0 deletions bitcash/network/APIs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ def get_default_endpoints(self, network):
:rtype: ``list`` of ``str``
"""

@abstractmethod
def get_blockheight(self, *args, **kwargs):
"""
Return the block height.
:returns: Blockheight
:rtype: ``int``
"""

@abstractmethod
def get_balance(self, address, *args, **kwargs):
"""
Expand Down
57 changes: 49 additions & 8 deletions bitcash/network/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,47 @@ class NetworkAPI:
requests.exceptions.StreamConsumedError,
)

@classmethod
def get_ordered_endpoints_for(cls, network="mainnet", remove_bad_endpoints=True):
"""Gets endpoints ordered by their blockheights.
Solves the problem when an endpoint is stuck on an older block.
:param network: network in ["mainnet", "testnet", "regtest"].
:param remove_bad_endpoints: remove unreachable or un-synced endpoints.
"""
endpoints = get_endpoints_for(network)

endpoints_blockheight = [0 for _ in range(len(endpoints))]

for i, endpoint in enumerate(endpoints):
try:
endpoints_blockheight[i] = endpoint.get_blockheight(
timeout=DEFAULT_TIMEOUT
)
except cls.IGNORED_ERRORS: # pragma: no cover
pass

if sum(endpoints_blockheight) == 0:
raise ConnectionError("All APIs are unreachable.") # pragma: no cover

ordered_endpoints_blockheight, ordered_endpoints = zip(
*sorted(
zip(endpoints_blockheight, endpoints),
key=lambda tup: tup[0],
reverse=True,
)
)

ordered_endpoints = list(ordered_endpoints)

if remove_bad_endpoints:
highest_blockheight = ordered_endpoints_blockheight[0]
for i in reversed(range(len(ordered_endpoints_blockheight))):
if ordered_endpoints_blockheight[i] != highest_blockheight:
ordered_endpoints.pop(i)

return ordered_endpoints

@classmethod
def get_balance(cls, address, network="mainnet"):
"""Gets the balance of an address in satoshi.
Expand All @@ -131,7 +172,7 @@ def get_balance(cls, address, network="mainnet"):
:raises ConnectionError: If all API services fail.
:rtype: ``int``
"""
for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_balance(address, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -148,7 +189,7 @@ def get_transactions(cls, address, network="mainnet"):
:raises ConnectionError: If all API services fail.
:rtype: ``list`` of ``str``
"""
for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_transactions(address, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -166,7 +207,7 @@ def get_transaction(cls, txid, network="mainnet"):
:rtype: ``Transaction``
"""

for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_transaction(txid, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -186,7 +227,7 @@ def get_tx_amount(cls, txid, txindex, network="mainnet"):
:rtype: ``Decimal``
"""

for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_tx_amount(txid, txindex, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -204,7 +245,7 @@ def get_unspent(cls, address, network="mainnet"):
:rtype: ``list`` of :class:`~bitcash.network.meta.Unspent`
"""

for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_unspent(address, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -222,7 +263,7 @@ def get_raw_transaction(cls, txid, network="mainnet"):
:rtype: ``Transaction``
"""

for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
try:
return endpoint.get_raw_transaction(txid, timeout=DEFAULT_TIMEOUT)
except cls.IGNORED_ERRORS: # pragma: no cover
Expand All @@ -240,7 +281,7 @@ def broadcast_tx(cls, tx_hex, network="mainnet"): # pragma: no cover
"""
success = None

for endpoint in get_endpoints_for(network):
for endpoint in cls.get_ordered_endpoints_for(network):
_ = [end[0] for end in ChaingraphAPI.get_default_endpoints(network)]
if endpoint in _ and network == "mainnet":
# Default chaingraph endpoints do not indicate failed broadcast
Expand All @@ -256,7 +297,7 @@ def broadcast_tx(cls, tx_hex, network="mainnet"): # pragma: no cover

if not success:
raise ConnectionError(
"Transaction broadcast failed, or " "Unspents were already used."
"Transaction broadcast failed, or Unspents were already used."
)

raise ConnectionError("All APIs are unreachable.")
6 changes: 6 additions & 0 deletions tests/network/APIs/test_BitcoinDotComAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ def setup_method(self):
self.monkeypatch = MonkeyPatch()
self.api = BitcoinDotComAPI("https://dummy.com/v2/")

def test_get_blockheight(self):
return_json = 800_000
self.monkeypatch.setattr(_bapi, "session", DummySession(return_json))
blockheight = self.api.get_blockheight()
assert blockheight == 800_000

def test_get_balance(self):
return_json = {
"balanceSat": 2500,
Expand Down
14 changes: 14 additions & 0 deletions tests/network/APIs/test_ChaingraphAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ def setup_method(self):
self.monkeypatch = MonkeyPatch()
self.api = ChaingraphAPI("https://dummy.com/v1/graphql")

def test_get_blockheight(self):
return_json = {
"data": {
"block": [
{
"height": "123456"
},
]
}
}
self.monkeypatch.setattr(_capi, "session", DummySession(return_json))
blockheight = self.api.get_blockheight()
assert blockheight == 123456

def test_get_balance(self):
return_json = {
"data": {
Expand Down
36 changes: 36 additions & 0 deletions tests/network/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

import pytest
import bitcash
from _pytest.monkeypatch import MonkeyPatch
from bitcash.exceptions import InvalidEndpointURLProvided
from bitcash.network import services as _services
from bitcash.network.meta import Unspent
from bitcash.network.services import (
BitcoinDotComAPI,
Expand Down Expand Up @@ -72,7 +74,41 @@ def get_raw_transaction(cls, *args, **kwargs):
raise_connection_error()


class MockEndpoint:
def __init__(self, blockheight):
self.blockheight = blockheight

def get_blockheight(self, *args, **kwargs):
if self.blockheight < 0:
raise NetworkAPI.IGNORED_ERRORS[0]
return self.blockheight


def mock_get_endpoints_for(network):
return (
MockEndpoint(4),
MockEndpoint(-1),
MockEndpoint(0),
MockEndpoint(4),
MockEndpoint(4),
MockEndpoint(4),
MockEndpoint(3),
)


class TestNetworkAPI:
def test_get_ordered_endpoints_for(self):
monkeypatch = MonkeyPatch()
monkeypatch.setattr(_services, "get_endpoints_for", mock_get_endpoints_for)
endpoints = NetworkAPI.get_ordered_endpoints_for(
network="mainnet",
remove_bad_endpoints=True,
)
assert len(endpoints) == 4
# monkeypatch doesn't unset the attribute
# this fails the rest of the tests
monkeypatch.setattr(_services, "get_endpoints_for", get_endpoints_for)

# Mainnet
def test_get_balance_mainnet(self):
time.sleep(1)
Expand Down

0 comments on commit eea963f

Please sign in to comment.