blockapi 2.5.5__tar.gz → 2.5.6__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 (111) hide show
  1. {blockapi-2.5.5 → blockapi-2.5.6}/PKG-INFO +1 -1
  2. blockapi-2.5.6/blockapi/test/v2/api/test_terra.py +305 -0
  3. blockapi-2.5.6/blockapi/v2/api/terra.py +59 -0
  4. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/coins.py +2 -2
  5. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi.egg-info/PKG-INFO +1 -1
  6. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi.egg-info/SOURCES.txt +1 -0
  7. {blockapi-2.5.5 → blockapi-2.5.6}/setup.py +1 -1
  8. blockapi-2.5.5/blockapi/v2/api/terra.py +0 -318
  9. {blockapi-2.5.5 → blockapi-2.5.6}/LICENSE.md +0 -0
  10. {blockapi-2.5.5 → blockapi-2.5.6}/README.md +0 -0
  11. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/__init__.py +0 -0
  12. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/services.py +0 -0
  13. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/__init__.py +0 -0
  14. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/test_blockapi.py +0 -0
  15. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/test_num.py +0 -0
  16. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/test_random_user_agent.py +0 -0
  17. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/__init__.py +0 -0
  18. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/__init__.py +0 -0
  19. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/conftest.py +0 -0
  20. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/covalenth/__init__.py +0 -0
  21. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/covalenth/test_ethereum.py +0 -0
  22. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/fake_sleep_provider.py +0 -0
  23. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/nft/__init__.py +0 -0
  24. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/nft/test_magic_eden.py +0 -0
  25. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/nft/test_opensea.py +0 -0
  26. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/nft/test_simple_hash.py +0 -0
  27. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/nft/test_unisat.py +0 -0
  28. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/perpetual/__init__.py +0 -0
  29. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/perpetual/test_perpetual.py +0 -0
  30. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/synthetix/__init__.py +0 -0
  31. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/synthetix/test_synthetix.py +0 -0
  32. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_blockchain_info.py +0 -0
  33. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_blockchainos.py +0 -0
  34. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_blockchair_btc.py +0 -0
  35. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_blockchair_doge.py +0 -0
  36. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_blockchair_ltc.py +0 -0
  37. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_cosmos.py +0 -0
  38. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_ethplorer.py +0 -0
  39. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_haskoin.py +0 -0
  40. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_multisources.py +0 -0
  41. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_optimistic_etherscan.py +0 -0
  42. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_solana.py +0 -0
  43. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_subscan_polkadot.py +0 -0
  44. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_sui.py +0 -0
  45. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_trezor_btc.py +0 -0
  46. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/api/test_trezor_zec.py +0 -0
  47. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_base.py +0 -0
  48. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_blockchain_api.py +0 -0
  49. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_blockchain_mapping.py +0 -0
  50. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_data.py +0 -0
  51. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_enumerate_classes.py +0 -0
  52. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_generic.py +0 -0
  53. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test/v2/test_models.py +0 -0
  54. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/test_data.py +0 -0
  55. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/utils/__init__.py +0 -0
  56. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/utils/address.py +0 -0
  57. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/utils/datetime.py +0 -0
  58. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/utils/num.py +0 -0
  59. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/utils/user_agent.py +0 -0
  60. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/__init__.py +0 -0
  61. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/__init__.py +0 -0
  62. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/blockchain_info.py +0 -0
  63. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/blockchainos.py +0 -0
  64. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/blockchair.py +0 -0
  65. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/cosmos.py +0 -0
  66. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/__init__.py +0 -0
  67. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/arbitrum.py +0 -0
  68. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/astar.py +0 -0
  69. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/avalanche.py +0 -0
  70. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/axie.py +0 -0
  71. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/base.py +0 -0
  72. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/binance_smart_chain.py +0 -0
  73. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/ethereum.py +0 -0
  74. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/fantom.py +0 -0
  75. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/heco.py +0 -0
  76. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/iotex.py +0 -0
  77. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/klaytn.py +0 -0
  78. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/moonbeam.py +0 -0
  79. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/palm.py +0 -0
  80. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/polygon.py +0 -0
  81. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/covalenth/rsk.py +0 -0
  82. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/debank.py +0 -0
  83. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/debank_maps.py +0 -0
  84. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/ethplorer.py +0 -0
  85. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/haskoin.py +0 -0
  86. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/nft/__init__.py +0 -0
  87. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/nft/magic_eden.py +0 -0
  88. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/nft/opensea.py +0 -0
  89. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/nft/simple_hash.py +0 -0
  90. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/nft/unisat.py +0 -0
  91. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/optimistic_etherscan.py +0 -0
  92. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/perpetual/__init__.py +0 -0
  93. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/perpetual/perp_abi.py +0 -0
  94. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/perpetual/perpetual.py +0 -0
  95. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/solana.py +0 -0
  96. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/subscan.py +0 -0
  97. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/sui.py +0 -0
  98. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/synthetix/__init__.py +0 -0
  99. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/synthetix/synthetix.py +0 -0
  100. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/synthetix/synthetix_abi.py +0 -0
  101. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/trezor.py +0 -0
  102. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/api/web3_utils.py +0 -0
  103. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/base.py +0 -0
  104. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/blockchain_mapping.py +0 -0
  105. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/coin_mapping.py +0 -0
  106. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi/v2/models.py +0 -0
  107. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi.egg-info/dependency_links.txt +0 -0
  108. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi.egg-info/requires.txt +0 -0
  109. {blockapi-2.5.5 → blockapi-2.5.6}/blockapi.egg-info/top_level.txt +0 -0
  110. {blockapi-2.5.5 → blockapi-2.5.6}/pyproject.toml +0 -0
  111. {blockapi-2.5.5 → blockapi-2.5.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockapi
3
- Version: 2.5.5
3
+ Version: 2.5.6
4
4
  Summary: BlockAPI library
5
5
  Home-page: https://github.com/crypkit/blockapi
6
6
  Author: Devmons s.r.o.
@@ -0,0 +1,305 @@
1
+ from decimal import Decimal
2
+
3
+ import pytest
4
+
5
+ from blockapi.v2.api.terra import TerraApi
6
+ from blockapi.v2.models import AssetType, Blockchain
7
+
8
+
9
+ @pytest.fixture()
10
+ def terra_api(requests_mock):
11
+ """TerraApi with token mapping disabled to avoid external calls."""
12
+ requests_mock.get(
13
+ 'https://raw.githubusercontent.com/PulsarDefi/IBC-Token-Data-Cosmos/main/native_token_data.min.json',
14
+ json={
15
+ 'uluna__terra': {
16
+ 'name': 'Terra Classic',
17
+ 'chain': 'terra',
18
+ 'denom': 'uluna',
19
+ 'symbol': 'LUNC',
20
+ 'decimals': 6,
21
+ 'coingecko_id': 'terra-luna',
22
+ 'bridge_asset': None,
23
+ 'logos': {},
24
+ },
25
+ },
26
+ )
27
+ requests_mock.get(
28
+ 'https://raw.githubusercontent.com/PulsarDefi/IBC-Token-Data-Cosmos/main/ibc_data.min.json',
29
+ json={},
30
+ )
31
+ return TerraApi()
32
+
33
+
34
+ @pytest.fixture()
35
+ def balances_response():
36
+ return {
37
+ 'balances': [
38
+ {'denom': 'uluna', 'amount': '23068009633'},
39
+ {'denom': 'uusd', 'amount': '85997844'},
40
+ ],
41
+ 'pagination': {'next_key': None, 'total': '2'},
42
+ }
43
+
44
+
45
+ @pytest.fixture()
46
+ def staking_response():
47
+ return {
48
+ 'delegation_responses': [
49
+ {
50
+ 'delegation': {
51
+ 'delegator_address': 'terra1test',
52
+ 'validator_address': 'terravaloper1test',
53
+ 'shares': '100000000.000000000000000000',
54
+ },
55
+ 'balance': {'denom': 'uluna', 'amount': '100000000'},
56
+ }
57
+ ],
58
+ 'pagination': {'next_key': None, 'total': '1'},
59
+ }
60
+
61
+
62
+ @pytest.fixture()
63
+ def unbonding_response():
64
+ return {
65
+ 'unbonding_responses': [],
66
+ 'pagination': {'next_key': None, 'total': '0'},
67
+ }
68
+
69
+
70
+ @pytest.fixture()
71
+ def rewards_response():
72
+ return {
73
+ 'rewards': [
74
+ {
75
+ 'validator_address': 'terravaloper1test',
76
+ 'reward': [
77
+ {'denom': 'uluna', 'amount': '5000000.000000000000000000'},
78
+ {'denom': 'uusd', 'amount': '1000000.000000000000000000'},
79
+ ],
80
+ }
81
+ ],
82
+ 'total': [
83
+ {'denom': 'uluna', 'amount': '5000000.000000000000000000'},
84
+ {'denom': 'uusd', 'amount': '1000000.000000000000000000'},
85
+ ],
86
+ }
87
+
88
+
89
+ @pytest.fixture()
90
+ def ibc_denom_trace_response():
91
+ return {
92
+ 'denom_trace': {
93
+ 'path': 'transfer/channel-7',
94
+ 'base_denom': 'xrowan',
95
+ }
96
+ }
97
+
98
+
99
+ ADDRESS = 'terra1yltenl48mhl370ldpyt83werd9x3s645509gaf'
100
+ BASE_URL = 'https://terra-classic-fcd.publicnode.com'
101
+
102
+
103
+ def test_terra_api_options():
104
+ api = TerraApi(enable_token_mapping=False)
105
+ assert api.api_options.blockchain == Blockchain.TERRA
106
+ assert api.coin.symbol == 'LUNC'
107
+ assert api.TOKENS_MAP_BLOCKCHAIN_KEY == 'terra'
108
+
109
+
110
+ def test_get_available_balances(terra_api, balances_response, requests_mock):
111
+ requests_mock.get(
112
+ f'{BASE_URL}/cosmos/bank/v1beta1/balances/{ADDRESS}',
113
+ json=balances_response,
114
+ )
115
+ requests_mock.get(
116
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegations/{ADDRESS}',
117
+ json={'delegation_responses': [], 'pagination': {}},
118
+ )
119
+ requests_mock.get(
120
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegators/{ADDRESS}/unbonding_delegations',
121
+ json={'unbonding_responses': [], 'pagination': {}},
122
+ )
123
+ requests_mock.get(
124
+ f'{BASE_URL}/cosmos/distribution/v1beta1/delegators/{ADDRESS}/rewards',
125
+ json={'rewards': [], 'total': []},
126
+ )
127
+
128
+ balances = terra_api.get_balance(ADDRESS)
129
+
130
+ available = [b for b in balances if b.asset_type == AssetType.AVAILABLE]
131
+ assert len(available) == 2
132
+
133
+ luna = next(b for b in available if b.coin.symbol == 'LUNC')
134
+ assert luna.balance == Decimal('23068.009633')
135
+
136
+ usd = next(b for b in available if b.coin.address == 'uusd')
137
+ assert usd.balance == Decimal('85.997844')
138
+
139
+
140
+ def test_get_staking_balances(
141
+ terra_api,
142
+ balances_response,
143
+ staking_response,
144
+ unbonding_response,
145
+ rewards_response,
146
+ requests_mock,
147
+ ):
148
+ requests_mock.get(
149
+ f'{BASE_URL}/cosmos/bank/v1beta1/balances/{ADDRESS}',
150
+ json=balances_response,
151
+ )
152
+ requests_mock.get(
153
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegations/{ADDRESS}',
154
+ json=staking_response,
155
+ )
156
+ requests_mock.get(
157
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegators/{ADDRESS}/unbonding_delegations',
158
+ json=unbonding_response,
159
+ )
160
+ requests_mock.get(
161
+ f'{BASE_URL}/cosmos/distribution/v1beta1/delegators/{ADDRESS}/rewards',
162
+ json=rewards_response,
163
+ )
164
+
165
+ balances = terra_api.get_balance(ADDRESS)
166
+
167
+ staked = [b for b in balances if b.asset_type == AssetType.STAKED]
168
+ assert len(staked) == 1
169
+ assert staked[0].balance == Decimal('100')
170
+ assert staked[0].coin.symbol == 'LUNC'
171
+
172
+ rewards = [b for b in balances if b.asset_type == AssetType.REWARDS]
173
+ assert len(rewards) == 2
174
+
175
+ luna_reward = next(b for b in rewards if b.coin.address == 'uluna')
176
+ assert luna_reward.balance == Decimal('5')
177
+
178
+
179
+ def test_resolve_ibc_denom(terra_api, ibc_denom_trace_response, requests_mock):
180
+ ibc_hash = '0A866A7A214C42CEF84430C8A4C7210C8C7A980548A9B9BE64316D1610A87C6C'
181
+ ibc_denom = f'ibc/{ibc_hash}'
182
+
183
+ requests_mock.get(
184
+ f'{BASE_URL}/ibc/apps/transfer/v1/denom_traces/{ibc_hash}',
185
+ json=ibc_denom_trace_response,
186
+ )
187
+
188
+ coin = terra_api.create_default_coin(ibc_denom)
189
+ assert coin.symbol == 'ROWAN'
190
+ assert coin.address == ibc_denom
191
+ assert 'ibc' in coin.standards
192
+
193
+
194
+ def test_resolve_ibc_denom_fallback_on_error(terra_api, requests_mock):
195
+ ibc_hash = 'DEADBEEF'
196
+ ibc_denom = f'ibc/{ibc_hash}'
197
+
198
+ requests_mock.get(
199
+ f'{BASE_URL}/ibc/apps/transfer/v1/denom_traces/{ibc_hash}',
200
+ status_code=404,
201
+ )
202
+
203
+ coin = terra_api.create_default_coin(ibc_denom)
204
+ assert coin.address == ibc_denom
205
+ assert coin.blockchain == Blockchain.TERRA
206
+
207
+
208
+ def test_non_ibc_denom_uses_default(terra_api):
209
+ coin = terra_api.create_default_coin('ufoo')
210
+ assert coin.address == 'ufoo'
211
+ assert coin.blockchain == Blockchain.TERRA
212
+ assert coin.decimals == 6
213
+
214
+
215
+ def test_get_balance_with_ibc_tokens(
216
+ terra_api,
217
+ staking_response,
218
+ unbonding_response,
219
+ rewards_response,
220
+ ibc_denom_trace_response,
221
+ requests_mock,
222
+ ):
223
+ ibc_hash = '0A866A7A214C42CEF84430C8A4C7210C8C7A980548A9B9BE64316D1610A87C6C'
224
+
225
+ requests_mock.get(
226
+ f'{BASE_URL}/cosmos/bank/v1beta1/balances/{ADDRESS}',
227
+ json={
228
+ 'balances': [
229
+ {'denom': 'uluna', 'amount': '1000000'},
230
+ {'denom': f'ibc/{ibc_hash}', 'amount': '500000'},
231
+ ],
232
+ 'pagination': {'next_key': None, 'total': '2'},
233
+ },
234
+ )
235
+ requests_mock.get(
236
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegations/{ADDRESS}',
237
+ json={'delegation_responses': [], 'pagination': {}},
238
+ )
239
+ requests_mock.get(
240
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegators/{ADDRESS}/unbonding_delegations',
241
+ json={'unbonding_responses': [], 'pagination': {}},
242
+ )
243
+ requests_mock.get(
244
+ f'{BASE_URL}/cosmos/distribution/v1beta1/delegators/{ADDRESS}/rewards',
245
+ json={'rewards': [], 'total': []},
246
+ )
247
+ requests_mock.get(
248
+ f'{BASE_URL}/ibc/apps/transfer/v1/denom_traces/{ibc_hash}',
249
+ json=ibc_denom_trace_response,
250
+ )
251
+
252
+ balances = terra_api.get_balance(ADDRESS)
253
+ assert len(balances) == 2
254
+
255
+ rowan = next(b for b in balances if b.coin.symbol == 'ROWAN')
256
+ assert rowan.balance == Decimal('0.5')
257
+ assert 'ibc' in rowan.coin.standards
258
+
259
+
260
+ def test_unbonding_included_in_staked(terra_api, requests_mock):
261
+ requests_mock.get(
262
+ f'{BASE_URL}/cosmos/bank/v1beta1/balances/{ADDRESS}',
263
+ json={'balances': [], 'pagination': {}},
264
+ )
265
+ requests_mock.get(
266
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegations/{ADDRESS}',
267
+ json={
268
+ 'delegation_responses': [
269
+ {
270
+ 'delegation': {
271
+ 'delegator_address': ADDRESS,
272
+ 'validator_address': 'terravaloper1test',
273
+ 'shares': '50000000',
274
+ },
275
+ 'balance': {'denom': 'uluna', 'amount': '50000000'},
276
+ }
277
+ ],
278
+ 'pagination': {},
279
+ },
280
+ )
281
+ requests_mock.get(
282
+ f'{BASE_URL}/cosmos/staking/v1beta1/delegators/{ADDRESS}/unbonding_delegations',
283
+ json={
284
+ 'unbonding_responses': [
285
+ {
286
+ 'delegator_address': ADDRESS,
287
+ 'validator_address': 'terravaloper1test2',
288
+ 'entries': [
289
+ {'balance': '25000000', 'completion_time': '2026-04-01'},
290
+ ],
291
+ }
292
+ ],
293
+ 'pagination': {},
294
+ },
295
+ )
296
+ requests_mock.get(
297
+ f'{BASE_URL}/cosmos/distribution/v1beta1/delegators/{ADDRESS}/rewards',
298
+ json={'rewards': [], 'total': []},
299
+ )
300
+
301
+ balances = terra_api.get_balance(ADDRESS)
302
+ staked = [b for b in balances if b.asset_type == AssetType.STAKED]
303
+ assert len(staked) == 1
304
+ # 50 delegated + 25 unbonding = 75
305
+ assert staked[0].balance == Decimal('75')
@@ -0,0 +1,59 @@
1
+ import logging
2
+ from functools import lru_cache
3
+
4
+ from blockapi.v2.api.cosmos import CosmosApiBase
5
+ from blockapi.v2.base import ApiException, ApiOptions
6
+ from blockapi.v2.coins import COIN_TERRA
7
+ from blockapi.v2.models import Blockchain, Coin
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class TerraApi(CosmosApiBase):
13
+ """
14
+ Terra Classic (LUNC) via standard Cosmos LCD endpoints.
15
+ Explorer: https://finder.terra.money
16
+ """
17
+
18
+ coin = COIN_TERRA
19
+ TOKENS_MAP_BLOCKCHAIN_KEY = 'terra'
20
+ api_options = ApiOptions(
21
+ blockchain=Blockchain.TERRA,
22
+ base_url='https://terra-classic-fcd.publicnode.com/',
23
+ rate_limit=CosmosApiBase.API_BASE_RATE_LIMIT,
24
+ )
25
+
26
+ supported_requests = {
27
+ **CosmosApiBase.supported_requests,
28
+ 'get_ibc_denom_trace': '/ibc/apps/transfer/v1/denom_traces/{hash}',
29
+ }
30
+
31
+ def create_default_coin(self, denom: str) -> Coin:
32
+ if denom.startswith('ibc/'):
33
+ return self._resolve_ibc_denom(denom)
34
+
35
+ return super().create_default_coin(denom)
36
+
37
+ @lru_cache(maxsize=64)
38
+ def _resolve_ibc_denom(self, denom: str) -> Coin:
39
+ hash_ = denom.split('/')[1]
40
+ try:
41
+ response = self.get('get_ibc_denom_trace', hash=hash_)
42
+ base_denom = response['denom_trace']['base_denom']
43
+ symbol = (
44
+ base_denom.lstrip('ux').upper()
45
+ if base_denom.startswith(('u', 'x'))
46
+ else base_denom.upper()
47
+ )
48
+ except (ApiException, KeyError) as e:
49
+ logger.warning(f'Failed to resolve IBC denom {denom}: {e}')
50
+ return super().create_default_coin(denom)
51
+
52
+ return Coin.from_api(
53
+ symbol=symbol,
54
+ name=symbol,
55
+ decimals=self.coin.decimals,
56
+ blockchain=self.api_options.blockchain,
57
+ address=denom,
58
+ standards=['ibc'],
59
+ )
@@ -34,8 +34,8 @@ COIN_SOL = Coin(
34
34
 
35
35
 
36
36
  COIN_TERRA = Coin(
37
- symbol='LUNA',
38
- name='Terra',
37
+ symbol='LUNC',
38
+ name='Terra Classic',
39
39
  decimals=6,
40
40
  blockchain=Blockchain.TERRA,
41
41
  address='uluna',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockapi
3
- Version: 2.5.5
3
+ Version: 2.5.6
4
4
  Summary: BlockAPI library
5
5
  Home-page: https://github.com/crypkit/blockapi
6
6
  Author: Devmons s.r.o.
@@ -39,6 +39,7 @@ blockapi/test/v2/api/test_optimistic_etherscan.py
39
39
  blockapi/test/v2/api/test_solana.py
40
40
  blockapi/test/v2/api/test_subscan_polkadot.py
41
41
  blockapi/test/v2/api/test_sui.py
42
+ blockapi/test/v2/api/test_terra.py
42
43
  blockapi/test/v2/api/test_trezor_btc.py
43
44
  blockapi/test/v2/api/test_trezor_zec.py
44
45
  blockapi/test/v2/api/covalenth/__init__.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.5.5"
9
+ __version__ = "2.5.6"
10
10
 
11
11
  setuptools.setup(
12
12
  name='blockapi',
@@ -1,318 +0,0 @@
1
- import json
2
- from functools import lru_cache
3
- from typing import Dict, List, Optional, Sequence, Tuple
4
-
5
- from cytoolz import concatv
6
- from requests import Response
7
-
8
- from blockapi.v2.base import (
9
- ApiException,
10
- ApiOptions,
11
- BalanceMixin,
12
- BlockchainApi,
13
- InvalidAddressException,
14
- )
15
- from blockapi.v2.coins import COIN_TERRA
16
- from blockapi.v2.models import (
17
- AssetType,
18
- BalanceItem,
19
- Blockchain,
20
- Coin,
21
- CoinInfo,
22
- FetchResult,
23
- ParseResult,
24
- )
25
-
26
-
27
- class TerraApi(BalanceMixin):
28
- """
29
- Terra Money, implemented by multiple api providers.
30
- Explorer: https://finder.terra.money
31
- """
32
-
33
- coin = COIN_TERRA
34
-
35
- def __init__(self):
36
- self.mantle = TerraMantleApi()
37
- self.fcd = TerraFcdApi()
38
-
39
- def fetch_balances(self, address: str) -> FetchResult:
40
- status, balances, balance_errors = self.fcd.fetch_native_balances(address)
41
- _, staking_balances, staking_errors = self.fcd.fetch_staking_balances(address)
42
- cw20_balances = self.mantle.fetch_cw20_balances(address)
43
-
44
- return FetchResult(
45
- status_code=status,
46
- data=dict(
47
- balances=balances,
48
- raw_staking_balances=staking_balances,
49
- raw_cw20_balances=cw20_balances,
50
- ),
51
- errors=list(concatv(balance_errors, staking_errors)),
52
- )
53
-
54
- def parse_balances(self, fetch_result: FetchResult) -> ParseResult:
55
- native_balances = self.fcd.parse_native_balances(
56
- fetch_result.data.get('balances')
57
- )
58
- staking_balances = self.fcd.parse_staking_balances(
59
- fetch_result.data.get('raw_staking_balances')
60
- )
61
- cw20_balances = self.mantle.parse_cw20_balances(
62
- fetch_result.data.get('raw_cw20_balances')
63
- )
64
- return ParseResult(
65
- data=list(concatv(native_balances, staking_balances, cw20_balances))
66
- )
67
-
68
-
69
- class TerraFcdApi(BlockchainApi):
70
- """
71
- Terra Money FCD
72
- API docs: https://fcd.terra.dev/swagger
73
- """
74
-
75
- coin = COIN_TERRA
76
- api_options = ApiOptions(
77
- blockchain=Blockchain.TERRA,
78
- base_url='https://fcd.terra.dev/',
79
- rate_limit=1,
80
- )
81
-
82
- supported_requests = {
83
- 'get_native_balances': '/v1/bank/{address}',
84
- 'get_ibc_denom_trace': '/ibc/apps/transfer/v1/denom_traces/{hash}',
85
- 'get_staking_data': '/v1/staking/{address}',
86
- }
87
-
88
- def fetch_native_balances(self, address: str) -> FetchResult:
89
- return self.get_data('get_native_balances', address=address)
90
-
91
- def parse_native_balances(self, response: dict) -> List[BalanceItem]:
92
- balances = []
93
- for b in response['balance']:
94
- if int(b['available']) == 0:
95
- continue
96
-
97
- coin = (
98
- self._get_terra_token_by_denom(b['denom'])
99
- if b['denom'].startswith('u')
100
- else self._get_ibc_token_by_denom(b['denom'])
101
- )
102
-
103
- balances.append(
104
- BalanceItem.from_api(balance_raw=b['available'], coin=coin, raw=b)
105
- )
106
-
107
- return balances
108
-
109
- def get_native_balances(self, address: str) -> List[BalanceItem]:
110
- _, response, _ = self.fetch_native_balances(address)
111
- return self.parse_native_balances(response)
112
-
113
- def fetch_staking_balances(self, address: str) -> FetchResult:
114
- return self.get_data('get_staking_data', address=address)
115
-
116
- def get_staking_balances(self, address: str) -> List[BalanceItem]:
117
- _, response, _ = self.fetch_staking_balances(address)
118
- return self.parse_staking_balances(response)
119
-
120
- def parse_staking_balances(self, response: dict) -> List[BalanceItem]:
121
- total_staked = 0
122
- balances = []
123
-
124
- # active stake
125
- if int(response['delegationTotal']) > 0:
126
- total_staked += int(response['delegationTotal'])
127
- # undelegated stake
128
- if response['undelegations']:
129
- total_staked += sum(int(u['amount']) for u in response['undelegations'])
130
- # total stake - sum of staked and undelegated
131
- # add redelegations?
132
- if total_staked:
133
- balances.append(
134
- BalanceItem.from_api(
135
- balance_raw=total_staked,
136
- coin=self.coin,
137
- asset_type=AssetType.STAKED,
138
- raw=response,
139
- )
140
- )
141
-
142
- # staking rewards
143
- for d in response['rewards']['denoms']:
144
- balances.append(
145
- BalanceItem.from_api(
146
- balance_raw=d['amount'],
147
- coin=self._get_terra_token_by_denom(d['denom']),
148
- asset_type=AssetType.CLAIMABLE,
149
- raw=d,
150
- )
151
- )
152
-
153
- return balances
154
-
155
- # It's possible to get cw20 balances, but it needs to be done one by one.
156
- # Use .terra_mantle.py for that
157
- # def get_cw20_balances(self):
158
-
159
- @staticmethod
160
- def _get_terra_token_by_denom(denom: str) -> Coin:
161
- if denom == 'uluna':
162
- return COIN_TERRA
163
- else:
164
- symbol = f'{denom[1:3].upper()}TC'
165
- return Coin.from_api(
166
- symbol=symbol,
167
- name=symbol,
168
- decimals=6,
169
- blockchain=Blockchain.TERRA,
170
- address=denom,
171
- standards=['terra-native'],
172
- )
173
-
174
- @lru_cache(maxsize=8)
175
- def _get_ibc_token_by_denom(self, denom: str) -> Coin:
176
- hash_ = denom.split('/')[1]
177
- try:
178
- response = self.get('get_ibc_denom_trace', hash=hash_)
179
- except ApiException:
180
- # add log
181
- symbol = None
182
- else:
183
- denom = response['denom_trace']['base_denom']
184
- symbol = denom[1:].upper()
185
-
186
- return Coin.from_api(
187
- symbol=symbol,
188
- name=symbol,
189
- decimals=6,
190
- blockchain=Blockchain.TERRA,
191
- address=hash_,
192
- standards=['ibc'],
193
- )
194
-
195
-
196
- class TerraMantleApi(BlockchainApi):
197
- """
198
- Terra Money Subgraph API
199
- API docs: https://mantle.terra.dev
200
- """
201
-
202
- coin = COIN_TERRA
203
- api_options = ApiOptions(
204
- blockchain=Blockchain.TERRA,
205
- base_url='https://mantle.terra.dev',
206
- rate_limit=1,
207
- )
208
-
209
- # API uses post requests
210
- supported_requests = {}
211
- _post_requests = {
212
- 'wasm_contract_address_store': """
213
- WasmContractsContractAddressStore(
214
- ContractAddress: "$CONTRACT_ADDRESS",
215
- QueryMsg: "$QUERY_MSG"
216
- ){
217
- Result
218
- }
219
- """
220
- }
221
-
222
- _tokens_map: Optional[Dict[str, Dict]] = None
223
-
224
- @property
225
- def tokens_map(self) -> Dict[str, Dict]:
226
- if self._tokens_map is None:
227
- response = self._session.get('https://assets.terra.money/cw20/tokens.json')
228
- token_list = response.json()
229
- self._tokens_map = token_list['classic']
230
-
231
- return self._tokens_map
232
-
233
- def fetch_cw20_balances(self, address) -> dict:
234
- return self._get_raw_balances(address)
235
-
236
- def get_cw20_balances(self, address: str):
237
- raw_balances = self._get_raw_balances(address)
238
- return self.parse_cw20_balances(raw_balances)
239
-
240
- def parse_cw20_balances(self, raw_balances):
241
- balances = []
242
- for contract, result_raw in raw_balances['data'].items():
243
- if not result_raw:
244
- # should be error in response, TODO add log
245
- continue
246
-
247
- data_raw = json.loads(result_raw['Result'])
248
- balance_raw = data_raw['balance']
249
- if int(balance_raw) == 0:
250
- continue
251
-
252
- balances.append(
253
- BalanceItem.from_api(
254
- balance_raw=balance_raw,
255
- coin=self._get_token_data(contract),
256
- raw=result_raw,
257
- )
258
- )
259
-
260
- return balances
261
-
262
- def _get_token_data(self, address: str) -> Coin:
263
- raw_token = self.tokens_map[address]
264
- return Coin(
265
- symbol=raw_token['symbol'],
266
- name=raw_token['name'] if raw_token.get('name') else raw_token['symbol'],
267
- decimals=6,
268
- blockchain=Blockchain.TERRA,
269
- address=address,
270
- standards=['CW20'],
271
- protocol_id=raw_token.get('protocol'),
272
- info=CoinInfo.from_api(logo_url=raw_token.get('icon')),
273
- )
274
-
275
- def _get_raw_balances(self, address: str) -> Dict:
276
- cw20_contracts = list(self.tokens_map.keys())
277
- message = '{\\"balance\\": {\\"address\\": \\"$ADDR\\"}}'.replace(
278
- '$ADDR', address
279
- )
280
-
281
- key_queries = [
282
- self._create_key_query(
283
- key=contract,
284
- query=self._build_query(
285
- method='wasm_contract_address_store',
286
- params={'$CONTRACT_ADDRESS': contract, '$QUERY_MSG': message},
287
- ),
288
- )
289
- for contract in cw20_contracts
290
- ]
291
- query = self._concat_key_queries(key_queries)
292
- return self.post(json={'query': query})
293
-
294
- def _build_query(self, method: str, params: Optional[Dict[str, str]] = None) -> str:
295
- query = self._post_requests.get(method)
296
- if params:
297
- for k, v in params.items():
298
- query = query.replace(k, v)
299
- return query
300
-
301
- @staticmethod
302
- def _create_key_query(key: str, query: str) -> str:
303
- return f'{key}: {query}'
304
-
305
- @staticmethod
306
- def _concat_key_queries(key_queries: Sequence[str]) -> str:
307
- return '{' + ',\n'.join(key_queries) + '}'
308
-
309
- def _opt_raise_on_other_error(self, response: Response) -> None:
310
- json_response = response.json()
311
- if json_response.get('errors') is None:
312
- return
313
-
314
- # pick first message
315
- err = json_response['errors'][0]
316
-
317
- if 'addr_canonicalize' in err['message']:
318
- raise InvalidAddressException(f'Invalid address format.')
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