Skip to content

Commit 2d1243c

Browse files
committed
fix: refactor Terra API using CosmosApiBase, add tests for balance and IBC denom handling
1 parent 59f09fc commit 2d1243c

3 files changed

Lines changed: 337 additions & 291 deletions

File tree

blockapi/test/v2/api/test_terra.py

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)