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.
Files changed (109) hide show
  1. {blockapi-2.4.0 → blockapi-2.5.0}/PKG-INFO +2 -1
  2. blockapi-2.5.0/blockapi/test/v2/api/test_blockchain_info.py +129 -0
  3. blockapi-2.5.0/blockapi/test/v2/api/test_haskoin.py +182 -0
  4. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/__init__.py +2 -0
  5. blockapi-2.5.0/blockapi/v2/api/blockchain_info.py +67 -0
  6. blockapi-2.5.0/blockapi/v2/api/haskoin.py +94 -0
  7. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/PKG-INFO +2 -1
  8. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/SOURCES.txt +4 -0
  9. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/requires.txt +1 -0
  10. {blockapi-2.4.0 → blockapi-2.5.0}/setup.py +2 -1
  11. {blockapi-2.4.0 → blockapi-2.5.0}/LICENSE.md +0 -0
  12. {blockapi-2.4.0 → blockapi-2.5.0}/README.md +0 -0
  13. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/__init__.py +0 -0
  14. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/services.py +0 -0
  15. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/__init__.py +0 -0
  16. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_blockapi.py +0 -0
  17. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_num.py +0 -0
  18. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/test_random_user_agent.py +0 -0
  19. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/__init__.py +0 -0
  20. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/__init__.py +0 -0
  21. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/conftest.py +0 -0
  22. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/covalenth/__init__.py +0 -0
  23. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/covalenth/test_ethereum.py +0 -0
  24. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/fake_sleep_provider.py +0 -0
  25. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/__init__.py +0 -0
  26. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_magic_eden.py +0 -0
  27. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_opensea.py +0 -0
  28. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_simple_hash.py +0 -0
  29. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/nft/test_unisat.py +0 -0
  30. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/perpetual/__init__.py +0 -0
  31. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/perpetual/test_perpetual.py +0 -0
  32. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/synthetix/__init__.py +0 -0
  33. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/synthetix/test_synthetix.py +0 -0
  34. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchainos.py +0 -0
  35. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_btc.py +0 -0
  36. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_doge.py +0 -0
  37. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_blockchair_ltc.py +0 -0
  38. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_cosmos.py +0 -0
  39. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_ethplorer.py +0 -0
  40. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_multisources.py +0 -0
  41. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_optimistic_etherscan.py +0 -0
  42. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_solana.py +0 -0
  43. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_subscan_polkadot.py +0 -0
  44. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_sui.py +0 -0
  45. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_trezor_btc.py +0 -0
  46. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/api/test_trezor_zec.py +0 -0
  47. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_base.py +0 -0
  48. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_blockchain_api.py +0 -0
  49. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_blockchain_mapping.py +0 -0
  50. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_data.py +0 -0
  51. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_enumerate_classes.py +0 -0
  52. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_generic.py +0 -0
  53. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test/v2/test_models.py +0 -0
  54. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/test_data.py +0 -0
  55. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/__init__.py +0 -0
  56. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/address.py +0 -0
  57. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/datetime.py +0 -0
  58. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/num.py +0 -0
  59. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/utils/user_agent.py +0 -0
  60. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/__init__.py +0 -0
  61. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/blockchainos.py +0 -0
  62. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/blockchair.py +0 -0
  63. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/cosmos.py +0 -0
  64. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/__init__.py +0 -0
  65. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/arbitrum.py +0 -0
  66. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/astar.py +0 -0
  67. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/avalanche.py +0 -0
  68. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/axie.py +0 -0
  69. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/base.py +0 -0
  70. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/binance_smart_chain.py +0 -0
  71. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/ethereum.py +0 -0
  72. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/fantom.py +0 -0
  73. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/heco.py +0 -0
  74. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/iotex.py +0 -0
  75. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/klaytn.py +0 -0
  76. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/moonbeam.py +0 -0
  77. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/palm.py +0 -0
  78. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/polygon.py +0 -0
  79. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/covalenth/rsk.py +0 -0
  80. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/debank.py +0 -0
  81. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/debank_maps.py +0 -0
  82. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/ethplorer.py +0 -0
  83. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/__init__.py +0 -0
  84. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/magic_eden.py +0 -0
  85. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/opensea.py +0 -0
  86. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/simple_hash.py +0 -0
  87. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/nft/unisat.py +0 -0
  88. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/optimistic_etherscan.py +0 -0
  89. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/__init__.py +0 -0
  90. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/perp_abi.py +0 -0
  91. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/perpetual/perpetual.py +0 -0
  92. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/solana.py +0 -0
  93. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/subscan.py +0 -0
  94. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/sui.py +0 -0
  95. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/__init__.py +0 -0
  96. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/synthetix.py +0 -0
  97. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/synthetix/synthetix_abi.py +0 -0
  98. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/terra.py +0 -0
  99. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/trezor.py +0 -0
  100. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/api/web3_utils.py +0 -0
  101. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/base.py +0 -0
  102. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/blockchain_mapping.py +0 -0
  103. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/coin_mapping.py +0 -0
  104. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/coins.py +0 -0
  105. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi/v2/models.py +0 -0
  106. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/dependency_links.txt +0 -0
  107. {blockapi-2.4.0 → blockapi-2.5.0}/blockapi.egg-info/top_level.txt +0 -0
  108. {blockapi-2.4.0 → blockapi-2.5.0}/pyproject.toml +0 -0
  109. {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.4.0
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.4.0
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
@@ -12,3 +12,4 @@ pytest-vcr
12
12
  requests_mock>=1.9.3
13
13
  attrs<23.0.0,>=17.4.0
14
14
  solders>=0.22.0
15
+ base58>=2.1.0
@@ -6,7 +6,7 @@ with open("README.md", "r") as f:
6
6
 
7
7
  PACKAGES = find_packages(where='.')
8
8
 
9
- __version__ = "2.4.0"
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