blockapi 2.5.5__tar.gz → 2.5.7__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.7}/PKG-INFO +1 -1
  2. blockapi-2.5.7/blockapi/test/v2/api/test_terra.py +305 -0
  3. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/debank.py +11 -0
  4. blockapi-2.5.7/blockapi/v2/api/terra.py +59 -0
  5. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/coins.py +2 -2
  6. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/models.py +32 -1
  7. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/PKG-INFO +1 -1
  8. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/SOURCES.txt +1 -0
  9. {blockapi-2.5.5 → blockapi-2.5.7}/setup.py +1 -1
  10. blockapi-2.5.5/blockapi/v2/api/terra.py +0 -318
  11. {blockapi-2.5.5 → blockapi-2.5.7}/LICENSE.md +0 -0
  12. {blockapi-2.5.5 → blockapi-2.5.7}/README.md +0 -0
  13. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/__init__.py +0 -0
  14. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/services.py +0 -0
  15. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/__init__.py +0 -0
  16. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_blockapi.py +0 -0
  17. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_num.py +0 -0
  18. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_random_user_agent.py +0 -0
  19. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/__init__.py +0 -0
  20. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/__init__.py +0 -0
  21. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/conftest.py +0 -0
  22. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/covalenth/__init__.py +0 -0
  23. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/covalenth/test_ethereum.py +0 -0
  24. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/fake_sleep_provider.py +0 -0
  25. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/__init__.py +0 -0
  26. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_magic_eden.py +0 -0
  27. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_opensea.py +0 -0
  28. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_simple_hash.py +0 -0
  29. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_unisat.py +0 -0
  30. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/perpetual/__init__.py +0 -0
  31. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/perpetual/test_perpetual.py +0 -0
  32. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/synthetix/__init__.py +0 -0
  33. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/synthetix/test_synthetix.py +0 -0
  34. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchain_info.py +0 -0
  35. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchainos.py +0 -0
  36. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_btc.py +0 -0
  37. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_doge.py +0 -0
  38. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_ltc.py +0 -0
  39. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_cosmos.py +0 -0
  40. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_ethplorer.py +0 -0
  41. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_haskoin.py +0 -0
  42. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_multisources.py +0 -0
  43. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_optimistic_etherscan.py +0 -0
  44. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_solana.py +0 -0
  45. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_subscan_polkadot.py +0 -0
  46. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_sui.py +0 -0
  47. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_trezor_btc.py +0 -0
  48. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_trezor_zec.py +0 -0
  49. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_base.py +0 -0
  50. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_blockchain_api.py +0 -0
  51. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_blockchain_mapping.py +0 -0
  52. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_data.py +0 -0
  53. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_enumerate_classes.py +0 -0
  54. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_generic.py +0 -0
  55. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_models.py +0 -0
  56. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test_data.py +0 -0
  57. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/__init__.py +0 -0
  58. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/address.py +0 -0
  59. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/datetime.py +0 -0
  60. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/num.py +0 -0
  61. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/user_agent.py +0 -0
  62. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/__init__.py +0 -0
  63. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/__init__.py +0 -0
  64. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchain_info.py +0 -0
  65. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchainos.py +0 -0
  66. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchair.py +0 -0
  67. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/cosmos.py +0 -0
  68. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/__init__.py +0 -0
  69. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/arbitrum.py +0 -0
  70. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/astar.py +0 -0
  71. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/avalanche.py +0 -0
  72. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/axie.py +0 -0
  73. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/base.py +0 -0
  74. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/binance_smart_chain.py +0 -0
  75. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/ethereum.py +0 -0
  76. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/fantom.py +0 -0
  77. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/heco.py +0 -0
  78. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/iotex.py +0 -0
  79. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/klaytn.py +0 -0
  80. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/moonbeam.py +0 -0
  81. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/palm.py +0 -0
  82. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/polygon.py +0 -0
  83. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/rsk.py +0 -0
  84. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/debank_maps.py +0 -0
  85. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/ethplorer.py +0 -0
  86. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/haskoin.py +0 -0
  87. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/__init__.py +0 -0
  88. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/magic_eden.py +0 -0
  89. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/opensea.py +0 -0
  90. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/simple_hash.py +0 -0
  91. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/unisat.py +0 -0
  92. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/optimistic_etherscan.py +0 -0
  93. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/__init__.py +0 -0
  94. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/perp_abi.py +0 -0
  95. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/perpetual.py +0 -0
  96. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/solana.py +0 -0
  97. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/subscan.py +0 -0
  98. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/sui.py +0 -0
  99. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/__init__.py +0 -0
  100. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/synthetix.py +0 -0
  101. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/synthetix_abi.py +0 -0
  102. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/trezor.py +0 -0
  103. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/web3_utils.py +0 -0
  104. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/base.py +0 -0
  105. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/blockchain_mapping.py +0 -0
  106. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/coin_mapping.py +0 -0
  107. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/dependency_links.txt +0 -0
  108. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/requires.txt +0 -0
  109. {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/top_level.txt +0 -0
  110. {blockapi-2.5.5 → blockapi-2.5.7}/pyproject.toml +0 -0
  111. {blockapi-2.5.5 → blockapi-2.5.7}/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.7
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')
@@ -36,6 +36,7 @@ from blockapi.v2.models import (
36
36
  DebankApp,
37
37
  DebankModelApp,
38
38
  DebankModelAppPortfolioItem,
39
+ DebankModelAppStats,
39
40
  DebankModelPredictionDetail,
40
41
  DebankPrediction,
41
42
  FetchResult,
@@ -70,6 +71,7 @@ class DebankModelPoolItemDetail(BaseModel):
70
71
  description: Optional[str] = None
71
72
  health_rate: Optional[float] = None
72
73
  unlock_at: Optional[float] = None
74
+ debt_ratio: Optional[float] = None
73
75
  token_list: Optional[list[dict]] = None
74
76
  supply_token_list: Optional[list[dict]] = None
75
77
  borrow_token_list: Optional[list[dict]] = None
@@ -90,6 +92,9 @@ class DebankModelPortfolioItem(BaseModel):
90
92
  pool_id: Optional[str] = None
91
93
  pool: Optional[DebankModelPoolItem] = None
92
94
  position_index: Optional[str] = None
95
+ stats: DebankModelAppStats
96
+ detail_types: list[str]
97
+ update_at: float
93
98
 
94
99
  @validator('pool')
95
100
  def require_pool_or_pool_id(cls, v, values, **kwargs):
@@ -481,6 +486,12 @@ class DebankPortfolioParser:
481
486
  locked_until=locked_until,
482
487
  health_rate=health_rate,
483
488
  items=[],
489
+ detail_types=item.detail_types,
490
+ asset_usd_value=item.stats.asset_usd_value,
491
+ debt_usd_value=item.stats.debt_usd_value,
492
+ net_usd_value=item.stats.net_usd_value,
493
+ debt_ratio=detail.debt_ratio,
494
+ update_at=item.update_at,
484
495
  )
485
496
 
486
497
  items = list(self._parse_balances(detail, item, pool.pool_info))
@@ -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',
@@ -1163,6 +1163,19 @@ class TransactionItem:
1163
1163
  )
1164
1164
 
1165
1165
 
1166
+ # uint256.max / 1e18 — Aave's sentinel for "no debt / infinite health factor"
1167
+ _HEALTH_RATE_SENTINEL_THRESHOLD = Decimal('1e18')
1168
+
1169
+
1170
+ def _normalize_health_rate(value) -> Optional[Decimal]:
1171
+ if value is None:
1172
+ return None
1173
+ d = to_decimal(value)
1174
+ if d >= _HEALTH_RATE_SENTINEL_THRESHOLD:
1175
+ return None
1176
+ return d
1177
+
1178
+
1166
1179
  @attr.s(auto_attribs=True, slots=True, frozen=True)
1167
1180
  class Pool:
1168
1181
  pool_info: PoolInfo
@@ -1170,6 +1183,12 @@ class Pool:
1170
1183
  items: List[BalanceItem]
1171
1184
  locked_until: Optional[datetime] = attr.ib(default=None)
1172
1185
  health_rate: Optional[Decimal] = attr.ib(default=None)
1186
+ detail_types: List[str] = attr.ib(factory=list)
1187
+ asset_usd_value: Decimal = attr.ib(default=Decimal('0'))
1188
+ debt_usd_value: Decimal = attr.ib(default=Decimal('0'))
1189
+ net_usd_value: Decimal = attr.ib(default=Decimal('0'))
1190
+ debt_ratio: Optional[Decimal] = attr.ib(default=None)
1191
+ update_at: Optional[datetime] = attr.ib(default=None)
1173
1192
 
1174
1193
  @classmethod
1175
1194
  def from_api(
@@ -1180,13 +1199,25 @@ class Pool:
1180
1199
  locked_until: Optional[Union[int, str, float]] = None,
1181
1200
  health_rate: Optional[Union[float, str]] = None,
1182
1201
  items: List[BalanceItem],
1202
+ detail_types: Optional[List[str]] = None,
1203
+ asset_usd_value: Union[float, str] = 0,
1204
+ debt_usd_value: Union[float, str] = 0,
1205
+ net_usd_value: Union[float, str] = 0,
1206
+ debt_ratio: Optional[Union[float, str]] = None,
1207
+ update_at: Optional[Union[int, str, float]] = None,
1183
1208
  ) -> 'Pool':
1184
1209
  return cls(
1185
1210
  pool_info=pool_info,
1186
1211
  protocol=protocol,
1187
1212
  items=items,
1188
1213
  locked_until=(parse_dt(locked_until) if locked_until is not None else None),
1189
- health_rate=to_decimal(health_rate) if health_rate is not None else None,
1214
+ health_rate=_normalize_health_rate(health_rate),
1215
+ detail_types=detail_types or [],
1216
+ asset_usd_value=to_decimal(asset_usd_value),
1217
+ debt_usd_value=to_decimal(debt_usd_value),
1218
+ net_usd_value=to_decimal(net_usd_value),
1219
+ debt_ratio=to_decimal(debt_ratio) if debt_ratio is not None else None,
1220
+ update_at=(parse_dt(update_at) if update_at is not None else None),
1190
1221
  )
1191
1222
 
1192
1223
  def append_items(self, items: List[BalanceItem]) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blockapi
3
- Version: 2.5.5
3
+ Version: 2.5.7
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.7"
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