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 5e28627..dbe3001 100644 --- a/blockapi/test/v2/api/debank/test_debank_app_parser.py +++ b/blockapi/test/v2/api/debank/test_debank_app_parser.py @@ -4,9 +4,10 @@ from blockapi.v2.api.debank import ( DebankApp, - DebankAppDeposit, DebankPrediction, ) +from blockapi.v2.models import BalanceItem, AssetType +from blockapi.v2.coins import COIN_USDC from blockapi.v2.models import Blockchain @@ -129,7 +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.""" + """Deposits should be parsed as BalanceItem objects.""" parsed_apps = debank_app_parser.parse(polymarket_response) app = parsed_apps[0] @@ -138,18 +139,11 @@ def test_parse_polymarket_deposits(debank_app_parser, polymarket_response): assert len(app.deposits) == 1 deposit = app.deposits[0] - assert isinstance(deposit, DebankAppDeposit) - assert deposit.name == "Deposit" - assert deposit.asset_usd_value == Decimal("290915.13432776055") - 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.token_symbols == ["USDC"] + assert isinstance(deposit, BalanceItem) + assert deposit.asset_type == AssetType.DEPOSITED + assert deposit.balance_raw == Decimal("290595.12768") + assert deposit.coin.symbol == COIN_USDC.symbol + assert deposit.coin.info.coingecko_id == COIN_USDC.info.coingecko_id def test_parse_polymarket_predictions(debank_app_parser, polymarket_response): @@ -186,8 +180,8 @@ def test_parse_multiple_apps(debank_app_parser): """Test parsing multiple apps.""" response = [ { - "id": "app1", - "name": "App 1", + "id": "polymarket", + "name": "Polymarket", "has_supported_portfolio": True, "portfolio_item_list": [ { @@ -211,8 +205,8 @@ def test_parse_multiple_apps(debank_app_parser): ], }, { - "id": "app2", - "name": "App 2", + "id": "hyperliquid", + "name": "Hyperliquid", "has_supported_portfolio": False, "portfolio_item_list": [], }, @@ -220,5 +214,5 @@ def test_parse_multiple_apps(debank_app_parser): parsed_apps = debank_app_parser.parse(response) assert len(parsed_apps) == 2 - assert parsed_apps[0].app_id == "app1" - assert parsed_apps[1].app_id == "app2" + assert parsed_apps[0].app_id == "polymarket" + assert parsed_apps[1].app_id == "hyperliquid" diff --git a/blockapi/v2/api/__init__.py b/blockapi/v2/api/__init__.py index 66405df..8a4f80c 100644 --- a/blockapi/v2/api/__init__.py +++ b/blockapi/v2/api/__init__.py @@ -8,7 +8,6 @@ from blockapi.v2.api.debank import ( DebankApi, DebankApp, - DebankAppDeposit, DebankPrediction, ) from blockapi.v2.api.ethplorer import EthplorerApi diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index 083c5aa..94de06e 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -5,7 +5,7 @@ from typing import Dict, Iterable, List, Optional, Union import attr -from pydantic import BaseModel, ValidationError, validator +from pydantic import BaseModel, validator from blockapi.utils.address import make_checksum_address from blockapi.utils.datetime import parse_dt @@ -37,13 +37,12 @@ PoolInfo, Protocol, DebankApp, - DebankAppDeposit, DebankPrediction, DebankModelAppPortfolioItem, DebankModelApp, DebankModelPredictionDetail, - DebankDepositToken, ) +from blockapi.v2.coin_mapping import symbol_to_coin_map logger = logging.getLogger(__name__) @@ -601,10 +600,8 @@ def _get_reward_asset_type(asset_type): class DebankAppParser: - """Parser for Debank complex_app_list responses.""" def parse(self, response: list) -> list[DebankApp]: - """Parse the full response from get_complex_app_list.""" if not response: return [] @@ -617,13 +614,17 @@ def parse(self, response: list) -> list[DebankApp]: return apps def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: - """Parse a single app from the response.""" model = DebankModelApp(**raw_app) deposits = [] predictions = [] chain = DEBANK_APP_CHAIN_MAP.get(model.id) + if not chain: + logger.error( + f'No chain mapping found for app {model.id} ({model.name}). Skipping.' + ) + return for portfolio_item in model.portfolio_item_list: detail_types = portfolio_item.detail_types @@ -636,7 +637,7 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: # Parse as deposit (common, etc.) deposit = self._parse_deposit(portfolio_item, chain) if deposit: - deposits.append(deposit) + deposits.extend(deposit) return DebankApp.from_api( app_id=model.id, @@ -649,9 +650,8 @@ def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: ) def _parse_prediction( - self, item: DebankModelAppPortfolioItem, chain: Optional[Blockchain] + self, item: DebankModelAppPortfolioItem, chain: Blockchain ) -> Optional[DebankPrediction]: - """Parse a prediction market position.""" detail = DebankModelPredictionDetail(**item.detail) return DebankPrediction.from_api( @@ -669,26 +669,28 @@ def _parse_prediction( ) def _parse_deposit( - self, item: DebankModelAppPortfolioItem, chain: Optional[Blockchain] - ) -> Optional[DebankAppDeposit]: - """Parse a deposit/common type portfolio item.""" - parsed_tokens = [ - self._parse_token(t.model_dump()) for t in item.asset_token_list or [] - ] - - 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=[t for t in parsed_tokens if t is not None], - chain=chain, - position_index=item.position_index, - update_at=item.update_at, - ) + self, item: DebankModelAppPortfolioItem, chain: Blockchain + ) -> list[BalanceItem]: + balances = [] + for token in item.asset_token_list or []: + coin = symbol_to_coin_map.get(token.symbol) + if not coin: + logger.error( + f'No coin mapping found for app deposit token {token.symbol} in chain {chain}. Skipping.' + ) + continue + + balance = BalanceItem.from_api( + balance=Decimal(token.amount), + balance_raw=token.amount, + asset_type=AssetType.DEPOSITED, + coin=coin, + raw=token.model_dump(), + last_updated=int(item.update_at) if item.update_at else None, + ) + balances.append(balance) - def _parse_token(self, raw_token: dict) -> Optional[DebankDepositToken]: - return DebankDepositToken.from_api(**raw_token) + return balances class DebankApi(CustomizableBlockchainApi, BalanceMixin, IPortfolio): diff --git a/blockapi/v2/coin_mapping.py b/blockapi/v2/coin_mapping.py index 46c8951..fac4ada 100644 --- a/blockapi/v2/coin_mapping.py +++ b/blockapi/v2/coin_mapping.py @@ -1,3 +1,6 @@ +import inspect + +from blockapi.v2 import coins as _coins_module from blockapi.v2.coins import ( COIN_DAI, COIN_ETH, @@ -8,6 +11,15 @@ ) from blockapi.v2.models import Blockchain, Coin +# Dynamically collect all COIN_* variables from coins module +coins: list[Coin] = [ + obj + for name, obj in inspect.getmembers(_coins_module) + if name.startswith('COIN_') and isinstance(obj, Coin) +] + +symbol_to_coin_map: dict[str, Coin] = {coin.symbol: coin for coin in coins} + OPENSEA_COINS: dict[str, Coin] = { 'ETH': COIN_ETH, 'USDC': COIN_USDC, diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index e4e9595..88168b9 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -1282,85 +1282,6 @@ def from_api( ) -@attr.s(auto_attribs=True, slots=True, frozen=True) -class DebankDepositToken: - id: str - symbol: str - name: str - amount: Decimal - decimals: int - app_id: str - price: Decimal - logo_url: Optional[str] - - @classmethod - def from_api( - cls, - *, - id: str, - symbol: str, - name: str, - amount: Union[str, float, int], - decimals: int, - app_id: str, - price: Union[str, float, int], - logo_url: Optional[str] = None, - ) -> 'DebankDepositToken': - return cls( - id=id, - symbol=symbol, - decimals=decimals, - name=name, - amount=to_decimal(amount), - app_id=app_id, - price=to_decimal(price), - logo_url=logo_url, - ) - - -@attr.s(auto_attribs=True, slots=True, frozen=True) -class DebankAppDeposit: - """Represents a deposit/holding within a Debank App (e.g., Polymarket cash deposit).""" - - name: str - asset_usd_value: Decimal - debt_usd_value: Decimal - net_usd_value: Decimal - tokens: list[DebankDepositToken] - chain: Optional[Blockchain] - position_index: Optional[str] - update_at: Optional[datetime] - - @classmethod - def from_api( - cls, - *, - name: str, - asset_usd_value: Union[str, float, int], - debt_usd_value: Union[str, float, int], - net_usd_value: Union[str, float, int], - position_index: str, - tokens: Optional[list[DebankDepositToken]] = None, - chain: Optional[Blockchain] = None, - update_at: Optional[Union[int, float]] = None, - ) -> 'DebankAppDeposit': - return cls( - name=name, - asset_usd_value=to_decimal(asset_usd_value), - 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, - ) - - @property - def token_symbols(self) -> list[str]: - """Get list of token symbols in this deposit.""" - return [t.symbol for t in self.tokens] - - @attr.s(auto_attribs=True, slots=True, frozen=True) class DebankApp: """Represents a Debank App with its deposits and predictions.""" @@ -1370,7 +1291,7 @@ class DebankApp: site_url: Optional[str] logo_url: Optional[str] has_supported_portfolio: bool - deposits: list[DebankAppDeposit] + deposits: list[BalanceItem] predictions: list[DebankPrediction] @classmethod @@ -1382,7 +1303,7 @@ def from_api( site_url: Optional[str] = None, logo_url: Optional[str] = None, has_supported_portfolio: bool = False, - deposits: Optional[list[DebankAppDeposit]] = None, + deposits: Optional[list[BalanceItem]] = None, predictions: Optional[list[DebankPrediction]] = None, ) -> 'DebankApp': return cls(