PyBook 02: Money & Quantity
Two non-negotiable rules, each with a named casualty:
Rule #1, "Money is a tagged decimal string." Plaid's
quantity/amount/price are format: double, ~15–17 significant
digits against 18-decimal tokens. "Silently corrupts exactly the
largest balances."
Rule #2, "Never emit a JSON integer for a raw amount." Allium
ships raw_balance (integer): "Any wei-scale value is past
Number.MAX_SAFE_INTEGER before it reaches a client."
auradefi.money makes both rules structural: Quantity is
an arbitrary-precision int count of base units plus its scale, Money
is an exact Decimal plus a currency tag. Nothing here may round.
from decimal import Decimal
from auradefi.money.quantity import Quantity
one_eth_ish = Quantity(raw=1234567890123456789, decimals=18)
assert str(one_eth_ish) == "1.234567890123456789"
assert one_eth_ish.as_decimal() == Decimal("1.234567890123456789")
# Equality is strict on (raw, decimals): equal numeric value at a
# different scale is a DIFFERENT quantity. Scale is part of identity.
assert Quantity(10, 1) != Quantity(100, 2)
assert str(Quantity(10, 1)) == str(Quantity(100, 2)) == "1"
print(one_eth_ish)
1.234567890123456789
Arbitrary precision: the 10^77 demo
raw is a Python int: "arbitrary precision, no ceiling".
A 78-digit balance, near the uint256 maximum a malicious or broken token
contract can actually mint, survives construction, arithmetic, and
rendering exactly.
whale = Quantity(raw=10**77 + 1, decimals=18)
rendered = str(whale)
assert "E" not in rendered and "e" not in rendered # never scientific
assert rendered == "1" + "0" * 59 + "." + "0" * 17 + "1"
# Exact Decimal at 78 digits. String construction is context-free, so
# this equality is exact, not rounded to the default 28-digit context.
assert whale.as_decimal() == Decimal(f"{10**77 + 1}E-18")
# Exact arithmetic at that magnitude: add one base unit (1e-18).
bumped = whale + Quantity(1, 18)
assert bumped.raw == 10**77 + 2
print(f"78-digit raw renders exactly: {rendered[:12]}…{rendered[-12:]} ({len(rendered)} chars)")
78-digit raw renders exactly: 100000000000…000000000001 (79 chars)
Exact string rendering: never scientific notation
__str__ is a wire-format contract: exact at any magnitude, trailing
fractional zeros trimmed, and 'E' never appears, not for huge values,
not for dust.
dust = Quantity(raw=1, decimals=77)
assert str(dust) == "0." + "0" * 76 + "1"
assert "E" not in str(dust)
# For contrast: naive Decimal printing goes scientific on exactly this value.
assert str(dust.as_decimal()) == "1E-77"
assert str(Quantity(15 * 10**17, 18)) == "1.5" # trailing zeros trimmed
assert str(Quantity(-15 * 10**17, 18)) == "-1.5" # sign preserved
assert str(Quantity(0, 18)) == "0"
print("dust:", str(dust)[:8] + "…" + str(dust)[-3:], "| naive Decimal:", dust.as_decimal())
dust: 0.000000…001 | naive Decimal: 1E-77
The pinned shape: "raw for arithmetic, float for display, exact
string for correctness, decimals inline so no second lookup":
"quantity": { "raw": "1234567890123456789", "decimals": 18,
"numeric": "1.234567890123456789", "float": 1.2345678901234568 }
"float is display-only and documented as lossy. raw is a
string in JSON, an int in Python."
import json
from auradefi.money.decimal_json import quantity_from_wire, quantity_to_wire
wire = quantity_to_wire(one_eth_ish)
print(json.dumps(wire, indent=2))
# The exact wire dict, asserted field by field:
assert wire == {
"raw": "1234567890123456789", # a STRING: rule #2
"decimals": 18,
"numeric": "1.234567890123456789", # exact, never scientific
"float": 1.2345678901234567, # lossy, display-only
}
assert isinstance(wire["raw"], str) and isinstance(wire["decimals"], int)
{
"raw": "1234567890123456789",
"decimals": 18,
"numeric": "1.234567890123456789",
"float": 1.2345678901234567
}
# float is LOSSY: the double dropped real precision...
assert Decimal(wire["float"]) != one_eth_ish.as_decimal()
# ...which is why reads reconstruct from raw + decimals ONLY. Corrupt
# display fields cannot corrupt the value:
tampered = {**wire, "numeric": "999", "float": 999.0}
assert quantity_from_wire(tampered) == one_eth_ish
# And the 78-digit whale round-trips exactly through the wire.
assert quantity_from_wire(quantity_to_wire(whale)) == whale
print("reads use raw + decimals only; float is never trusted")
reads use raw + decimals only; float is never trusted
Strict wire grammar
Python's int() and Decimal() accept far more than a wire amount
should: underscores, whitespace, '+', scientific notation, even
non-ASCII digits. The wire grammar is pinned to -?[0-9]+ (and
-?[0-9]+(\.[0-9]+)? for money). Everything else is a
ValidationError, including a JSON integer in raw (rule #2,
rejected on read).
from auradefi.errors import ValidationError
# Python's parsers are lax: exactly the laxness the wire must not inherit:
assert int("1_000") == 1000 # underscores
assert int(" +5\n") == 5 # whitespace and '+'
assert int("٥٥") == 55 # Arabic-Indic digits!
assert Decimal("1e5") == 100000 # scientific notation
# None of that is a wire amount. Every one is rejected on read:
for bad_raw in ["1_000", "1e5", " 5", "+5", "5\n", "٥٥"]:
try:
quantity_from_wire({"raw": bad_raw, "decimals": 18})
except ValidationError as exc:
print("rejected:", repr(bad_raw), "→", type(exc).__name__)
else:
raise AssertionError(f"{bad_raw!r} must not parse")
# A JSON integer raw is a rule #2 violation: rejected outright.
try:
quantity_from_wire({"raw": 1234567890123456789, "decimals": 18})
except ValidationError as exc:
print("rejected: JSON-integer raw →", exc)
else:
raise AssertionError("integer raw must not parse")
rejected: '1_000' → ValidationError
rejected: '1e5' → ValidationError
rejected: ' 5' → ValidationError
rejected: '+5' → ValidationError
rejected: '5\n' → ValidationError
rejected: '٥٥' → ValidationError
rejected: JSON-integer raw → wire 'raw' must be a string, got int (rule #2)
Money: a tagged decimal string
Rule #1's shape, verbatim:
{"amount": "-741.027368947745798389", "currency": "USD"}.
amount is a Decimal in Python and an exact string on the wire; a
float amount is rejected on read, as are the non-finite specials
("NaN", "Infinity") and scientific notation.
from auradefi.money.decimal_json import money_from_wire, money_to_wire
from auradefi.money.fiat import Money
# The exact value from rule #1: 21 significant digits, no double.
spec_example = Money(Decimal("-741.027368947745798389"), "USD")
wire_money = money_to_wire(spec_example)
assert wire_money == {"amount": "-741.027368947745798389", "currency": "USD"}
assert money_from_wire(wire_money) == spec_example
print(json.dumps(wire_money))
for bad in [
{"amount": 4321.55, "currency": "USD"}, # a float: rule #1
{"amount": "NaN", "currency": "USD"}, # not an exact amount
{"amount": "1e5", "currency": "USD"}, # no scientific notation
{"amount": "1_000", "currency": "USD"}, # no int()-isms
]:
try:
money_from_wire(bad)
except ValidationError as exc:
print("rejected:", repr(bad["amount"]), "→", type(exc).__name__)
else:
raise AssertionError(f"{bad!r} must not parse")
{"amount": "-741.027368947745798389", "currency": "USD"}
rejected: 4321.55 → ValidationError
rejected: 'NaN' → ValidationError
rejected: '1e5' → ValidationError
rejected: '1_000' → ValidationError
Mismatches raise: never coerce
Arithmetic is defined only between quantities of equal decimals and
money of equal currency. Mixing scales or currencies is always a caller
bug, so it raises from the one taxonomy (DecimalsMismatchError,
CurrencyMismatchError) instead of silently rescaling or converting.
from auradefi.errors import CurrencyMismatchError, DecimalsMismatchError
try:
Quantity(1, 18) + Quantity(1, 6) # ETH-scale + USDC-scale
except DecimalsMismatchError as exc:
print("quantity:", exc)
else:
raise AssertionError("expected DecimalsMismatchError")
try:
Money(Decimal("1"), "USD") + Money(Decimal("1"), "EUR")
except CurrencyMismatchError as exc:
print("money: ", exc)
else:
raise AssertionError("expected CurrencyMismatchError")
# Same scale / same currency: exact, context-free arithmetic.
assert Quantity(1, 6) + Quantity(2, 6) == Quantity(3, 6)
assert (Money(Decimal("0.1"), "USD") + Money(Decimal("0.2"), "USD")).amount == Decimal("0.3")
print("\nmoney walkthrough complete: every assertion held")
quantity: decimals mismatch: 18 vs 6
money: currency mismatch: 'USD' vs 'EUR'
money walkthrough complete: every assertion held