You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:82 — fromWeiAmount 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.
~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)
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.
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.
Dimensionless ratios stay float (factors, percentages, leverage) — they are not monetary and have no base unit.
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 beforeparseFloat (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 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.
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:
This continues the ledger work in #4280 / PR #4282, which added exact-integer
ledger_leg.amountBaseUnitsstorage but is currently capped at 8 dp because its input is already a rounded float.Why this is needed (measured)
@Column({ type: 'float' })(PostgreSQLdouble precision) across the app — 237 float columns in 48 entity files (~200 genuinely monetary; ~25 are dimensionless ratios such asrefFactor/percentFee/leveragethat must not convert).src/integration/blockchain/shared/evm/evm.util.ts:82—fromWeiAmountbuilds the exact decimal string viaformatUnits(rawValue, decimals)thenparseFloat(...)collapses it to a double; the raw hex value is not retained.node-client.tsreturns BTC as a JSON decimal parsed straight to float; no satoshi integer is ever constructed. Same for the other chains.PayInEntry.amountis typednumber, so no adapter can carry an integer through even where one exists.amountBaseUnitsis derived bytoBaseUnits(leg.amount, decimals)from the already-lossy float and capped atMath.min(decimals, 8)— exact-integer storage of an 8-dp float, not source-wei-exactness.Scope & blast radius (measured)
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.number-typed monetary fields and ~38 mappers.Util.round/amountToChf/CryptoAmounthelpers, 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)
Stages (each = its own reviewed PR)
amountBaseUnits(numeric ↔ bigint) tocrypto_inputandpayout_order; widen the ingestion path to capture the raw integer as a string beforeparseFloat(EVMrawValue, UTXO satoshis via string scaling, other register strategies); book the persisted raw integer straight intoledger_leg.amountBaseUnitsinstead of deriving from the float. Additive, fail-open on legacy rows, no wire-format change. ← in progress.trading_order,liquidity_order(DEX),liquidity_managementon-chain legs — same pattern.exchange_txand its ledger legs.buy_crypto,buy_fiat,sell,bank_tx,refReward, …): exact monetary columns + DTO/mapper serialization (backward-compatible).Non-goals
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 = 0DB 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.