The Alef Portfolio
LEVERAGE
1.3–1.5Γ—

Simulation includes trading fees and interest. Does not include slippage.

Market price: $0.9700 / ALEF β†’ Buy: Raydium Biconomy

Media

Proof of Reserves

The Alef portfolio is 100% on-chain and auditable by anyone in real time. Reserve addresses across all active chains are published below. Proof of ownership of any address can be requested using the form at the bottom of this section.

How to read these addresses: Portfolio assets are deployed into on-chain lending protocols rather than held as raw tokens. On Ethereum, assets are supplied to Aave, which issues position NFTs to the depositor's wallet as proof of entitlement β€” a standard balance check will show these NFTs and a small gas reserve, not the full underlying value. On BNB Chain, assets are supplied to Venus. On Tron, assets are supplied to JustLend, which issues fungible jTokens (TRC-20 receipt tokens) in return β€” the underlying value is held in the protocol, not visible as a raw token balance. In all cases, the actual position value must be read from the relevant lending protocol's interface rather than a block explorer balance view.

You can also audit all assets and all liabilities using this open source program:

import requests
import warnings

# Some older Anaconda environments emit harmless pandas optional-dependency
# warnings while importing yfinance. They do not affect ALEF calculations.
with warnings.catch_warnings():
    warnings.filterwarnings(
        "ignore",
        message=r"Pandas requires version .* of 'numexpr'.*",
        category=UserWarning,
    )
    warnings.filterwarnings(
        "ignore",
        message=r"Pandas requires version .* of 'bottleneck'.*",
        category=UserWarning,
    )
    import yfinance as yf

import sys
import os
import re
import json
import math
import html as html_lib
import base64
import hashlib
from decimal import Decimal, getcontext
from datetime import datetime, timezone
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


# ===============================================================
# ALEF AUDITOR / OPEN-SOURCE BUILD
# ===============================================================
# This build uses the same NAV calculation engine as the operational updater.
# It is designed for auditors and public review:
#   - no Excel/price-string output
#   - no clipboard writes
#   - live non-financial progress messages while network calls run
#   - a human-readable audit report after the calculation completes
#   - exceptions propagate to the caller
#
# Call calculate_alef_nav() to obtain the complete structured calculation.
# Call print_audit_report(result) to render the human-readable report.
# Running the full file/cell performs both automatically.
AUDIT_BUILD = "v18-audit-v5"


def _audit_log(*args, **kwargs):
    """Financial/detail diagnostics stay silent in the public auditor build."""
    return None


def _progress(message):
    """Emit non-financial progress only, immediately flushed for Jupyter/auditors."""
    print(f"[ALEF audit] {message}", file=sys.stderr, flush=True)

getcontext().prec = 50

# ===============================================================
# ALEF CONFIGURATION
# ===============================================================

# Change this whenever you want to change the management fee.
# 0.005 = 0.50% per year.
ANNUAL_FEE_RATE = Decimal("0.005")

# ALEF has NO opening-price anchor or normalization.
# Price is always exactly:
#     fee-adjusted NAV / current ALEF supply
# The state file stores only the continuously accrued management-fee factor.
# Deleting it restarts fee accrual from the next successful run.
# __file__ is unavailable in notebooks/Jupyter, so fall back to the current working directory.
try:
    SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
    SCRIPT_DIR = os.getcwd()

ALEF_STATE_FILE = os.path.join(SCRIPT_DIR, "alef_nav_state.json")
ALEF_MULTIPLIER_CACHE_FILE = os.path.join(SCRIPT_DIR, "alef_xstock_multiplier_cache.json")

ALEF_WEBSITE = "https://alef.money/"
AAVE_V4_GRAPHQL = "https://api.v4.aave.com/graphql"
AAVE_V3_GRAPHQL = "https://api.v3.aave.com/graphql"
MORPHO_GRAPHQL = "https://api.morpho.org/graphql"
JUSTLEND_API = "https://openapi.just.network"
VENUS_API = "https://api.venus.io"
FLUID_API = "https://api.fluid.instadapp.io"

# ALEF currently uses one known Venus/Fluid Flux vault position on BNB Chain.
# Query that position directly instead of scanning every Venus Core market.
VENUS_FLUX_VAULT_NFT_ID = "800"

BNB_RPC = "https://bsc-dataseed.binance.org/"
SOLANA_RPC = "https://api.mainnet-beta.solana.com"
ETHEREUM_RPC = "https://ethereum-rpc.publicnode.com"
TRONGRID_API = "https://api.trongrid.io"
TONCENTER_API = "https://toncenter.com/api/v2"

# Optional keys. The script does not require these for the known ALEF holdings,
# but if supplied they improve arbitrary-token discovery/pricing.
ETHERSCAN_API_KEY = os.getenv("ETHERSCAN_API_KEY") or os.getenv("BSCSCAN_API_KEY")
JUPITER_API_KEY = os.getenv("JUPITER_API_KEY")

# Known Solana reserve assets whose mint->price identity can be verified
# independently of legacy contract-price endpoints. Add entries here if ALEF
# intentionally acquires another tokenized security that lacks a DEX quote.
SOLANA_KNOWN_ASSETS = {
    "Xst6eFD4YT6sz9RLMysN9SyvaZWtraSdVJQGu5ZkAme": {
        "symbol": "PPLTx",
        "coingecko_id": "abrdn-physical-platinum-shares-xstock",
        "yahoo_ticker": "PPLT",
    },
}
SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"

# Portfolio-level security filter. Wallet scanners enumerate holdings first;
# NAV includes only securities that map to the portfolio described on alef.money:
# QQQ + Magnificent Seven, plus precious-metal exposures. USDC is the liquidity reserve.
PORTFOLIO_SECURITY_TICKERS = {
    # Broad / leveraged technology exposures used by ALEF.
    "QQQ", "TQQQ", "MAGX",
    # Magnificent Seven.
    "AAPL", "MSFT", "AMZN", "NVDA", "META", "TSLA", "GOOG", "GOOGL",
    # Precious-metal ETF/proxy tickers used by tokenized-security issuers.
    "GLD", "IAU", "UGL", "SLV", "PPLT", "PLTM", "3GOL", "3GOL.L",
}
PORTFOLIO_CASH_SYMBOLS = {"USDC"}


# -----------------------------------------------------------------
# NAV PRICE SOURCE
# -----------------------------------------------------------------
# IMPORTANT: blockchain / issuer APIs are used to discover WHAT ALEF owns and
# HOW MANY economic units it owns. USD marks for portfolio securities come
# from the same Yahoo Finance function used by the original Excel updater;
# crypto marks come from the same CoinGecko/yfinance function above.
#
# Issuer/on-chain multiplier data is NOT a price. On Solana and TON it is only
# used to convert raw xStock units into share-equivalent units after dividends
# and splits. EVM xStock balanceOf() is already economically adjusted.
_NAV_STOCK_PRICE_CACHE = {}
_NAV_CRYPTO_PRICE_CACHE = {}

PORTFOLIO_CRYPTO_SYMBOLS = {
    "BTC", "ETH", "BNB", "TRX", "XAUT", "PAXG",
}
PORTFOLIO_STABLE_SYMBOLS = {"USDC", "USDT", "USDON", "RUSDY", "USDY"}
PORTFOLIO_SYMBOL_ALIASES = {
    "WBTC": "BTC", "BTCB": "BTC",
    "WETH": "ETH", "WBNB": "BNB",
    "USDC.E": "USDC", "USDCET": "USDC",
}

# Optional per-token quantity fallbacks. Keep empty unless a multiplier has been
# independently verified. Price is never stored here.
XSTOCK_KNOWN_MULTIPLIER_FALLBACKS = {}


def _seed_nav_price_caches(crypto_symbols, crypto_prices, stock_tickers, stock_prices):
    _NAV_CRYPTO_PRICE_CACHE.clear()
    _NAV_STOCK_PRICE_CACHE.clear()
    for sym, p in zip(crypto_symbols, crypto_prices):
        if sym and p is not None:
            _NAV_CRYPTO_PRICE_CACHE[str(sym).upper()] = D(p)
    for ticker, p in zip(stock_tickers, stock_prices):
        if ticker and p is not None:
            _NAV_STOCK_PRICE_CACHE[str(ticker).upper()] = D(p)


def _nav_stock_price_usd(ticker):
    """Yahoo Finance only. No blockchain/DEX/issuer security price marks."""
    t = str(ticker or "").upper().strip().replace(".", "-")
    if not t:
        return None
    cached = _NAV_STOCK_PRICE_CACHE.get(t)
    if cached is not None and cached > 0:
        return cached
    try:
        p = fetch_stock_prices([t])[0]
        if p is not None and p > 0:
            d = D(p)
            _NAV_STOCK_PRICE_CACHE[t] = d
            return d
    except Exception:
        pass
    # GOOG/GOOGL are distinct share classes but are close substitutes for a
    # price fallback if Yahoo temporarily fails on one symbol.
    if t == "GOOGL":
        return _nav_stock_price_usd("GOOG")
    return None


def _nav_crypto_price_usd(symbol):
    """Original CoinGecko -> yfinance crypto price path only."""
    s = str(symbol or "").upper().strip()
    s = PORTFOLIO_SYMBOL_ALIASES.get(s, s)
    if s in PORTFOLIO_STABLE_SYMBOLS:
        return Decimal("1")
    cached = _NAV_CRYPTO_PRICE_CACHE.get(s)
    if cached is not None and cached > 0:
        return cached
    try:
        p = fetch_crypto_prices([s])[0]
        if p is not None and p > 0:
            d = D(p)
            _NAV_CRYPTO_PRICE_CACHE[s] = d
            return d
    except Exception:
        pass
    return None


def _portfolio_raw_token_price_usd(symbol):
    """Price only raw tokens that belong to the declared portfolio/cash set."""
    s = str(symbol or "").upper().strip()
    s = PORTFOLIO_SYMBOL_ALIASES.get(s, s)
    if s in PORTFOLIO_STABLE_SYMBOLS:
        return Decimal("1")
    if s in PORTFOLIO_CRYPTO_SYMBOLS:
        return _nav_crypto_price_usd(s)
    return None


def _load_multiplier_cache():
    try:
        with open(ALEF_MULTIPLIER_CACHE_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return data if isinstance(data, dict) else {}
    except Exception:
        return {}


def _save_multiplier_cache(data):
    try:
        tmp = ALEF_MULTIPLIER_CACHE_FILE + '.tmp'
        with open(tmp, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, sort_keys=True)
        os.replace(tmp, ALEF_MULTIPLIER_CACHE_FILE)
    except Exception as e:
        _audit_log(f"[WARN] Could not save xStock multiplier cache: {e}", file=sys.stderr)


def _cached_xstock_multiplier(symbol, network):
    key = f"{str(network or '').upper()}:{str(symbol or '').upper()}"
    row = _load_multiplier_cache().get(key)
    if isinstance(row, dict):
        try:
            m = D(row.get('multiplier'))
            return m if m > 0 else None
        except Exception:
            return None
    return None


def _remember_xstock_multiplier(symbol, network, multiplier, source):
    try:
        m = D(multiplier)
        if m <= 0:
            return
    except Exception:
        return
    key = f"{str(network or '').upper()}:{str(symbol or '').upper()}"
    data = _load_multiplier_cache()
    data[key] = {
        'multiplier': str(m),
        'source': str(source),
        'updated_at_utc': datetime.now(timezone.utc).isoformat(),
    }
    _save_multiplier_cache(data)


def _resolve_xstock_multiplier(symbol, network, *, onchain=None, registry_meta=None):
    """
    Quantity conversion only; never a USD price source.
    Order: on-chain -> registry -> issuer multiplier API -> last-known cache ->
    verified per-token fallback -> 1.0 (non-fatal, loudly diagnosed).
    """
    symbol = str(symbol or '')
    network = str(network or '').upper()
    if onchain is not None:
        try:
            m = D(onchain)
            if m > 0:
                _remember_xstock_multiplier(symbol, network, m, 'onchain')
                return m, 'onchain'
        except Exception:
            pass
    if isinstance(registry_meta, dict):
        try:
            m = D(registry_meta.get('multiplier'))
            if m > 0:
                _remember_xstock_multiplier(symbol, network, m, 'issuer-registry')
                return m, 'issuer-registry'
        except Exception:
            pass
    m = _xstocks_official_multiplier(symbol, network=network)
    if m is not None and m > 0:
        _remember_xstock_multiplier(symbol, network, m, 'issuer-api')
        return m, 'issuer-api'
    m = _cached_xstock_multiplier(symbol, network)
    if m is not None and m > 0:
        return m, 'last-known-cache'
    m = XSTOCK_KNOWN_MULTIPLIER_FALLBACKS.get((network, symbol.upper()))
    if m is not None and m > 0:
        return m, 'known-fallback'
    _audit_log(
        f"[WARN] {network} multiplier unavailable for {symbol}; using 1.0 provisionally. "
        "USD price still comes from Yahoo Finance.", file=sys.stderr,
    )
    return Decimal('1'), 'provisional-1x'

def _portfolio_security_allowed(ticker=None, symbol=None):
    t = str(ticker or "").upper().strip()
    s = str(symbol or "").upper().strip()
    # Provider suffixes: xStocks -> x, Ondo -> on, bStocks -> B.
    if not t and s.endswith("X"):
        t = s[:-1]
    if not t and s.endswith("ON"):
        t = s[:-2]
    if not t and s.endswith("B"):
        t = s[:-1]
    return t in PORTFOLIO_SECURITY_TICKERS or s in PORTFOLIO_CASH_SYMBOLS


def _portfolio_registry_subset(registry, provider):
    """Limit direct issuer contract probes to assets that can enter ALEF NAV.

    Generic wallet/indexer scans still enumerate every token they can see.
    This optimization applies only to direct provider contract probing.
    """
    subset = {}
    for addr, meta in (registry or {}).items():
        meta = meta or {}
        symbol = str(meta.get("symbol") or "")
        ticker = str(meta.get("ticker") or meta.get("underlyingSymbol") or "")
        kind = str(meta.get("kind") or "")
        if provider == "xstocks" and not ticker and symbol.upper().endswith("X"):
            ticker = symbol[:-1]
        elif provider == "bstocks" and not ticker and symbol.upper().endswith("B"):
            ticker = symbol[:-1]
        elif provider == "ondo" and kind == "ondo-stock" and not ticker and symbol.upper().endswith("ON"):
            ticker = symbol[:-2]
        if provider in ("xstocks", "bstocks") or kind == "ondo-stock":
            keep = _portfolio_security_allowed(ticker=ticker, symbol=symbol)
        else:
            keep = symbol.upper() in PORTFOLIO_CASH_SYMBOLS
        if keep:
            subset[addr] = meta
    return subset

# Only explicitly trusted raw TRON assets are included in NAV. Public wallets
# commonly receive unsolicited TRC-20 dust/spam; valuing every token returned
# by TronGrid can let an attacker inflate reported NAV. Add an intentional
# holding here if ALEF later holds another raw TRC-20 asset.
TRON_TRUSTED_ASSETS = {
    "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t": {"symbol": "USDT", "coingecko_id": "tether"},
    "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8": {"symbol": "USDC", "coingecko_id": "usd-coin"},
    "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9": {"symbol": "BTC", "coingecko_id": "bitcoin"},
}

# Venus's official BNB-chain toolkit exposes Flux/Fluid positions separately
# from ordinary Venus Core/isolated-pool vTokens. These are the currently
# published BNB Flux fTokens.
VENUS_FLUX_FTOKENS = [
    {"symbol": "fU", "address": "0x007df53Cda786450Cf8145a73B2748B241a0069c"},
    {"symbol": "fUSDC", "address": "0xfE60462E93cee34319F48Cfc6AcFbc13c2882Df9"},
    {"symbol": "fUSDT", "address": "0xA5b8FCa32E5252B0B58EAbf1A8c79d958F8EE6A2"},
    {"symbol": "fWBNB", "address": "0x527C2a0B8A3eDD9696B4A9443ef66Ec30fD7B84a"},
]
# Official Venus Agent Skills / Fluid deployment resolver for BNB Flux positions.
# Unlike ERC-20 balanceOf(), this resolver returns the user's actual underlyingAssets.
VENUS_FLUX_LENDING_RESOLVER = "0x48D32f49aFeAEC7AE66ad7B9264f446fc11a1569"
WBNB_BSC = "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"

# Current plus legacy ALEF mints must never be treated as portfolio backing.
# 9B3... was an older ALEF mint that can still be present in the reserve wallet.
ALEF_LEGACY_MINTS = {
    "9B3rXYVdL2iUxGnY54TKhR9NroqGB1RBAivVQWb26Zqt",
}
METAPLEX_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"


# Public indexers used only to enumerate raw wallet ERC-20 balances.
# Protocol receipt/debt tokens are excluded to avoid double-counting.
EVM_TOKEN_INDEXERS = {
    "ethereum": [
        "https://eth.blockscout.com",
    ],
    "bsc": [],
}

SOLANA_TOKEN_PROGRAMS = [
    "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
    "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",  # Token-2022
]

# Fee-state schema/version. v18 keeps pure NAV/supply pricing, reads the known
# Venus/Fluid vault NFT directly, and derives the USDC reserve from the live Raydium pool.
ALEF_NAV_ENGINE_VERSION = 16

# Aave V3 Ethereum main market / Pool.
# The ALEF reserve EVM wallet is discovered from alef.money.
AAVE_ETHEREUM_MARKETS = [
    {"address": "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2", "chainId": 1}
]

# Optional adjustment for assets/liabilities that are part of NAV but
# are NOT held in Aave, Morpho, JustLend or Venus (for example an LP,
# exchange balance, TON position, or other reserve not yet automated).
MANUAL_ASSETS_USD = Decimal("0")
MANUAL_LIABILITIES_USD = Decimal("0")

# USDC liquidity reserve β€” read live from the ALEF/USDC Raydium pool on Solana.
# The operator maintains the pool balance close to the committed reserve level;
# whatever USDC is in the pool at query time is used directly as the NAV input.
# No manual number is asserted β€” the value is fully on-chain verifiable.

# Date from which the current fee period is accruing.
# Updated automatically by settle_fees() each time fees are collected.
FEE_ACCRUAL_START = datetime(2026, 6, 17, 15, 53, 5, tzinfo=timezone.utc)  # ALEF deployed on Solana

RAYDIUM_MANUAL_FALLBACK_USD = Decimal("0")
RAYDIUM_CLMM_PROGRAM_ID = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"

# Fail closed: do not publish an ALEF price from a partial NAV if one
# of the required protocol reads fails.
ALEF_STRICT_MODE = True

# Current official values from alef.money, used only if scraping the
# site fails or the page layout changes.
ALEF_ADDRESS_FALLBACKS = {
    "evm": "0x34292E356Def567bA65a2351ec1DD2b738ec4A57",
    "tron": "TFMDSKrH2FJx3pdGACb5oHoKPJRzaH8dKv",
    "ton": "UQDd7e4W8l4W0UMMwT--rqOjcd9-O8mLPYYZDoc4uFsS7YyY",
    "solana": "9JRZdE4rcSUQRDRUTqNrqsFtxm7caT4GfyiNxFJqBHUe",
    "alef_mint": "FBHd9upXFkeWSwe9qEdcgRLa4Y6uzsLCSawrpfPkQZGg",
}


# ===============================================================
# HTTP helpers
# ===============================================================

def _http_session(max_retries=3, backoff_factor=1.0) -> requests.Session:
    session = requests.Session()
    retry = Retry(
        total=max_retries,
        read=max_retries,
        connect=max_retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "POST"],
        respect_retry_after_header=True,
    )
    adapter = HTTPAdapter(max_retries=retry)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    session.headers.update({"User-Agent": "Alef-NAV-Updater/1.0"})
    return session


def _coin_gecko_session(max_retries=3, backoff_factor=1.0) -> requests.Session:
    return _http_session(max_retries=max_retries, backoff_factor=backoff_factor)


def _json_get(url, *, params=None, timeout=20, headers=None):
    s = _http_session()
    r = s.get(url, params=params, timeout=timeout, headers=headers)
    r.raise_for_status()
    return r.json()


def _json_post(url, payload, *, timeout=25, headers=None):
    s = _http_session()
    h = {"Content-Type": "application/json"}
    if headers:
        h.update(headers)
    r = s.post(url, json=payload, timeout=timeout, headers=h)
    r.raise_for_status()
    data = r.json()
    if isinstance(data, dict) and data.get("errors"):
        raise RuntimeError(f"GraphQL error from {url}: {data['errors']}")
    return data


def D(x, default="0") -> Decimal:
    if x is None or x == "":
        return Decimal(default)
    return Decimal(str(x))


# ===============================================================
# Crypto via CoinGecko (with yfinance fallback)
# ===============================================================

def fetch_crypto_prices(symbols, timeout=20):
    """
    Try CoinGecko first; fall back to yfinance (BTC-USD style) on failure.
    Returns a list of floats in the same order as `symbols`.
    """
    cg_map_primary = {
        'BTC': 'bitcoin', 'ETH': 'ethereum', 'BNB': 'binancecoin',
        'XRP': 'ripple', 'DOGE': 'dogecoin', 'ADA': 'cardano',
        'POL': 'polygon-ecosystem-token', 'TRX': 'tron',
        'XAUT': 'tether-gold', 'SOL': 'solana',
        'LEO': 'leo-token', 'PAXG': 'pax-gold', 'GRAM': 'gram'
    }
    cg_map_fallback = {'POL': 'matic-network'}

    yf_map = {
        'BTC': 'BTC-USD', 'ETH': 'ETH-USD', 'BNB': 'BNB-USD',
        'XRP': 'XRP-USD', 'DOGE': 'DOGE-USD', 'ADA': 'ADA-USD',
        'POL': 'POL-USD', 'TRX': 'TRX-USD', 'SOL': 'SOL-USD',
        'XAUT': 'XAUT-USD', 'LEO': 'LEO-USD', 'PAXG': 'PAXG-USD'
    }

    ids_to_query = set(cg_map_primary.values()) | set(cg_map_fallback.values())
    url = (
        "https://api.coingecko.com/api/v3/simple/price"
        f"?ids={','.join(sorted(ids_to_query))}&vs_currencies=usd"
    )
    session = _coin_gecko_session()
    cg_data = None
    try:
        resp = session.get(url, timeout=timeout)
        resp.raise_for_status()
        cg_data = resp.json()
    except Exception as e:
        _audit_log(f"[WARN] CoinGecko unavailable, falling back to yfinance: {e}", file=sys.stderr)

    prices = []
    for sym in symbols:
        if not sym:
            prices.append(None)
            continue

        val = None
        if cg_data:
            coin_id = cg_map_primary.get(sym)
            val = cg_data.get(coin_id, {}).get('usd') if coin_id else None
            if val is None and sym in cg_map_fallback:
                val = cg_data.get(cg_map_fallback[sym], {}).get('usd')

        if val is None and sym in yf_map:
            try:
                tk = yf.Ticker(yf_map[sym])
                fi = getattr(tk, "fast_info", None)
                if fi and fi.get("last_price") is not None:
                    val = float(fi["last_price"])
                else:
                    hist = tk.history(period="1d", interval="1d")
                    if not hist.empty:
                        val = float(hist["Close"].iloc[-1])
            except Exception as e:
                _audit_log(f"[WARN] yfinance fallback failed for {sym}: {e}", file=sys.stderr)

        prices.append(float(val) if val is not None else None)

    return prices


# ===============================================================
# Stocks via Yahoo Finance
# ===============================================================

def fetch_stock_prices(tickers):
    """
    Return a list of floats (last price) in same order as `tickers`,
    using yfinance fast_info with a safe fallback.
    """
    out = []
    for t in tickers:
        if not t:
            out.append(None)
            continue

        price = None
        try:
            tk = yf.Ticker(t)
            fi = getattr(tk, "fast_info", None)
            if fi and "last_price" in fi and fi["last_price"] is not None:
                price = float(fi["last_price"])
            else:
                hist = tk.history(period="1d", interval="1d")
                if not hist.empty and "Close" in hist.columns:
                    price = float(hist["Close"].iloc[-1])
        except Exception:
            price = None
        out.append(price)
    return out


# ===============================================================
# alef.money address discovery
# ===============================================================

def _extract_after_label(text, label, pattern, span=1200):
    i = text.lower().find(label.lower())
    if i < 0:
        return None
    m = re.search(pattern, text[i:i + span])
    return m.group(0) if m else None


def fetch_alef_addresses(timeout=20):
    """
    Read the published reserve addresses and ALEF mint from alef.money.
    Falls back to the currently published values if the page cannot be parsed.
    """
    found = {}
    try:
        r = _http_session().get(ALEF_WEBSITE, timeout=timeout)
        r.raise_for_status()
        raw = html_lib.unescape(r.text)
        plain = re.sub(r"<[^>]+>", " ", raw)
        plain = re.sub(r"\s+", " ", plain)

        found["evm"] = _extract_after_label(
            plain,
            "Reserve Address (Ethereum & BNB Chain)",
            r"0x[a-fA-F0-9]{40}",
        )
        found["tron"] = _extract_after_label(
            plain,
            "Reserve Address (Tron)",
            r"T[1-9A-HJ-NP-Za-km-z]{33}",
        )
        found["ton"] = _extract_after_label(
            plain,
            "Reserve Address (TON)",
            r"(?:EQ|UQ)[A-Za-z0-9_-]{46}",
        )
        found["solana"] = _extract_after_label(
            plain,
            "Reserve Address (Solana)",
            r"[1-9A-HJ-NP-Za-km-z]{32,44}",
        )
        found["alef_mint"] = _extract_after_label(
            plain,
            "Token Mint",
            r"[1-9A-HJ-NP-Za-km-z]{32,44}",
        )
    except Exception as e:
        _audit_log(f"[WARN] Could not read addresses from alef.money: {e}", file=sys.stderr)

    for key, fallback in ALEF_ADDRESS_FALLBACKS.items():
        if not found.get(key):
            found[key] = fallback
            _audit_log(f"[WARN] Using fallback {key} address: {fallback}", file=sys.stderr)

    return found


# ===============================================================
# Aave (V4 positions first; V3 legacy fallback)
# ===============================================================

def _graphql_data(url, query):
    payload = _json_post(url, {"query": query}, timeout=25)
    if payload.get("errors"):
        msgs = "; ".join(str(x.get("message", x)) for x in payload["errors"])
        raise RuntimeError(f"GraphQL error from {url}: {msgs}")
    return payload.get("data") or {}


def _aave_exchange_value(node):
    """Extract the quoted value from Aave ExchangeAmount/WithChange shapes."""
    node = node or {}
    # WithChange is normally {current:{value,symbol}, ...}.  Keep a few
    # shape fallbacks so minor schema presentation changes do not zero NAV.
    for key in ("current", "amount"):
        if isinstance(node, dict) and isinstance(node.get(key), dict):
            v = _aave_exchange_value(node[key])
            if v is not None:
                return v
    if isinstance(node, dict) and node.get("value") is not None:
        return D(node.get("value"))
    return None


def _fetch_aave_v4(wallet):
    # The V4 user summary is the least fragile accounting endpoint: Aave
    # exposes totalPositions, totalSupplied and totalDebt directly. Alef's
    # published reserve description specifies that its Aave positions are on
    # Ethereum, so the reserve-address V4 summary is the desired Aave book.
    summary_query = f"""
    query AlefAaveSummary {{
      userSummary(request: {{ user: "{wallet}" }}) {{
        totalPositions
        totalSupplied {{ value symbol }}
        totalDebt {{ value symbol }}
      }}
    }}
    """
    summary_error = None
    try:
        data = _graphql_data(AAVE_V4_GRAPHQL, summary_query)
        summary = data.get("userSummary")
        if summary:
            assets = _aave_exchange_value(summary.get("totalSupplied")) or Decimal("0")
            liabilities = _aave_exchange_value(summary.get("totalDebt")) or Decimal("0")
            return {
                "protocol": "Aave",
                "version": "v4",
                "position_count": int(summary.get("totalPositions") or 0),
                "assets_usd": assets,
                "liabilities_usd": liabilities,
                "summary": summary,
                "positions": [],
                "supplies": [],
                "borrows": [],
            }
    except Exception as e:
        summary_error = e

    # Fallback to chain-filtered positions if the summary query/schema changes.
    query = f"""
    query AlefAavePositions {{
      userPositions(request: {{
        user: "{wallet}",
        filter: {{ chainIds: [1] }}
      }}) {{
        id
        totalSupplied {{ current {{ value symbol }} }}
        totalDebt {{ current {{ value symbol }} }}
        spoke {{ name chain {{ chainId name }} }}
      }}
    }}
    """
    try:
        data = _graphql_data(AAVE_V4_GRAPHQL, query)
        positions = data.get("userPositions") or []
        assets = Decimal("0")
        liabilities = Decimal("0")
        for pos in positions:
            supplied = _aave_exchange_value(pos.get("totalSupplied"))
            debt = _aave_exchange_value(pos.get("totalDebt"))
            if supplied is not None:
                assets += supplied
            if debt is not None:
                liabilities += debt
        return {
            "protocol": "Aave",
            "version": "v4",
            "position_count": len(positions),
            "assets_usd": assets,
            "liabilities_usd": liabilities,
            "positions": positions,
            "supplies": [],
            "borrows": [],
        }
    except Exception as e:
        if summary_error:
            raise RuntimeError(f"Aave V4 summary failed ({summary_error}); positions query failed ({e})")
        raise


def _fetch_aave_v3(wallet):
    markets_literal = ",".join(
        f'{{address:"{m["address"]}",chainId:{m["chainId"]}}}'
        for m in AAVE_ETHEREUM_MARKETS
    )
    query = f"""
    {{
      userSupplies(request: {{
        markets: [{markets_literal}], user: "{wallet}"
      }}) {{ currency {{ symbol }} balance {{ amount {{ value }} usd }} }}
      userBorrows(request: {{
        markets: [{markets_literal}], user: "{wallet}"
      }}) {{ currency {{ symbol }} debt {{ amount {{ value }} usd }} }}
    }}
    """
    data = _graphql_data(AAVE_V3_GRAPHQL, query)
    supplies = data.get("userSupplies") or []
    borrows = data.get("userBorrows") or []
    assets = sum((D((x.get("balance") or {}).get("usd")) for x in supplies), Decimal("0"))
    liabilities = sum((D((x.get("debt") or {}).get("usd")) for x in borrows), Decimal("0"))
    return {
        "protocol": "Aave",
        "version": "v3",
        "position_count": int(bool(supplies or borrows)),
        "assets_usd": assets,
        "liabilities_usd": liabilities,
        "supplies": supplies,
        "borrows": borrows,
    }


def fetch_aave_position_usd(wallet):
    """
    Read Aave V4 positions first.  If V4 is unavailable or genuinely has no
    positions, check legacy V3.  We deliberately do not add V3 and V4 totals
    together automatically, which protects NAV from accidental duplication
    during migrations.
    """
    v4_error = None
    try:
        v4 = _fetch_aave_v4(wallet)
        if v4["position_count"] > 0 or v4["assets_usd"] or v4["liabilities_usd"]:
            return v4
    except Exception as e:
        v4_error = e
        _audit_log(f"[WARN] Aave V4 read failed; trying V3 fallback: {e}", file=sys.stderr)

    try:
        v3 = _fetch_aave_v3(wallet)
        if v3["assets_usd"] or v3["liabilities_usd"]:
            return v3
        if v4_error:
            raise RuntimeError(f"Aave V4 failed ({v4_error}); V3 returned no position")
        # V4 succeeded with zero and V3 is also zero.
        return v3
    except Exception:
        if v4_error:
            raise
        raise


# ===============================================================
# Morpho (Ethereum)
# ===============================================================

def fetch_morpho_position_usd(wallet):
    query = f"""
    {{
      userByAddress(chainId: 1, address: "{wallet}") {{
        marketPositions {{
          market {{ marketId }}
          state {{
            supplyAssetsUsd
            borrowAssetsUsd
            collateralUsd
          }}
        }}
        vaultPositions {{
          vault {{ address name }}
          state {{ assetsUsd }}
        }}
        vaultV2Positions {{
          vault {{ address name }}
          assetsUsd
        }}
      }}
    }}
    """

    data = _json_post(MORPHO_GRAPHQL, {"query": query}).get("data", {})
    user = data.get("userByAddress")
    if not user:
        return {
            "protocol": "Morpho",
            "assets_usd": Decimal("0"),
            "liabilities_usd": Decimal("0"),
            "market_positions": [],
            "vault_positions": [],
            "vault_v2_positions": [],
        }

    assets = Decimal("0")
    liabilities = Decimal("0")

    markets = user.get("marketPositions") or []
    for p in markets:
        st = p.get("state") or {}
        assets += D(st.get("supplyAssetsUsd"))
        assets += D(st.get("collateralUsd"))
        liabilities += D(st.get("borrowAssetsUsd"))

    vaults = user.get("vaultPositions") or []
    for p in vaults:
        assets += D((p.get("state") or {}).get("assetsUsd"))

    vaults_v2 = user.get("vaultV2Positions") or []
    for p in vaults_v2:
        assets += D(p.get("assetsUsd"))

    return {
        "protocol": "Morpho",
        "assets_usd": assets,
        "liabilities_usd": liabilities,
        "market_positions": markets,
        "vault_positions": vaults,
        "vault_v2_positions": vaults_v2,
    }


# ===============================================================
# JustLend V1 + V2 (TRON)
# ===============================================================

def fetch_justlend_position_usd(wallet, trx_usd):
    trx_usd = D(trx_usd)
    if trx_usd <= 0:
        raise RuntimeError("TRX/USD price is required for JustLend V1 valuation")

    markets_resp = _json_get(f"{JUSTLEND_API}/lend/jtoken")
    if markets_resp.get("code") != 0:
        raise RuntimeError(f"JustLend market API error: {markets_resp}")
    markets = (markets_resp.get("data") or {}).get("tokenList") or []
    market_by_jtoken = {m.get("address"): m for m in markets if m.get("address")}

    account_resp = _json_get(
        f"{JUSTLEND_API}/lend/account",
        params={"addresses": wallet, "pageNo": 1, "pageSize": 100},
    )
    if account_resp.get("code") != 0:
        raise RuntimeError(f"JustLend account API error: {account_resp}")

    account_list = (account_resp.get("data") or {}).get("list") or []
    v1_assets = Decimal("0")
    v1_liabilities = Decimal("0")
    v1_positions = []

    for acct in account_list:
        if (acct.get("address") or "").lower() != wallet.lower():
            continue
        for p in acct.get("tokens") or []:
            market = market_by_jtoken.get(p.get("address"))
            if not market:
                raise RuntimeError(f"Missing JustLend market metadata for {p.get('address')}")

            underlying_price_trx = D(market.get("underlyingPriceInTrx"))
            price_usd = underlying_price_trx * trx_usd
            supplied = D(p.get("supplyBalanceUnderlying"))
            borrowed = D(p.get("borrowBalanceUnderlying"))
            supply_usd = supplied * price_usd
            borrow_usd = borrowed * price_usd

            v1_assets += supply_usd
            v1_liabilities += borrow_usd
            v1_positions.append({
                "symbol": p.get("underlyingSymbol"),
                "supply_usd": str(supply_usd),
                "borrow_usd": str(borrow_usd),
            })

    # JustLend V2 / Moolah is a separate architecture, so it is additive.
    v2_resp = _json_get(
        f"{JUSTLEND_API}/v2/index/position",
        params={"address": wallet},
    )
    if v2_resp.get("code") != 200:
        raise RuntimeError(f"JustLend V2 position API error: {v2_resp}")

    v2 = v2_resp.get("data") or {}
    v2_assets = D(v2.get("totalCollateralUsd")) + D(v2.get("totalSupplyUsd"))
    v2_liabilities = D(v2.get("totalBorrowUsd"))

    return {
        "protocol": "JustLend",
        "assets_usd": v1_assets + v2_assets,
        "liabilities_usd": v1_liabilities + v2_liabilities,
        "v1_assets_usd": v1_assets,
        "v1_liabilities_usd": v1_liabilities,
        "v2_assets_usd": v2_assets,
        "v2_liabilities_usd": v2_liabilities,
        "v1_positions": v1_positions,
        "v2_position": v2,
        "jtoken_addresses": list(market_by_jtoken.keys()),
    }


# ===============================================================
# Venus (BNB Chain) -- official market API + on-chain snapshots
# ===============================================================

SEL_GET_ACCOUNT_SNAPSHOT = "c37f68e2"  # getAccountSnapshot(address)
SEL_MINTED_VAI = "2bc7e29e"           # mintedVAIs(address)


def _pad_evm_addr(addr):
    return addr.lower().replace("0x", "").rjust(64, "0")


def _decode_uint256_words(hexdata):
    raw = hexdata[2:] if hexdata.startswith("0x") else hexdata
    if len(raw) % 64 != 0:
        return []
    return [int(raw[i:i + 64], 16) for i in range(0, len(raw), 64)]


def _evm_rpc_call(rpc_url, to, data):
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "eth_call",
        "params": [{"to": to, "data": data}, "latest"],
    }
    out = _json_post(rpc_url, payload, timeout=15)
    if out.get("error"):
        raise RuntimeError(out["error"])
    return out.get("result", "0x")


def _venus_all_markets():
    """Fetch every listed BNB-chain Venus Core/isolated market with pagination."""
    markets = []
    page = 0
    limit = 100
    seen = set()
    while True:
        resp = _json_get(
            f"{VENUS_API}/markets",
            params={"chainId": 56, "limit": limit, "page": page},
            headers={"accept-version": "stable"},
        )
        rows = resp.get("result") or []
        if not isinstance(rows, list):
            raise RuntimeError(f"Unexpected Venus market API response: {resp}")
        for m in rows:
            addr = (m.get("address") or "").lower()
            if addr and addr not in seen:
                seen.add(addr)
                markets.append(m)
        total = int(D(resp.get("total"), str(len(markets))))
        if not rows or len(markets) >= total:
            break
        page += 1
        if page > 25:
            raise RuntimeError("Venus market pagination exceeded safety limit")
    return markets


def _evm_uint_call(rpc_url, contract, selector, arg_hex=""):
    ret = _evm_rpc_call(rpc_url, contract, "0x" + selector + arg_hex)
    words = _decode_uint256_words(ret)
    if not words:
        raise RuntimeError(f"Empty uint return from {contract} selector {selector}")
    return words[0]


def _evm_address_call(rpc_url, contract, selector):
    ret = _evm_rpc_call(rpc_url, contract, "0x" + selector)
    raw = ret[2:] if ret.startswith("0x") else ret
    if len(raw) < 64:
        raise RuntimeError(f"Invalid address return from {contract}")
    return "0x" + raw[-40:]


def _evm_rpc_method(rpc_url, method, params):
    payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
    out = _json_post(rpc_url, payload, timeout=20)
    if out.get("error"):
        raise RuntimeError(out["error"])
    return out.get("result")


def _evm_selector_from_rpc(rpc_url, signature):
    """Ask the EVM node for keccak(signature), avoiding a web3.py dependency."""
    sig_hex = "0x" + signature.encode("utf-8").hex()
    hashed = _evm_rpc_method(rpc_url, "web3_sha3", [sig_hex])
    if not isinstance(hashed, str) or len(hashed) < 10:
        raise RuntimeError(f"web3_sha3 returned invalid result for {signature}: {hashed}")
    return hashed[:10]


def _abi_u256(raw, off):
    if off < 0 or off + 32 > len(raw):
        raise RuntimeError(f"ABI offset outside return data: {off}")
    return int.from_bytes(raw[off:off + 32], "big")


def _abi_address(raw, off):
    if off < 0 or off + 32 > len(raw):
        raise RuntimeError(f"ABI address offset outside return data: {off}")
    return "0x" + raw[off + 12:off + 32].hex()


def _abi_string(raw, tuple_base, rel_off):
    pos = tuple_base + rel_off
    n = _abi_u256(raw, pos)
    start = pos + 32
    end = start + n
    if end > len(raw):
        raise RuntimeError("ABI string exceeds return data")
    return raw[start:end].decode("utf-8", errors="replace").rstrip("\x00")


def _decode_flux_user_positions(hexdata):
    """
    Decode only the fields Alef needs from Fluid LendingResolver.getUserPositions().

    Official return shape:
      ((fTokenDetails...), (fTokenShares, underlyingAssets,
                            underlyingBalance, allowance))[]

    fTokenDetails contains dynamic name/symbol strings, so each array element is
    dynamic. We intentionally decode only tokenAddress, symbol, decimals, asset,
    and the four user-position uints. This keeps the script dependency-free.
    """
    if not hexdata or hexdata == "0x":
        return []
    raw = bytes.fromhex(hexdata[2:] if hexdata.startswith("0x") else hexdata)
    if len(raw) < 64:
        raise RuntimeError("Flux resolver returned too little ABI data")

    array_base = _abi_u256(raw, 0)
    n = _abi_u256(raw, array_base)
    offsets_base = array_base + 32
    if n > 1000:
        raise RuntimeError(f"Implausible Flux position count: {n}")

    rows = []
    for i in range(n):
        # For a dynamic array of dynamic tuples, offsets are relative to the
        # tuple-offset table (i.e. immediately after the array length word).
        elem_rel = _abi_u256(raw, offsets_base + 32 * i)
        elem_base = offsets_base + elem_rel

        # Outer tuple = (dynamic fTokenDetails, static userPosition tuple).
        fdetails_rel = _abi_u256(raw, elem_base)
        fbase = elem_base + fdetails_rel
        shares = _abi_u256(raw, elem_base + 32)
        underlying_assets = _abi_u256(raw, elem_base + 64)
        underlying_balance = _abi_u256(raw, elem_base + 96)
        allowance = _abi_u256(raw, elem_base + 128)

        # fTokenDetails field order from the official Venus Flux ABI:
        # 0 tokenAddress, 1 eip2612Deposits, 2 isNativeUnderlying,
        # 3 name(offset), 4 symbol(offset), 5 decimals, 6 asset, ...
        token_address = _abi_address(raw, fbase)
        symbol_rel = _abi_u256(raw, fbase + 4 * 32)
        symbol = _abi_string(raw, fbase, symbol_rel)
        decimals = _abi_u256(raw, fbase + 5 * 32)
        asset = _abi_address(raw, fbase + 6 * 32)
        is_native = bool(_abi_u256(raw, fbase + 2 * 32))

        rows.append({
            "symbol": symbol,
            "fToken": token_address,
            "underlying": asset,
            "decimals": int(decimals),
            "is_native": is_native,
            "shares_raw": shares,
            "underlying_assets_raw": underlying_assets,
            "underlying_balance_raw": underlying_balance,
            "allowance_raw": allowance,
        })
    return rows


def _fetch_venus_flux_supply_resolver_usd(wallet, bnb_usd=None):
    """Use Flux resolver for quantities; use our normal price functions for USD marks."""
    selector = _evm_selector_from_rpc(BNB_RPC, "getUserPositions(address)")
    data = selector + _pad_evm_addr(wallet)
    ret = _evm_rpc_call(BNB_RPC, VENUS_FLUX_LENDING_RESOLVER, data)
    rows = _decode_flux_user_positions(ret)

    total = Decimal("0")
    positions = []
    for row in rows:
        assets_raw = int(row["underlying_assets_raw"])
        if assets_raw <= 0:
            continue
        amount = Decimal(assets_raw) / (Decimal(10) ** int(row["decimals"]))
        sym = str(row.get("symbol") or "").upper()
        if "WBNB" in sym or sym == "FBNB":
            price = _nav_crypto_price_usd("BNB") or (D(bnb_usd) if bnb_usd is not None else None)
        elif "USDC" in sym:
            price = Decimal("1")
        elif "USDT" in sym or sym == "FU":
            price = Decimal("1")
        else:
            price = None
        if price is None or price <= 0:
            _audit_log(f"[WARN] Flux position {sym} found but no configured Yahoo/CoinGecko price mapping", file=sys.stderr)
            continue
        value = amount * price
        total += value
        positions.append({
            **row, "amount": amount, "source": "flux-resolver",
            "price_usd": str(price), "supply_usd": str(value), "borrow_usd": "0",
            "price_source": "coingecko/yfinance",
        })
    return total, positions, rows

def _fetch_venus_flux_supply_direct_usd(wallet, bnb_usd=None):
    """Legacy Flux quantity fallback; USD marks come from our normal price functions."""
    SEL_BALANCE_OF = "70a08231"
    SEL_CONVERT_TO_ASSETS = "07a2d13a"
    SEL_ASSET = "38d52e0f"
    SEL_DECIMALS = "313ce567"
    total = Decimal("0")
    positions = []
    for ft in VENUS_FLUX_FTOKENS:
        ftoken = ft["address"]
        try:
            shares = _evm_uint_call(BNB_RPC, ftoken, SEL_BALANCE_OF, _pad_evm_addr(wallet))
            if shares == 0:
                continue
            assets_raw = _evm_uint_call(BNB_RPC, ftoken, SEL_CONVERT_TO_ASSETS, hex(shares)[2:].rjust(64, "0"))
            underlying = _evm_address_call(BNB_RPC, ftoken, SEL_ASSET)
            decimals = _evm_uint_call(BNB_RPC, underlying, SEL_DECIMALS)
            amount = Decimal(assets_raw) / (Decimal(10) ** int(decimals))
        except Exception:
            continue
        sym = str(ft["symbol"] or "").upper()
        if "WBNB" in sym:
            price = _nav_crypto_price_usd("BNB") or (D(bnb_usd) if bnb_usd is not None else None)
        elif "USDC" in sym or "USDT" in sym or sym == "FU":
            price = Decimal("1")
        else:
            price = None
        if price is None or price <= 0:
            continue
        value = amount * price
        total += value
        positions.append({
            "source": "flux-direct-fallback", "symbol": ft["symbol"], "fToken": ftoken,
            "underlying": underlying, "amount": amount, "price_usd": str(price),
            "supply_usd": str(value), "borrow_usd": "0", "price_source": "coingecko/yfinance",
        })
    return total, positions

def _fetch_venus_flux_supply_usd(wallet, bnb_usd=None):
    """Resolver-first Flux reader, with direct-fToken fallback only on resolver failure."""
    try:
        total, positions, all_rows = _fetch_venus_flux_supply_resolver_usd(wallet, bnb_usd=bnb_usd)
        raw_nonzero = sum(1 for r in all_rows if int(r.get("underlying_assets_raw", 0)) > 0)
        return total, positions, "resolver", len(all_rows), raw_nonzero
    except Exception as e:
        _audit_log(f"[WARN] Venus Flux resolver failed; trying direct fToken fallback: {e}", file=sys.stderr)
        total, positions = _fetch_venus_flux_supply_direct_usd(wallet, bnb_usd=bnb_usd)
        return total, positions, "direct-fallback", len(VENUS_FLUX_FTOKENS), len(positions)


ZERO_EVM_ADDRESSES = {
    "0x0000000000000000000000000000000000000000",
    "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
}


def _fluid_primary_asset(pair):
    """Return token0 plus whether a token1 leg is present."""
    pair = pair or {}
    token0 = pair.get("token0") or {}
    token1 = pair.get("token1") or {}
    a1 = (token1.get("address") or "").lower()
    has_token1 = bool(a1 and a1 not in ZERO_EVM_ADDRESSES)
    return token0, has_token1


def _fluid_raw_asset_value(raw_amount, token):
    """Value a Fluid amount from our CoinGecko/yfinance/cash price source."""
    if not token:
        return Decimal("0"), Decimal("0"), "?"
    decimals = int(D(token.get("decimals"), "18"))
    amount = D(raw_amount) / (Decimal(10) ** decimals)
    symbol = token.get("symbol") or token.get("name") or "?"
    price = _portfolio_raw_token_price_usd(symbol)
    if price is None or price <= 0:
        raise RuntimeError(f"No configured NAV price for Fluid token {symbol}")
    return amount, amount * price, symbol

def _fetch_venus_flux_vault_nfts_usd(wallet):
    """
    Read Fluid/Venus Flux borrow-vault NFTs on BNB Chain.

    The lending resolver used above only reports fToken lending. Borrow/multiply
    positions are ERC-721 vault positions and are exposed separately by Fluid's
    official /v2/{chainId}/users/{wallet}/nfts API.

    T1/single-asset vaults can be valued directly from supply/borrow amounts.
    T2-T4 smart collateral/debt vaults use DEX shares, so we fail closed rather
    than pretending the share count is a token amount.
    """
    data = _json_get(f"{FLUID_API}/v2/56/users/{wallet}/nfts", timeout=25)
    if isinstance(data, dict):
        rows = data.get("data") or data.get("nfts") or data.get("positions") or []
    elif isinstance(data, list):
        rows = data
    else:
        rows = []

    assets = Decimal("0")
    liabilities = Decimal("0")
    positions = []
    unsupported = []

    # ALEF's canonical Venus exposure is the known Flux vault NFT below.
    # The API is already scoped to ALEF's wallet; filtering by NFT id avoids
    # accidentally pulling unrelated/legacy Fluid positions into NAV.
    wanted_id = str(VENUS_FLUX_VAULT_NFT_ID)
    matching_rows = [
        row for row in rows
        if isinstance(row, dict) and str(row.get("id") or "") == wanted_id
    ]
    if not matching_rows:
        raise RuntimeError(
            f"Venus/Fluid vault NFT #{wanted_id} was not found for wallet {wallet}"
        )

    for row in matching_rows:
        supply_raw = D(row.get("supply"))
        borrow_raw = D(row.get("borrow"))
        dust_raw = D(row.get("dustBorrow"))
        # Fully empty/liquidated records do not contribute to current NAV.
        if supply_raw <= 0 and borrow_raw <= 0 and dust_raw <= 0:
            continue

        vault = row.get("vault") or {}
        supply_token, supply_has_second = _fluid_primary_asset(vault.get("supplyToken"))
        borrow_token, borrow_has_second = _fluid_primary_asset(vault.get("borrowToken"))
        vault_type = str(vault.get("type") or "?")

        # For T2/T3/T4, supply/borrow can be DEX-share amounts. Directly
        # multiplying them by token0 price would materially misstate NAV.
        if vault_type != "1" or supply_has_second or borrow_has_second:
            unsupported.append({
                "id": str(row.get("id") or "?"),
                "vault": vault.get("address"),
                "type": vault_type,
                "supply_raw": str(supply_raw),
                "borrow_raw": str(borrow_raw),
            })
            continue

        s_amount, s_usd, s_symbol = _fluid_raw_asset_value(supply_raw, supply_token)
        b_amount, b_usd, b_symbol = _fluid_raw_asset_value(borrow_raw, borrow_token)

        # dustBorrow is protocol accounting dust. Include it only if the main
        # borrow is zero; adding it unconditionally may double-count the same
        # debt depending on API version.
        if borrow_raw <= 0 and dust_raw > 0:
            d_amount, d_usd, _ = _fluid_raw_asset_value(dust_raw, borrow_token)
            b_amount += d_amount
            b_usd += d_usd

        assets += s_usd
        liabilities += b_usd
        positions.append({
            "source": "flux-vault-nft",
            "nft_id": str(row.get("id") or "?"),
            "vault": vault.get("address"),
            "vault_type": vault_type,
            "supply_symbol": s_symbol,
            "supply_amount": str(s_amount),
            "supply_usd": str(s_usd),
            "borrow_symbol": b_symbol,
            "borrow_amount": str(b_amount),
            "borrow_usd": str(b_usd),
            "owner": row.get("ownerAddress"),
        })

    if unsupported:
        detail = ", ".join(f"NFT {x['id']} type={x['type']}" for x in unsupported)
        raise RuntimeError(
            "Venus/Fluid found smart-vault Flux position(s) that require DEX-share "
            f"conversion before NAV can be published: {detail}"
        )

    return assets, liabilities, positions, len(matching_rows)

def fetch_venus_position_usd(wallet, bnb_usd=None):
    """Read ALEF's known Venus/Fluid Flux vault directly.

    ALEF currently has no Venus Core/isolated-pool or Flux fToken lending
    exposure that needs discovery.  Scanning ~87 Venus markets was slow and
    caused noisy public-RPC batch failures.  The canonical Venus position is
    Fluid/Flux vault NFT #800, so NAV reads only the wallet-scoped Fluid NFT
    endpoint and selects that NFT.
    """
    flux_vault_assets, flux_vault_liabilities, flux_vault_positions, flux_vault_rows = (
        _fetch_venus_flux_vault_nfts_usd(wallet)
    )

    return {
        "protocol": "Venus",
        "assets_usd": flux_vault_assets,
        "liabilities_usd": flux_vault_liabilities,
        "core_assets_usd": Decimal("0"),
        "core_liabilities_usd": Decimal("0"),
        "flux_assets_usd": flux_vault_assets,
        "flux_liabilities_usd": flux_vault_liabilities,
        "positions": flux_vault_positions,
        "core_position_count": 0,
        "flux_position_count": len(flux_vault_positions),
        "flux_lend_position_count": 0,
        "flux_vault_position_count": len(flux_vault_positions),
        "flux_source": f"fluid-nft-api:nft-{VENUS_FLUX_VAULT_NFT_ID}",
        "flux_rows_seen": 0,
        "flux_raw_position_count": 0,
        "flux_vault_rows_seen": flux_vault_rows,
        "market_count_scanned": 0,
        "venus_snapshot_rpc_batches": 0,
        "venus_vai_rpc_batches": 0,
        "receipt_token_addresses": [x["address"] for x in VENUS_FLUX_FTOKENS],
    }


# ===============================================================
# Raw wallet holdings: EVM, TRON, Solana and TON/GRAM
# ===============================================================

# Symbols that are protocol accounting/receipt tokens rather than extra
# portfolio assets. Their underlying value is already counted above.
def _looks_like_protocol_receipt(symbol):
    s = (symbol or "").strip().lower()
    return (
        s.startswith("variabledebt") or
        s.startswith("stabledebt") or
        s.startswith("aeth") or
        s.startswith("aave") and "debt" in s or
        s.startswith("vbnb") or
        s.startswith("vbusd") or
        s.startswith("vusdc") or
        s.startswith("vusdt") or
        s.startswith("jtrx") or
        s.startswith("jusdc") or
        s.startswith("jusdt")
    )


def _coingecko_simple_id_prices(ids):
    ids = [x for x in dict.fromkeys(ids) if x]
    if not ids:
        return {}
    data = _json_get(
        "https://api.coingecko.com/api/v3/simple/price",
        params={"ids": ",".join(ids), "vs_currencies": "usd"},
    )
    return {i: D((data.get(i) or {}).get("usd")) for i in ids if (data.get(i) or {}).get("usd") is not None}


def _coingecko_contract_prices(platform, contracts):
    """Return {lowercase_contract: USD Decimal} for contracts CoinGecko knows."""
    contracts = [x for x in dict.fromkeys(contracts) if x]
    out = {}
    for i in range(0, len(contracts), 80):
        chunk = contracts[i:i + 80]
        try:
            data = _json_get(
                f"https://api.coingecko.com/api/v3/simple/token_price/{platform}",
                params={"contract_addresses": ",".join(chunk), "vs_currencies": "usd"},
            )
            for addr, row in (data or {}).items():
                if isinstance(row, dict) and row.get("usd") is not None:
                    out[addr.lower()] = D(row["usd"])
        except Exception as e:
            _audit_log(f"[WARN] CoinGecko {platform} contract pricing failed: {e}", file=sys.stderr)
    return out


def _dexscreener_prices(chain, contracts):
    """Best-effort USD pricing; supports requested token on either side of a pair."""
    contracts = [x for x in dict.fromkeys(contracts) if x]
    wanted = {x.lower() for x in contracts}
    out = {}
    best_liq = {}
    for i in range(0, len(contracts), 30):
        chunk = contracts[i:i + 30]
        try:
            data = _json_get(
                f"https://api.dexscreener.com/tokens/v1/{chain}/" + ",".join(chunk),
                timeout=20,
            )
            for pair in data if isinstance(data, list) else []:
                base = ((pair.get("baseToken") or {}).get("address") or "").lower()
                quote = ((pair.get("quoteToken") or {}).get("address") or "").lower()
                base_usd = D(pair.get("priceUsd"))
                base_in_quote = D(pair.get("priceNative"))
                liq = D(((pair.get("liquidity") or {}).get("usd")))

                candidates = []
                if base in wanted and base_usd > 0:
                    candidates.append((base, base_usd))
                # DexScreener priceUsd is the base token USD price and
                # priceNative is base units per one quote unit. Therefore
                # quote USD = base USD / (base/quote).
                if quote in wanted and base_usd > 0 and base_in_quote > 0:
                    candidates.append((quote, base_usd / base_in_quote))
                for token, price in candidates:
                    if price > 0 and liq >= best_liq.get(token, Decimal("-1")):
                        best_liq[token] = liq
                        out[token] = price
        except Exception as e:
            _audit_log(f"[WARN] DexScreener {chain} pricing failed: {e}", file=sys.stderr)
    return out


def _yf_price_for_token_symbol(symbol):
    """Price tokenized listed securities such as TQQQon from the underlying Yahoo ticker."""
    if not symbol:
        return None
    s = symbol.strip()
    upper = s.upper()
    # Ondo tokenized securities commonly append 'on' to the listed ticker.
    if upper.endswith("ON") and len(upper) > 2:
        ticker = upper[:-2]
    else:
        return None
    try:
        p = fetch_stock_prices([ticker])[0]
        return D(p) if p is not None else None
    except Exception:
        return None


def _eth_get_native_balance(rpc_url, wallet):
    payload = {"jsonrpc": "2.0", "id": 1, "method": "eth_getBalance", "params": [wallet, "latest"]}
    data = _json_post(rpc_url, payload, timeout=15)
    if data.get("error"):
        raise RuntimeError(data["error"])
    return Decimal(int(data.get("result", "0x0"), 16)) / (Decimal(10) ** 18)


def _etherscan_token_balances(chain, wallet):
    """Optional Etherscan V2 holdings reader (needs a key; holdings endpoint may be plan-gated)."""
    if not ETHERSCAN_API_KEY:
        raise RuntimeError("no ETHERSCAN_API_KEY/BSCSCAN_API_KEY configured")
    chainid = "1" if chain == "ethereum" else "56"
    data = _json_get(
        "https://api.etherscan.io/v2/api",
        params={
            "chainid": chainid,
            "module": "account",
            "action": "addresstokenbalance",
            "address": wallet,
            "page": 1,
            "offset": 100,
            "apikey": ETHERSCAN_API_KEY,
        },
        timeout=25,
    )
    if str(data.get("status")) != "1" or not isinstance(data.get("result"), list):
        raise RuntimeError(str(data.get("result") or data.get("message") or data))
    rows = []
    for x in data["result"]:
        rows.append({
            "value": x.get("TokenQuantity"),
            "token": {
                "type": "ERC-20",
                "address": x.get("TokenAddress"),
                "symbol": x.get("TokenSymbol") or "",
                "name": x.get("TokenName") or "",
                "decimals": x.get("TokenDivisor") or "18",
                "exchange_rate": x.get("TokenPriceUSD"),
            },
        })
    return rows


def _blockscout_token_balances(chain, wallet):
    errors = []
    for base in EVM_TOKEN_INDEXERS.get(chain, []):
        try:
            data = _json_get(f"{base}/api/v2/addresses/{wallet}/token-balances", timeout=20)
            if isinstance(data, list):
                return data
            errors.append(f"{base}: unexpected response")
        except Exception as e:
            errors.append(f"{base}: {e}")
    # Blockscout's BSC instances have changed/vanished more than once.  If an
    # Etherscan-family API key is available, use its multichain holdings API.
    try:
        return _etherscan_token_balances(chain, wallet)
    except Exception as e:
        errors.append(f"Etherscan V2 fallback: {e}")
    raise RuntimeError("; ".join(errors) if errors else f"No token indexer configured for {chain}")



def _evm_batch_eth_call(rpc_url, calls, batch_size=100):
    """Run eth_call in JSON-RPC batches. Returns {key: result_hex}."""
    out = {}
    calls = list(calls)
    for start in range(0, len(calls), batch_size):
        chunk = calls[start:start + batch_size]
        payload = []
        id_to_key = {}
        for j, (key, to, data) in enumerate(chunk, 1):
            req_id = start + j
            id_to_key[req_id] = key
            payload.append({
                "jsonrpc": "2.0", "id": req_id, "method": "eth_call",
                "params": [{"to": to, "data": data}, "latest"],
            })
        response = _json_post(rpc_url, payload, timeout=30)
        if not isinstance(response, list):
            raise RuntimeError(f"EVM batch RPC returned non-list response: {response}")
        returned = set()
        for row in response:
            rid = row.get("id")
            key = id_to_key.get(rid)
            if key is None:
                continue
            returned.add(key)
            if row.get("error"):
                # A single xStock deployment should not poison the whole batch.
                out[key] = None
            else:
                out[key] = row.get("result")
        for key, _, _ in chunk:
            if key not in returned:
                out[key] = None
    return out




# ===============================================================
# Tokenized securities registries: Binance bStocks + Ondo
# ===============================================================

_BSTOCK_REGISTRY_CACHE = None
_ONDO_EVM_REGISTRY_CACHE = None
_ONDO_SOLANA_REGISTRY_CACHE = None
_ONDO_BINANCE_META_CACHE = None

# Official Binance-published contracts used as a fallback if current registry
# discovery is temporarily unavailable. Dynamic discovery is still attempted
# on every fresh process so newly launched bStocks can be picked up.
_BSTOCK_STATIC_FALLBACK = {
    "TSLAB": {"address": "0x5b1910eAaD6450E50f816082Aa078C41F10C292f", "ticker": "TSLA"},
    "NVDAB": {"address": "0x02Fca66C1D1aFB4E2A7884261eB00F63598a7436", "ticker": "NVDA"},
    "SNDKB": {"address": "0x3eE4dF61bd4F867E349BEaE8bFE07bc31b4850fb", "ticker": "SNDK"},
    "CRCLB": {"address": "0x80f3D493EBCe97e343c53D29a137942416B4ffC0", "ticker": "CRCL"},
    "MUB": {"address": "0xcdf2f3e0fa43C47A6662a91C9E4a7C5f69762699", "ticker": "MU"},
    "LITEB": {"address": "0x64748bea17b6d19e242adf20425de2440c656142", "ticker": "LITE"},
    "METAB": {"address": "0x7425889fe94f9d693e8daefe88bcced6acfef4c0", "ticker": "META"},
    "MSFTB": {"address": "0x80106cb3ead06659a5ad19df39d9b4733863b9b0", "ticker": "MSFT"},
    "PLTRB": {"address": "0x0ca5d51d0277bd006fd9607d3e560785ebad8222", "ticker": "PLTR"},
    "QQQB": {"address": "0x205812cdbed920aff76c6580abd681a46d11efc7", "ticker": "QQQ"},
    "SPCXB": {"address": "0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1", "ticker": "SPCX"},
    "GOOGLB": {"address": "0x3f53de71c126bdabae20f9cd64848d317f6c3238", "ticker": "GOOGL"},
    "AAPLB": {"address": "0x431a3bee82e2ca41e49895cbece5bb0f76a89b7a", "ticker": "AAPL"},
}


def _binance_headers():
    return {"Accept-Encoding": "identity", "User-Agent": "binance-web3/1.1 (Alef NAV)"}


def _walk_dicts(obj):
    if isinstance(obj, dict):
        yield obj
        for v in obj.values():
            yield from _walk_dicts(v)
    elif isinstance(obj, list):
        for v in obj:
            yield from _walk_dicts(v)


def _bstocks_registry():
    """Return BNB bStock contracts relevant to ALEF's portfolio.

    v11 searched the wider bStock universe. v12 asks Binance only about
    underlying tickers that can actually enter ALEF NAV.
    """
    global _BSTOCK_REGISTRY_CACHE
    if _BSTOCK_REGISTRY_CACHE is not None:
        return _BSTOCK_REGISTRY_CACHE

    out = {}
    for sym, meta in _BSTOCK_STATIC_FALLBACK.items():
        if not _portfolio_security_allowed(ticker=meta.get("ticker"), symbol=sym):
            continue
        row = dict(meta)
        row.update({"symbol": sym, "provider": "bstocks", "source": "binance-official-fallback"})
        out[row["address"].lower()] = row

    candidates = set()
    for ticker in PORTFOLIO_SECURITY_TICKERS:
        clean = ticker.replace(".", "").replace("-", "")
        if clean.isalnum():
            candidates.add(clean + "B")

    def lookup(symbol):
        try:
            data = _json_get(
                "https://web3.binance.com/api/v1/dex/market/rwa/search",
                params={"keyword": symbol, "platformId": "bstock"},
                headers=_binance_headers(), timeout=6,
            )
            found = []
            for d in _walk_dicts(data):
                platform = str(d.get("platformId") or "").lower()
                chain_id = str(d.get("binanceChainId") or d.get("chainId") or "")
                addr = d.get("tokenContractAddress") or d.get("contractAddress")
                token_symbol = str(d.get("tokenSymbol") or d.get("symbol") or symbol).upper()
                ticker = str(d.get("ticker") or "").upper()
                if not ticker and token_symbol.endswith("B"):
                    ticker = token_symbol[:-1]
                if (platform == "bstock" and chain_id == "56" and isinstance(addr, str)
                        and addr.startswith("0x") and _portfolio_security_allowed(ticker=ticker, symbol=token_symbol)):
                    found.append((addr.lower(), {
                        "address": addr, "symbol": token_symbol, "ticker": ticker,
                        "provider": "bstocks", "source": "binance-rwa-search",
                    }))
            return found
        except Exception:
            return []

    try:
        from concurrent.futures import ThreadPoolExecutor, as_completed
        with ThreadPoolExecutor(max_workers=6) as pool:
            futures = [pool.submit(lookup, sym) for sym in sorted(candidates)]
            for fut in as_completed(futures):
                for addr, row in fut.result():
                    out[addr] = row
    except Exception as e:
        _audit_log(f"[WARN] Binance bStocks portfolio discovery failed; using verified fallback contracts: {e}", file=sys.stderr)

    _BSTOCK_REGISTRY_CACHE = out
    return out


def _bstock_price_usd(symbol, ticker=None):
    """Value bStocks from the underlying Yahoo Finance ticker only."""
    symbol = str(symbol or "").upper()
    ticker = str(ticker or "").upper()
    if not ticker and symbol.endswith("B"):
        ticker = symbol[:-1]
    return _nav_stock_price_usd(ticker)

def _ondo_binance_meta_index():
    """Current Ondo EVM stock metadata incl. ticker + shares multiplier."""
    global _ONDO_BINANCE_META_CACHE
    if _ONDO_BINANCE_META_CACHE is not None:
        return _ONDO_BINANCE_META_CACHE
    out = {"ethereum": {}, "bsc": {}}
    try:
        data = _json_get(
            "https://www.binance.com/bapi/defi/v1/public/wallet-direct/buw/wallet/market/token/rwa/stock/detail/list/ai",
            params={"type": 1}, headers=_binance_headers(), timeout=30,
        )
        for row in data.get("data") or []:
            cid = str(row.get("chainId") or "")
            chain = "ethereum" if cid == "1" else "bsc" if cid == "56" else None
            addr = row.get("contractAddress")
            if chain and isinstance(addr, str) and addr.startswith("0x"):
                meta = dict(row)
                meta["provider"] = "ondo"
                meta["source"] = "binance-ondo-public-list"
                out[chain][addr.lower()] = meta
    except Exception as e:
        _audit_log(f"[WARN] Ondo/Binance live stock metadata lookup failed: {e}", file=sys.stderr)
    _ONDO_BINANCE_META_CACHE = out
    return out


# Separate Ondo products that are not Global Markets equities/ETFs.
_ONDO_EXTRA_EVM = {
    "ethereum": {
        "0x1b19c19393e2d034d8ff31ff34c81252fcbbee92": {"symbol": "OUSG", "name": "Ondo Short-Term US Government Bond Fund", "decimals": 18, "kind": "ondo-fund"},
        "0x96f6ef951840721adbf46ac996b59e0235cb985c": {"symbol": "USDY", "name": "US Dollar Yield", "decimals": 18, "kind": "ondo-yield"},
        "0xaf37c1167910ebc994e266949387d2c7c326b879": {"symbol": "rUSDY", "name": "Rebasing USDY", "decimals": 18, "kind": "ondo-yield"},
        "0xace8e719899f6e91831b18ae746c9a965c2119f1": {"symbol": "USDon", "name": "Ondo U.S. Dollar Token", "decimals": 18, "kind": "ondo-cash"},
    },
    "bsc": {
        "0x608593d17a2decbbc4399e4185be4922f97ed32e": {"symbol": "USDY", "name": "US Dollar Yield", "decimals": 18, "kind": "ondo-yield"},
        "0x1f8955e640cbd9abc3c3bb408c9e2e1f5f20dfe6": {"symbol": "USDon", "name": "Ondo U.S. Dollar Token", "decimals": 18, "kind": "ondo-cash"},
    },
}


def _ondo_evm_registry(chain):
    """Official Ondo Global Markets token list + separate Ondo products."""
    global _ONDO_EVM_REGISTRY_CACHE
    if _ONDO_EVM_REGISTRY_CACHE is None:
        out = {"ethereum": {}, "bsc": {}}
        try:
            data = _json_get(
                "https://raw.githubusercontent.com/ondoprotocol/ondo-global-markets-token-list/main/tokenlist.json",
                timeout=30,
            )
            for row in data.get("tokens") or []:
                cid = str(row.get("chainId") or "")
                ch = "ethereum" if cid == "1" else "bsc" if cid == "56" else None
                addr = row.get("address")
                if ch and isinstance(addr, str) and addr.startswith("0x"):
                    symbol = row.get("symbol") or "Ondo"
                    ticker = symbol[:-2] if str(symbol).lower().endswith("on") else ""
                    out[ch][addr.lower()] = {
                        "address": addr, "symbol": symbol, "name": row.get("name") or "",
                        "decimals": row.get("decimals"), "ticker": ticker,
                        "provider": "ondo", "kind": "ondo-stock",
                        "source": "ondo-official-tokenlist",
                    }
        except Exception as e:
            _audit_log(f"[WARN] Ondo official Global Markets token list failed: {e}", file=sys.stderr)
        # Binance mirrors current Ondo EVM metadata and supplies multipliers.
        live_meta = _ondo_binance_meta_index()
        for ch in ("ethereum", "bsc"):
            for addr, bm in live_meta.get(ch, {}).items():
                meta = out[ch].setdefault(addr, {
                    "address": bm.get("contractAddress"), "symbol": bm.get("symbol") or "Ondo",
                    "name": "", "decimals": 18, "provider": "ondo", "kind": "ondo-stock",
                    "source": "binance-ondo-public-list",
                })
                if bm.get("ticker"):
                    meta["ticker"] = bm.get("ticker")
                if bm.get("multiplier") is not None:
                    meta["multiplier"] = bm.get("multiplier")
            for addr, extra in _ONDO_EXTRA_EVM.get(ch, {}).items():
                meta = dict(extra)
                meta.update({"address": addr, "provider": "ondo", "source": "ondo-official-address"})
                out[ch][addr.lower()] = meta
        _ONDO_EVM_REGISTRY_CACHE = out
    return _ONDO_EVM_REGISTRY_CACHE.get(chain, {})


def _ondo_dynamic_evm_price(chain, address, meta):
    """
    Value an Ondo Global Markets stock from the underlying Yahoo ticker.
    Any Ondo `multiplier` is a share-conversion factor, not a USD price source.
    """
    ticker = str(meta.get("ticker") or "").upper()
    underlying = _nav_stock_price_usd(ticker)
    if underlying is None or underlying <= 0:
        return None
    try:
        mult = D(meta.get("multiplier"), "1")
        if mult <= 0:
            mult = Decimal("1")
    except Exception:
        mult = Decimal("1")
    return underlying * mult

def _ondo_nonstock_evm_price(chain, address, meta):
    """
    Non-equity Ondo products are not security prices from Yahoo. Dollar-like
    reserve tokens are marked at $1; other fund/yield products are excluded
    unless an explicit price rule is added here.
    """
    symbol = str(meta.get("symbol") or "").upper()
    if symbol in ("USDON", "RUSDY", "USDY"):
        return Decimal("1")
    return None

def _scan_evm_provider(wallet, chain, rpc_url, registry, provider):
    """Direct balanceOf scan of portfolio-relevant provider contracts only."""
    full_count = len(registry or {})
    registry = _portfolio_registry_subset(registry, provider)
    if not registry:
        return {
            "assets_usd": Decimal("0"), "items": [], "contracts": set(),
            "deployments_scanned": 0, "registry_candidates": full_count, "positions": 0,
        }

    arg = _pad_evm_addr(wallet)
    calls = [(addr, meta.get("address") or addr, "0x70a08231" + arg) for addr, meta in registry.items()]
    balances = _evm_batch_eth_call(rpc_url, calls)
    positives = []
    for key, ret in balances.items():
        if not ret or ret == "0x":
            continue
        try:
            raw = int(ret, 16)
        except Exception:
            continue
        if raw > 0:
            positives.append((key, raw, registry[key]))

    total = Decimal("0")
    items = []
    failures = []
    for key, raw, meta in positives:
        addr = meta.get("address") or key
        try:
            decimals = int(meta.get("decimals")) if meta.get("decimals") is not None else int(_evm_uint_call(rpc_url, addr, "313ce567"))
        except Exception as e:
            failures.append(f"{meta.get('symbol') or addr}: decimals {e}")
            continue
        amount = Decimal(raw) / (Decimal(10) ** decimals)
        symbol = meta.get("symbol") or provider
        ticker = meta.get("ticker") or ""
        if provider == "bstocks" or meta.get("kind") == "ondo-stock":
            include = _portfolio_security_allowed(ticker=ticker, symbol=symbol)
        else:
            include = str(symbol).upper() in PORTFOLIO_CASH_SYMBOLS
        _audit_log(
            f"[ALEF] {chain} {provider}: {symbol} amount={amount} underlying={ticker or '?'} portfolio_include={include}",
            file=sys.stderr,
        )
        if not include:
            continue
        if provider == "bstocks":
            price = _bstock_price_usd(symbol, ticker)
        elif meta.get("kind") == "ondo-stock":
            price = _ondo_dynamic_evm_price(chain, addr, meta)
        else:
            price = _ondo_nonstock_evm_price(chain, addr, meta)
        if price is None or price <= 0:
            failures.append(f"{meta.get('symbol') or addr}: no USD price")
            continue
        value = amount * price
        total += value
        items.append({
            "symbol": meta.get("symbol") or provider, "address": addr,
            "amount": str(amount), "price_usd": str(price), "value_usd": str(value),
            "underlying_symbol": meta.get("ticker") or "", "provider": provider,
            "asset_type": meta.get("kind") or ("bstock" if provider == "bstocks" else "ondo"),
            "identity_source": meta.get("source") or provider,
        })
    if failures:
        raise RuntimeError(f"{provider} holdings found but could not be fully valued: " + "; ".join(failures[:12]))
    return {
        "assets_usd": total, "items": items,
        "contracts": {str(x.get("address") or "").lower() for x in items if x.get("address")},
        "deployments_scanned": len(registry), "registry_candidates": full_count,
        "positions": len(items),
    }


def _fetch_bstocks_bnb_usd(wallet, rpc_url):
    return _scan_evm_provider(wallet, "bsc", rpc_url, _bstocks_registry(), "bstocks")


def _fetch_ondo_evm_usd(wallet, chain, rpc_url):
    return _scan_evm_provider(wallet, chain, rpc_url, _ondo_evm_registry(chain), "ondo")


def _ondo_solana_registry():
    """Official Ondo Solana GM mint list from Ondo's simulator repository."""
    global _ONDO_SOLANA_REGISTRY_CACHE
    if _ONDO_SOLANA_REGISTRY_CACHE is not None:
        return _ONDO_SOLANA_REGISTRY_CACHE
    out = {}
    try:
        sess = _http_session()
        r = sess.get(
            "https://raw.githubusercontent.com/ondoprotocol/gm-solana-simulator/main/constants.rs",
            timeout=30,
        )
        r.raise_for_status()
        text = r.text
        for symbol, mint in re.findall(r'\("([A-Za-z0-9.\-]+on)",\s*"([1-9A-HJ-NP-Za-km-z]{32,48})"\)', text):
            out[mint.lower()] = {
                "symbol": symbol, "mint": mint, "ticker": symbol[:-2],
                "provider": "ondo", "kind": "ondo-stock", "source": "ondo-official-solana-list",
            }
    except Exception as e:
        _audit_log(f"[WARN] Ondo official Solana GM registry failed: {e}", file=sys.stderr)
    # Separate Ondo products on Solana.
    extras = {
        "A1KLoBrKBde8Ty9qtNQUtq3C2ortoC3u7twggz7sEto6": ("USDY", "ondo-yield"),
        "i7u4r16TcsJTgq1kAG8opmVZyVnAKBwLKu6ZPMwzxNc": ("OUSG", "ondo-fund"),
        "ZPFtoCe7WWqG4N3ZFRccS8T9SMBeHsd1Vmgv2i7ondo": ("USDon", "ondo-cash"),
    }
    for mint, (symbol, kind) in extras.items():
        out[mint.lower()] = {"symbol": symbol, "mint": mint, "ticker": "", "provider": "ondo", "kind": kind, "source": "ondo-official-address"}
    _ONDO_SOLANA_REGISTRY_CACHE = out
    return out


def _fetch_evm_xstocks_usd(wallet, chain, rpc_url):
    """
    Directly scan official EVM xStocks. balanceOf() supplies the adjusted
    economic share quantity; Yahoo Finance supplies the USD price.
    """
    full_registry = _xstocks_chain_index(chain)
    full_count = len(full_registry)
    registry = _portfolio_registry_subset(full_registry, "xstocks")
    if not registry:
        return {
            "assets_usd": Decimal("0"), "items": [], "contracts": set(),
            "deployments_scanned": 0, "registry_candidates": full_count, "positions": 0,
        }

    arg = _pad_evm_addr(wallet)
    calls = [(addr, meta.get("address") or addr, "0x70a08231" + arg) for addr, meta in registry.items()]
    balances = _evm_batch_eth_call(rpc_url, calls)

    positives = []
    for key, ret in balances.items():
        if not ret or ret == "0x":
            continue
        try:
            raw = int(ret, 16)
        except Exception:
            continue
        if raw > 0:
            positives.append((key, raw, registry[key]))

    total = Decimal("0")
    items = []
    failures = []
    for key, raw, meta in positives:
        addr = meta.get("address") or key
        decimals = meta.get("decimals")
        try:
            decimals = int(decimals) if decimals is not None else int(_evm_uint_call(rpc_url, addr, "313ce567"))
        except Exception as e:
            failures.append(f"{meta.get('symbol') or addr}: decimals: {e}")
            continue
        amount = Decimal(raw) / (Decimal(10) ** decimals)
        symbol = meta.get("symbol") or "xStock"
        ticker = meta.get("underlyingSymbol") or (symbol[:-1] if symbol.lower().endswith("x") else "")
        include = _portfolio_security_allowed(ticker=ticker, symbol=symbol)
        _audit_log(
            f"[ALEF] {chain} xStock: {symbol} amount={amount} underlying={ticker} portfolio_include={include}",
            file=sys.stderr,
        )
        if not include:
            continue
        price = _nav_stock_price_usd(ticker)
        if price is None or price <= 0:
            failures.append(f"{symbol}: Yahoo Finance has no price for {ticker}")
            continue
        value = amount * price
        total += value
        items.append({
            "symbol": symbol, "address": addr, "amount": str(amount),
            "price_usd": str(price), "value_usd": str(value),
            "underlying_symbol": ticker, "provider": "xstocks", "asset_type": "xstock",
            "identity_source": "xstocks-official-registry", "price_source": "yfinance",
        })

    if failures:
        raise RuntimeError("xStocks EVM holdings found but could not be fully valued: " + "; ".join(failures[:10]))
    return {
        "assets_usd": total,
        "items": items,
        "contracts": {str(x.get("address") or "").lower() for x in items if x.get("address")},
        "deployments_scanned": len(registry),
        "registry_candidates": full_count,
        "positions": len(items),
    }

def fetch_evm_wallet_assets_usd(wallet, *, chain, rpc_url, native_symbol, native_usd, exclude_contracts=None):
    """
    Value native coin + raw ERC-20 holdings plus issuer-authenticated tokenized assets.

    Ethereum: xStocks + Ondo.
    BNB Chain: bStocks + Ondo (NOT xStocks for Alef's BNB holdings).
    Provider contracts are queried directly with balanceOf() and then excluded
    from generic enumeration to prevent double-counting.
    """
    exclude_contracts = {x.lower() for x in (exclude_contracts or [])}
    native_amount = _eth_get_native_balance(rpc_url, wallet)
    total = native_amount * D(native_usd)
    items = [{"symbol": native_symbol, "amount": str(native_amount), "price_usd": str(native_usd), "value_usd": str(total)}]

    provider_scans = []
    if chain == "ethereum":
        xs = _fetch_evm_xstocks_usd(wallet, chain, rpc_url)
        provider_scans.append(("xStocks", xs))
    elif chain == "bsc":
        bs = _fetch_bstocks_bnb_usd(wallet, rpc_url)
        provider_scans.append(("bStocks", bs))

    ondo = _fetch_ondo_evm_usd(wallet, chain, rpc_url)
    provider_scans.append(("Ondo", ondo))

    provider_contracts = set()
    stock_items = []
    provider_diagnostics = {}
    for label, scan in provider_scans:
        total += scan["assets_usd"]
        items.extend(scan["items"])
        provider_contracts.update(scan.get("contracts") or set())
        provider_diagnostics[label] = {
            "deployments_scanned": scan.get("deployments_scanned", 0),
            "registry_candidates": scan.get("registry_candidates", scan.get("deployments_scanned", 0)),
            "positions": scan.get("positions", 0),
        }
        for it in scan.get("items") or []:
            if it.get("asset_type") in ("xstock", "bstock", "ondo-stock"):
                stock_items.append(it)

    try:
        if chain == "bsc" and not ETHERSCAN_API_KEY:
            raise RuntimeError("generic BNB token indexer skipped (no Etherscan/BscScan API key)")
        rows = _blockscout_token_balances(chain, wallet)
    except Exception as e:
        if chain != "bsc":
            _audit_log(f"[WARN] {chain} generic ERC-20 wallet enumeration failed: {e}", file=sys.stderr)
        else:
            _audit_log("[ALEF] BNB generic token enumeration skipped; portfolio bStocks/Ondo contracts were probed directly.", file=sys.stderr)
        return {
            "chain": chain, "assets_usd": total, "items": items,
            "token_scan_complete": False, "tokenized_scan_complete": True,
            "stock_items": stock_items, "provider_diagnostics": provider_diagnostics,
        }

    token_rows = []
    contracts = []
    for row in rows:
        tok = row.get("token") or {}
        if (tok.get("type") or "ERC-20").upper() not in ("ERC-20", "ERC20"):
            continue
        symbol = tok.get("symbol") or ""
        if _looks_like_protocol_receipt(symbol):
            continue
        addr = tok.get("address") or tok.get("contract_address")
        if not addr:
            continue
        al = addr.lower()
        if al in exclude_contracts or al in provider_contracts:
            continue
        decimals = int(D(tok.get("decimals"), "18"))
        raw = D(row.get("value"))
        amount = raw / (Decimal(10) ** decimals)
        if amount <= 0:
            continue
        token_rows.append((addr, symbol, amount, tok))
        contracts.append(addr)

    for addr, symbol, amount, tok in token_rows:
        price = _portfolio_raw_token_price_usd(symbol)
        include = price is not None and price > 0
        _audit_log(
            f"[ALEF] {chain} raw token: {symbol or '?'} amount={amount} contract={addr} "
            f"portfolio_include={include}", file=sys.stderr,
        )
        if not include:
            continue
        value = amount * price
        total += value
        items.append({
            "symbol": symbol, "address": addr, "amount": str(amount),
            "price_usd": str(price), "value_usd": str(value),
            "price_source": "coingecko/yfinance",
        })

    return {
        "chain": chain, "assets_usd": total, "items": items,
        "token_scan_complete": True, "tokenized_scan_complete": True,
        "stock_items": stock_items, "provider_diagnostics": provider_diagnostics,
    }

def _tron_constant_uint(wallet, contract, selector):
    payload = {
        "owner_address": wallet,
        "contract_address": contract,
        "function_selector": selector,
        "parameter": "",
        "visible": True,
    }
    data = _json_post(f"{TRONGRID_API}/wallet/triggerconstantcontract", payload, timeout=15)
    vals = data.get("constant_result") or []
    if not vals:
        raise RuntimeError(f"TRON constant call failed for {contract} {selector}")
    return int(vals[0], 16)


def fetch_tron_wallet_assets_usd(wallet, trx_usd, exclude_contracts=None):
    """
    Enumerate every non-zero TRC-20 first. Protocol receipts are diagnosed but
    excluded because their underlying is already counted by JustLend. All other
    raw tokens are then filtered by portfolio identity; USD prices come only
    from the original CoinGecko/yfinance crypto price function (or $1 cash).
    """
    exclude = {x.lower() for x in (exclude_contracts or [])}
    data = _json_get(f"{TRONGRID_API}/v1/accounts/{wallet}", timeout=20)
    rows = data.get("data") or []
    if not rows:
        return {"chain": "tron", "assets_usd": Decimal("0"), "items": [], "token_scan_complete": True}
    acct = rows[0]
    trx = D(acct.get("balance")) / Decimal(1_000_000)
    total = trx * D(trx_usd)
    items = [{"symbol": "TRX", "amount": str(trx), "price_usd": str(trx_usd), "value_usd": str(total), "price_source": "coingecko/yfinance"}]

    discovered = {}
    for entry in acct.get("trc20") or []:
        if isinstance(entry, dict):
            for contract, raw in entry.items():
                if contract:
                    discovered[contract] = D(raw)

    all_tokens = []
    for contract, raw in discovered.items():
        known = TRON_TRUSTED_ASSETS.get(contract, {})
        symbol = known.get("symbol") or "?"
        is_receipt = contract.lower() in exclude
        decimals = None
        try:
            decimals = int(_tron_constant_uint(wallet, contract, "decimals()"))
        except Exception:
            pass
        amount = None if decimals is None else raw / (Decimal(10) ** decimals)
        # The account endpoint does not reliably return token metadata. Known
        # portfolio contracts are identified by address; unknown contracts are
        # still printed and never silently discarded.
        include = (not is_receipt) and symbol != "?" and _portfolio_raw_token_price_usd(symbol) is not None
        diag = {
            "contract": contract, "symbol": symbol,
            "amount": str(amount) if amount is not None else "?",
            "protocol_receipt": is_receipt, "portfolio_include": include,
        }
        all_tokens.append(diag)
        _audit_log(
            f"[ALEF] TRON token: {symbol} amount={diag['amount']} contract={contract} "
            f"protocol_receipt={is_receipt} portfolio_include={include}", file=sys.stderr,
        )
        if not include or amount is None or amount <= 0:
            continue
        price = _portfolio_raw_token_price_usd(symbol)
        if price is None or price <= 0:
            continue
        value = amount * price
        total += value
        items.append({
            "symbol": symbol, "address": contract, "amount": str(amount),
            "price_usd": str(price), "value_usd": str(value), "price_source": "coingecko/yfinance",
        })

    return {
        "chain": "tron", "assets_usd": total, "items": items,
        "token_scan_complete": True, "all_tokens": all_tokens,
    }

def _solana_rpc(method, params):
    # Public Solana RPCs can stall. v11 used the global 3-retry / 25-second
    # path; one bad call could therefore dominate the entire NAV update.
    payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
    sess = _http_session(max_retries=1, backoff_factor=0.25)
    r = sess.post(SOLANA_RPC, json=payload, timeout=10, headers={"Content-Type": "application/json"})
    r.raise_for_status()
    data = r.json()
    if data.get("error"):
        raise RuntimeError(f"Solana {method} error: {data['error']}")
    return data.get("result")


def _solana_metaplex_metadata(mint):
    """Best-effort name/symbol discovery from the canonical Metaplex metadata program."""
    try:
        rows = _solana_rpc("getProgramAccounts", [
            METAPLEX_METADATA_PROGRAM,
            {
                "encoding": "base64",
                "filters": [{"memcmp": {"offset": 33, "bytes": mint}}],
            },
        ]) or []
        if not rows:
            return None
        encoded = ((rows[0].get("account") or {}).get("data") or [None])[0]
        if not encoded:
            return None
        raw = base64.b64decode(encoded)
        # Metadata V1 begins: key(1), updateAuthority(32), mint(32), then
        # Borsh strings name, symbol, uri (u32 little-endian length + bytes).
        pos = 65
        strings = []
        for _ in range(3):
            if pos + 4 > len(raw):
                return None
            n = int.from_bytes(raw[pos:pos + 4], "little")
            pos += 4
            if n < 0 or n > 2000 or pos + n > len(raw):
                return None
            strings.append(raw[pos:pos + n].decode("utf-8", errors="replace").rstrip("\x00").strip())
            pos += n
        return {"name": strings[0], "symbol": strings[1], "uri": strings[2]}
    except Exception as e:
        _audit_log(f"[WARN] Solana metadata lookup failed for {mint}: {e}", file=sys.stderr)
        return None


def _xstock_yahoo_ticker(metadata):
    """Infer the listed ticker only for metadata that explicitly identifies an xStock."""
    if not metadata:
        return None
    symbol = (metadata.get("symbol") or "").strip()
    name = (metadata.get("name") or "").strip()
    # Official xStocks symbols conventionally append lowercase x (AAPLx, NVDAx,
    # PPLTx, QQQx...). Require either xStock in the name or a CoinGecko ID/name
    # that explicitly identifies xStock; this avoids classifying arbitrary SPL
    # tokens ending in x as securities.
    cg_id = (metadata.get("coingecko_id") or "").lower()
    identified_xstock = "xstock" in name.lower() or "xstock" in cg_id
    if not symbol.lower().endswith("x") or not identified_xstock:
        return None
    ticker = symbol[:-1].upper().replace(".", "-")
    return ticker or None


def _price_xstock_from_metadata(metadata):
    ticker = _xstock_yahoo_ticker(metadata)
    if not ticker:
        return None
    try:
        p = fetch_stock_prices([ticker])[0]
        if p is not None and p > 0:
            return D(p)
    except Exception:
        pass
    return None


def _jupiter_solana_prices(mint_decimals):
    """Optional Jupiter Price API v3 lookup when JUPITER_API_KEY is configured."""
    if not JUPITER_API_KEY or not mint_decimals:
        return {}
    out = {}
    mints = list(mint_decimals)
    for i in range(0, len(mints), 50):
        chunk = mints[i:i+50]
        try:
            data = _json_get(
                "https://api.jup.ag/price/v3",
                params={"ids": ",".join(chunk)},
                headers={"x-api-key": JUPITER_API_KEY},
                timeout=20,
            )
            for mint, row in (data or {}).items():
                if isinstance(row, dict) and row.get("usdPrice") is not None:
                    out[mint.lower()] = D(row["usdPrice"])
        except TypeError:
            # Older local copy of _json_get without a headers kwarg: use session directly.
            try:
                r = _http_session().get(
                    "https://api.jup.ag/price/v3",
                    params={"ids": ",".join(chunk)},
                    headers={"x-api-key": JUPITER_API_KEY}, timeout=20)
                r.raise_for_status()
                for mint, row in (r.json() or {}).items():
                    if isinstance(row, dict) and row.get("usdPrice") is not None:
                        out[mint.lower()] = D(row["usdPrice"])
            except Exception as e:
                _audit_log(f"[WARN] Jupiter price lookup failed: {e}", file=sys.stderr)
        except Exception as e:
            _audit_log(f"[WARN] Jupiter price lookup failed: {e}", file=sys.stderr)
    return out



_XSTOCKS_REGISTRY_CACHE = None


def _xstocks_chain_key(network):
    """Normalize xStocks API network labels to the chains used by this script."""
    if isinstance(network, dict):
        network = (network.get("name") or network.get("slug") or network.get("id") or
                   network.get("identifier") or network.get("network"))
    n = str(network or "").strip().lower().replace("_", " ").replace("-", " ")
    if "solana" in n:
        return "solana"
    if n == "ton" or "open network" in n:
        return "ton"
    if "bnb" in n or "binance smart" in n or n == "bsc":
        return "bsc"
    # Keep Ethereum separate from Ethereum L2s such as Arbitrum/Ink/Mantle.
    if n in ("ethereum", "ethereum mainnet", "eth", "mainnet"):
        return "ethereum"
    return None


def _xstocks_all_chain_index():
    """
    Return {chain: {contract_or_mint: metadata}} from xStocks' public Assets API.

    Unlike the old Solana-only index, this is authoritative discovery for
    Ethereum, BNB Smart Chain, Solana and TON.  It lets us query xStocks
    directly even when a generic wallet token indexer is unavailable.
    """
    global _XSTOCKS_REGISTRY_CACHE
    if _XSTOCKS_REGISTRY_CACHE is not None:
        return _XSTOCKS_REGISTRY_CACHE

    bases = (
        "https://api.xstocks.fi/api/v2",
        "https://api.backed.fi/api/v2",
    )
    last_error = None
    for base in bases:
        try:
            out = {"ethereum": {}, "bsc": {}, "solana": {}, "ton": {}}
            page = 1
            while page <= 25:
                data = _json_get(
                    f"{base}/public/assets",
                    params={"page": page, "pageSize": 100}, timeout=30,
                )
                payload = data.get("data") if isinstance(data, dict) and isinstance(data.get("data"), dict) else data
                nodes = ((payload or {}).get("nodes") or (payload or {}).get("assets") or
                         (payload or {}).get("items") or [])
                for asset in nodes:
                    if not isinstance(asset, dict):
                        continue
                    deployments = (asset.get("deployments") or asset.get("contracts") or
                                   asset.get("tokens") or [])
                    for dep in deployments:
                        if not isinstance(dep, dict):
                            continue
                        contract = dep.get("contract") if isinstance(dep.get("contract"), dict) else {}
                        network = (dep.get("network") or dep.get("chain") or dep.get("blockchain") or
                                   contract.get("network") or contract.get("chain"))
                        chain = _xstocks_chain_key(network)
                        if not chain:
                            continue
                        address = (dep.get("address") or dep.get("contractAddress") or dep.get("tokenAddress") or
                                   dep.get("mint") or contract.get("address") or contract.get("contractAddress"))
                        if not address:
                            continue
                        meta = {
                            "id": asset.get("id"),
                            "name": asset.get("name") or "",
                            "symbol": asset.get("symbol") or asset.get("identifier") or "",
                            "underlyingSymbol": asset.get("underlyingSymbol") or asset.get("underlyingTicker") or "",
                            "decimals": (dep.get("decimals") if dep.get("decimals") is not None else
                                         contract.get("decimals") if contract.get("decimals") is not None else
                                         asset.get("decimals")),
                            "multiplier": asset.get("currentMultiplier") or asset.get("multiplier"),
                            "network": network,
                            "address": address,
                            "source": "xstocks-official-registry",
                            "api_base": base,
                        }
                        out[chain][address.lower()] = meta
                page_info = (payload or {}).get("page") or (payload or {}).get("pagination") or {}
                has_next = page_info.get("hasNextPage")
                if has_next is None:
                    # Some API variants expose totalPages/currentPage instead.
                    total_pages = page_info.get("totalPages")
                    current_page = page_info.get("currentPage") or page
                    has_next = bool(total_pages and int(current_page) < int(total_pages))
                if not has_next:
                    break
                page += 1
            if any(out.values()):
                _XSTOCKS_REGISTRY_CACHE = out
                return out
        except Exception as e:
            last_error = e
            continue

    if last_error:
        _audit_log(f"[WARN] xStocks cross-chain registry lookup failed: {last_error}", file=sys.stderr)
    _XSTOCKS_REGISTRY_CACHE = {"ethereum": {}, "bsc": {}, "solana": {}, "ton": {}}
    return _XSTOCKS_REGISTRY_CACHE


def _xstocks_chain_index(chain):
    return _xstocks_all_chain_index().get(chain, {})


def _xstocks_solana_mint_index():
    # Backward-compatible wrapper used by the Solana scanner below.
    return _xstocks_chain_index("solana")


def _xstocks_official_price(symbol):
    """Current per-share USD quote from the issuer's public price endpoint."""
    if not symbol:
        return None
    for base in ("https://api.xstocks.fi/api/v2", "https://api.backed.fi/api/v2"):
        try:
            data = _json_get(f"{base}/public/assets/{symbol}/price-data", timeout=20)
            payload = data.get("data") if isinstance(data, dict) and isinstance(data.get("data"), dict) else data
            q = (payload or {}).get("quote") if isinstance(payload, dict) else None
            if q is not None and D(q) > 0:
                return D(q)
        except Exception:
            continue
    return None



def _extract_multiplier_value(obj):
    """Best-effort parser for current multiplier across xStocks API versions."""
    if isinstance(obj, dict):
        # Prefer explicitly-current fields before walking the structure.
        for key in ("currentMultiplier", "current_multiplier", "activeMultiplier", "active_multiplier"):
            if key in obj:
                v = obj[key]
                if isinstance(v, dict):
                    for kk in ("value", "multiplier", "amount"):
                        if kk in v:
                            try:
                                d = D(v[kk])
                                if d > 0:
                                    return d
                            except Exception:
                                pass
                else:
                    try:
                        d = D(v)
                        if d > 0:
                            return d
                    except Exception:
                        pass
        # A top-level multiplier/value is common on the public endpoint.
        for key in ("multiplier", "value"):
            if key in obj and not isinstance(obj[key], (dict, list)):
                try:
                    d = D(obj[key])
                    if d > 0:
                        return d
                except Exception:
                    pass
        for v in obj.values():
            found = _extract_multiplier_value(v)
            if found is not None:
                return found
    elif isinstance(obj, list):
        for v in obj:
            found = _extract_multiplier_value(v)
            if found is not None:
                return found
    return None


def _xstocks_official_multiplier(symbol, network=None):
    """Current xStock quantity multiplier. This is NOT a USD price source."""
    if not symbol:
        return None
    n = str(network or "")
    variants = [n] if n else [""]
    if n.upper() == "TON":
        variants += ["ton", "The Open Network"]
    elif n.upper() == "SOLANA":
        variants += ["Solana", "solana"]
    # preserve order, remove duplicates
    variants = list(dict.fromkeys(variants))
    for base in ("https://api.backed.fi/api/v2", "https://api.xstocks.fi/api/v2"):
        for net in variants:
            params = {"network": net} if net else {}
            try:
                data = _json_get(f"{base}/public/assets/{symbol}/multiplier", params=params, timeout=20)
                payload = data.get("data") if isinstance(data, dict) and isinstance(data.get("data"), dict) else data
                m = _extract_multiplier_value(payload)
                if m is not None and m > 0:
                    return m
            except Exception:
                pass
    for base in ("https://api.backed.fi/api/v1", "https://api.xstocks.fi/api/v1"):
        for net in variants:
            try:
                data = _json_get(
                    f"{base}/token/{symbol}/multiplier",
                    params=({"network": net} if net else None), timeout=20,
                )
                m = _extract_multiplier_value(data)
                if m is not None and m > 0:
                    return m
            except Exception:
                pass
    return None

def _solana_scaled_ui_multiplier(mint):
    """
    Read Token-2022 scaledUiAmountConfig from the mint and return the multiplier
    that is active *now*.  Returns 1 for a mint with no scaled-UI extension.
    """
    result = _solana_rpc("getAccountInfo", [
        mint, {"commitment": "confirmed", "encoding": "jsonParsed"},
    ]) or {}
    value = result.get("value") or {}
    data = value.get("data") or {}
    parsed = data.get("parsed") if isinstance(data, dict) else None
    info = (parsed or {}).get("info") if isinstance(parsed, dict) else {}
    extensions = (info or {}).get("extensions") or []
    for ext in extensions:
        if not isinstance(ext, dict):
            continue
        name = str(ext.get("extension") or ext.get("extensionType") or "").lower()
        if "scaleduiamount" not in name.replace("_", "").replace("-", ""):
            continue
        state = ext.get("state") or ext
        current = D(state.get("multiplier"), "1")
        new = D(state.get("newMultiplier"), str(current))
        ts_raw = state.get("newMultiplierEffectiveTimestamp")
        try:
            ts = float(ts_raw)
        except Exception:
            ts = float("inf")
        now_ts = datetime.now(timezone.utc).timestamp()
        active = new if now_ts >= ts else current
        if active <= 0:
            raise RuntimeError(f"invalid scaled UI multiplier {active} for {mint}")
        return active
    return Decimal("1")


_CG_SOLANA_INDEX_CACHE = None


def _coingecko_solana_mint_index():
    """
    Build mint -> CoinGecko identity from /coins/list?include_platform=true.
    This is more reliable for Solana than the legacy simple/token_price/solana
    route, and discovers xStocks without hard-coding every mint.
    """
    global _CG_SOLANA_INDEX_CACHE
    if _CG_SOLANA_INDEX_CACHE is not None:
        return _CG_SOLANA_INDEX_CACHE
    try:
        rows = _json_get(
            "https://api.coingecko.com/api/v3/coins/list",
            params={"include_platform": "true"}, timeout=30,
        )
        out = {}
        for row in rows if isinstance(rows, list) else []:
            platforms = row.get("platforms") or {}
            mint = platforms.get("solana")
            if mint:
                out[mint.lower()] = {
                    "id": row.get("id"),
                    "symbol": row.get("symbol"),
                    "name": row.get("name"),
                }
        _CG_SOLANA_INDEX_CACHE = out
        return out
    except Exception as e:
        _audit_log(f"[WARN] CoinGecko Solana mint registry lookup failed: {e}", file=sys.stderr)
        _CG_SOLANA_INDEX_CACHE = {}
        return {}


def _coingecko_solana_registry_prices(mints):
    index = _coingecko_solana_mint_index()
    identities = {}
    ids = []
    for mint in mints:
        meta = index.get(mint.lower())
        if meta and meta.get("id"):
            identities[mint] = meta
            ids.append(meta["id"])
    id_prices = _coingecko_simple_id_prices(ids)
    prices = {}
    for mint, meta in identities.items():
        p = id_prices.get(meta["id"])
        if p is not None and p > 0:
            prices[mint.lower()] = p
    return prices, identities


def _known_solana_asset_prices(mints):
    out = {}
    ids = []
    id_to_mints = {}
    for mint in mints:
        meta = SOLANA_KNOWN_ASSETS.get(mint)
        if meta and meta.get("coingecko_id"):
            cid = meta["coingecko_id"]
            ids.append(cid)
            id_to_mints.setdefault(cid, []).append(mint)
    cg = _coingecko_simple_id_prices(ids)
    for cid, price in cg.items():
        for mint in id_to_mints.get(cid, []):
            if price and price > 0:
                out[mint.lower()] = price
    # yfinance fallback is useful for tokenized listed securities if the
    # CoinGecko coin endpoint is rate-limited.
    for mint in mints:
        if mint.lower() in out:
            continue
        meta = SOLANA_KNOWN_ASSETS.get(mint) or {}
        ticker = meta.get("yahoo_ticker")
        if ticker:
            try:
                p = fetch_stock_prices([ticker])[0]
                if p is not None and p > 0:
                    out[mint.lower()] = D(p)
            except Exception:
                pass
    return out


def fetch_solana_wallet_assets_usd(wallet, sol_usd, alef_mint, extra_excluded_mints=None):
    """
    Enumerate all SPL/Token-2022 balances first. Portfolio securities are priced
    from Yahoo Finance; recognized portfolio crypto/cash uses the original
    CoinGecko/yfinance path. Chain/issuer calls are used only for identity and
    xStock/Ondo quantity multipliers, never for USD prices.
    """
    bal = _solana_rpc("getBalance", [wallet, {"commitment": "confirmed"}]) or {}
    sol = D(bal.get("value")) / Decimal(1_000_000_000)
    # SOL is gas, not one of Alef's declared crypto holdings. Diagnose it but do
    # not add it to NAV unless you intentionally add SOL to PORTFOLIO_CRYPTO_SYMBOLS.
    total = Decimal("0")
    items = []
    if "SOL" in PORTFOLIO_CRYPTO_SYMBOLS:
        sol_price = _nav_crypto_price_usd("SOL") or D(sol_usd)
        sol_value = sol * sol_price
        total += sol_value
        items.append({"symbol": "SOL", "amount": str(sol), "price_usd": str(sol_price), "value_usd": str(sol_value), "price_source": "coingecko/yfinance"})
    else:
        _audit_log(f"[ALEF] Solana native SOL gas excluded from NAV: amount={sol}", file=sys.stderr)

    excluded_mints = {alef_mint, *ALEF_LEGACY_MINTS, *(extra_excluded_mints or set())}
    amounts, ui_amounts, decimals_by_mint, token_program_by_mint = {}, {}, {}, {}
    successful_program_scans = 0
    for program in SOLANA_TOKEN_PROGRAMS:
        try:
            result = _solana_rpc("getTokenAccountsByOwner", [
                wallet, {"programId": program}, {"commitment": "confirmed", "encoding": "jsonParsed"},
            ]) or {}
            successful_program_scans += 1
        except Exception as e:
            _audit_log(f"[WARN] Solana token-program scan failed for {program}: {e}", file=sys.stderr)
            continue
        for row in result.get("value") or []:
            try:
                info = row["account"]["data"]["parsed"]["info"]
                mint = info["mint"]
                if mint in excluded_mints:
                    continue
                ta = info["tokenAmount"]
                decimals = int(ta.get("decimals", 0))
                raw_integer = D(ta.get("amount"))
                amount = raw_integer / (Decimal(10) ** decimals)
                # For Token-2022 Scaled UI Amount tokens, Solana RPC already
                # returns multiplier-adjusted uiAmountString.  This avoids a
                # second mint RPC call (the source of the PPLTx timeouts).
                ui_raw = ta.get("uiAmountString")
                ui_amount = D(ui_raw) if ui_raw not in (None, "") else amount
                decimals_by_mint[mint] = decimals
                token_program_by_mint[mint] = program
                if amount > 0:
                    amounts[mint] = amounts.get(mint, Decimal("0")) + amount
                    ui_amounts[mint] = ui_amounts.get(mint, Decimal("0")) + ui_amount
            except Exception:
                continue
    if successful_program_scans == 0:
        raise RuntimeError("both Solana token-program scans failed")

    xstock_registry = _xstocks_solana_mint_index()
    ondo_registry = _ondo_solana_registry()
    cg_identity = _coingecko_solana_mint_index()  # identity only, never its price
    all_tokens, stock_items = [], []

    for mint, raw_amount in amounts.items():
        low = mint.lower()
        xs = xstock_registry.get(low)
        od = ondo_registry.get(low)
        known = SOLANA_KNOWN_ASSETS.get(mint) or {}
        cg_meta = cg_identity.get(low) or {}
        meta = dict(xs or od or known or {})
        if not meta and int(decimals_by_mint.get(mint, 0) or 0) > 0:
            # Skip expensive Metaplex program scans for obvious 0-decimal
            # NFTs/position receipts; fungible unknowns still get metadata.
            meta = _solana_metaplex_metadata(mint) or {}
        symbol = meta.get("symbol") or cg_meta.get("symbol") or "?"
        name = meta.get("name") or cg_meta.get("name") or ""
        economic_amount = raw_amount
        multiplier = Decimal("1")
        multiplier_source = "none"
        provider = None
        asset_type = None
        ticker = None
        price = None
        include = False

        if xs or (known and str(known.get("symbol") or "").lower().endswith("x")):
            xsmeta = xs or known
            symbol = xsmeta.get("symbol") or symbol
            ticker = xsmeta.get("underlyingSymbol") or xsmeta.get("yahoo_ticker") or (symbol[:-1] if symbol.lower().endswith("x") else "")
            economic_amount = ui_amounts.get(mint, raw_amount)
            multiplier = (economic_amount / raw_amount) if raw_amount > 0 else Decimal("1")
            multiplier_source = "solana-rpc-uiAmountString"
            include = _portfolio_security_allowed(ticker=ticker, symbol=symbol)
            price = _nav_stock_price_usd(ticker) if include else None
            provider, asset_type = "xstocks", "xstock"
        elif od:
            symbol = od.get("symbol") or symbol
            ticker = od.get("ticker") or ""
            provider, asset_type = "ondo", od.get("kind")
            if od.get("kind") == "ondo-stock":
                # Ondo Token-2022 security quantities can also use scaled UI.
                economic_amount = ui_amounts.get(mint, raw_amount)
                multiplier = (economic_amount / raw_amount) if raw_amount > 0 else Decimal("1")
                multiplier_source = "solana-rpc-uiAmountString"
                include = _portfolio_security_allowed(ticker=ticker, symbol=symbol)
                price = _nav_stock_price_usd(ticker) if include else None
            else:
                include = str(symbol or "").upper() in PORTFOLIO_STABLE_SYMBOLS
                price = _portfolio_raw_token_price_usd(symbol) if include else None
        else:
            # Unknown tokens are enumerated and diagnosed first, then filtered.
            ticker_guess = _xstock_yahoo_ticker({**meta, "coingecko_id": cg_meta.get("id")})
            if ticker_guess and _portfolio_security_allowed(ticker=ticker_guess, symbol=symbol):
                ticker = ticker_guess
                include = True
                price = _nav_stock_price_usd(ticker)
                provider, asset_type = "identified-by-metadata", "security"
            else:
                include = _portfolio_raw_token_price_usd(symbol) is not None
                price = _portfolio_raw_token_price_usd(symbol) if include else None

        diag = {
            "mint": mint, "symbol": symbol, "name": name,
            "raw_amount": str(raw_amount), "amount": str(economic_amount),
            "decimals": decimals_by_mint.get(mint), "program": token_program_by_mint.get(mint),
            "provider": provider, "underlying_ticker": ticker,
            "portfolio_include": include, "multiplier": str(multiplier),
            "multiplier_source": multiplier_source,
        }
        all_tokens.append(diag)
        _audit_log(
            f"[ALEF] Solana token: {symbol} raw={raw_amount} economic={economic_amount} mint={mint} "
            f"provider={provider or '?'} portfolio_include={include}", file=sys.stderr,
        )

        if not include or price is None or price <= 0:
            continue
        value = economic_amount * price
        total += value
        item = {
            "symbol": symbol, "mint": mint, "raw_amount": str(raw_amount),
            "multiplier": str(multiplier), "multiplier_source": multiplier_source,
            "amount": str(economic_amount), "price_usd": str(price), "value_usd": str(value),
            "underlying_ticker": ticker or "", "provider": provider or "raw",
            "asset_type": asset_type or "portfolio-token", "price_source": "yfinance/coingecko",
        }
        items.append(item)
        if ticker:
            stock_items.append(item)

    return {
        "chain": "solana", "assets_usd": total, "items": items,
        "token_scan_complete": True, "tokenized_scan_complete": True,
        "all_tokens": all_tokens, "stock_items": stock_items,
        "provider_diagnostics": {
            "xStocks": {"deployments_scanned": len(xstock_registry), "positions": sum(1 for x in stock_items if x.get('provider') == 'xstocks')},
            "Ondo": {"deployments_scanned": len(ondo_registry), "positions": sum(1 for x in stock_items if x.get('provider') == 'ondo')},
        },
        # Raydium CLMM positions are represented by 1-of-1 position NFTs.
        "candidate_position_nfts": [
            mint for mint, amt in amounts.items()
            if amt == Decimal("1") and int(decimals_by_mint.get(mint, -1)) == 0
        ],
    }


# ===============================================================
# Raydium CLMM positions (position NFT -> PersonalPositionState PDA)
# ===============================================================
_B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
_B58_MAP = {c: i for i, c in enumerate(_B58_ALPHABET)}

def _b58decode(s):
    n = 0
    for ch in str(s):
        n = n * 58 + _B58_MAP[ch]
    body = n.to_bytes((n.bit_length() + 7) // 8, "big") if n else b""
    zeros = len(str(s)) - len(str(s).lstrip("1"))
    return b"\x00" * zeros + body

def _b58encode(b):
    b = bytes(b)
    zeros = len(b) - len(b.lstrip(b"\x00"))
    n = int.from_bytes(b, "big")
    chars = []
    while n:
        n, r = divmod(n, 58)
        chars.append(_B58_ALPHABET[r])
    return "1" * zeros + ("".join(reversed(chars)) if chars else "")

def _ed25519_point_is_on_curve(pubkey32):
    # Solana PDAs must fall OFF the Ed25519 curve. This is the standard
    # compressed-point validity test used only for PDA derivation.
    if len(pubkey32) != 32:
        return False
    p = 2**255 - 19
    d = (-121665 * pow(121666, p - 2, p)) % p
    y = int.from_bytes(pubkey32, "little") & ((1 << 255) - 1)
    if y >= p:
        return False
    y2 = (y * y) % p
    u = (y2 - 1) % p
    v = (d * y2 + 1) % p
    x2 = (u * pow(v, p - 2, p)) % p
    x = pow(x2, (p + 3) // 8, p)
    if (x * x - x2) % p != 0:
        x = (x * pow(2, (p - 1) // 4, p)) % p
    return (x * x - x2) % p == 0

def _solana_find_program_address(seeds, program_id):
    program = _b58decode(program_id)
    if len(program) != 32:
        raise ValueError("invalid Solana program id")
    for bump in range(255, -1, -1):
        h = hashlib.sha256()
        for seed in seeds:
            if len(seed) > 32:
                raise ValueError("PDA seed too long")
            h.update(seed)
        h.update(bytes([bump]))
        h.update(program)
        h.update(b"ProgramDerivedAddress")
        digest = h.digest()
        if not _ed25519_point_is_on_curve(digest):
            return _b58encode(digest), bump
    raise RuntimeError("unable to derive Solana PDA")

def fetch_raydium_clmm_assets_usd(candidate_nft_mints):
    """
    Count ONLY the USDC side of ALEF's Raydium CLMM positions.

    Raydium's aggregate positionInfo.usdValue includes both sides of the LP.
    That is inappropriate for ALEF NAV when the other side is ALEF itself,
    because ALEF must not count its own token as backing. Therefore this
    function uses the token-specific amountA/amountB fields and the matching
    unclaimedFee amountA/amountB field, counting only canonical Solana USDC.

    Non-USDC principal, ALEF principal, non-USDC fees, and rewards are all
    deliberately excluded from NAV.
    """
    items = []
    total = Decimal("0")
    seen = set()

    for nft_mint in candidate_nft_mints or []:
        try:
            mint_bytes = _b58decode(nft_mint)
            if len(mint_bytes) != 32:
                continue
            position_pda, _ = _solana_find_program_address(
                [b"position", mint_bytes], RAYDIUM_CLMM_PROGRAM_ID
            )
            data = _json_get(
                "https://dynamic-ipfs.raydium.io/clmm/position",
                params={"id": position_pda}, timeout=25,
            )
            pinfo = (data or {}).get("positionInfo") or {}
            pool = (data or {}).get("poolInfo") or {}
            if not pinfo or position_pda in seen:
                continue
            seen.add(position_pda)

            mint_a = pool.get("mintA") or {}
            mint_b = pool.get("mintB") or {}
            sym_a = (mint_a.get("symbol") or "?").upper()
            sym_b = (mint_b.get("symbol") or "?").upper()
            addr_a = mint_a.get("address") or ""
            addr_b = mint_b.get("address") or ""

            amount_a = D(pinfo.get("amountA"))
            amount_b = D(pinfo.get("amountB"))
            uf = pinfo.get("unclaimedFee") or {}
            fee_a = D(uf.get("amountA"))
            fee_b = D(uf.get("amountB"))

            # Match the canonical Solana USDC mint first; symbol is retained as
            # a compatibility fallback in case Raydium omits/changes formatting.
            a_is_usdc = (addr_a == SOLANA_USDC_MINT) or (sym_a == "USDC")
            b_is_usdc = (addr_b == SOLANA_USDC_MINT) or (sym_b == "USDC")

            if a_is_usdc:
                usdc_principal = amount_a
                usdc_fees = fee_a
                usdc_side = "A"
            elif b_is_usdc:
                usdc_principal = amount_b
                usdc_fees = fee_b
                usdc_side = "B"
            else:
                # It may be a genuine Raydium position, but it contributes no
                # Raydium value to ALEF NAV unless one side is USDC.
                continue

            # USDC is treated at $1.00, consistent with the rest of the NAV
            # engine's cash/stablecoin accounting.
            value = usdc_principal + usdc_fees
            if value <= 0:
                continue

            item = {
                "symbol": f"Raydium USDC only ({sym_a}/{sym_b})",
                "provider": "raydium-clmm-usdc-only",
                "position_nft_mint": nft_mint,
                "position_pda": position_pda,
                "pool_id": pool.get("id"),
                "usdc_side": usdc_side,
                "usdc_principal": str(usdc_principal),
                "usdc_unclaimed_fees": str(usdc_fees),
                "excluded_non_usdc_amount_a": str(Decimal("0") if a_is_usdc else amount_a),
                "excluded_non_usdc_amount_b": str(Decimal("0") if b_is_usdc else amount_b),
                "value_usd": str(value),
            }
            items.append(item)
            total += value
        except Exception:
            # Most 1-of-1 NFTs are not Raydium positions. Silence those misses.
            continue

    live = bool(items)
    if not live and RAYDIUM_MANUAL_FALLBACK_USD > 0:
        total = D(RAYDIUM_MANUAL_FALLBACK_USD)
        items = [{
            "symbol": "Raydium USDC only (manual fallback)",
            "provider": "raydium-manual-fallback-usdc-only",
            "value_usd": str(total),
        }]
        _audit_log(
            f"[WARN] Live Raydium USDC-side discovery returned no value; "
            f"using configured USDC-only fallback ${total:.2f}.", file=sys.stderr,
        )

    return {
        "chain": "raydium", "assets_usd": total, "items": items,
        "token_scan_complete": True, "tokenized_scan_complete": True,
        "stock_items": [], "live_position_count": len(items) if live else 0,
        "used_manual_fallback": not live and total > 0,
    }

def _ton_raw_address(address):
    """Normalize raw or user-friendly TON address to '<workchain>:<64 hex>'."""
    a = str(address or "").strip()
    if not a:
        return None
    if ":" in a:
        wc, h = a.split(":", 1)
        h = h.lower()
        if len(h) == 64:
            try:
                int(h, 16)
                return f"{int(wc)}:{h}"
            except Exception:
                pass
    try:
        padded = a + "=" * ((4 - len(a) % 4) % 4)
        raw = base64.urlsafe_b64decode(padded.encode("ascii"))
        # User-friendly address = tag, signed workchain byte, 32-byte hash, CRC16.
        if len(raw) == 36:
            wc = int.from_bytes(raw[1:2], "big", signed=True)
            return f"{wc}:{raw[2:34].hex()}"
    except Exception:
        pass
    return a


def _toncenter_owner_jettons(wallet):
    """Enumerate all non-zero Jetton wallets owned by a TON address."""
    all_rows = []
    metadata = {}
    address_book = {}
    offset = 0
    while True:
        data = _json_get(
            "https://toncenter.com/api/v3/jetton/wallets",
            params={
                "owner_address": wallet,
                "exclude_zero_balance": "true",
                "limit": 1000,
                "offset": offset,
            },
            timeout=30,
        )
        rows = data.get("jetton_wallets") or []
        if not isinstance(rows, list):
            raise RuntimeError(f"TON Center jetton-wallet response malformed: {data}")
        all_rows.extend(rows)
        metadata.update(data.get("metadata") or {})
        address_book.update(data.get("address_book") or {})
        if len(rows) < 1000:
            break
        offset += len(rows)
        if offset > 10000:
            raise RuntimeError("TON jetton pagination exceeded safety limit")
    return {"jetton_wallets": all_rows, "metadata": metadata, "address_book": address_book}


def _ton_jetton_decimals(master, ton_data, xmeta=None):
    """Read jetton decimals from xStocks deployment metadata or TON Center metadata."""
    xmeta = xmeta or {}
    if xmeta.get("decimals") is not None:
        try:
            return int(xmeta["decimals"])
        except Exception:
            pass
    target = _ton_raw_address(master)
    metadata = ton_data.get("metadata") or {}
    for key, entry in metadata.items():
        if _ton_raw_address(key) != target or not isinstance(entry, dict):
            continue
        infos = entry.get("token_info") or []
        for info in infos if isinstance(infos, list) else []:
            if not isinstance(info, dict):
                continue
            for candidate in (info.get("decimals"), (info.get("extra") or {}).get("decimals")):
                if candidate is not None:
                    try:
                        return int(candidate)
                    except Exception:
                        pass
    return None


def _ton_metadata_identity(master, ton_data):
    """Best-effort symbol/name for a TON jetton master from Toncenter metadata."""
    target = _ton_raw_address(master)
    metadata = ton_data.get("metadata") or {}
    for key, entry in metadata.items():
        if _ton_raw_address(key) != target or not isinstance(entry, dict):
            continue
        out = {"symbol": "", "name": ""}
        infos = entry.get("token_info") or []
        if isinstance(infos, list):
            for info in infos:
                if not isinstance(info, dict):
                    continue
                if not out["symbol"]:
                    out["symbol"] = str(info.get("symbol") or (info.get("extra") or {}).get("symbol") or "")
                if not out["name"]:
                    out["name"] = str(info.get("name") or (info.get("extra") or {}).get("name") or "")
        return out
    return {"symbol": "", "name": ""}


def _ton_stack_numbers(obj):
    """Extract integer TVM stack values from Toncenter's several response shapes."""
    vals = []
    def walk(x):
        if isinstance(x, dict):
            # Common Toncenter forms: {"number":"123"}, {"value":"0x..."}.
            for k in ("number", "value"):
                v = x.get(k)
                if isinstance(v, str):
                    try:
                        vals.append(int(v, 0))
                        break
                    except Exception:
                        pass
            for v in x.values():
                if isinstance(v, (dict, list, tuple)):
                    walk(v)
        elif isinstance(x, (list, tuple)):
            # Classic stack entry: ["num", "0x123"] or ["num", "123"].
            if len(x) >= 2 and isinstance(x[0], str) and x[0].lower() in ("num", "int"):
                try:
                    vals.append(int(str(x[1]), 0))
                    return
                except Exception:
                    pass
            for v in x:
                walk(v)
    walk(obj)
    return vals


def _ton_display_multiplier(master):
    """Read TEP-526 get_display_multiplier() = numerator / denominator on-chain."""
    try:
        data = _json_get(
            f"{TONCENTER_API}/runGetMethod",
            params={"address": master, "method": "get_display_multiplier", "stack": "[]"},
            timeout=20,
        )
        if data.get("ok") is False:
            return None
        stack = (data.get("result") or {}).get("stack") or data.get("stack") or []
        nums = _ton_stack_numbers(stack)
        if len(nums) >= 2 and nums[0] > 0 and nums[1] > 0:
            return Decimal(nums[0]) / Decimal(nums[1])
    except Exception:
        pass
    return None


def fetch_ton_stock_assets_usd(wallet):
    """
    Enumerate ALL non-zero TON jettons first. Native TON/GRAM is excluded.
    Recognized portfolio xStocks are valued with their Yahoo Finance underlying
    price. TON/issuer calls are used only for the display multiplier (quantity).
    """
    ton_data = _toncenter_owner_jettons(wallet)
    registry_raw = _xstocks_chain_index("ton")
    registry = {}
    for _, meta in registry_raw.items():
        raw = _ton_raw_address(meta.get("address"))
        if raw:
            registry[raw] = meta

    all_tokens = []
    stock_items = []
    failures = []
    total = Decimal("0")

    for row in ton_data.get("jetton_wallets") or []:
        master = row.get("jetton")
        raw_master = _ton_raw_address(master)
        identity = _ton_metadata_identity(master, ton_data)
        meta = registry.get(raw_master)
        if not meta:
            book = (ton_data.get("address_book") or {}).get(master) or {}
            friendly = book.get("user_friendly") if isinstance(book, dict) else None
            meta = registry.get(_ton_raw_address(friendly)) if friendly else None

        raw_balance = D(row.get("balance"))
        if raw_balance <= 0:
            raw_balance = D((row.get("mintless_info") or {}).get("amount"))
        if raw_balance <= 0:
            continue

        decimals = _ton_jetton_decimals(master, ton_data, meta or {})
        raw_amount = None if decimals is None else raw_balance / (Decimal(10) ** decimals)
        symbol = (meta or {}).get("symbol") or identity.get("symbol") or "?"
        name = (meta or {}).get("name") or identity.get("name") or ""
        ticker = (meta or {}).get("underlyingSymbol") or (symbol[:-1] if symbol.lower().endswith("x") else "")
        include = bool(meta) and _portfolio_security_allowed(ticker=ticker, symbol=symbol)

        token_diag = {
            "master": master, "symbol": symbol, "name": name,
            "raw_amount": str(raw_amount) if raw_amount is not None else "?",
            "decimals": decimals, "recognized_xstock": bool(meta),
            "portfolio_included": include, "underlying_ticker": ticker,
        }
        all_tokens.append(token_diag)
        _audit_log(
            f"[ALEF] TON jetton: {symbol} amount={token_diag['raw_amount']} "
            f"master={master} xstock={bool(meta)} portfolio_include={include}", file=sys.stderr,
        )

        if not include:
            continue
        if raw_amount is None:
            failures.append(f"{symbol}: unknown decimals")
            continue

        onchain_mult = _ton_display_multiplier(master)
        multiplier, mult_source = _resolve_xstock_multiplier(
            symbol, "TON", onchain=onchain_mult, registry_meta=(meta or {}),
        )
        amount = raw_amount * multiplier
        price = _nav_stock_price_usd(ticker)
        if price is None or price <= 0:
            failures.append(f"{symbol}: Yahoo Finance has no price for {ticker}")
            continue

        value = amount * price
        total += value
        item = {
            "symbol": symbol, "address": (meta or {}).get("address") or master,
            "raw_amount": str(raw_amount), "multiplier": str(multiplier),
            "multiplier_source": mult_source, "amount": str(amount),
            "price_usd": str(price), "value_usd": str(value),
            "underlying_symbol": ticker, "provider": "xstocks", "asset_type": "xstock",
            "identity_source": "xstocks-official-registry", "price_source": "yfinance",
        }
        stock_items.append(item)
        _audit_log(
            f"        {symbol}: {amount} {ticker}-equivalent Γ— Yahoo ${price} = ${value:.6f} "
            f"(multiplier={multiplier} via {mult_source})", file=sys.stderr,
        )

    if failures:
        raise RuntimeError("TON portfolio stock holdings found but could not be fully valued: " + "; ".join(failures[:10]))

    return {
        "chain": "ton", "assets_usd": total, "items": stock_items,
        "token_scan_complete": True, "tokenized_scan_complete": True,
        "all_tokens": all_tokens, "stock_items": stock_items,
        "xstocks_positions": len(stock_items),
        "xstocks_deployments_scanned": len(registry),
        "native_gram_included": False,
    }

# ===============================================================
# ALEF SPL token supply from Solana
# ===============================================================

def fetch_solana_token_supply(mint):
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "getTokenSupply",
        "params": [mint, {"commitment": "confirmed"}],
    }
    data = _json_post(SOLANA_RPC, payload, timeout=20)
    if data.get("error"):
        raise RuntimeError(f"Solana getTokenSupply error: {data['error']}")
    value = ((data.get("result") or {}).get("value") or {})
    ui = value.get("uiAmountString")
    if ui is not None:
        supply = D(ui)
    else:
        amount = D(value.get("amount"))
        decimals = int(value.get("decimals", 0))
        supply = amount / (Decimal(10) ** decimals)
    if supply <= 0:
        raise RuntimeError("ALEF token supply is zero or unavailable")
    return supply


# ===============================================================
# Continuous fee state / anchor
# ===============================================================

def _load_fee_state():
    if not os.path.exists(ALEF_STATE_FILE):
        return None
    with open(ALEF_STATE_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def _save_fee_state(state):
    tmp = ALEF_STATE_FILE + ".tmp"
    with open(tmp, "w", encoding="utf-8") as f:
        json.dump(state, f, indent=2)
    os.replace(tmp, ALEF_STATE_FILE)


def _parse_utc(s):
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"
    dt = datetime.fromisoformat(s)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def fetch_last_alef_supply_event():
    """
    Return the datetime of the most recent mintTo/mintToChecked/burn/burnChecked
    instruction on the ALEF mint, fetched directly from the Solana blockchain.

    This makes the fee accrual start self-enforcing: every time ALEF is minted
    or burned, the fee clock automatically resets to that transaction's timestamp.
    That means newly issued supply is never over-charged, and no manual settlement
    call is required β€” the blockchain IS the settlement record.

    Falls back to FEE_ACCRUAL_START (ALEF deployment date) if the query fails or
    returns no matching transactions.
    """
    _ALEF_MINT = "FBHd9upXFkeWSwe9qEdcgRLa4Y6uzsLCSawrpfPkQZGg"
    _SUPPLY_TYPES = {"mintTo", "mintToChecked", "burn", "burnChecked"}
    # Reliable public endpoints tried in order; the official mainnet-beta RPC
    # frequently times out on getSignaturesForAddress under load.
    _RPCS = [
        "https://solana-rpc.publicnode.com",
        "https://rpc.ankr.com/solana",
        "https://api.mainnet-beta.solana.com",
    ]

    def _rpc(url, method, params):
        payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params}
        r = requests.post(url, json=payload, timeout=25,
                          headers={"Content-Type": "application/json"})
        r.raise_for_status()
        data = r.json()
        if data.get("error"):
            raise RuntimeError(f"Solana RPC error: {data['error']}")
        return data.get("result")

    for rpc_url in _RPCS:
        try:
            before = None
            # Scan up to 100 transactions in pages of 20; most transfers cluster
            # near the top so a mint/burn is usually found within the first page.
            for _page in range(5):
                params = {"limit": 20, "commitment": "finalized"}
                if before:
                    params["before"] = before
                sigs = _rpc(rpc_url, "getSignaturesForAddress", [_ALEF_MINT, params]) or []
                if not sigs:
                    break
                before = sigs[-1].get("signature")
                for sig_info in sigs:
                    sig = sig_info.get("signature")
                    if not sig:
                        continue
                    tx = _rpc(rpc_url, "getTransaction",
                              [sig, {"encoding": "jsonParsed",
                                     "maxSupportedTransactionVersion": 0,
                                     "commitment": "finalized"}])
                    if not tx:
                        continue
                    block_time = tx.get("blockTime")
                    if not block_time:
                        continue
                    msg = (tx.get("transaction") or {}).get("message") or {}
                    all_ixs = list(msg.get("instructions") or [])
                    for inner in (tx.get("meta") or {}).get("innerInstructions") or []:
                        all_ixs.extend(inner.get("instructions") or [])
                    for ix in all_ixs:
                        parsed = ix.get("parsed")
                        if not isinstance(parsed, dict):
                            continue
                        if parsed.get("type") not in _SUPPLY_TYPES:
                            continue
                        info = parsed.get("info") or {}
                        if info.get("mint") != _ALEF_MINT:
                            continue
                        return datetime.fromtimestamp(block_time, tz=timezone.utc)
            # Exhausted 100 txs with no mint/burn found on this RPC β€” fall through
            break
        except Exception as exc:
            _progress(f"fetch_last_alef_supply_event: {rpc_url} failed ({exc}), trying next RPC")

    _progress("fetch_last_alef_supply_event: all RPCs failed or no event found β€” using FEE_ACCRUAL_START fallback")
    return FEE_ACCRUAL_START


def settle_fees():
    """
    With blockchain-derived fee accrual, settlement is automatic: every mint or
    burn of ALEF resets the fee clock on-chain. No manual action is needed.

    If you want to manually record a settlement (e.g. for accounting purposes),
    call this function to update FEE_ACCRUAL_START in the script. Otherwise the
    fee start date is always fetched live from the blockchain by apply_continuous_fee().
    """
    now = datetime.now(timezone.utc)
    new_line = (
        f"FEE_ACCRUAL_START = datetime({now.year}, {now.month}, {now.day}, "
        f"{now.hour}, {now.minute}, {now.second}, tzinfo=timezone.utc)"
        f"  # settled {now.strftime('%Y-%m-%d')}"
    )
    try:
        script = os.path.abspath(__file__)
    except NameError:
        print("settle_fees(): cannot locate script file in notebook context β€” "
              f"update FEE_ACCRUAL_START manually to: {new_line}")
        return
    with open(script, "r", encoding="utf-8") as f:
        src = f.read()
    import re as _re
    new_src = _re.sub(r"^FEE_ACCRUAL_START = datetime\(.*$", new_line, src, flags=_re.MULTILINE)
    if new_src == src:
        print("settle_fees(): FEE_ACCRUAL_START line not found β€” update manually.")
        return
    with open(script, "w", encoding="utf-8") as f:
        f.write(new_src)
    print(f"Fee settlement recorded. FEE_ACCRUAL_START updated to {now.strftime('%Y-%m-%d %H:%M:%S')} UTC.")


def apply_continuous_fee(gross_nav_usd, token_supply):
    """
    Apply the continuously accrued annual management fee.

    The fee start date is the most recent ALEF mint-or-burn event on Solana,
    fetched live from the blockchain. This makes fee accounting self-enforcing:
    every supply change automatically resets the fee clock, so newly issued ALEF
    is never over-charged with fees it hasn't accrued.

        accrual_start    = last mintTo/mintToChecked/burn/burnChecked on ALEF mint
        elapsed_years    = (now - accrual_start) in years
        fee_factor       = exp(-ANNUAL_FEE_RATE * elapsed_years)
        fee_adjusted_nav = gross_nav * fee_factor
        ALEF price       = fee_adjusted_nav / token_supply

    FEE_ACCRUAL_START (ALEF deployment date) is used only as a fallback if the
    blockchain query fails.
    """
    gross_nav_usd = D(gross_nav_usd)
    token_supply = D(token_supply)
    if gross_nav_usd <= 0 or token_supply <= 0:
        raise RuntimeError("Gross NAV and ALEF token supply must be positive")

    _progress("Fetching last ALEF mint/burn event for fee accrual start...")
    accrual_start = fetch_last_alef_supply_event()
    _progress(f"Fee accrual start: {accrual_start.strftime('%Y-%m-%d %H:%M:%S')} UTC")

    now = datetime.now(timezone.utc)
    elapsed_seconds = max(0.0, (now - accrual_start).total_seconds())
    elapsed_years = elapsed_seconds / (365.25 * 24 * 60 * 60)

    fee_factor = Decimal(str(math.exp(-float(ANNUAL_FEE_RATE) * elapsed_years)))
    fee_adjusted_nav = gross_nav_usd * fee_factor
    fee_drag_usd = gross_nav_usd - fee_adjusted_nav
    alef_price = fee_adjusted_nav / token_supply

    return {
        "gross_nav_per_token": gross_nav_usd / token_supply,
        "fee_adjusted_gross_nav_usd": fee_adjusted_nav,
        "fee_adjusted_nav_usd": fee_adjusted_nav,
        "fee_drag_usd": fee_drag_usd,
        "alef_price_usd": alef_price,
        "fee_multiplier": fee_factor,
        "fee_accrual_start_utc": accrual_start.isoformat(),
        "fee_elapsed_days": elapsed_seconds / 86400,
        "newly_initialized": False,
    }


# ===============================================================
# Aggregate ALEF NAV
# ===============================================================

def fetch_alef_nav_and_price(trx_usd, eth_usd=None, bnb_usd=None, sol_usd=None):
    _progress("Loading published ALEF reserve addresses...")
    addresses = fetch_alef_addresses()
    _progress("Reserve addresses loaded.")
    evm_wallet = addresses["evm"]
    tron_wallet = addresses["tron"]
    ton_wallet = addresses["ton"]
    solana_wallet = addresses["solana"]
    alef_mint = addresses["alef_mint"]

    # Fill native-chain prices if they were not already fetched by main().
    missing_ids = []
    if eth_usd is None: missing_ids.append("ethereum")
    if bnb_usd is None: missing_ids.append("binancecoin")
    if sol_usd is None: missing_ids.append("solana")
    if missing_ids:
        _progress("Fetching missing native-chain reference prices...")
    native = _coingecko_simple_id_prices(missing_ids)
    eth_usd = D(eth_usd) if eth_usd is not None else native.get("ethereum")
    bnb_usd = D(bnb_usd) if bnb_usd is not None else native.get("binancecoin")
    sol_usd = D(sol_usd) if sol_usd is not None else native.get("solana")
    if None in (eth_usd, bnb_usd, sol_usd):
        raise RuntimeError("Missing one or more native prices required for ETH/BNB/SOL wallet NAV")

    protocol_fetches = [
        ("Aave", lambda: fetch_aave_position_usd(evm_wallet)),
        ("Morpho", lambda: fetch_morpho_position_usd(evm_wallet)),
        ("JustLend", lambda: fetch_justlend_position_usd(tron_wallet, trx_usd)),
        ("Venus", lambda: fetch_venus_position_usd(evm_wallet, bnb_usd=bnb_usd)),
    ]

    results = []
    failures = []
    for name, fn in protocol_fetches:
        _progress(f"Reading {name} positions...")
        try:
            r = fn()
            results.append(r)
            _progress(f"{name} complete.")
            extra = ""
            if name == "Aave":
                extra = f" version={r.get('version', '?')} positions={r.get('position_count', '?')}"
            elif name == "Venus":
                extra = (f" core_positions={r.get('core_position_count', 0)} "
                         f"flux_positions={r.get('flux_position_count', 0)} "
                         f"flux_lend_positions={r.get('flux_lend_position_count', 0)} "
                         f"flux_vault_positions={r.get('flux_vault_position_count', 0)} "
                         f"flux_source={r.get('flux_source', '?')} "
                         f"flux_rows={r.get('flux_rows_seen', 0)} "
                         f"flux_raw_positions={r.get('flux_raw_position_count', 0)} "
                         f"flux_vault_rows={r.get('flux_vault_rows_seen', 0)} "
                         f"markets_scanned={r.get('market_count_scanned', 0)} "
                         f"snapshot_batches={r.get('venus_snapshot_rpc_batches', 0)} "
                         f"vai_batches={r.get('venus_vai_rpc_batches', 0)}")
            _audit_log(
                f"[ALEF] {name}: assets=${r['assets_usd']:.6f} "
                f"liabilities=${r['liabilities_usd']:.6f}{extra}",
                file=sys.stderr,
            )
            if name == "Venus":
                for pos in r.get("positions", []):
                    if str(pos.get("source", "")).startswith("flux"):
                        if pos.get("source") == "flux-vault-nft":
                            _audit_log(
                                f"        Flux vault NFT #{pos.get('nft_id','?')}: "
                                f"supply {pos.get('supply_amount','?')} {pos.get('supply_symbol','?')} "
                                f"=${D(pos.get('supply_usd')):.6f}; "
                                f"borrow {pos.get('borrow_amount','?')} {pos.get('borrow_symbol','?')} "
                                f"=${D(pos.get('borrow_usd')):.6f}",
                                file=sys.stderr,
                            )
                        else:
                            _audit_log(
                                f"        Flux {pos.get('symbol','?')}: amount={pos.get('amount','?')} "
                                f"value=${D(pos.get('supply_usd')):.6f}",
                                file=sys.stderr,
                            )
        except Exception as e:
            _progress(f"{name} failed.")
            failures.append((name, e))
            _audit_log(f"[WARN] ALEF {name} read failed: {e}", file=sys.stderr)

    if failures and ALEF_STRICT_MODE:
        names = ", ".join(name for name, _ in failures)
        raise RuntimeError(
            f"ALEF NAV aborted because these protocol reads failed: {names}. "
            "Strict mode prevents publishing a partial NAV."
        )

    # Exclude JustLend receipt tokens from the raw TRON wallet so the supplied
    # underlying is not counted twice.
    justlend = next((r for r in results if r.get("protocol") == "JustLend"), {})
    jtokens = justlend.get("jtoken_addresses") or []

    # Morpho vault shares are already included by the Morpho API. Exclude
    # their ERC-20 share tokens from the raw Ethereum wallet scan.
    morpho = next((r for r in results if r.get("protocol") == "Morpho"), {})
    morpho_receipts = []
    for vp in (morpho.get("vault_positions") or []) + (morpho.get("vault_v2_positions") or []):
        addr = (vp.get("vault") or {}).get("address")
        if addr:
            morpho_receipts.append(addr)

    venus = next((r for r in results if r.get("protocol") == "Venus"), {})
    venus_receipts = venus.get("receipt_token_addresses") or []

    wallet_fetches = [
        ("Ethereum wallet", lambda: fetch_evm_wallet_assets_usd(
            evm_wallet, chain="ethereum", rpc_url=ETHEREUM_RPC,
            native_symbol="ETH", native_usd=eth_usd, exclude_contracts=morpho_receipts)),
        ("BNB wallet", lambda: fetch_evm_wallet_assets_usd(
            evm_wallet, chain="bsc", rpc_url=BNB_RPC,
            native_symbol="BNB", native_usd=bnb_usd, exclude_contracts=venus_receipts)),
        ("TRON wallet", lambda: fetch_tron_wallet_assets_usd(
            tron_wallet, trx_usd, exclude_contracts=jtokens)),
        ("Solana wallet", lambda: fetch_solana_wallet_assets_usd(
            solana_wallet, sol_usd, alef_mint,
            extra_excluded_mints={SOLANA_USDC_MINT})),
        ("TON stock wallet", lambda: fetch_ton_stock_assets_usd(ton_wallet)),
    ]

    wallet_results = []
    wallet_failures = []
    for name, fn in wallet_fetches:
        _progress(f"Reading {name}...")
        try:
            r = fn()
            if ALEF_STRICT_MODE and not r.get("tokenized_scan_complete", True):
                raise RuntimeError("tokenized-asset cross-chain scan was incomplete")
            if ALEF_STRICT_MODE and not r.get("token_scan_complete", True):
                if r.get("chain") == "bsc":
                    _audit_log(
                        "[ALEF] BNB raw BEP-20 enumeration unavailable; native BNB and Venus "
                        "Core/isolated/Flux positions are still included. Optional ETHERSCAN_API_KEY enables raw-token scanning.",
                        file=sys.stderr,
                    )
                else:
                    raise RuntimeError("token enumeration was incomplete")
            wallet_results.append(r)
            _progress(f"{name} complete.")
            stock_items = r.get("stock_items") or []
            if stock_items:
                chain_label = {"ethereum": "Ethereum", "bsc": "BNB", "solana": "Solana", "ton": "TON"}.get(r.get("chain"), r.get("chain", "chain"))
                summary = ", ".join(
                    f"{x.get('symbol','token')}[{x.get('provider','?')}]=${D(x.get('value_usd')):.2f}"
                    for x in stock_items
                )
                _audit_log(
                    f"[ALEF] {chain_label} tokenized stocks/ETFs ({len(stock_items)}): {summary}",
                    file=sys.stderr,
                )
            provider_diag = r.get("provider_diagnostics") or {}
            for provider, pd in provider_diag.items():
                _audit_log(
                    f"[ALEF] {name}: {provider} portfolio_contracts_probed={pd.get('deployments_scanned', 0)} "
                    f"registry_candidates={pd.get('registry_candidates', pd.get('deployments_scanned', 0))} "
                    f"positions_found={pd.get('positions', 0)}", file=sys.stderr,
                )
            if r.get("xstocks_deployments_scanned") is not None and not provider_diag:
                _audit_log(
                    f"[ALEF] {name}: xStocks deployments scanned={r.get('xstocks_deployments_scanned')} "
                    f"positions_found={len(stock_items)}", file=sys.stderr,
                )
            _audit_log(f"[ALEF] {name}: raw assets=${r['assets_usd']:.6f}", file=sys.stderr)
            for item in r.get("items") or []:
                if D(item.get("value_usd")) >= Decimal("0.01"):
                    label = item.get("symbol") or item.get("mint") or item.get("address") or "asset"
                    _audit_log(f"        {label}: ${D(item.get('value_usd')):.6f}", file=sys.stderr)
        except Exception as e:
            _progress(f"{name} failed.")
            wallet_failures.append((name, e))
            _audit_log(f"[WARN] ALEF {name} read failed: {e}", file=sys.stderr)

    # USDC reserve: live Raydium pool USDC, fully on-chain.
    # ALEF tokens on the other side of the pool are excluded β€” only USDC counts.
    _progress("Fetching Raydium ALEF/USDC pool USDC for NAV (wallet LP share only)...")
    _ray_usdc, _ray_pools = fetch_raydium_alef_usdc_reserves(solana_wallet)
    if _ray_usdc is None:
        _progress("WARNING: Raydium API unavailable β€” USDC reserve excluded from this NAV run.")
        _ray_usdc = Decimal("0")
    usdc_reserve = {
        "chain": "usdc_reserve",
        "assets_usd": _ray_usdc,
        "liabilities_usd": Decimal("0"),
        "items": [{
            "symbol": "USDC (Raydium pool, on-chain)",
            "amount": str(_ray_usdc),
            "value_usd": str(_ray_usdc),
            "source": "raydium-live",
        }],
        "raydium_usdc": str(_ray_usdc),
        "token_scan_complete": True,
        "tokenized_scan_complete": True,
    }
    _progress(f"Raydium USDC: ${_ray_usdc:,.2f}")
    wallet_results.append(usdc_reserve)
    _audit_log(f"[ALEF] Raydium USDC={_ray_usdc:.6f}", file=sys.stderr)

    # A failed chain-level wallet read makes NAV materially incomplete. In
    # strict mode, fail closed rather than silently publishing a low ALEF price.
    if wallet_failures and ALEF_STRICT_MODE:
        names = ", ".join(name for name, _ in wallet_failures)
        raise RuntimeError(
            f"ALEF NAV aborted because these wallet reads failed: {names}. "
            "Strict mode prevents publishing a partial NAV."
        )

    protocol_assets = sum((r["assets_usd"] for r in results), Decimal("0"))
    raw_wallet_assets = sum((r["assets_usd"] for r in wallet_results), Decimal("0"))
    total_assets = protocol_assets + raw_wallet_assets + MANUAL_ASSETS_USD
    total_liabilities = sum((r["liabilities_usd"] for r in results), Decimal("0")) + MANUAL_LIABILITIES_USD
    gross_nav = total_assets - total_liabilities

    _progress("Reading live ALEF token supply from Solana...")
    token_supply = fetch_solana_token_supply(alef_mint)
    _progress("Token supply loaded; applying continuous management fee...")
    fee_data = apply_continuous_fee(gross_nav, token_supply)
    _progress("NAV calculation complete.")

    # Human-auditable accounting table. Protocol rows are aggregate where the
    # upstream API provides aggregate USD values; wallet/issuer rows are shown
    # asset-by-asset.
    _audit_log("\n[ALEF] ================= NAV ACCOUNTING TABLE =================", file=sys.stderr)
    _audit_log(f"{'TYPE':<10} {'LOCATION':<18} {'ASSET / POSITION':<34} {'USD':>16}", file=sys.stderr)
    _audit_log("-" * 82, file=sys.stderr)
    for r in results:
        name = r.get("protocol", "protocol")
        av = D(r.get("assets_usd"))
        lv = D(r.get("liabilities_usd"))
        if av:
            _audit_log(f"{'ASSET':<10} {name:<18} {'supplied/collateral':<34} {av:>16,.2f}", file=sys.stderr)
        if lv:
            _audit_log(f"{'LIABILITY':<10} {name:<18} {'borrow/debt':<34} {lv:>16,.2f}", file=sys.stderr)
    for r in wallet_results:
        loc = {
            'ethereum':'Ethereum', 'bsc':'BNB Chain', 'tron':'TRON',
            'solana':'Solana', 'ton':'TON', 'raydium':'Raydium', 'usdc_reserve':'USDC Reserve'
        }.get(r.get('chain'), str(r.get('chain') or 'wallet'))
        for item in r.get("items") or []:
            v = D(item.get("value_usd"))
            if v <= 0:
                continue
            label = item.get("symbol") or item.get("mint") or item.get("address") or "asset"
            _audit_log(f"{'ASSET':<10} {loc:<18} {str(label)[:34]:<34} {v:>16,.2f}", file=sys.stderr)
    if MANUAL_ASSETS_USD:
        _audit_log(f"{'ASSET':<10} {'Manual':<18} {'manual adjustment':<34} {MANUAL_ASSETS_USD:>16,.2f}", file=sys.stderr)
    if MANUAL_LIABILITIES_USD:
        _audit_log(f"{'LIABILITY':<10} {'Manual':<18} {'manual adjustment':<34} {MANUAL_LIABILITIES_USD:>16,.2f}", file=sys.stderr)
    _audit_log("-" * 82, file=sys.stderr)
    _audit_log(f"{'TOTAL':<10} {'':<18} {'ASSETS':<34} {total_assets:>16,.2f}", file=sys.stderr)
    _audit_log(f"{'TOTAL':<10} {'':<18} {'LIABILITIES':<34} {total_liabilities:>16,.2f}", file=sys.stderr)
    _audit_log(f"{'NAV':<10} {'':<18} {'GROSS NAV':<34} {gross_nav:>16,.2f}", file=sys.stderr)
    _audit_log("=" * 82 + "\n", file=sys.stderr)

    _audit_log(
        f"[ALEF] PROTOCOL assets=${protocol_assets:.6f} | "
        f"RAW WALLETS=${raw_wallet_assets:.6f}", file=sys.stderr)
    if fee_data.get("newly_initialized"):
        _audit_log(
            "[ALEF] Fee clock initialized. No opening-price anchor is used; "
            "ALEF price is always fee_adjusted_NAV / supply.",
            file=sys.stderr,
        )

    _audit_log(
        f"[ALEF] TOTAL assets=${total_assets:.6f} liabilities=${total_liabilities:.6f} "
        f"gross_NAV=${gross_nav:.6f} supply={token_supply} "
        f"fee_rate={float(ANNUAL_FEE_RATE) * 100:.4f}%/yr "
        f"fee_multiplier={fee_data['fee_multiplier']:.10f} "
        f"fee_drag=${fee_data['fee_drag_usd']:.6f} "
        f"fee_adjusted_NAV=${fee_data['fee_adjusted_nav_usd']:.6f} "
        f"NAV_per_ALEF=${fee_data['alef_price_usd']:.8f} "
        f"ALEF=${fee_data['alef_price_usd']:.8f}",
        file=sys.stderr,
    )

    return {
        "addresses": addresses,
        "protocols": results,
        "wallets": wallet_results,
        "protocol_assets_usd": protocol_assets,
        "raw_wallet_assets_usd": raw_wallet_assets,
        "assets_usd": total_assets,
        "liabilities_usd": total_liabilities,
        "gross_nav_usd": gross_nav,
        "token_supply": token_supply,
        **fee_data,
        "raydium_usdc": next((w.get("raydium_usdc") for w in wallet_results if "raydium_usdc" in w), None),
    }


def fetch_raydium_alef_usdc_reserves(solana_wallet):
    """
    Return the USDC in all ALEF/USDC Raydium pools by reading the pool state
    directly from the Raydium API. The reserve address is the sole liquidity
    provider in these pools, so total pool USDC equals the reserve's USDC.
    Returns (Decimal, int) = (usdc_amount, pool_count).
    """
    _ALEF_MINT = "FBHd9upXFkeWSwe9qEdcgRLa4Y6uzsLCSawrpfPkQZGg"
    try:
        resp = _json_get(
            "https://api-v3.raydium.io/pools/info/mint"
            f"?mint1={_ALEF_MINT}&mint2={SOLANA_USDC_MINT}"
            "&poolType=all&poolSortField=liquidity&sortType=desc&pageSize=20&page=1",
            timeout=20,
        )
        pools = (resp or {}).get("data", {}).get("data") or []
        total_usdc = Decimal("0")
        pool_count = 0
        for pool in pools:
            mint_a = (pool.get("mintA") or {}).get("address", "")
            mint_b = (pool.get("mintB") or {}).get("address", "")
            if mint_a == SOLANA_USDC_MINT:
                total_usdc += D(pool.get("mintAmountA") or 0)
                pool_count += 1
            elif mint_b == SOLANA_USDC_MINT:
                total_usdc += D(pool.get("mintAmountB") or 0)
                pool_count += 1
        return total_usdc, pool_count
    except Exception as exc:
        _progress(f"Raydium pool query failed: {exc}")
        return None, 0


# ===============================================================
# Public auditor entry point
# ===============================================================
def calculate_alef_nav():
    """
    Calculate ALEF NAV/share and return the complete structured audit result.

    Only non-financial progress is printed; no prices/balances are printed and
    nothing is copied to the clipboard. USD marks use
    the same Yahoo Finance / CoinGecko price functions as the operational build.
    The returned result includes:
      addresses, protocols, wallets, protocol_assets_usd, raw_wallet_assets_usd,
      assets_usd, liabilities_usd, gross_nav_usd, token_supply, fee_multiplier,
      fee_drag_usd, fee_adjusted_nav_usd, and alef_price_usd.
    """
    _progress("Starting live NAV audit.")
    crypto_symbols = ['BTC', 'BNB', 'XRP', 'TRX', 'ETH', 'LEO', '', 'XAUT', 'PAXG']
    stock_tickers = ['SLV', '3GOL.L', 'UGL', 'PLTM', '', 'TQQQ', 'MAGX', 'AAPL',
                     'NVDA', 'AMZN', 'GOOG', 'TSLA', 'MSFT', 'META']

    _progress("Fetching CoinGecko/Yahoo reference prices...")
    crypto_prices = fetch_crypto_prices(crypto_symbols)
    stock_prices = fetch_stock_prices(stock_tickers)
    _progress("Reference prices loaded.")
    _seed_nav_price_caches(crypto_symbols, crypto_prices, stock_tickers, stock_prices)

    crypto_price_map = {
        sym: price for sym, price in zip(crypto_symbols, crypto_prices)
        if sym and price is not None
    }

    trx_usd = crypto_price_map.get("TRX")
    if trx_usd is None:
        raise RuntimeError("Cannot calculate ALEF NAV without TRX/USD for JustLend")

    return fetch_alef_nav_and_price(
        trx_usd,
        eth_usd=crypto_price_map.get("ETH"),
        bnb_usd=crypto_price_map.get("BNB"),
        sol_usd=crypto_price_map.get("SOL"),
    )


def _money(x):
    return f"${D(x):,.2f}"


def _qty(x, places=8):
    d = D(x)
    s = f"{d:,.{places}f}".rstrip("0").rstrip(".")
    return s if s else "0"


def _wallet_location(chain):
    return {
        "ethereum": "Ethereum",
        "bsc": "BNB Chain",
        "tron": "TRON",
        "solana": "Solana",
        "ton": "TON",
        "usdc_reserve": "USDC Reserve",
    }.get(str(chain or ""), str(chain or "Wallet"))


def print_audit_report(result):
    """Print a human-readable financial audit report from calculate_alef_nav()."""
    line = "=" * 88
    thin = "-" * 88

    print("\n" + line)
    print("ALEF NAV AUDIT REPORT")
    print(line)
    print(f"Generated (UTC):          {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Audit build:              {AUDIT_BUILD}")
    print(f"Management fee:           {float(ANNUAL_FEE_RATE) * 100:.4f}% per year, continuous")
    print("Price sources:            Yahoo Finance / CoinGecko")
    print("ALEF pricing rule:        fee-adjusted NAV / live ALEF supply")

    addresses = result.get("addresses") or {}
    if addresses:
        print("\nRESERVE ADDRESSES")
        print(thin)
        labels = [
            ("EVM (Ethereum/BNB)", "evm"),
            ("TRON", "tron"),
            ("Solana", "solana"),
            ("TON", "ton"),
            ("ALEF mint", "alef_mint"),
        ]
        for label, key in labels:
            if addresses.get(key):
                print(f"{label:<22} {addresses[key]}")

    print("\nPROTOCOL POSITIONS")
    print(thin)
    print(f"{'Protocol':<18}{'Assets':>20}{'Liabilities':>20}{'Net':>20}")
    print(thin)
    for row in result.get("protocols") or []:
        name = str(row.get("protocol") or "Protocol")
        assets = D(row.get("assets_usd"))
        liabilities = D(row.get("liabilities_usd"))
        print(f"{name:<18}{_money(assets):>20}{_money(liabilities):>20}{_money(assets-liabilities):>20}")

    print("\nWALLET / RESERVE ASSETS")
    print(thin)
    print(f"{'Location':<18}{'Asset':<34}{'Amount':>16}{'USD value':>20}")
    print(thin)
    item_count = 0
    for wallet in result.get("wallets") or []:
        location = _wallet_location(wallet.get("chain"))
        items = wallet.get("items") or []
        for item in items:
            value = D(item.get("value_usd"))
            if value <= 0:
                continue
            label = str(item.get("symbol") or item.get("ticker") or item.get("mint") or item.get("address") or "asset")
            amount = item.get("amount")
            amount_text = _qty(amount) if amount not in (None, "") else "β€”"
            print(f"{location:<18}{label[:34]:<34}{amount_text:>16}{_money(value):>20}")
            item_count += 1
        # Some wallet readers expose only an aggregate value. Show it if there
        # are no item rows so the report still reconciles to the returned NAV.
        if not items and D(wallet.get("assets_usd")) > 0:
            value = D(wallet.get("assets_usd"))
            print(f"{location:<18}{'(aggregate wallet assets)':<34}{'β€”':>16}{_money(value):>20}")
            item_count += 1
    if item_count == 0:
        print("(No positive wallet/reserve asset rows returned.)")

    print("\nNAV SUMMARY")
    print(thin)
    summary_rows = [
        ("Protocol assets", result.get("protocol_assets_usd")),
        ("Wallet/reserve assets", result.get("raw_wallet_assets_usd")),
        ("TOTAL ASSETS", result.get("assets_usd")),
        ("TOTAL LIABILITIES", result.get("liabilities_usd")),
        ("GROSS NAV", result.get("gross_nav_usd")),
        ("Management-fee drag", result.get("fee_drag_usd")),
        ("FEE-ADJUSTED NAV", result.get("fee_adjusted_nav_usd")),
    ]
    for label, value in summary_rows:
        print(f"{label:<36}{_money(value):>22}")

    supply = D(result.get("token_supply"))
    fee_mult = D(result.get("fee_multiplier"), "1")
    price = D(result.get("alef_price_usd"))
    recomputed = D(result.get("fee_adjusted_nav_usd")) / supply if supply > 0 else Decimal("0")
    fee_start = result.get("fee_accrual_start_utc", "β€”")
    fee_days = result.get("fee_elapsed_days")

    print(f"{'Fee accrual since':<36}{fee_start:>22}")
    if fee_days is not None:
        print(f"{'Elapsed':<36}{f'{fee_days:.2f} days':>22}")
    print(f"{'Live ALEF supply':<36}{_qty(supply, 8):>22}")
    print(thin)
    print(f"{'ALEF NAV PER TOKEN':<36}{('$' + _qty(price, 8)):>22}")
    print(line)

    diff = abs(price - recomputed)
    print(
        "Accounting check: "
        + ("PASS" if diff <= Decimal("0.000000000001") else "FAIL")
        + " β€” reported ALEF price "
        + ("equals" if diff <= Decimal("0.000000000001") else "does not equal")
        + " fee-adjusted NAV / live supply."
    )
    print(line)
    print()
    _ray_nav = result.get("raydium_usdc")
    if _ray_nav is not None:
        print("\nRAYDIUM POOL USDC (used in NAV)")
        print(thin)
        print(f"{'Raydium pool USDC (on-chain)':<36}{_money(_ray_nav):>22}")
    print(line + "\n")



# Familiar alias for users who expect a main() entry point.
def main():
    result = calculate_alef_nav()
    print_audit_report(result)
    return result


# ===============================================================
# Automatic auditor execution
# ===============================================================
# Running the full source in Jupyter or as a script performs one live audit,
# shows progress while network calls run, prints the human-readable report,
# and stores the complete structured result in ALEF_RESULT for inspection.
if __name__ == "__main__":
    ALEF_RESULT = calculate_alef_nav()
    _progress("Live calculation complete; rendering audit report.")
    print_audit_report(ALEF_RESULT)
    _progress("Complete. Structured result stored in ALEF_RESULT.")

Reserve Address (Ethereum & BNB Chain)
0x34292E356Def567bA65a2351ec1DD2b738ec4A57
View on Etherscan β†— View on BscScan β†—
Reserve Address (Tron)
TFMDSKrH2FJx3pdGACb5oHoKPJRzaH8dKv
View on Tronscan β†—
Reserve Address (TON)
UQDd7e4W8l4W0UMMwT--rqOjcd9-O8mLPYYZDoc4uFsS7YyY
View on Tonscan β†—
Reserve Address (Solana)
9JRZdE4rcSUQRDRUTqNrqsFtxm7caT4GfyiNxFJqBHUe
View on Solscan β†—
Raydium Liquidity Pool (ALEF)
Pool Liquidity (USD)
$42,937.60
Pool
USDC/ALEF
24h Volume
$0.00

Request a Proof of Ownership

Send a small token amount to the reserve address you want verified, then submit this form. We will send a transaction back from that same address to confirm control. The token amount will be returned.

About the Creator

Betancourt coat of arms

Alef Financial is led by the House of Betancourt β€” a Sephardi Jewish family, heir to a 600-year-old legal and financial tradition. Juan Correa, who publishes under the pen name J.β„΅. Bettencourt β€” philosopher (Oxford 2016), scientist (Bocconi 2021), author of How to Make the Most Money β€” is the head of the project. He can be reached directly via @AlefMoney on Telegram or at +1 321 448 3108.

Regulatory Status & Audit β€” Alef Financial is currently working towards formal regulatory registration and has engaged an independent third-party auditor to conduct a comprehensive review of the portfolio and its on-chain infrastructure. Updates will be published here as these processes are completed.

Contact

Telegram: @AlefMoney  Β·  WhatsApp / Phone: +1 321 448 3108

ALEF Admin

MULTISIG GOVERNANCE

Token Overview

β€”
β€”
β€”
β€”
Loading…
β€”

Current Admins

Mint Tokens

Burn Tokens

Freeze / Thaw Account

Update NAV

Add Admin

Remove Admin

Emergency Controls

Pause halts all minting and transfers. Requires admin majority.

Pending Proposals β€” click to approve

Connect wallet to load proposals

Proof of Reserves Requests

Loading…