Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion blockapi/test/v2/api/debank/test_debank_app_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
DebankAppDeposit,
DebankPrediction,
)
from blockapi.v2.models import Blockchain


@pytest.fixture
Expand Down Expand Up @@ -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]

Expand All @@ -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"]


Expand All @@ -168,13 +171,15 @@ 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?"
assert pred2.side == "Yes"
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):
Expand Down
8 changes: 8 additions & 0 deletions blockapi/test/v2/api/debank/test_debank_fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 13 additions & 21 deletions blockapi/v2/api/debank.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,7 +41,6 @@
DebankPrediction,
DebankModelAppPortfolioItem,
DebankModelApp,
DebankModelDepositDetail,
DebankModelPredictionDetail,
)

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may lead to incorrect chains.. I prefer None or 'unknown' 🤷


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)

Expand All @@ -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:
Expand All @@ -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,
)
Expand Down
8 changes: 8 additions & 0 deletions blockapi/v2/api/debank_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
30 changes: 20 additions & 10 deletions blockapi/v2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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):
Expand All @@ -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
Comment on lines 1219 to 1222
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asset_token_list uses a mutable default (=[]) on a Pydantic model. This can lead to shared state between instances and unexpected cross-test/request contamination. Prefer Field(default_factory=list) (or make it Optional[...] = None and normalize at use sites).

Copilot uses AI. Check for mistakes.
proxy_detail: Optional[dict] = None

Expand Down Expand Up @@ -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]

Expand All @@ -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,
Expand All @@ -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,
)
Expand All @@ -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]

Expand All @@ -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(
Expand All @@ -1304,14 +1313,15 @@ 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,
)

@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)
Expand Down