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.
- {blockapi-2.5.5 → blockapi-2.5.7}/PKG-INFO +1 -1
- blockapi-2.5.7/blockapi/test/v2/api/test_terra.py +305 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/debank.py +11 -0
- blockapi-2.5.7/blockapi/v2/api/terra.py +59 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/coins.py +2 -2
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/models.py +32 -1
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/PKG-INFO +1 -1
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/SOURCES.txt +1 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/setup.py +1 -1
- blockapi-2.5.5/blockapi/v2/api/terra.py +0 -318
- {blockapi-2.5.5 → blockapi-2.5.7}/LICENSE.md +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/README.md +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/services.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_blockapi.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_num.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/test_random_user_agent.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/conftest.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/covalenth/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/covalenth/test_ethereum.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/fake_sleep_provider.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_magic_eden.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_opensea.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_simple_hash.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/nft/test_unisat.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/perpetual/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/perpetual/test_perpetual.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/synthetix/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/synthetix/test_synthetix.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchain_info.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchainos.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_btc.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_doge.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_blockchair_ltc.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_cosmos.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_ethplorer.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_haskoin.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_multisources.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_optimistic_etherscan.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_solana.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_subscan_polkadot.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_sui.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_trezor_btc.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/api/test_trezor_zec.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_base.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_blockchain_api.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_blockchain_mapping.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_data.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_enumerate_classes.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_generic.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test/v2/test_models.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/test_data.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/address.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/datetime.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/num.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/utils/user_agent.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchain_info.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchainos.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/blockchair.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/cosmos.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/arbitrum.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/astar.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/avalanche.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/axie.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/base.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/binance_smart_chain.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/ethereum.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/fantom.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/heco.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/iotex.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/klaytn.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/moonbeam.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/palm.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/polygon.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/covalenth/rsk.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/debank_maps.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/ethplorer.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/haskoin.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/magic_eden.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/opensea.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/simple_hash.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/nft/unisat.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/optimistic_etherscan.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/perp_abi.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/perpetual/perpetual.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/solana.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/subscan.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/sui.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/__init__.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/synthetix.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/synthetix/synthetix_abi.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/trezor.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/api/web3_utils.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/base.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/blockchain_mapping.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi/v2/coin_mapping.py +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/dependency_links.txt +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/requires.txt +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/blockapi.egg-info/top_level.txt +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/pyproject.toml +0 -0
- {blockapi-2.5.5 → blockapi-2.5.7}/setup.cfg +0 -0
|
@@ -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
|
+
)
|
|
@@ -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=
|
|
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:
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|