Skip to content
Open
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
118 changes: 79 additions & 39 deletions quantara/web_app/contract_tools/blockchain_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@ async def get_balance(

return "0"

async def _get_account_data(self, holder_address: str) -> dict | None:
"""
Fetch full account data from Horizon.
"""
if not holder_address:
return None
url = f"{self.horizon_url.rstrip('/')}/accounts/{holder_address}"
try:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 404:
logger.info(
"Account %s not found on Stellar network",
holder_address,
)
return None
if response.status != 200:
logger.warning(
"Horizon returned %d for %s", response.status, url
)
return None
return await response.json()
except aiohttp.ClientError as exc:
logger.error("Network error fetching account %s: %s", holder_address, exc)
return None
except (ValueError, KeyError, TypeError) as exc:
logger.error("Data error fetching account %s: %s", holder_address, exc)
return None

async def get_token_balances(
self, holder_address: str
) -> dict[str, str]:
Expand All @@ -116,37 +145,30 @@ async def get_token_balances(
:param holder_address: Stellar account public key.
:return: dict mapping token symbols to balance strings.
"""
balances: dict[str, str] = {}
for token in TokenParams.tokens():
try:
bal = await self.get_balance(
asset_code=token.asset_code,
holder_address=holder_address,
asset_issuer=getattr(token, "asset_issuer", None),
)
balances[token.name] = bal
except (aiohttp.ClientError, ValueError, KeyError) as exc:
logger.info("token_balance_fetch_failed", token=token.name, error=str(exc))
return balances
balances: dict[str, str] = {
token.name: "0" for token in TokenParams.tokens()
}
account = await self._get_account_data(holder_address)
if not account:
return balances

for balance in account.get("balances", []):
asset_type = balance.get("asset_type", "")
asset_code = balance.get("asset_code", "").lower()
asset_issuer = balance.get("asset_issuer", "")

async def _fetch() -> dict[str, str]:
balances: dict[str, str] = {}
for token in TokenParams.tokens():
try:
bal = await self.get_balance(
asset_code=token.asset_code,
holder_address=holder_address,
asset_issuer=getattr(token, "asset_issuer", None),
)
balances[token.name] = bal
except (aiohttp.ClientError, ValueError, KeyError) as exc:
logger.info(
"Failed to get balance for %s: %s", token.name, exc
)
return balances
target_code = token.asset_code.lower()
target_issuer = getattr(token, "asset_issuer", None)

if target_code == "native" or target_code == "xlm":
if asset_type == "native":
balances[token.name] = str(balance.get("balance", "0"))
elif asset_code == target_code:
if target_issuer is None or asset_issuer == target_issuer:
balances[token.name] = str(balance.get("balance", "0"))

cache_key = f"quantara:balances:{holder_address}"
return await get_cached_or_fetch(cache_key, 60, _fetch)
return balances

# ------------------------------------------------------------------ #
# Loop liquidity / repay data stubs (pool-agnostic for Soroban)
Expand Down Expand Up @@ -247,18 +269,36 @@ async def fetch_portfolio(self, contract_address: str) -> dict:
:return: dict mapping token keys to balance info.
"""
results = {}
# Pre-initialize with zero balances
for token in TokenParams.tokens():
try:
balance = await self.get_balance(
asset_code=token.asset_code,
holder_address=contract_address,
)
results[token.name] = {
"balance": balance,
"decimals": token.decimals,
}
except (aiohttp.ClientError, ValueError, KeyError) as exc:
logger.info("portfolio_balance_fetch_failed", token=token.name, error=str(exc))
results[token.name] = {
"balance": "0",
"decimals": token.decimals,
}

account = await self._get_account_data(contract_address)
if not account:
return results

for balance in account.get("balances", []):
asset_type = balance.get("asset_type", "")
asset_code = balance.get("asset_code", "").lower()
asset_issuer = balance.get("asset_issuer", "")

for token in TokenParams.tokens():
target_code = token.asset_code.lower()
target_issuer = getattr(token, "asset_issuer", None)

if target_code == "native" or target_code == "xlm":
if asset_type == "native":
results[token.name]["balance"] = str(
balance.get("balance", "0")
)
elif asset_code == target_code:
if target_issuer is None or asset_issuer == target_issuer:
results[token.name]["balance"] = str(
balance.get("balance", "0")
)
return results


Expand Down