Skip to content

Commit 3abc791

Browse files
authored
feat: Allow partial fetching from debank (#269)
1 parent 49549b0 commit 3abc791

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

blockapi/test/v2/api/debank/test_debank.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from blockapi.utils.address import make_checksum_address
44
from blockapi.v2.api import DebankApi
5+
from blockapi.v2.models import FetchResult
56

67

78
def test_build_balance_request_url(debank_api):
@@ -57,6 +58,79 @@ def test_build_portfolio_request_url(debank_api):
5758
)
5859

5960

61+
def test_build_token_list_for_chain_request_url(debank_api):
62+
url = debank_api._build_request_url(
63+
'get_token_list_for_chain',
64+
address='0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca',
65+
chain_id='eth',
66+
is_all=True,
67+
)
68+
assert (
69+
url
70+
== 'https://pro-openapi.debank.com/v1/user/token_list?id=0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca&chain_id=eth&is_all=True'
71+
)
72+
73+
74+
def test_build_protocol_for_address_request_url(debank_api):
75+
url = debank_api._build_request_url(
76+
'get_protocol_for_address',
77+
address='0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca',
78+
protocol_id='yflink',
79+
)
80+
assert (
81+
url
82+
== 'https://pro-openapi.debank.com/v1/user/protocol?id=0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca&protocol_id=yflink'
83+
)
84+
85+
86+
def test_parse_pool_for_address_wraps_single_object(
87+
debank_api, yflink_protocol_response_raw, portfolio_response, requests_mock
88+
):
89+
# /v1/user/protocol returns a single protocol object (dict, not list).
90+
# parse_pool_for_address must wrap it so DebankPortfolioParser can iterate.
91+
requests_mock.get(
92+
'https://pro-openapi.debank.com/v1/protocol/all_list',
93+
text=yflink_protocol_response_raw,
94+
)
95+
requests_mock.get(
96+
'https://pro-openapi.debank.com/v1/user/protocol?id=0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca&protocol_id=avax_traderjoexyz_lending',
97+
json=portfolio_response,
98+
)
99+
debank_api._protocol_cache.invalidate()
100+
fetched = debank_api.fetch_protocol_for_address(
101+
'0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca',
102+
'avax_traderjoexyz_lending',
103+
)
104+
parsed = debank_api.parse_pool_for_address(fetched)
105+
assert parsed.errors is None
106+
assert len(parsed.data) > 0
107+
assert (
108+
parsed.data[0].pool_info.pool_id == '0xdc13687554205e5b89ac783db14bb5bba4a1edac'
109+
)
110+
111+
112+
def test_parse_pool_for_address_handles_empty_response(debank_api, protocol_cache):
113+
# When Debank returns null/None for an address with no positions in this protocol.
114+
protocol_cache.update({})
115+
parsed = debank_api.parse_pool_for_address(FetchResult(status_code=200, data=None))
116+
assert parsed.errors is None
117+
assert parsed.data == []
118+
119+
120+
def test_fetch_token_list_for_chain_uses_api_key(
121+
debank_api, protocol_cache, requests_mock
122+
):
123+
req = requests_mock.get(
124+
'https://pro-openapi.debank.com/v1/user/token_list?id=0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca&chain_id=eth&is_all=True',
125+
text='[]',
126+
)
127+
protocol_cache.update({})
128+
debank_api.fetch_token_list_for_chain(
129+
'0xca8fa8f0b631ecdb18cda619c4fc9d197c8affca', 'eth'
130+
)
131+
assert req.last_request.headers.get('AccessKey') == 'dummy-key'
132+
133+
60134
def test_error_response_returns_empty_balances(
61135
debank_api, protocol_cache, error_response_raw, requests_mock
62136
):

blockapi/v2/api/debank.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,12 @@ class DebankApi(CustomizableBlockchainApi, BalanceMixin, IPortfolio):
735735
'get_protocols': '/v1/protocol/all_list',
736736
'usage': '/v1/account/units',
737737
'get_complex_app_list': '/v1/user/complex_app_list?id={address}',
738+
'get_token_list_for_chain': (
739+
'/v1/user/token_list?id={address}&chain_id={chain_id}&is_all={is_all}'
740+
),
741+
'get_protocol_for_address': (
742+
'/v1/user/protocol?id={address}&protocol_id={protocol_id}'
743+
),
738744
}
739745

740746
default_protocol_cache = DebankProtocolCache()
@@ -783,6 +789,23 @@ def fetch_debank_apps(self, address: str) -> FetchResult:
783789
address=address,
784790
)
785791

792+
def fetch_token_list_for_chain(self, address: str, chain_id: str) -> FetchResult:
793+
return self.get_data(
794+
'get_token_list_for_chain',
795+
headers=self._headers,
796+
address=address,
797+
chain_id=chain_id,
798+
is_all=self._is_all,
799+
)
800+
801+
def fetch_protocol_for_address(self, address: str, protocol_id: str) -> FetchResult:
802+
return self.get_data(
803+
'get_protocol_for_address',
804+
headers=self._headers,
805+
address=address,
806+
protocol_id=protocol_id,
807+
)
808+
786809
def fetch_protocols(self) -> FetchResult:
787810
return self.get_data(
788811
'get_protocols',
@@ -809,6 +832,17 @@ def parse_pools(self, fetch_result: FetchResult) -> ParseResult:
809832
self._maybe_update_protocols()
810833
return ParseResult(data=self._portfolio_parser.parse(fetch_result.data))
811834

835+
def parse_pool_for_address(self, fetch_result: FetchResult) -> ParseResult:
836+
if error := self._get_error(fetch_result.data):
837+
return ParseResult(errors=[error])
838+
839+
self._maybe_update_protocols()
840+
data = fetch_result.data
841+
# /v1/user/protocol returns a single protocol object; wrap in a list
842+
# so the portfolio parser (which iterates) handles it uniformly.
843+
wrapped = [data] if isinstance(data, dict) else (data or [])
844+
return ParseResult(data=self._portfolio_parser.parse(wrapped))
845+
812846
def get_protocols(self) -> Dict[str, Protocol]:
813847
response = self.get('get_protocols', headers=self._headers)
814848
if self._has_error(response):

0 commit comments

Comments
 (0)