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 b39a1f4..93d9a35 100644 --- a/blockapi/test/v2/api/debank/test_debank_app_parser.py +++ b/blockapi/test/v2/api/debank/test_debank_app_parser.py @@ -224,6 +224,58 @@ def test_parse_polymarket_predictions(debank_app_parser, polymarket_response): assert pred2.chain == Blockchain.POLYGON +def test_parse_deposit_unmapped_symbol_is_not_dropped(debank_app_parser): + """A deposit token missing from symbol_to_coin_map must be kept, with the + coin built from the token's own fields (not silently dropped).""" + response = [ + { + "id": "hyperliquid", + "name": "Hyperliquid", + "has_supported_portfolio": True, + "portfolio_item_list": [ + { + "stats": { + "asset_usd_value": 1234.5, + "debt_usd_value": 0, + "net_usd_value": 1234.5, + }, + "asset_token_list": [ + { + "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6", + "name": "Hyperliquid", + "symbol": "HYPE", + "decimals": 18, + "app_id": "hyperliquid", + "price": 41.2, + "amount": 30.0, + } + ], + "update_at": 1779790996.4148061, + "name": "Deposit", + "detail_types": ["common"], + "detail": {}, + "position_index": "spot_0xabc", + } + ], + } + ] + + parsed_apps = debank_app_parser.parse(response) + app = parsed_apps[0] + + assert len(app.deposits) == 1 + deposit = app.deposits[0] + assert deposit.asset_type == AssetType.DEPOSITED + assert deposit.balance_raw == Decimal("30.0") + assert deposit.coin.symbol == "HYPE" + assert deposit.coin.name == "Hyperliquid" + assert deposit.coin.decimals == 18 + assert deposit.coin.blockchain == Blockchain.HYPERLIQUID + # token.id is passed through as address (unique per token) so unmapped + # tokens don't collapse into one unknown currency downstream. + assert deposit.coin.address == "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" + + def test_parse_multiple_apps(debank_app_parser): """Test parsing multiple apps.""" response = [ diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index 982f0ed..e2ec9e5 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -37,6 +37,7 @@ DebankModelApp, DebankModelAppPortfolioItem, DebankModelAppStats, + DebankModelDepositToken, DebankModelPredictionDetail, DebankPrediction, FetchResult, @@ -688,26 +689,24 @@ def _parse_deposit( ) -> 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.' + # Map first so well-known tokens keep their canonical coingecko_id. + mapped = symbol_to_coin_map.get(token.symbol) + if mapped: + coin = Coin.from_api( + blockchain=chain, + decimals=mapped.decimals, + name=mapped.name, + symbol=mapped.symbol, + info=mapped.info, ) - continue - - coin_with_app_chain = Coin.from_api( - blockchain=chain, - decimals=coin.decimals, - name=coin.name, - symbol=coin.symbol, - info=coin.info, - ) + else: + coin = self._coin_from_deposit_token(token, chain) balance = BalanceItem.from_api( balance=Decimal(token.amount), balance_raw=token.amount, asset_type=AssetType.DEPOSITED, - coin=coin_with_app_chain, + coin=coin, raw=token.model_dump(), last_updated=int(item.update_at) if item.update_at else None, ) @@ -715,6 +714,24 @@ def _parse_deposit( return balances + @staticmethod + def _coin_from_deposit_token( + token: DebankModelDepositToken, chain: Blockchain + ) -> Coin: + # Address must stay unique per token: a null one makes downstream + # add_unknown_currency_v2 key the currency by chain, collapsing all + # unmapped tokens on a chain into one. + address = make_checksum_address(token.id) or token.id + coingecko_id = get_coingecko_id(token.id, token.symbol) + return Coin.from_api( + symbol=token.symbol, + name=token.name, + decimals=token.decimals, + blockchain=chain, + address=address, + info=CoinInfo(logo_url=token.logo_url, coingecko_id=coingecko_id), + ) + class DebankApi(CustomizableBlockchainApi, BalanceMixin, IPortfolio): """