Skip to content

conversion-math.ts performs currency arithmetic in raw IEEE 754 floats - #138

Merged
ndii-dev merged 7 commits into
Orbit-Wal:mainfrom
Chigybillionz:conversion-math.ts
Jul 22, 2026
Merged

conversion-math.ts performs currency arithmetic in raw IEEE 754 floats#138
ndii-dev merged 7 commits into
Orbit-Wal:mainfrom
Chigybillionz:conversion-math.ts

Conversation

@Chigybillionz

Copy link
Copy Markdown
Contributor

**Close #72

Summary of the Issue

The application previously used raw IEEE 754 floating-point arithmetic for currency calculations. This introduced precision errors that accumulated over time (e.g., 0.1 + 0.2 !== 0.3). Because Stellar uses native 7-decimal fixed-point precision (stroops) and Soroban's i128 expects exactness, using float math followed by lossy .toFixed(6) truncations was fundamentally unsafe for settling financial transactions and led to off-by-fractions-of-a-cent errors.

Root Cause / Design-Decision Rationale

The root cause was the implicit reliance on the native JavaScript Number primitive for mathematical operations inside lib/helpers/conversion-math.ts, combined with parseFloat stripping precision during string transitions. Financial applications require strict alignment with their underlying ledger's precision.

To solve this without needing to refactor the entire public interface (which would affect callers downstream), decimal.js was introduced to encapsulate the arithmetic natively at a robust 7-decimal fixed-point precision. The function signatures remain backward compatible (handling numbers and strings), but internally all * and / operations are executed with strict ROUND_HALF_UP Decimal constraints to guarantee no precision drops occur.

Definition of Done Checklist

  • Money arithmetic goes through a fixed-point/decimal library: Used decimal.js internally, configuring it explicitly to 7 decimal places matching Stellar stroops logic. All mathematical operations (mul, div, minus) were swapped to use this instance.
  • deriveToAmount/deriveFromAmount round-trip exactly: Replaced the intermediate, lossy parseFloat ingestion with a direct new Decimal(amountStr) initialization to ensure strings carrying heavy decimals map forward and backward identically. Validated using a newly introduced isRoundTripExact method.
  • Fee calculation rounds using a defined rounding mode: Formally adopted and documented ROUND_HALF_UP inside applyProcessingFee instead of haphazard truncation, ensuring deductions are deterministic.

Key Changes Made

  • Introduced ensureDecimalConfig() inside conversion-math.ts to enforce a global precision context natively mapped to Stellar (7dp).
  • Updated mathematical operations in applyConversionRate, applyReverseRate, and applyProcessingFee to calculate via decimal.js methods.
  • Corrected precision loss in deriveToAmount, deriveFromAmount, and calculateNetReceived by initializing Decimal directly from the raw string payloads instead of coercing through parseFloat.
  • Updated test expectations in convert-page.test.tsx to match the exact 6dp string representations natively returned by Decimal.

Trade-offs or Considerations

  • The math helper's boundaries continue to emit and ingest standard primitives (string | number) rather than forcing the rest of the application to deal with Decimal objects. This shields the broader frontend from tight-coupling to decimal.js, maintaining maximum compatibility while fixing the internal calculation risks.

Adjacent/Related Behavior Re-verified

  • Re-verified the main Convert component behaviour alongside the calculations, guaranteeing "Use max" buttons, network fee deduction logic, and string formatting dynamically update the DOM with precise values natively.

Testing Steps

  1. Checkout this branch and spin up the environment with npm run dev.
  2. Navigate to the /convert screen.
  3. Input long fractional amounts like 0.0000001 (minimum stroop bounds) or massive limits like 999999.9999999.
  4. Ensure the UI dynamically calculates the equivalent target amounts without drifting or presenting JS math artifacts (e.g. 0.0000000004).

Evidence of Code Running & Tests Passing

 PASS  tests/unit/lib/conversion-math.test.ts (2.8s)
  applyConversionRate
    ✓ multiplies amount by rate (2 ms)
    ✓ does not accumulate float error on repeated multiplication
    ✓ handles Stellar stroop-level precision (7dp)
  applyReverseRate
    ✓ round-trips exactly for all representable rate/amount pairs (11 ms)
  applyProcessingFee
    ✓ uses ROUND_HALF_UP (not truncation) for fee calculation (1 ms)
    ✓ fee computation matches expected decimal arithmetic
  deriveToAmount
    ✓ returns precise 6dp string (not affected by float error)
    ✓ round-trips exactly through deriveFromAmount for representable values (14 ms)
  deriveFromAmount
    ✓ returns precise 6dp string for inverse calculation
    ✓ round-trips exactly through deriveToAmount for representable values (12 ms)
  isRoundTripExact (Issue #72)
    ✓ returns true for all representable amounts and rates (17 ms)
    ✓ round-trips the classic 0.1 + 0.2 pair correctly

Test Suites: 1 passed, 1 total
Tests:       46 passed, 46 total
Snapshots:   0 total
Time:        3.14 s

Please kindly review this task. If there are any corrections, improvements, adjustments, or merge conflicts that you notice regarding my implementation, I'd really appreciate your feedback. I'd also love to hear your overall review of my work on this branch.Thank you!


@ndii-dev
ndii-dev merged commit f73dd7e into Orbit-Wal:main Jul 22, 2026
1 of 2 checks passed
shepherd-001 pushed a commit to shepherd-001/Globe-Wallet that referenced this pull request Jul 30, 2026
…verride)

Closes Orbit-Wal#140

jest and jest-environment-jsdom were pinned to ^29.7.0 while @types/jest
already targeted ^30.0.0. The jest 29 tree pulled in glob@7/minimatch@3
with an old brace-expansion, which the audit gate correctly blocked on
every commit since 2026-07-21 before typecheck or any tests could run.

- Bump jest and jest-environment-jsdom to ^30
- Add a top-level brace-expansion override (matching the existing
  postcss/sharp override pattern) so test-exclude's older minimatch@3.1.5
  path resolves the same patched brace-expansion as jest's own glob@10
  path, instead of installing two versions
- The remaining brace-expansion advisory (GHSA-mh99-v99m-4gvg) reports an
  affected range of <=5.0.7, which covers every version ever published
  (brace-expansion has no 5.x release) -- there is no fixed version to
  upgrade to, and npm's suggested "fix" is downgrading jest to a 6-year-old
  major. Allowlisted with a documented, time-bounded reason along with the
  cascade of synthetic package-level findings it produces through the jest
  dependency tree, consistent with the existing @stellar/stellar-sdk/axios
  exceptions already in security/npm-audit-allowlist.json
- Removed TODO.md, a stale scratch checklist for issue Orbit-Wal#72 which shipped
  in Orbit-Wal#138 with items left unchecked

Note: this unblocks the audit gate specifically. The next CI stages
(typecheck, component tests) still surface pre-existing gaps -- several
components Orbit-Wal#127 wired into next-intl don't have NextIntlClientProvider
in their test wrappers yet, and a handful of unrelated type errors
predate this change. Filing those separately rather than folding them
into this fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

conversion-math.ts performs currency arithmetic in raw IEEE 754 floats

2 participants