Skip to content
Merged
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
34 changes: 14 additions & 20 deletions blockapi/test/v2/api/debank/test_debank_app_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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]
Expand All @@ -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):
Expand Down Expand Up @@ -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": [
{
Expand All @@ -211,14 +205,14 @@ def test_parse_multiple_apps(debank_app_parser):
],
},
{
"id": "app2",
"name": "App 2",
"id": "hyperliquid",
"name": "Hyperliquid",
"has_supported_portfolio": False,
"portfolio_item_list": [],
},
]

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"
1 change: 0 additions & 1 deletion blockapi/v2/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from blockapi.v2.api.debank import (
DebankApi,
DebankApp,
DebankAppDeposit,
DebankPrediction,
)
from blockapi.v2.api.ethplorer import EthplorerApi
Expand Down
58 changes: 30 additions & 28 deletions blockapi/v2/api/debank.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__)

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

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions blockapi/v2/coin_mapping.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import inspect

from blockapi.v2 import coins as _coins_module
from blockapi.v2.coins import (
COIN_DAI,
COIN_ETH,
Expand All @@ -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,
Expand Down
83 changes: 2 additions & 81 deletions blockapi/v2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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
Expand All @@ -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(
Expand Down