Skip to content

Epic: exact (float-free) monetary amounts — wei-exact on-chain, source-exact everywhere #4287

Description

@TaprootFreak

Goal

Make every monetary amount in the system exact (float-free) end-to-end, so the bookkeeping is truly native-first exact rather than exact-to-8-dp-of-a-float. Concretely:

  • Wei-exact where an on-chain integer truth exists (crypto deposits, withdrawals, DEX swaps, bridges) — carry the raw base-unit integer from the chain.
  • Source-exact everywhere else — preserve the value exactly as the source reports it (exchange decimal strings, bank amounts) with no IEEE-754 drift, even when that precision is ≤ 8 dp.

This continues the ledger work in #4280 / PR #4282, which added exact-integer ledger_leg.amountBaseUnits storage but is currently capped at 8 dp because its input is already a rounded float.

Why this is needed (measured)

  • Monetary amounts are stored as @Column({ type: 'float' }) (PostgreSQL double precision) across the app — 237 float columns in 48 entity files (~200 genuinely monetary; ~25 are dimensionless ratios such as refFactor/percentFee/leverage that must not convert).
  • The full on-chain precision is captured nowhere today — it is discarded at ingestion, before anything is persisted:
    • EVM: src/integration/blockchain/shared/evm/evm.util.ts:82fromWeiAmount builds the exact decimal string via formatUnits(rawValue, decimals) then parseFloat(...) collapses it to a double; the raw hex value is not retained.
    • Bitcoin/UTXO: node-client.ts returns BTC as a JSON decimal parsed straight to float; no satoshi integer is ever constructed. Same for the other chains.
    • The unified PayInEntry.amount is typed number, so no adapter can carry an integer through even where one exists.
  • PR feat(ledger): native-first exactness — exact integer base units end-to-end (#4280) #4282's amountBaseUnits is derived by toBaseUnits(leg.amount, decimals) from the already-lossy float and capped at Math.min(decimals, 8) — exact-integer storage of an 8-dp float, not source-wei-exactness.

Scope & blast radius (measured)

  • ~200 monetary float columns; biggest carriers: buy-crypto.entity (27), buy-fiat.entity (26), user/user-data (27), bank-tx (13), trading-order (12), plus every payin/payout/dex/pricing/fee/transaction-request entity.
  • Contract surface: ~104 DTO files with ~427 number-typed monetary fields and ~38 mappers.
  • Arithmetic surface: 105+ files using Util.round/amountToChf/CryptoAmount helpers, plus 60+ service files doing money arithmetic — each moves to exact (bigint / decimal) math.

Why staged, not one PR (professional standard on a live financial system)

A single atomic PR of this size cannot be reviewed, tested, or rolled back safely on live money; it also carries a breaking API wire-format change (number → string/decimal) and an unsolved historical backfill (legacy rows have no raw value; re-querying millions of legacy tx across chains is infeasible and some chains are pruned). The professional, no-shortcut delivery is a sequenced series of additive, individually reviewed, fail-open PRs.

Decisions / defaults (documented; revisit per stage)

  1. Legacy backfill = fail-open. New rows carry the exact value; legacy rows keep the existing 8-dp-derived value (nullable exact column). No mass chain re-query.
  2. API compatibility preserved. External wire format stays backward-compatible (exact values serialized as strings / additive fields / versioned), so the frontend is not broken by a stage.
  3. Dimensionless ratios stay float (factors, percentages, leverage) — they are not monetary and have no base unit.
  4. No invented precision. Where a source reports ≤ 8 dp (exchanges, fiat), the target is "exact as reported, no float drift" — not fabricated sub-units.

Stages (each = its own reviewed PR)

  • Stage 1 — On-chain wei-exact ingestion → ledger (deposits & withdrawals). Add nullable exact amountBaseUnits (numeric ↔ bigint) to crypto_input and payout_order; widen the ingestion path to capture the raw integer as a string before parseFloat (EVM rawValue, UTXO satoshis via string scaling, other register strategies); book the persisted raw integer straight into ledger_leg.amountBaseUnits instead of deriving from the float. Additive, fail-open on legacy rows, no wire-format change. ← in progress.
  • Stage 2 — On-chain swaps & bridges. trading_order, liquidity_order (DEX), liquidity_management on-chain legs — same pattern.
  • Stage 3 — Exchange legs source-exact. Preserve the exact exchange-reported decimal (ccxt / Scrypt) as a string, no float rounding, into exchange_tx and its ledger legs.
  • Stage 4 — Supporting/payment amount columns → exact. Subdomain by subdomain (buy_crypto, buy_fiat, sell, bank_tx, refReward, …): exact monetary columns + DTO/mapper serialization (backward-compatible).
  • Stage 5 — Derived/valuation columns. CHF valuations, fees, prices → exact decimals to eliminate float drift in computed values.
  • Stage 6 — Retire float shadows. Once all readers use the exact columns, deprecate/remove the float columns.

Non-goals

  • Dimensionless ratios; fabricating precision the source does not have; a single atomic mega-PR.

Relation to existing work

Builds on #4280 and PR #4282 (exact-integer ledger storage). Interacts with the enforced CHF-first per-tx invariant (ledger_tx.amountChfSum = 0 DB CHECK): CHF remains the universal per-tx balance; native exactness is enforced via exact base-unit storage + §7 per-asset reconciliation against custody, per the analysis in #4280.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions