From cd5799f4873bac1cb812ebf6003be674f940197c Mon Sep 17 00:00:00 2001 From: Jakub Vins Date: Tue, 3 Feb 2026 19:13:23 +0100 Subject: [PATCH 1/2] feat: Added chain generating for debank apps --- .../v2/api/debank/test_debank_app_parser.py | 7 +++- .../test/v2/api/debank/test_debank_fetch.py | 8 +++++ blockapi/v2/api/debank.py | 34 +++++++------------ blockapi/v2/api/debank_maps.py | 8 +++++ blockapi/v2/models.py | 30 ++++++++++------ 5 files changed, 55 insertions(+), 32 deletions(-) diff --git a/blockapi/test/v2/api/debank/test_debank_app_parser.py b/blockapi/test/v2/api/debank/test_debank_app_parser.py index 06fb8328..5e28627d 100644 --- a/blockapi/test/v2/api/debank/test_debank_app_parser.py +++ b/blockapi/test/v2/api/debank/test_debank_app_parser.py @@ -7,6 +7,7 @@ DebankAppDeposit, DebankPrediction, ) +from blockapi.v2.models import Blockchain @pytest.fixture @@ -129,6 +130,7 @@ def test_parse_polymarket_app(debank_app_parser, polymarket_response): def test_parse_polymarket_deposits(debank_app_parser, polymarket_response): """Deposits should be parsed as DebankAppDeposit objects.""" + parsed_apps = debank_app_parser.parse(polymarket_response) app = parsed_apps[0] @@ -142,10 +144,11 @@ def test_parse_polymarket_deposits(debank_app_parser, polymarket_response): assert deposit.debt_usd_value == Decimal("0") assert deposit.net_usd_value == Decimal("290915.13432776055") assert deposit.position_index == "cash_0x5c23dead9ecf271448411096f349133e0bb9c465" + assert deposit.chain == Blockchain.POLYGON # Should have 1 token (USDC) assert len(deposit.tokens) == 1 - assert deposit.tokens[0]["symbol"] == "USDC" + assert deposit.tokens[0].symbol == "USDC" assert deposit.token_symbols == ["USDC"] @@ -168,6 +171,7 @@ def test_parse_polymarket_predictions(debank_app_parser, polymarket_response): assert pred1.usd_value == Decimal("27068.1993") assert pred1.claimable is True assert pred1.is_market_closed is False + assert pred1.chain == Blockchain.POLYGON pred2 = app.predictions[1] assert pred2.prediction_name == "Gensyn FDV above $600M one day after launch?" @@ -175,6 +179,7 @@ def test_parse_polymarket_predictions(debank_app_parser, polymarket_response): assert pred2.amount == Decimal("19999.9704") assert pred2.price == Decimal("0.255") assert pred2.claimable is False + assert pred2.chain == Blockchain.POLYGON def test_parse_multiple_apps(debank_app_parser): diff --git a/blockapi/test/v2/api/debank/test_debank_fetch.py b/blockapi/test/v2/api/debank/test_debank_fetch.py index 5b314ad9..178fa9cc 100644 --- a/blockapi/test/v2/api/debank/test_debank_fetch.py +++ b/blockapi/test/v2/api/debank/test_debank_fetch.py @@ -36,6 +36,14 @@ def test_fetch_usage(): assert False +@pytest.mark.integration +def test_fetch_debank_apps(real_debank_api): + response = real_debank_api.fetch_debank_apps( + '0x807A2E2e469df84b299Da5f90f15DdA4380dAcA1' + ) + _save('fetch-result-debank-apps', response) + + def _save(name: str, data: FetchResult): if not OPTION_SAVE_DATA: return diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index 8c83e79e..ab060c38 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -12,6 +12,7 @@ from blockapi.utils.num import decimals_to_raw, to_decimal from blockapi.v2.api.debank_maps import ( COINGECKO_IDS_BY_CONTRACTS, + DEBANK_APP_CHAIN_MAP, DEBANK_ASSET_TYPES, NATIVE_COIN_MAP, REWARD_ASSET_TYPE_MAP, @@ -40,7 +41,6 @@ DebankPrediction, DebankModelAppPortfolioItem, DebankModelApp, - DebankModelDepositDetail, DebankModelPredictionDetail, ) @@ -626,16 +626,21 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: deposits = [] predictions = [] + chain = DEBANK_APP_CHAIN_MAP.get(model.id) + if not chain: + logger.warning(f'Unknown chain for app {model.id}, using default') + chain = Blockchain.ETHEREUM + for portfolio_item in model.portfolio_item_list: detail_types = portfolio_item.detail_types if 'prediction' in detail_types: - prediction = self._parse_prediction(portfolio_item) + prediction = self._parse_prediction(portfolio_item, chain) if prediction: predictions.append(prediction) else: # Parse as deposit (common, etc.) - deposit = self._parse_deposit(portfolio_item) + deposit = self._parse_deposit(portfolio_item, chain) if deposit: deposits.append(deposit) @@ -650,7 +655,7 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: ) def _parse_prediction( - self, item: DebankModelAppPortfolioItem + self, item: DebankModelAppPortfolioItem, chain: Blockchain ) -> Optional[DebankPrediction]: """Parse a prediction market position.""" try: @@ -668,35 +673,22 @@ def _parse_prediction( claimable=detail.claimable, event_end_at=detail.event_end_at, is_market_closed=detail.is_market_closed, + chain=chain, position_index=item.position_index, update_at=item.update_at, ) def _parse_deposit( - self, item: DebankModelAppPortfolioItem + self, item: DebankModelAppPortfolioItem, chain: Blockchain ) -> Optional[DebankAppDeposit]: """Parse a deposit/common type portfolio item.""" - try: - detail = DebankModelDepositDetail(**item.detail) - except Exception as e: - logger.warning(f'Failed to parse deposit detail: {e}') - detail = DebankModelDepositDetail() - - # Collect all tokens from supply, borrow, and reward lists - tokens = [] - if detail.supply_token_list: - tokens.extend(detail.supply_token_list) - if detail.borrow_token_list: - tokens.extend(detail.borrow_token_list) - if detail.reward_token_list: - tokens.extend(detail.reward_token_list) - return DebankAppDeposit.from_api( name=item.name, asset_usd_value=item.stats.asset_usd_value, debt_usd_value=item.stats.debt_usd_value, net_usd_value=item.stats.net_usd_value, - tokens=tokens, + tokens=item.asset_token_list, + chain=chain, position_index=item.position_index, update_at=item.update_at, ) diff --git a/blockapi/v2/api/debank_maps.py b/blockapi/v2/api/debank_maps.py index 436d83e4..952e8e48 100644 --- a/blockapi/v2/api/debank_maps.py +++ b/blockapi/v2/api/debank_maps.py @@ -35,6 +35,14 @@ if coin.info and coin.info.coingecko_id } +# From DeBank API /v1/app_protocol/list endpoint (docs.cloud.debank.com) +DEBANK_APP_CHAIN_MAP: dict[str, Blockchain] = { + 'hyperliquid': Blockchain.HYPERLIQUID, + 'lighter': Blockchain.ARBITRUM, + 'opinion': Blockchain.BINANCE_SMART_CHAIN, + 'polymarket': Blockchain.POLYGON, +} + COINGECKO_IDS_BY_CONTRACTS: list[CoingeckoMapping] = [ CoingeckoMapping( symbol='ETH', diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index c3d2b787..ea268428 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -8,7 +8,7 @@ from blockapi.utils.datetime import parse_dt from blockapi.utils.num import raw_to_decimals, to_decimal, to_int -from pydantic import BaseModel +from pydantic import BaseModel, Field UNKNOWN = 'unknown' @@ -1197,12 +1197,16 @@ class DebankModelPredictionDetail(BaseModel): event_end_at: Optional[float] = None -class DebankModelDepositDetail(BaseModel): - """Detail for deposit/common type portfolio items.""" +class DebankDepositToken(BaseModel): + """Token within deposit/common type portfolio items.""" - supply_token_list: Optional[list[dict]] = None - borrow_token_list: Optional[list[dict]] = None - reward_token_list: Optional[list[dict]] = None + id: str + symbol: str + name: str + amount: float + app_id: str + price: float + logo_url: Optional[str] = None class DebankModelAppPortfolioItem(BaseModel): @@ -1214,7 +1218,7 @@ class DebankModelAppPortfolioItem(BaseModel): detail: dict position_index: str asset_dict: Optional[dict] = None - asset_token_list: Optional[list[dict]] = None + asset_token_list: list[DebankDepositToken] = Field(default_factory=list) update_at: Optional[float] = None proxy_detail: Optional[dict] = None @@ -1242,6 +1246,7 @@ class DebankPrediction: claimable: bool event_end_at: Optional[datetime] is_market_closed: bool + chain: Blockchain position_index: Optional[str] update_at: Optional[datetime] @@ -1256,6 +1261,7 @@ def from_api( usd_value: Union[str, float, int], claimable: bool, is_market_closed: bool, + chain: Blockchain, event_end_at: Optional[Union[int, float]] = None, position_index: Optional[str] = None, update_at: Optional[Union[int, float]] = None, @@ -1269,6 +1275,7 @@ def from_api( claimable=claimable, event_end_at=parse_dt(event_end_at) if event_end_at is not None else None, is_market_closed=is_market_closed, + chain=chain, position_index=position_index, update_at=parse_dt(update_at) if update_at is not None else None, ) @@ -1282,7 +1289,8 @@ class DebankAppDeposit: asset_usd_value: Decimal debt_usd_value: Decimal net_usd_value: Decimal - tokens: list[dict] # Raw token data for flexibility + tokens: list[DebankDepositToken] + chain: Blockchain position_index: Optional[str] update_at: Optional[datetime] @@ -1295,7 +1303,8 @@ def from_api( debt_usd_value: Union[str, float, int], net_usd_value: Union[str, float, int], position_index: str, - tokens: Optional[list[dict]] = None, + tokens: Optional[list[DebankDepositToken]] = None, + chain: Blockchain, update_at: Optional[Union[int, float]] = None, ) -> 'DebankAppDeposit': return cls( @@ -1304,6 +1313,7 @@ def from_api( debt_usd_value=to_decimal(debt_usd_value), net_usd_value=to_decimal(net_usd_value), tokens=tokens or [], + chain=chain, position_index=position_index, update_at=parse_dt(update_at) if update_at else None, ) @@ -1311,7 +1321,7 @@ def from_api( @property def token_symbols(self) -> list[str]: """Get list of token symbols in this deposit.""" - return [t.get('symbol', t.get('name', '')) for t in self.tokens] + return [t.symbol for t in self.tokens] @attr.s(auto_attribs=True, slots=True, frozen=True) From 70e2072ac663664daca82aec718ee208578223dc Mon Sep 17 00:00:00 2001 From: Jakub Vins Date: Thu, 5 Feb 2026 10:43:42 +0100 Subject: [PATCH 2/2] Review tweaks --- blockapi/v2/api/debank.py | 7 ++----- blockapi/v2/models.py | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index ab060c38..585a066a 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -627,9 +627,6 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: predictions = [] chain = DEBANK_APP_CHAIN_MAP.get(model.id) - if not chain: - logger.warning(f'Unknown chain for app {model.id}, using default') - chain = Blockchain.ETHEREUM for portfolio_item in model.portfolio_item_list: detail_types = portfolio_item.detail_types @@ -655,7 +652,7 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: ) def _parse_prediction( - self, item: DebankModelAppPortfolioItem, chain: Blockchain + self, item: DebankModelAppPortfolioItem, chain: Optional[Blockchain] ) -> Optional[DebankPrediction]: """Parse a prediction market position.""" try: @@ -679,7 +676,7 @@ def _parse_prediction( ) def _parse_deposit( - self, item: DebankModelAppPortfolioItem, chain: Blockchain + self, item: DebankModelAppPortfolioItem, chain: Optional[Blockchain] ) -> Optional[DebankAppDeposit]: """Parse a deposit/common type portfolio item.""" return DebankAppDeposit.from_api( diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index ea268428..0679cafe 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -1246,7 +1246,7 @@ class DebankPrediction: claimable: bool event_end_at: Optional[datetime] is_market_closed: bool - chain: Blockchain + chain: Optional[Blockchain] position_index: Optional[str] update_at: Optional[datetime] @@ -1261,7 +1261,7 @@ def from_api( usd_value: Union[str, float, int], claimable: bool, is_market_closed: bool, - chain: Blockchain, + chain: Optional[Blockchain] = None, event_end_at: Optional[Union[int, float]] = None, position_index: Optional[str] = None, update_at: Optional[Union[int, float]] = None, @@ -1290,7 +1290,7 @@ class DebankAppDeposit: debt_usd_value: Decimal net_usd_value: Decimal tokens: list[DebankDepositToken] - chain: Blockchain + chain: Optional[Blockchain] position_index: Optional[str] update_at: Optional[datetime] @@ -1304,7 +1304,7 @@ def from_api( net_usd_value: Union[str, float, int], position_index: str, tokens: Optional[list[DebankDepositToken]] = None, - chain: Blockchain, + chain: Optional[Blockchain] = None, update_at: Optional[Union[int, float]] = None, ) -> 'DebankAppDeposit': return cls(