From 27a78edaac0ad5962b308c946d1dfdcdb1f9ee36 Mon Sep 17 00:00:00 2001 From: Jakub Vins Date: Fri, 23 Jan 2026 10:21:11 +0100 Subject: [PATCH 1/2] feat: Added support for fetchin apps from debank --- .vscode/settings.json | 3 + blockapi/test/v2/api/debank/conftest.py | 6 + .../v2/api/debank/test_debank_app_parser.py | 228 ++++++++++++++ blockapi/v2/api/__init__.py | 7 +- blockapi/v2/api/debank.py | 297 +++++++++++++++++- blockapi/v2/models.py | 1 + 6 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 blockapi/test/v2/api/debank/test_debank_app_parser.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f44ca33b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/blockapi/test/v2/api/debank/conftest.py b/blockapi/test/v2/api/debank/conftest.py index d1f3d02e..063b9d0d 100644 --- a/blockapi/test/v2/api/debank/conftest.py +++ b/blockapi/test/v2/api/debank/conftest.py @@ -6,6 +6,7 @@ from blockapi.test.v2.api.conftest import read_file, read_json_file from blockapi.v2.api.debank import ( DebankApi, + DebankAppParser, DebankBalanceParser, DebankChain, DebankPortfolioParser, @@ -58,6 +59,11 @@ def portfolio_parser(protocol_parser, balance_parser): return DebankPortfolioParser(protocol_parser, balance_parser) +@pytest.fixture +def app_parser(): + return DebankAppParser() + + @pytest.fixture def balance_item(): return BalanceItem.from_api( diff --git a/blockapi/test/v2/api/debank/test_debank_app_parser.py b/blockapi/test/v2/api/debank/test_debank_app_parser.py new file mode 100644 index 00000000..c40a9751 --- /dev/null +++ b/blockapi/test/v2/api/debank/test_debank_app_parser.py @@ -0,0 +1,228 @@ +from decimal import Decimal + +import pytest + +from blockapi.v2.api.debank import ( + DebankApp, + DebankAppDeposit, + DebankPrediction, +) + + +@pytest.fixture +def polymarket_response(): + """Sample response from get_complex_app_list for Polymarket.""" + return [ + { + "id": "polymarket", + "name": "Polymarket", + "site_url": "https://polymarket.com/", + "logo_url": "https://static.debank.com/image/project/logo_url/app_polymarket/265aca8cef9212e094ef24c71a01c175.png", + "has_supported_portfolio": True, + "portfolio_item_list": [ + { + "stats": { + "asset_usd_value": 290915.13432776055, + "debt_usd_value": 0, + "net_usd_value": 290915.13432776055, + }, + "asset_dict": {"be0eecf639f4e6a57e375123e46ed7b4": 290595.12768}, + "asset_token_list": [ + { + "id": "be0eecf639f4e6a57e375123e46ed7b4", + "name": "USDC", + "symbol": "USDC", + "decimals": 6, + "logo_url": "https://static.debank.com/image/app_token/logo_url/polymarket/fc98c076b66fa798bcd8755cd859032e.png", + "app_id": "polymarket", + "price": 1.0011012113324658, + "amount": 290595.12768, + } + ], + "update_at": 1768998504.1334498, + "name": "Deposit", + "detail_types": ["common"], + "detail": { + "supply_token_list": [ + { + "id": "be0eecf639f4e6a57e375123e46ed7b4", + "name": "USDC", + "symbol": "USDC", + "decimals": 6, + "logo_url": "https://static.debank.com/image/app_token/logo_url/polymarket/fc98c076b66fa798bcd8755cd859032e.png", + "app_id": "polymarket", + "price": 1.0011012113324658, + "amount": 290595.12768, + } + ] + }, + "proxy_detail": {}, + "position_index": "cash_0x5c23dead9ecf271448411096f349133e0bb9c465", + }, + { + "stats": { + "asset_usd_value": 27068.1993, + "debt_usd_value": 0, + "net_usd_value": 27068.1993, + }, + "asset_dict": {}, + "asset_token_list": [], + "update_at": 1768998504.1336002, + "name": "Prediction", + "detail_types": ["prediction"], + "detail": { + "name": "Lighter market cap (FDV) >$1B one day after launch?", + "side": "Yes", + "amount": 27068.1993, + "price": 1, + "claimable": True, + "event_end_at": None, + "is_market_closed": False, + }, + "proxy_detail": {}, + "position_index": "0x5c23dead9ecf271448411096f349133e0bb9c465_108054592060808479303370270554306028883916458239782628449790057811735078958789", + }, + { + "stats": { + "asset_usd_value": 5099.9924519999995, + "debt_usd_value": 0, + "net_usd_value": 5099.9924519999995, + }, + "asset_dict": {}, + "asset_token_list": [], + "update_at": 1768998504.1336374, + "name": "Prediction", + "detail_types": ["prediction"], + "detail": { + "name": "Gensyn FDV above $600M one day after launch?", + "side": "Yes", + "amount": 19999.9704, + "price": 0.255, + "claimable": False, + "event_end_at": None, + "is_market_closed": False, + }, + "proxy_detail": {}, + "position_index": "0x5c23dead9ecf271448411096f349133e0bb9c465_101101625858935510994152869873088213062714890530116131353411379193297614599911", + }, + ], + } + ] + + +def test_empty_response(app_parser): + parsed_apps = app_parser.parse([]) + assert parsed_apps == [] + + +def test_parse_polymarket_app(app_parser, polymarket_response): + parsed_apps = app_parser.parse(polymarket_response) + assert len(parsed_apps) == 1 + + app = parsed_apps[0] + assert isinstance(app, DebankApp) + assert app.app_id == "polymarket" + assert app.name == "Polymarket" + assert app.site_url == "https://polymarket.com/" + assert app.has_supported_portfolio is True + + +def test_parse_polymarket_deposits(app_parser, polymarket_response): + """Deposits should be parsed as DebankAppDeposit objects.""" + parsed_apps = app_parser.parse(polymarket_response) + app = parsed_apps[0] + + # Should have 1 deposit + 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" + + # Should have 1 token (USDC) + assert len(deposit.tokens) == 1 + assert deposit.tokens[0]["symbol"] == "USDC" + assert deposit.token_symbols == ["USDC"] + + +def test_parse_polymarket_predictions(app_parser, polymarket_response): + """Predictions should be parsed as DebankPrediction objects.""" + parsed_apps = app_parser.parse(polymarket_response) + app = parsed_apps[0] + + # Should have 2 predictions + assert len(app.predictions) == 2 + + pred1 = app.predictions[0] + assert isinstance(pred1, DebankPrediction) + assert ( + pred1.prediction_name == "Lighter market cap (FDV) >$1B one day after launch?" + ) + assert pred1.side == "Yes" + assert pred1.amount == Decimal("27068.1993") + assert pred1.price == Decimal("1") + assert pred1.usd_value == Decimal("27068.1993") + assert pred1.claimable is True + assert pred1.is_market_closed is False + + 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 + + +def test_prediction_stores_raw(app_parser, polymarket_response): + """Predictions should store raw data for debugging.""" + parsed_apps = app_parser.parse(polymarket_response) + pred = parsed_apps[0].predictions[0] + + assert pred.raw is not None + assert "stats" in pred.raw + assert "detail" in pred.raw + + +def test_parse_multiple_apps(app_parser): + """Test parsing multiple apps.""" + response = [ + { + "id": "app1", + "name": "App 1", + "has_supported_portfolio": True, + "portfolio_item_list": [ + { + "stats": { + "asset_usd_value": 100, + "debt_usd_value": 0, + "net_usd_value": 100, + }, + "name": "Prediction", + "detail_types": ["prediction"], + "detail": { + "name": "Test prediction", + "side": "Yes", + "amount": 100, + "price": 0.5, + "claimable": False, + "is_market_closed": False, + }, + } + ], + }, + { + "id": "app2", + "name": "App 2", + "has_supported_portfolio": False, + "portfolio_item_list": [], + }, + ] + + parsed_apps = app_parser.parse(response) + assert len(parsed_apps) == 2 + assert parsed_apps[0].app_id == "app1" + assert parsed_apps[1].app_id == "app2" diff --git a/blockapi/v2/api/__init__.py b/blockapi/v2/api/__init__.py index 3f1b0332..66405df2 100644 --- a/blockapi/v2/api/__init__.py +++ b/blockapi/v2/api/__init__.py @@ -5,7 +5,12 @@ BlockchairDogecoinApi, BlockchairLitecoinApi, ) -from blockapi.v2.api.debank import DebankApi +from blockapi.v2.api.debank import ( + DebankApi, + DebankApp, + DebankAppDeposit, + DebankPrediction, +) from blockapi.v2.api.ethplorer import EthplorerApi from blockapi.v2.api.optimistic_etherscan import OptimismEtherscanApi from blockapi.v2.api.perpetual import PerpetualApi, perp_contract_address diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index a5234b7c..740f19db 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -9,7 +9,7 @@ from blockapi.utils.address import make_checksum_address from blockapi.utils.datetime import parse_dt -from blockapi.utils.num import decimals_to_raw +from blockapi.utils.num import decimals_to_raw, to_decimal from blockapi.v2.api.debank_maps import ( COINGECKO_IDS_BY_CONTRACTS, DEBANK_ASSET_TYPES, @@ -154,6 +154,187 @@ class DebankChain(BaseModel): logo_url: str +# --- Debank App Models --- + + +class DebankModelAppStats(BaseModel): + """Stats for a portfolio item.""" + + asset_usd_value: float + debt_usd_value: float + net_usd_value: float + + +class DebankModelPredictionDetail(BaseModel): + """Detail for prediction type portfolio items.""" + + name: str + side: str + amount: float + price: float + claimable: bool + event_end_at: Optional[float] = None + is_market_closed: bool + + +class DebankModelDepositDetail(BaseModel): + """Detail for 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 + + +class DebankModelAppPortfolioItem(BaseModel): + """Portfolio item within an app.""" + + stats: DebankModelAppStats + asset_dict: Optional[dict] = None + asset_token_list: Optional[list[dict]] = None + update_at: Optional[float] = None + name: str + detail_types: list[str] + detail: dict + proxy_detail: Optional[dict] = None + position_index: Optional[str] = None + + +class DebankModelApp(BaseModel): + """Debank App model from complex_app_list endpoint.""" + + id: str + name: str + site_url: Optional[str] = None + logo_url: Optional[str] = None + has_supported_portfolio: bool = False + portfolio_item_list: list[DebankModelAppPortfolioItem] + + +@attr.s(auto_attribs=True, slots=True, frozen=True) +class DebankPrediction: + """Represents a prediction market position (e.g., Polymarket).""" + + prediction_name: str + side: str + amount: Decimal + price: Decimal + usd_value: Decimal + claimable: bool + event_end_at: Optional[datetime] + is_market_closed: bool + position_index: Optional[str] + update_at: Optional[datetime] + raw: dict + + @classmethod + def from_api( + cls, + *, + prediction_name: str, + side: str, + amount: Union[str, float, int], + price: Union[str, float, int], + usd_value: Union[str, float, int], + claimable: bool, + event_end_at: Optional[Union[int, float]] = None, + is_market_closed: bool, + position_index: Optional[str] = None, + update_at: Optional[Union[int, float]] = None, + raw: Optional[dict] = None, + ) -> 'DebankPrediction': + return cls( + prediction_name=prediction_name, + side=side, + amount=to_decimal(amount), + price=to_decimal(price), + usd_value=to_decimal(usd_value), + claimable=claimable, + event_end_at=parse_dt(event_end_at) if event_end_at else None, + is_market_closed=is_market_closed, + position_index=position_index, + update_at=parse_dt(update_at) if update_at else None, + raw=raw or {}, + ) + + +@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[dict] # Raw token data for flexibility + position_index: Optional[str] + update_at: Optional[datetime] + raw: dict + + @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], + tokens: Optional[list[dict]] = None, + position_index: str, + update_at: Optional[Union[int, float]] = None, + raw: Optional[dict] = 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 [], + position_index=position_index, + update_at=parse_dt(update_at) if update_at else None, + raw=raw or {}, + ) + + @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] + + +@attr.s(auto_attribs=True, slots=True, frozen=True) +class DebankApp: + """Represents a Debank App with its deposits and predictions.""" + + app_id: str + name: str + site_url: Optional[str] + logo_url: Optional[str] + has_supported_portfolio: bool + deposits: list[DebankAppDeposit] + predictions: list[DebankPrediction] + + @classmethod + def from_api( + cls, + *, + app_id: str, + name: str, + site_url: Optional[str] = None, + logo_url: Optional[str] = None, + has_supported_portfolio: bool = False, + deposits: Optional[list[DebankAppDeposit]] = None, + predictions: Optional[list[DebankPrediction]] = None, + ) -> 'DebankApp': + return cls( + app_id=app_id, + name=name, + site_url=site_url, + logo_url=logo_url, + has_supported_portfolio=has_supported_portfolio, + deposits=deposits or [], + predictions=predictions or [], + ) + + class DebankProtocolParser: def parse(self, response: List) -> Dict[str, Protocol]: protocols = {} @@ -592,6 +773,111 @@ def _get_reward_asset_type(asset_type): return REWARD_ASSET_TYPE_MAP.get(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 [] + + apps = [] + for item in response: + app = self._parse_app(item) + if app: + apps.append(app) + + return apps + + def _parse_app(self, raw_app: dict) -> Optional[DebankApp]: + """Parse a single app from the response.""" + try: + model = DebankModelApp(**raw_app) + except Exception as e: + logger.error(f'Failed to parse app: {e}') + return None + + deposits = [] + predictions = [] + + 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) + if prediction: + predictions.append(prediction) + else: + # Parse as deposit (common, etc.) + deposit = self._parse_deposit(portfolio_item) + if deposit: + deposits.append(deposit) + + return DebankApp.from_api( + app_id=model.id, + name=model.name, + site_url=model.site_url, + logo_url=model.logo_url, + has_supported_portfolio=model.has_supported_portfolio, + deposits=deposits, + predictions=predictions, + ) + + def _parse_prediction( + self, item: DebankModelAppPortfolioItem + ) -> Optional[DebankPrediction]: + """Parse a prediction market position.""" + try: + detail = DebankModelPredictionDetail(**item.detail) + except Exception as e: + logger.error(f'Failed to parse prediction detail: {e}') + return None + + return DebankPrediction.from_api( + prediction_name=detail.name, + side=detail.side, + amount=detail.amount, + price=detail.price, + usd_value=item.stats.net_usd_value, + claimable=detail.claimable, + event_end_at=detail.event_end_at, + is_market_closed=detail.is_market_closed, + position_index=item.position_index, + update_at=item.update_at, + raw=item.dict(), + ) + + def _parse_deposit( + self, item: DebankModelAppPortfolioItem + ) -> 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, + position_index=item.position_index, + update_at=item.update_at, + raw=item.dict(), + ) + + class DebankApi(CustomizableBlockchainApi, BalanceMixin, IPortfolio): """ DeBank OpenApi: https://open.debank.com/ @@ -613,6 +899,7 @@ class DebankApi(CustomizableBlockchainApi, BalanceMixin, IPortfolio): 'get_portfolio': '/v1/user/all_complex_protocol_list?id={address}', 'get_protocols': '/v1/protocol/all_list', 'usage': '/v1/account/units', + 'get_complex_app_list': '/v1/user/complex_app_list?id={address}', } default_protocol_cache = DebankProtocolCache() @@ -636,6 +923,7 @@ def __init__( self._protocol_parser, self._balance_parser ) self._usage_parser = DebankUsageParser() + self._app_parser = DebankAppParser() def fetch_balances(self, address: str) -> FetchResult: return self.get_data( @@ -652,6 +940,13 @@ def fetch_pools(self, address: str) -> FetchResult: address=address, ) + def fetch_debank_apps(self, address: str) -> FetchResult: + return self.get_data( + 'get_complex_app_list', + headers=self._headers, + address=address, + ) + def fetch_protocols(self) -> FetchResult: return self.get_data( 'get_protocols', diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index d72f3094..23a96cc5 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -386,6 +386,7 @@ class AssetType(str, Enum): LOCKED = 'locked' NFT = 'nft' PENDING_TRANSACTION = 'pending_transaction' + PREDICTION = 'prediction' PRICED_VESTING = 'priced_vesting' REWARDS = 'rewards' STAKED = 'staked' From 411ad1ccdd33b4b266ba886841535987e6c9c543 Mon Sep 17 00:00:00 2001 From: Jakub Vins Date: Fri, 23 Jan 2026 10:35:21 +0100 Subject: [PATCH 2/2] Fixes --- .../v2/api/debank/test_debank_app_parser.py | 11 +- blockapi/v2/api/debank.py | 197 ++---------------- blockapi/v2/api/terra.py | 6 +- blockapi/v2/models.py | 178 +++++++++++++++- 4 files changed, 194 insertions(+), 198 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 c40a9751..c8c9dd03 100644 --- a/blockapi/test/v2/api/debank/test_debank_app_parser.py +++ b/blockapi/test/v2/api/debank/test_debank_app_parser.py @@ -177,16 +177,6 @@ def test_parse_polymarket_predictions(app_parser, polymarket_response): assert pred2.claimable is False -def test_prediction_stores_raw(app_parser, polymarket_response): - """Predictions should store raw data for debugging.""" - parsed_apps = app_parser.parse(polymarket_response) - pred = parsed_apps[0].predictions[0] - - assert pred.raw is not None - assert "stats" in pred.raw - assert "detail" in pred.raw - - def test_parse_multiple_apps(app_parser): """Test parsing multiple apps.""" response = [ @@ -211,6 +201,7 @@ def test_parse_multiple_apps(app_parser): "claimable": False, "is_market_closed": False, }, + "position_index": "test_position_index", } ], }, diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index 740f19db..8c83e79e 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -35,6 +35,13 @@ Pool, PoolInfo, Protocol, + DebankApp, + DebankAppDeposit, + DebankPrediction, + DebankModelAppPortfolioItem, + DebankModelApp, + DebankModelDepositDetail, + DebankModelPredictionDetail, ) logger = logging.getLogger(__name__) @@ -154,187 +161,6 @@ class DebankChain(BaseModel): logo_url: str -# --- Debank App Models --- - - -class DebankModelAppStats(BaseModel): - """Stats for a portfolio item.""" - - asset_usd_value: float - debt_usd_value: float - net_usd_value: float - - -class DebankModelPredictionDetail(BaseModel): - """Detail for prediction type portfolio items.""" - - name: str - side: str - amount: float - price: float - claimable: bool - event_end_at: Optional[float] = None - is_market_closed: bool - - -class DebankModelDepositDetail(BaseModel): - """Detail for 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 - - -class DebankModelAppPortfolioItem(BaseModel): - """Portfolio item within an app.""" - - stats: DebankModelAppStats - asset_dict: Optional[dict] = None - asset_token_list: Optional[list[dict]] = None - update_at: Optional[float] = None - name: str - detail_types: list[str] - detail: dict - proxy_detail: Optional[dict] = None - position_index: Optional[str] = None - - -class DebankModelApp(BaseModel): - """Debank App model from complex_app_list endpoint.""" - - id: str - name: str - site_url: Optional[str] = None - logo_url: Optional[str] = None - has_supported_portfolio: bool = False - portfolio_item_list: list[DebankModelAppPortfolioItem] - - -@attr.s(auto_attribs=True, slots=True, frozen=True) -class DebankPrediction: - """Represents a prediction market position (e.g., Polymarket).""" - - prediction_name: str - side: str - amount: Decimal - price: Decimal - usd_value: Decimal - claimable: bool - event_end_at: Optional[datetime] - is_market_closed: bool - position_index: Optional[str] - update_at: Optional[datetime] - raw: dict - - @classmethod - def from_api( - cls, - *, - prediction_name: str, - side: str, - amount: Union[str, float, int], - price: Union[str, float, int], - usd_value: Union[str, float, int], - claimable: bool, - event_end_at: Optional[Union[int, float]] = None, - is_market_closed: bool, - position_index: Optional[str] = None, - update_at: Optional[Union[int, float]] = None, - raw: Optional[dict] = None, - ) -> 'DebankPrediction': - return cls( - prediction_name=prediction_name, - side=side, - amount=to_decimal(amount), - price=to_decimal(price), - usd_value=to_decimal(usd_value), - claimable=claimable, - event_end_at=parse_dt(event_end_at) if event_end_at else None, - is_market_closed=is_market_closed, - position_index=position_index, - update_at=parse_dt(update_at) if update_at else None, - raw=raw or {}, - ) - - -@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[dict] # Raw token data for flexibility - position_index: Optional[str] - update_at: Optional[datetime] - raw: dict - - @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], - tokens: Optional[list[dict]] = None, - position_index: str, - update_at: Optional[Union[int, float]] = None, - raw: Optional[dict] = 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 [], - position_index=position_index, - update_at=parse_dt(update_at) if update_at else None, - raw=raw or {}, - ) - - @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] - - -@attr.s(auto_attribs=True, slots=True, frozen=True) -class DebankApp: - """Represents a Debank App with its deposits and predictions.""" - - app_id: str - name: str - site_url: Optional[str] - logo_url: Optional[str] - has_supported_portfolio: bool - deposits: list[DebankAppDeposit] - predictions: list[DebankPrediction] - - @classmethod - def from_api( - cls, - *, - app_id: str, - name: str, - site_url: Optional[str] = None, - logo_url: Optional[str] = None, - has_supported_portfolio: bool = False, - deposits: Optional[list[DebankAppDeposit]] = None, - predictions: Optional[list[DebankPrediction]] = None, - ) -> 'DebankApp': - return cls( - app_id=app_id, - name=name, - site_url=site_url, - logo_url=logo_url, - has_supported_portfolio=has_supported_portfolio, - deposits=deposits or [], - predictions=predictions or [], - ) - - class DebankProtocolParser: def parse(self, response: List) -> Dict[str, Protocol]: protocols = {} @@ -844,7 +670,6 @@ def _parse_prediction( is_market_closed=detail.is_market_closed, position_index=item.position_index, update_at=item.update_at, - raw=item.dict(), ) def _parse_deposit( @@ -874,7 +699,6 @@ def _parse_deposit( tokens=tokens, position_index=item.position_index, update_at=item.update_at, - raw=item.dict(), ) @@ -980,6 +804,13 @@ def get_protocols(self) -> Dict[str, Protocol]: return self._protocol_parser.parse(response) + def parse_debank_apps(self, fetch_result: FetchResult) -> ParseResult: + if error := self._get_error(fetch_result.data): + return ParseResult(errors=[error]) + + apps = self._app_parser.parse(fetch_result.data) + return ParseResult(data=apps) + def get_chains(self) -> list[DebankChain]: response = self.get('get_chains', headers=self._headers) if self._has_error(response): diff --git a/blockapi/v2/api/terra.py b/blockapi/v2/api/terra.py index 71200979..330843b4 100644 --- a/blockapi/v2/api/terra.py +++ b/blockapi/v2/api/terra.py @@ -206,16 +206,14 @@ class TerraMantleApi(BlockchainApi): # API uses post requests supported_requests = {} - _post_requests = { - 'wasm_contract_address_store': """ + _post_requests = {'wasm_contract_address_store': """ WasmContractsContractAddressStore( ContractAddress: "$CONTRACT_ADDRESS", QueryMsg: "$QUERY_MSG" ){ Result } - """ - } + """} _tokens_map: Optional[Dict[str, Dict]] = None diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index 23a96cc5..25d3e036 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -8,6 +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 UNKNOWN = 'unknown' @@ -1174,6 +1175,181 @@ def append_items(self, items: List[BalanceItem]) -> None: self.items.extend(items) +# --- Debank App Models --- + + +class DebankModelAppStats(BaseModel): + """Stats for a portfolio item.""" + + asset_usd_value: float + debt_usd_value: float + net_usd_value: float + + +class DebankModelPredictionDetail(BaseModel): + """Detail for prediction type portfolio items.""" + + name: str + side: str + amount: float + price: float + claimable: bool + is_market_closed: bool + event_end_at: Optional[float] = None + + +class DebankModelDepositDetail(BaseModel): + """Detail for 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 + + +class DebankModelAppPortfolioItem(BaseModel): + """Portfolio item within an app.""" + + stats: DebankModelAppStats + name: str + detail_types: list[str] + detail: dict + position_index: str + asset_dict: Optional[dict] = None + asset_token_list: Optional[list[dict]] = None + update_at: Optional[float] = None + proxy_detail: Optional[dict] = None + + +class DebankModelApp(BaseModel): + """Debank App model from complex_app_list endpoint.""" + + id: str + name: str + portfolio_item_list: list[DebankModelAppPortfolioItem] + site_url: Optional[str] = None + logo_url: Optional[str] = None + has_supported_portfolio: bool = False + + +@attr.s(auto_attribs=True, slots=True, frozen=True) +class DebankPrediction: + """Represents a prediction market position (e.g., Polymarket).""" + + prediction_name: str + side: str + amount: Decimal + price: Decimal + usd_value: Decimal + claimable: bool + event_end_at: Optional[datetime] + is_market_closed: bool + position_index: Optional[str] + update_at: Optional[datetime] + + @classmethod + def from_api( + cls, + *, + prediction_name: str, + side: str, + amount: Union[str, float, int], + price: Union[str, float, int], + usd_value: Union[str, float, int], + claimable: bool, + is_market_closed: bool, + event_end_at: Optional[Union[int, float]] = None, + position_index: Optional[str] = None, + update_at: Optional[Union[int, float]] = None, + ) -> 'DebankPrediction': + return cls( + prediction_name=prediction_name, + side=side, + amount=to_decimal(amount), + price=to_decimal(price), + usd_value=to_decimal(usd_value), + 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, + position_index=position_index, + update_at=parse_dt(update_at) if update_at is not None else None, + ) + + +@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[dict] # Raw token data for flexibility + 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[dict]] = 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 [], + 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] + + +@attr.s(auto_attribs=True, slots=True, frozen=True) +class DebankApp: + """Represents a Debank App with its deposits and predictions.""" + + app_id: str + name: str + site_url: Optional[str] + logo_url: Optional[str] + has_supported_portfolio: bool + deposits: list[DebankAppDeposit] + predictions: list[DebankPrediction] + + @classmethod + def from_api( + cls, + *, + app_id: str, + name: str, + site_url: Optional[str] = None, + logo_url: Optional[str] = None, + has_supported_portfolio: bool = False, + deposits: Optional[list[DebankAppDeposit]] = None, + predictions: Optional[list[DebankPrediction]] = None, + ) -> 'DebankApp': + return cls( + app_id=app_id, + name=name, + site_url=site_url, + logo_url=logo_url, + has_supported_portfolio=has_supported_portfolio, + deposits=deposits or [], + predictions=predictions or [], + ) + + @attr.s(auto_attribs=True, slots=True) class FetchResult: status_code: Optional[int] = None @@ -1206,7 +1382,7 @@ def from_fetch_results(cls, **kwargs): @attr.s(auto_attribs=True, slots=True, frozen=True) class ParseResult: data: Optional[ - list[Union[BalanceItem, Pool, NftToken, NftCollection, NftOffer]] + list[Union[BalanceItem, Pool, NftToken, NftCollection, NftOffer, DebankApp]] ] = None warnings: Optional[list[Union[str, dict]]] = None errors: Optional[list[Union[str, dict]]] = None