auradefi 0.1.1
PyPI GitHub

PyBook 05: Balances to Holdings

What this notebook proves: "EVM balances → holdings. Etherscan V2 + DefiLlama prices. Single-tenant, library-only. Done when: a known-rich address returns a USD total within a few % of an incumbent."

Three collaborators, one report:

seam contract this book
source balances(chain_id, address) -> [BalanceRecord] sources.evm.etherscan.EtherscanV2
prices usd_prices([caip19]) -> {caip19: Money} prices.inquirer.Inquirer over DefiLlamaOracle
clock now_ms() -> int FrozenClock

portfolio/ is transport-free: it never imports httpx, and tests/style/test_layering.py enforces that. Everything below replays tests/cassettes/phase1_vitalik.json, so this book needs no API key and no network.

from pathlib import Path


def repo_root() -> Path:
    """The checkout this book lives in, wherever it is executed from."""
    here = Path.cwd().resolve()
    for candidate in (here, *here.parents):
        if (candidate / "tests" / "cassettes").is_dir():
            return candidate
    raise RuntimeError("run this book from inside a checkout of auradefi")


CASSETTES = repo_root() / "tests" / "cassettes"
print("cassettes:", CASSETTES.relative_to(repo_root()))
cassettes: tests/cassettes

The source: Etherscan V2, one key, 50+ chains

EtherscanV2 takes an injected httpx.Client (that is the whole reason a cassette can stand in for the internet) and issues three kinds of GET against one /v2/api base URL:

  1. action=balance: the native balance, as a wei string;
  2. action=tokentx: paged transfer history, purely to discover which ERC-20 contracts this address has ever touched;
  3. action=tokenbalance: one call per discovered contract, in ascending lowercased-contract order so a recording is deterministic.

Two pieces of hygiene are visible in the fixture: a mixed-case duplicate of the USDC contract collapses after lowercasing (canonical form), and a spam row whose tokenDecimal is "" is skipped additively: a garbage row never crashes the scan (rule #9).

from auradefi.money.quantity import Quantity
from auradefi.sources.evm.etherscan import EtherscanV2
from auradefi.testing.cassettes import load

CHAIN = "eip155:1"
ADDRESS = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"


def fresh_client():
    "A new replay client: one cassette serves etherscan AND llama."
    return load(CASSETTES / "phase1_vitalik.json").client()


records = EtherscanV2(fresh_client(), api_key="TESTKEY").balances(CHAIN, ADDRESS)

# Four discovery rows in the cassette; three survive: the mixed-case USDC
# duplicate dedupes, the tokenDecimal="" spam row is skipped.
assert [record.symbol for record in records] == ["ETH", "DAI", "USDC"]
assert records[0].quantity == Quantity(4878123456789012345678, 18)
assert records[1].quantity == Quantity(255000000000000000000000, 18)
assert records[2].quantity == Quantity(1250000750000, 6)
assert records[0].contract_address is None  # native coin
assert records[2].caip19 == "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"

for record in records:
    print(f"{record.symbol:>5}  raw={record.quantity.raw:<25} -> {record.quantity}")
  ETH  raw=4878123456789012345678    -> 4878.123456789012345678
  DAI  raw=255000000000000000000000  -> 255000
 USDC  raw=1250000750000             -> 1250000.75

The prices seam: an oracle is anything with usd_prices

Inquirer composes oracles first-wins: it validates every CAIP-19 up front (a bad id raises CaipParseError before any HTTP), deduplicates, then asks each oracle only for what is still unpriced. The package ships one oracle, DefiLlama, so the "first" and the "only" coincide today; that is recorded honestly in the README coverage table.

prices/inquirer.py never imports the concrete oracle: the seam is structural (Protocol), which is what lets a host drop in its own price feed without touching auradefi.

from decimal import Decimal

from auradefi.errors import CaipParseError
from auradefi.money.fiat import Money
from auradefi.prices.inquirer import Inquirer
from auradefi.prices.oracles.defillama import DefiLlamaOracle

ETH = "eip155:1/slip44:60"
DAI = "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f"
USDC = "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"

prices = Inquirer([DefiLlamaOracle(fresh_client())])
quoted = prices.usd_prices([ETH, DAI, USDC])

assert quoted == {
    ETH: Money(Decimal("3584.17"), "USD"),
    DAI: Money(Decimal("0.99985"), "USD"),
    USDC: Money(Decimal("0.999839"), "USD"),
}
print({asset.split("/")[-1]: str(price) for asset, price in quoted.items()})

# An unparseable id is refused BEFORE any oracle is consulted.
try:
    prices.usd_prices(["not-a-caip19"])
except CaipParseError as exc:
    print("refused pre-flight:", exc)
{'slip44:60': '3584.17 USD', 'erc20:0x6b175474e89094c44da98b954eedeac495271d0f': '0.99985 USD', 'erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': '0.999839 USD'}
refused pre-flight: not a CAIP-19 (need exactly one '/'): 'not-a-caip19'

Assembly: exact Decimal, no rounding anywhere

HoldingsService.holdings is pinned (rule #5, and tests/golden/test_phase1_holdings.py asserts these very numbers):

  1. one source.balances call;
  2. exactly one prices.usd_prices call, ids in record order;
  3. value = quantity × price, multiplied context-free. The integer coefficients are multiplied and the exponents added, so a 78-digit raw survives intact where a Decimal context multiply would silently round to 28 significant digits (rule #1);
  4. HoldingsReport.assemble(...) echoes the caller's address and chain id verbatim and sums the priced values exactly.
from decimal import Decimal

from auradefi.clock import FrozenClock
from auradefi.portfolio.holdings import HoldingsService

AS_OF_MS = 1_754_000_000_000
client = fresh_client()  # ONE client for both hosts, as a host would wire it

service = HoldingsService(
    EtherscanV2(client, api_key="TESTKEY"),
    Inquirer([DefiLlamaOracle(client)]),
    clock=FrozenClock(AS_OF_MS),
)
report = service.holdings(CHAIN, ADDRESS)

assert report.holdings[0].value.amount == Decimal("17484023.75011947437900871726")
assert report.holdings[1].value.amount == Decimal("254961.75")
assert report.holdings[2].value.amount == Decimal("1249799.49987925")
assert report.total_value == Money(Decimal("18988784.99999872437900871726"), "USD")
assert report.unpriced == ()
assert (report.address, report.chain_id, report.as_of_ms) == (ADDRESS, CHAIN, AS_OF_MS)

for holding in report.holdings:
    print(f"{holding.symbol:>5}  {str(holding.quantity):>26}  @ {str(holding.price):>12}  = {holding.value}")
print(f"{'TOTAL':>5}  {'':>26}    {'':>12}  = {report.total_value}")
  ETH     4878.123456789012345678  @  3584.17 USD  = 17484023.75011947437900871726 USD
  DAI                      255000  @  0.99985 USD  = 254961.75000000000000000000000 USD
 USDC                  1250000.75  @ 0.999839 USD  = 1249799.499879250000 USD
TOTAL                                              = 18988784.99999872437900871726000 USD

The gate, stated as arithmetic

The target is a total "within a few % of an incumbent". The incumbent reference for this address at this recording is 19,000,000 USD; the delta is asserted rather than eyeballed.

incumbent = Decimal("19000000")
delta = abs(report.total_value.amount - incumbent) / incumbent
assert delta < Decimal("0.05")
print(f"total {report.total_value.amount} vs incumbent {incumbent} -> delta {delta * 100:.4f}%")

# And the reason the total is trustworthy: it is not a float.
float_total = sum(
    float(holding.quantity.as_decimal()) * float(holding.price.amount)
    for holding in report.holdings
)
assert Decimal(str(float_total)) != report.total_value.amount
print("float would have said:", repr(float_total), "(truncated at 17 digits)")
print("exact answer  says:  ", report.total_value.amount.normalize())
total 18988784.99999872437900871726000 vs incumbent 19000000 -> delta 0.0590%
float would have said: 18988784.999998722 (truncated at 17 digits)
exact answer  says:   18988784.99999872437900871726

The wire form: a raw amount is never a JSON number

Rule #2 in one assertion: every quantity crossing a boundary is the pinned four-field object whose raw is a string. float rides along as a convenience for charting and is explicitly lossy. The exact value is raw/numeric, and the notebook proves the float has already drifted.

import json

from auradefi.money.decimal_json import money_to_wire, quantity_to_wire

wire = quantity_to_wire(report.holdings[0].quantity)
assert wire["raw"] == "4878123456789012345678"
assert isinstance(wire["raw"], str)
assert wire["numeric"] == "4878.123456789012345678"
assert isinstance(wire["float"], float)
assert Decimal(str(wire["float"])) != Decimal(wire["numeric"])  # lossy on purpose
print(json.dumps(wire, indent=2))
print(json.dumps(money_to_wire(report.total_value)))
{
  "raw": "4878123456789012345678",
  "decimals": 18,
  "numeric": "4878.123456789012345678",
  "float": 4878.123456789012
}
{"amount": "18988784.99999872437900871726000", "currency": "USD"}

Incompleteness is first class, never silent

An asset the oracle cannot price is kept as a holding with price=None, value=None and its id listed in report.unpriced. The consumer can see exactly what the total omits (the data_quality spirit). A half-priced holding, a price with no value, or the reverse, is refused at construction, because that is how a portfolio silently under-reports.

from auradefi.errors import ValidationError
from auradefi.portfolio.models import Holding, HoldingsReport

SHIB = "eip155:1/erc20:0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce"
partial = HoldingsReport.assemble(
    ADDRESS,
    CHAIN,
    [
        report.holdings[1],  # DAI, priced
        Holding(caip19=SHIB, symbol="SHIB", quantity=Quantity(10**24, 18), price=None, value=None),
    ],
    AS_OF_MS,
)
assert partial.unpriced == (SHIB,)
assert partial.total_value == Money(Decimal("254961.75"), "USD")  # the unpriced row is NOT a zero-guess
print("total:", partial.total_value, "| unpriced:", partial.unpriced)

try:
    Holding(caip19=SHIB, symbol="SHIB", quantity=Quantity(1, 18), price=Money(Decimal("1"), "USD"), value=None)
except ValidationError as exc:
    print("refused:", exc)
total: 254961.75000000000000000000000 USD | unpriced: ('eip155:1/erc20:0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce',)
refused: holding 'eip155:1/erc20:0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce': price and value must both be set or both be None, never one without the other

What this book did not show

Honest coverage (rule #10):

Next: 06_tenancy: the same holdings, but for somebody else's users.