blockapi 2.4.0__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {blockapi-2.4.0 → blockapi-2.5.0}/PKG-INFO +2 -1
- blockapi-2.5.0/blockapi/test/v2/api/test_blockchain_info.py +129 -0
- blockapi-2.5.0/blockapi/test/v2/api/test_haskoin.py +182 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/__init__.py +2 -0
- blockapi-2.5.0/blockapi/v2/api/blockchain_info.py +67 -0
- blockapi-2.5.0/blockapi/v2/api/haskoin.py +94 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/PKG-INFO +2 -1
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/SOURCES.txt +4 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/requires.txt +1 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/setup.py +2 -1
- {blockapi-2.4.0 → blockapi-2.5.0}/LICENSE.md +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/README.md +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/services.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_blockapi.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_num.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_random_user_agent.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/conftest.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/covalenth/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/covalenth/test_ethereum.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/fake_sleep_provider.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_magic_eden.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_opensea.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_simple_hash.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_unisat.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/perpetual/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/perpetual/test_perpetual.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/synthetix/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/synthetix/test_synthetix.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchainos.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_btc.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_doge.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_ltc.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_cosmos.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_ethplorer.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_multisources.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_optimistic_etherscan.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_solana.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_subscan_polkadot.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_sui.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_trezor_btc.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_trezor_zec.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_base.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_blockchain_api.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_blockchain_mapping.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_data.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_enumerate_classes.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_generic.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_models.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test_data.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/address.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/datetime.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/num.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/user_agent.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/blockchainos.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/blockchair.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/cosmos.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/arbitrum.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/astar.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/avalanche.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/axie.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/base.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/binance_smart_chain.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/ethereum.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/fantom.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/heco.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/iotex.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/klaytn.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/moonbeam.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/palm.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/polygon.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/rsk.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/debank.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/debank_maps.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/ethplorer.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/magic_eden.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/opensea.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/simple_hash.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/unisat.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/optimistic_etherscan.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/perp_abi.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/perpetual.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/solana.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/subscan.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/sui.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/__init__.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/synthetix.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/synthetix_abi.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/terra.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/trezor.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/web3_utils.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/base.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/blockchain_mapping.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/coin_mapping.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/coins.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/models.py +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/dependency_links.txt +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/top_level.txt +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/pyproject.toml +0 -0
- {blockapi-2.4.0 → blockapi-2.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blockapi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: BlockAPI library
|
|
5
5
|
Home-page: https://github.com/crypkit/blockapi
|
|
6
6
|
Author: Devmons s.r.o.
|
|
@@ -21,6 +21,7 @@ Requires-Dist: pytest-vcr
|
|
|
21
21
|
Requires-Dist: requests_mock>=1.9.3
|
|
22
22
|
Requires-Dist: attrs<23.0.0,>=17.4.0
|
|
23
23
|
Requires-Dist: solders>=0.22.0
|
|
24
|
+
Requires-Dist: base58>=2.1.0
|
|
24
25
|
Dynamic: author
|
|
25
26
|
Dynamic: description
|
|
26
27
|
Dynamic: description-content-type
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from blockapi.test.v2.api.conftest import read_file
|
|
6
|
+
|
|
7
|
+
# noinspection SpellCheckingInspection
|
|
8
|
+
btc_test_address = '35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP'
|
|
9
|
+
# noinspection SpellCheckingInspection
|
|
10
|
+
xpub_test_address = 'xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz'
|
|
11
|
+
from blockapi.v2.api import BlockchainInfoApi
|
|
12
|
+
from blockapi.v2.base import ApiException
|
|
13
|
+
from blockapi.v2.models import FetchResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_fetch_balances(requests_mock, blockchain_info_balance_response):
|
|
17
|
+
requests_mock.get(
|
|
18
|
+
'https://blockchain.info/multiaddr',
|
|
19
|
+
text=blockchain_info_balance_response,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
api = BlockchainInfoApi()
|
|
23
|
+
balances = api.get_balance(btc_test_address)
|
|
24
|
+
assert len(balances) == 1
|
|
25
|
+
assert balances[0].balance == Decimal('0.00064363')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_fetch_balances_xpub(requests_mock, blockchain_info_xpub_response):
|
|
29
|
+
requests_mock.get(
|
|
30
|
+
'https://blockchain.info/multiaddr',
|
|
31
|
+
text=blockchain_info_xpub_response,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
api = BlockchainInfoApi()
|
|
35
|
+
balances = api.get_balance(xpub_test_address)
|
|
36
|
+
assert len(balances) == 1
|
|
37
|
+
assert balances[0].balance == Decimal('0.12706308')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_fetch_only(requests_mock, blockchain_info_balance_response):
|
|
41
|
+
requests_mock.get(
|
|
42
|
+
'https://blockchain.info/multiaddr',
|
|
43
|
+
text=blockchain_info_balance_response,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
api = BlockchainInfoApi()
|
|
47
|
+
result = api.fetch_balances(btc_test_address)
|
|
48
|
+
assert result.data['wallet']['final_balance'] == 64363
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_parse_only():
|
|
52
|
+
api = BlockchainInfoApi()
|
|
53
|
+
fetch_result = FetchResult(data={'wallet': {'final_balance': 64363}})
|
|
54
|
+
result = api.parse_balances(fetch_result)
|
|
55
|
+
assert result.data[0].balance == Decimal('0.00064363')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_parse_zero_balance():
|
|
59
|
+
api = BlockchainInfoApi()
|
|
60
|
+
fetch_result = FetchResult(data={'wallet': {'final_balance': 0}})
|
|
61
|
+
result = api.parse_balances(fetch_result)
|
|
62
|
+
assert result.data is None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_parse_empty_wallet():
|
|
66
|
+
api = BlockchainInfoApi()
|
|
67
|
+
fetch_result = FetchResult(data={'wallet': {}})
|
|
68
|
+
result = api.parse_balances(fetch_result)
|
|
69
|
+
assert result.data is None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_fetch_invalid_address(requests_mock):
|
|
73
|
+
invalid_address = 'not-a-valid-btc-address'
|
|
74
|
+
requests_mock.get(
|
|
75
|
+
'https://blockchain.info/multiaddr',
|
|
76
|
+
status_code=400,
|
|
77
|
+
reason='Bad Request',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
api = BlockchainInfoApi()
|
|
81
|
+
result = api.fetch_balances(invalid_address)
|
|
82
|
+
assert result.errors == ['Bad Request']
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_get_balance_invalid_address_should_raise(requests_mock):
|
|
86
|
+
invalid_address = 'not-a-valid-btc-address'
|
|
87
|
+
requests_mock.get(
|
|
88
|
+
'https://blockchain.info/multiaddr',
|
|
89
|
+
status_code=400,
|
|
90
|
+
reason='Bad Request',
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
api = BlockchainInfoApi()
|
|
94
|
+
with pytest.raises(ApiException, match='Bad Request'):
|
|
95
|
+
api.get_balance(invalid_address)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_fetch_error_response(requests_mock):
|
|
99
|
+
requests_mock.get(
|
|
100
|
+
'https://blockchain.info/multiaddr',
|
|
101
|
+
status_code=500,
|
|
102
|
+
reason='Internal Server Error',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
api = BlockchainInfoApi()
|
|
106
|
+
result = api.fetch_balances(btc_test_address)
|
|
107
|
+
assert result.errors == ['Internal Server Error']
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_get_balance_should_raise(requests_mock):
|
|
111
|
+
requests_mock.get(
|
|
112
|
+
'https://blockchain.info/multiaddr',
|
|
113
|
+
status_code=500,
|
|
114
|
+
reason='Internal Server Error',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
api = BlockchainInfoApi()
|
|
118
|
+
with pytest.raises(ApiException, match='Internal Server Error'):
|
|
119
|
+
api.get_balance(btc_test_address)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.fixture
|
|
123
|
+
def blockchain_info_balance_response():
|
|
124
|
+
return read_file('data/blockchain_info_balance_response.json')
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@pytest.fixture
|
|
128
|
+
def blockchain_info_xpub_response():
|
|
129
|
+
return read_file('data/blockchain_info_xpub_response.json')
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from blockapi.test.v2.api.conftest import read_file
|
|
6
|
+
from blockapi.v2.api.haskoin import HaskoinApi, _to_xpub
|
|
7
|
+
from blockapi.v2.base import ApiException
|
|
8
|
+
from blockapi.v2.models import FetchResult
|
|
9
|
+
|
|
10
|
+
# noinspection SpellCheckingInspection
|
|
11
|
+
btc_test_address = '35hK24tcLEWcgNA4JxpvbkNkoAcDGqQPsP'
|
|
12
|
+
# noinspection SpellCheckingInspection
|
|
13
|
+
xpub_test_address = 'xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKrhko4egpiMZbpiaQL2jkwSB1icqYh2cfDfVxdx4df189oLKnC5fSwqPfgyP3hooxujYzAu3fDVmz'
|
|
14
|
+
# noinspection SpellCheckingInspection
|
|
15
|
+
# ypub/zpub derived from xpub_test_address by swapping version bytes
|
|
16
|
+
ypub_test_address = 'ypub6XJXj9Uhi7wYJp5aC8n9qwcj4wxxGLKMcsKHS5ibjZyhmgDZHPvW4Efre3WH2XK9595ShYEDTnWMDcPkoMrxddMHqik8PinQ1H3pHbCYAtS'
|
|
17
|
+
# noinspection SpellCheckingInspection
|
|
18
|
+
zpub_test_address = 'zpub6r8o2p9croV2A7Gh2VZn42iEEv7QCxJrXyqWDUcV7aMapn2nY464gJKzfFTs2Ry4UnCFT1pmvSru6u1KX4GyRs2ti4SYydbtH17Tg8wL57f'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_fetch_balances(requests_mock, haskoin_balance_response):
|
|
22
|
+
requests_mock.get(
|
|
23
|
+
f'https://api.haskoin.com/btc/address/{btc_test_address}/balance',
|
|
24
|
+
text=haskoin_balance_response,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
api = HaskoinApi()
|
|
28
|
+
balances = api.get_balance(btc_test_address)
|
|
29
|
+
assert len(balances) == 1
|
|
30
|
+
assert balances[0].balance == Decimal('0.00064363')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_fetch_balances_xpub(requests_mock, haskoin_xpub_response):
|
|
34
|
+
requests_mock.get(
|
|
35
|
+
f'https://api.haskoin.com/btc/xpub/{xpub_test_address}',
|
|
36
|
+
text=haskoin_xpub_response,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
api = HaskoinApi()
|
|
40
|
+
balances = api.get_balance(xpub_test_address)
|
|
41
|
+
assert len(balances) == 1
|
|
42
|
+
assert balances[0].balance == Decimal('0.12706308')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_fetch_only(requests_mock, haskoin_balance_response):
|
|
46
|
+
requests_mock.get(
|
|
47
|
+
f'https://api.haskoin.com/btc/address/{btc_test_address}/balance',
|
|
48
|
+
text=haskoin_balance_response,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
api = HaskoinApi()
|
|
52
|
+
result = api.fetch_balances(btc_test_address)
|
|
53
|
+
assert result.data['confirmed'] == 64363
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_parse_address():
|
|
57
|
+
api = HaskoinApi()
|
|
58
|
+
fetch_result = FetchResult(data={'confirmed': 64363})
|
|
59
|
+
result = api.parse_balances(fetch_result)
|
|
60
|
+
assert result.data[0].balance == Decimal('0.00064363')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_parse_xpub():
|
|
64
|
+
api = HaskoinApi()
|
|
65
|
+
fetch_result = FetchResult(data={'balance': {'confirmed': 12706308}, 'indices': {}})
|
|
66
|
+
result = api.parse_balances(fetch_result)
|
|
67
|
+
assert result.data[0].balance == Decimal('0.12706308')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_fetch_balances_ypub(requests_mock, haskoin_xpub_response):
|
|
71
|
+
converted_xpub = _to_xpub(ypub_test_address)
|
|
72
|
+
requests_mock.get(
|
|
73
|
+
f'https://api.haskoin.com/btc/xpub/{converted_xpub}',
|
|
74
|
+
text=haskoin_xpub_response,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
api = HaskoinApi()
|
|
78
|
+
balances = api.get_balance(ypub_test_address)
|
|
79
|
+
assert len(balances) == 1
|
|
80
|
+
assert balances[0].balance == Decimal('0.12706308')
|
|
81
|
+
|
|
82
|
+
# Verify derive=compat was passed
|
|
83
|
+
assert requests_mock.last_request.qs['derive'] == ['compat']
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_fetch_balances_zpub(requests_mock, haskoin_xpub_response):
|
|
87
|
+
converted_xpub = _to_xpub(zpub_test_address)
|
|
88
|
+
requests_mock.get(
|
|
89
|
+
f'https://api.haskoin.com/btc/xpub/{converted_xpub}',
|
|
90
|
+
text=haskoin_xpub_response,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
api = HaskoinApi()
|
|
94
|
+
balances = api.get_balance(zpub_test_address)
|
|
95
|
+
assert len(balances) == 1
|
|
96
|
+
assert balances[0].balance == Decimal('0.12706308')
|
|
97
|
+
|
|
98
|
+
# Verify derive=segwit was passed
|
|
99
|
+
assert requests_mock.last_request.qs['derive'] == ['segwit']
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_to_xpub_roundtrip():
|
|
103
|
+
"""ypub/zpub converted to xpub should start with 'xpub' and be valid base58."""
|
|
104
|
+
converted = _to_xpub(ypub_test_address)
|
|
105
|
+
assert converted.startswith('xpub')
|
|
106
|
+
|
|
107
|
+
converted = _to_xpub(zpub_test_address)
|
|
108
|
+
assert converted.startswith('xpub')
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_parse_zero_balance_address():
|
|
112
|
+
api = HaskoinApi()
|
|
113
|
+
fetch_result = FetchResult(data={'confirmed': 0})
|
|
114
|
+
result = api.parse_balances(fetch_result)
|
|
115
|
+
assert result.data is None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_parse_zero_balance_xpub():
|
|
119
|
+
api = HaskoinApi()
|
|
120
|
+
fetch_result = FetchResult(data={'balance': {'confirmed': 0}, 'indices': {}})
|
|
121
|
+
result = api.parse_balances(fetch_result)
|
|
122
|
+
assert result.data is None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_fetch_invalid_address(requests_mock):
|
|
126
|
+
invalid_address = 'not-a-valid-btc-address'
|
|
127
|
+
requests_mock.get(
|
|
128
|
+
f'https://api.haskoin.com/btc/address/{invalid_address}/balance',
|
|
129
|
+
status_code=400,
|
|
130
|
+
reason='Bad Request',
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
api = HaskoinApi()
|
|
134
|
+
result = api.fetch_balances(invalid_address)
|
|
135
|
+
assert result.errors == ['Bad Request']
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_get_balance_invalid_address_should_raise(requests_mock):
|
|
139
|
+
invalid_address = 'not-a-valid-btc-address'
|
|
140
|
+
requests_mock.get(
|
|
141
|
+
f'https://api.haskoin.com/btc/address/{invalid_address}/balance',
|
|
142
|
+
status_code=400,
|
|
143
|
+
reason='Bad Request',
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
api = HaskoinApi()
|
|
147
|
+
with pytest.raises(ApiException, match='Bad Request'):
|
|
148
|
+
api.get_balance(invalid_address)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_fetch_error_response(requests_mock):
|
|
152
|
+
requests_mock.get(
|
|
153
|
+
f'https://api.haskoin.com/btc/address/{btc_test_address}/balance',
|
|
154
|
+
status_code=500,
|
|
155
|
+
reason='Internal Server Error',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
api = HaskoinApi()
|
|
159
|
+
result = api.fetch_balances(btc_test_address)
|
|
160
|
+
assert result.errors == ['Internal Server Error']
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_get_balance_should_raise(requests_mock):
|
|
164
|
+
requests_mock.get(
|
|
165
|
+
f'https://api.haskoin.com/btc/address/{btc_test_address}/balance',
|
|
166
|
+
status_code=500,
|
|
167
|
+
reason='Internal Server Error',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
api = HaskoinApi()
|
|
171
|
+
with pytest.raises(ApiException, match='Internal Server Error'):
|
|
172
|
+
api.get_balance(btc_test_address)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.fixture
|
|
176
|
+
def haskoin_balance_response():
|
|
177
|
+
return read_file('data/haskoin_balance_response.json')
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.fixture
|
|
181
|
+
def haskoin_xpub_response():
|
|
182
|
+
return read_file('data/haskoin_xpub_response.json')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from blockapi.v2.api.blockchain_info import BlockchainInfoApi
|
|
1
2
|
from blockapi.v2.api.blockchainos import BlockchainosApi
|
|
2
3
|
from blockapi.v2.api.blockchair import (
|
|
3
4
|
BlockchairApi,
|
|
@@ -7,6 +8,7 @@ from blockapi.v2.api.blockchair import (
|
|
|
7
8
|
)
|
|
8
9
|
from blockapi.v2.api.debank import DebankApi, DebankApp, DebankPrediction
|
|
9
10
|
from blockapi.v2.api.ethplorer import EthplorerApi
|
|
11
|
+
from blockapi.v2.api.haskoin import HaskoinApi
|
|
10
12
|
from blockapi.v2.api.optimistic_etherscan import OptimismEtherscanApi
|
|
11
13
|
from blockapi.v2.api.perpetual import PerpetualApi, perp_contract_address
|
|
12
14
|
from blockapi.v2.api.solana import SolanaApi, SolscanApi
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from blockapi.v2.base import BalanceMixin, BlockchainApi
|
|
2
|
+
from blockapi.v2.coins import COIN_BTC
|
|
3
|
+
from blockapi.v2.models import (
|
|
4
|
+
ApiOptions,
|
|
5
|
+
AssetType,
|
|
6
|
+
BalanceItem,
|
|
7
|
+
Blockchain,
|
|
8
|
+
FetchResult,
|
|
9
|
+
ParseResult,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlockchainInfoApi(BlockchainApi, BalanceMixin):
|
|
14
|
+
"""
|
|
15
|
+
Coin: Bitcoin
|
|
16
|
+
API docs: https://www.blockchain.com/explorer/api/blockchain_api
|
|
17
|
+
Explorer: https://www.blockchain.com/explorer
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
coin = COIN_BTC
|
|
21
|
+
api_options = ApiOptions(
|
|
22
|
+
blockchain=Blockchain.BITCOIN,
|
|
23
|
+
base_url='https://blockchain.info',
|
|
24
|
+
rate_limit=0.2,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
supported_requests = {
|
|
28
|
+
'get_balance': '/multiaddr',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def fetch_balances(self, address: str) -> FetchResult:
|
|
32
|
+
return self.get_data(
|
|
33
|
+
'get_balance',
|
|
34
|
+
params={'active': address, 'n': 0},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def parse_balances(self, fetch_result: FetchResult) -> ParseResult:
|
|
38
|
+
"""Parse balance from blockchain.info ``/multiaddr`` response.
|
|
39
|
+
|
|
40
|
+
Note: ``final_balance`` includes **unconfirmed** transactions (0-conf)
|
|
41
|
+
and may therefore be higher than balances reported by Trezor/Blockbook
|
|
42
|
+
or Haskoin for high-activity addresses. There is no blockchain.info
|
|
43
|
+
endpoint that reliably returns confirmed-only balance (the ``/unspent``
|
|
44
|
+
endpoint supports ``confirmations=1`` but is capped at 1 000 UTXOs
|
|
45
|
+
with broken pagination).
|
|
46
|
+
"""
|
|
47
|
+
if not fetch_result.data:
|
|
48
|
+
return ParseResult()
|
|
49
|
+
|
|
50
|
+
wallet = fetch_result.data.get('wallet', {})
|
|
51
|
+
if not wallet:
|
|
52
|
+
return ParseResult()
|
|
53
|
+
|
|
54
|
+
balance_raw = wallet.get('final_balance')
|
|
55
|
+
if not balance_raw:
|
|
56
|
+
return ParseResult()
|
|
57
|
+
|
|
58
|
+
balances = [
|
|
59
|
+
BalanceItem.from_api(
|
|
60
|
+
balance_raw=balance_raw,
|
|
61
|
+
coin=self.coin,
|
|
62
|
+
asset_type=AssetType.AVAILABLE,
|
|
63
|
+
raw=fetch_result.data,
|
|
64
|
+
)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
return ParseResult(data=balances)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import base58
|
|
2
|
+
|
|
3
|
+
from blockapi.v2.base import BalanceMixin, BlockchainApi
|
|
4
|
+
from blockapi.v2.coins import COIN_BTC
|
|
5
|
+
from blockapi.v2.models import (
|
|
6
|
+
ApiOptions,
|
|
7
|
+
AssetType,
|
|
8
|
+
BalanceItem,
|
|
9
|
+
Blockchain,
|
|
10
|
+
FetchResult,
|
|
11
|
+
ParseResult,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Version bytes for extended public keys (BIP32)
|
|
15
|
+
_XPUB_VERSION = bytes.fromhex('0488B21E')
|
|
16
|
+
_YPUB_VERSION = bytes.fromhex('049D7CB2')
|
|
17
|
+
_ZPUB_VERSION = bytes.fromhex('04B24746')
|
|
18
|
+
|
|
19
|
+
# Haskoin derive param: ypub -> compat (P2SH), zpub -> segwit (P2WPKH)
|
|
20
|
+
_DERIVE_TYPES = {
|
|
21
|
+
'ypub': 'compat',
|
|
22
|
+
'zpub': 'segwit',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _to_xpub(key: str) -> str:
|
|
27
|
+
"""Convert ypub/zpub to xpub by replacing version bytes."""
|
|
28
|
+
decoded = base58.b58decode_check(key)
|
|
29
|
+
return base58.b58encode_check(_XPUB_VERSION + decoded[4:]).decode()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HaskoinApi(BlockchainApi, BalanceMixin):
|
|
33
|
+
"""
|
|
34
|
+
Coin: Bitcoin
|
|
35
|
+
API docs: https://api.haskoin.com/
|
|
36
|
+
Source: https://github.com/jprupp/haskoin-store
|
|
37
|
+
|
|
38
|
+
Supports xpub, ypub (BIP49, P2SH-segwit), and zpub (BIP84, native segwit).
|
|
39
|
+
ypub/zpub are converted to xpub and passed with the appropriate derive param.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
coin = COIN_BTC
|
|
43
|
+
api_options = ApiOptions(
|
|
44
|
+
blockchain=Blockchain.BITCOIN,
|
|
45
|
+
base_url='https://api.haskoin.com/btc/',
|
|
46
|
+
rate_limit=0.2,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
supported_requests = {
|
|
50
|
+
'get_balance': 'address/{address}/balance',
|
|
51
|
+
'get_balance_xpub': 'xpub/{address}',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def fetch_balances(self, address: str) -> FetchResult:
|
|
55
|
+
prefix = address[:4]
|
|
56
|
+
derive = _DERIVE_TYPES.get(prefix)
|
|
57
|
+
|
|
58
|
+
if derive:
|
|
59
|
+
xpub = _to_xpub(address)
|
|
60
|
+
return self.get_data(
|
|
61
|
+
'get_balance_xpub',
|
|
62
|
+
params={'derive': derive},
|
|
63
|
+
address=xpub,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if prefix == 'xpub':
|
|
67
|
+
return self.get_data('get_balance_xpub', address=address)
|
|
68
|
+
|
|
69
|
+
return self.get_data('get_balance', address=address)
|
|
70
|
+
|
|
71
|
+
def parse_balances(self, fetch_result: FetchResult) -> ParseResult:
|
|
72
|
+
if not fetch_result.data:
|
|
73
|
+
return ParseResult()
|
|
74
|
+
|
|
75
|
+
# xpub response: {"balance": {"confirmed": ...}, "indices": {...}}
|
|
76
|
+
# address response: {"address": ..., "confirmed": ..., ...}
|
|
77
|
+
if 'balance' in fetch_result.data:
|
|
78
|
+
balance_raw = fetch_result.data['balance'].get('confirmed')
|
|
79
|
+
else:
|
|
80
|
+
balance_raw = fetch_result.data.get('confirmed')
|
|
81
|
+
|
|
82
|
+
if not balance_raw:
|
|
83
|
+
return ParseResult()
|
|
84
|
+
|
|
85
|
+
balances = [
|
|
86
|
+
BalanceItem.from_api(
|
|
87
|
+
balance_raw=balance_raw,
|
|
88
|
+
coin=self.coin,
|
|
89
|
+
asset_type=AssetType.AVAILABLE,
|
|
90
|
+
raw=fetch_result.data,
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
return ParseResult(data=balances)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blockapi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: BlockAPI library
|
|
5
5
|
Home-page: https://github.com/crypkit/blockapi
|
|
6
6
|
Author: Devmons s.r.o.
|
|
@@ -21,6 +21,7 @@ Requires-Dist: pytest-vcr
|
|
|
21
21
|
Requires-Dist: requests_mock>=1.9.3
|
|
22
22
|
Requires-Dist: attrs<23.0.0,>=17.4.0
|
|
23
23
|
Requires-Dist: solders>=0.22.0
|
|
24
|
+
Requires-Dist: base58>=2.1.0
|
|
24
25
|
Dynamic: author
|
|
25
26
|
Dynamic: description
|
|
26
27
|
Dynamic: description-content-type
|
|
@@ -26,12 +26,14 @@ blockapi/test/v2/test_models.py
|
|
|
26
26
|
blockapi/test/v2/api/__init__.py
|
|
27
27
|
blockapi/test/v2/api/conftest.py
|
|
28
28
|
blockapi/test/v2/api/fake_sleep_provider.py
|
|
29
|
+
blockapi/test/v2/api/test_blockchain_info.py
|
|
29
30
|
blockapi/test/v2/api/test_blockchainos.py
|
|
30
31
|
blockapi/test/v2/api/test_blockchair_btc.py
|
|
31
32
|
blockapi/test/v2/api/test_blockchair_doge.py
|
|
32
33
|
blockapi/test/v2/api/test_blockchair_ltc.py
|
|
33
34
|
blockapi/test/v2/api/test_cosmos.py
|
|
34
35
|
blockapi/test/v2/api/test_ethplorer.py
|
|
36
|
+
blockapi/test/v2/api/test_haskoin.py
|
|
35
37
|
blockapi/test/v2/api/test_multisources.py
|
|
36
38
|
blockapi/test/v2/api/test_optimistic_etherscan.py
|
|
37
39
|
blockapi/test/v2/api/test_solana.py
|
|
@@ -62,12 +64,14 @@ blockapi/v2/coin_mapping.py
|
|
|
62
64
|
blockapi/v2/coins.py
|
|
63
65
|
blockapi/v2/models.py
|
|
64
66
|
blockapi/v2/api/__init__.py
|
|
67
|
+
blockapi/v2/api/blockchain_info.py
|
|
65
68
|
blockapi/v2/api/blockchainos.py
|
|
66
69
|
blockapi/v2/api/blockchair.py
|
|
67
70
|
blockapi/v2/api/cosmos.py
|
|
68
71
|
blockapi/v2/api/debank.py
|
|
69
72
|
blockapi/v2/api/debank_maps.py
|
|
70
73
|
blockapi/v2/api/ethplorer.py
|
|
74
|
+
blockapi/v2/api/haskoin.py
|
|
71
75
|
blockapi/v2/api/optimistic_etherscan.py
|
|
72
76
|
blockapi/v2/api/solana.py
|
|
73
77
|
blockapi/v2/api/subscan.py
|
|
@@ -6,7 +6,7 @@ with open("README.md", "r") as f:
|
|
|
6
6
|
|
|
7
7
|
PACKAGES = find_packages(where='.')
|
|
8
8
|
|
|
9
|
-
__version__ = "2.
|
|
9
|
+
__version__ = "2.5.0"
|
|
10
10
|
|
|
11
11
|
setuptools.setup(
|
|
12
12
|
name='blockapi',
|
|
@@ -32,6 +32,7 @@ setuptools.setup(
|
|
|
32
32
|
'requests_mock>=1.9.3',
|
|
33
33
|
'attrs>=17.4.0,<23.0.0',
|
|
34
34
|
'solders>=0.22.0',
|
|
35
|
+
'base58>=2.1.0',
|
|
35
36
|
],
|
|
36
37
|
url="https://github.com/crypkit/blockapi",
|
|
37
38
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|