Priority: High · Area: Numeric correctness / money handling · Est. effort: 10–14 h
📌 Problem
Every monetary field in src/models/liquidity.ts is a JavaScript number — an IEEE-754 double:
amount: number; // :12, :30, :49, :63
remainingBalance: number; // :32
total: number; // :40
portion: number; // :57
src/services/quoteService.ts does arithmetic on them:
const taken = Math.min(remaining, entry.amount); // :49 (inside the routing loop)
const fee = Math.ceil((amount * this.feeBps) / BPS_DIVISOR); // :54
Two distinct problems.
1. Precision loss above 2^53. Doubles represent integers exactly only up to 9,007,199,254,740,991. Stellar amounts are commonly held in stroops (1 XLM = 10,000,000 stroops), so 900,719,925 XLM exceeds the safe range. Past it, addition silently stops being exact.
2. Accumulated error in the routing loop. The service selects liquidity largest-first, subtracting taken from remaining each iteration. Repeated float subtraction accumulates representation error, so the sum of the route portions can fail to equal the requested amount by a small residue — and Math.ceil on the fee then rounds a value that is already slightly wrong.
The service's own doc comment calls the quote "deterministic". Float arithmetic across a variable number of iterations, in a variable order, is not reliably so.
🎯 Design decision required
Post your approach in a comment on this issue before writing code — this touches the models, the services and the API boundary. State and defend:
- Representation.
bigint for integer minor units, or a decimal library? bigint has no dependency and is exact, but does not serialise to JSON natively and cannot express fractions. Argue one, and state the unit you standardise on.
- API compatibility. JSON has no integer type beyond double precision, so large values must serialise as strings to survive the round trip. That is a breaking API change. State it plainly and describe the migration.
- Rounding policy.
Math.ceil on the fee rounds in the protocol's favour. Whatever representation you choose, state the rounding rule explicitly, apply it consistently, and prove the route portions sum exactly to the requested amount.
🧩 Requirements and context
- Add tests demonstrating the current defect first — a value above
Number.MAX_SAFE_INTEGER, and a multi-anchor route whose portions do not sum exactly. Those tests are the evidence the change is warranted.
- The invariant "sum of route portions + fee equals the requested total, exactly" must hold after the change, with a property-style test over many inputs.
src/models/settlement.ts and the settlement path use the same amounts — check and include them, or scope explicitly and say what you left.
- All 42 test files must pass.
- Update
src/openapi.ts to reflect any serialisation change.
🛠️ Suggested execution
- Write the two failing tests (large value, non-summing route).
- Post your representation and API-compatibility plan; wait for agreement.
- Convert models, then services.
- Add the exact-sum property test.
- Update the OpenAPI spec and document the breaking change.
✅ Acceptance criteria
🚫 Out of scope
- The persistence layer — separate issue.
- Changing the largest-first selection strategy.
- Changing the fee rate.
🧪 Verification
npm ci
npm test src/services
npm run lint && npm run build && npm test
📤 What your PR must include
- The two failing tests and their output.
- A link to the agreed representation plan.
- Your rounding policy.
- The breaking-change description and migration.
Closes #<n>.
🔒 Security notes
Float arithmetic on money is a correctness failure with financial consequences: a route whose portions do not sum to the requested amount means the system's accounting does not balance, and the discrepancy is small enough to escape notice while being systematic rather than random. Math.ceil on the fee compounds this by always rounding the same direction. Any residue that repeatedly favours one side of a settlement is a value leak, whether or not anyone is deliberately exploiting it.
📋 Guidelines
- Minimum 95% test coverage on changed lines
- Clear documentation
- Timeframe: 96 hours from assignment
- One logical change per commit; no merge commits
💬 Join our community
Working on this, or want to sanity-check your approach before you start? Come and ask — the maintainers are there and happy to help.
Telegram: https://t.me/Grainlify
Priority: High · Area: Numeric correctness / money handling · Est. effort: 10–14 h
📌 Problem
Every monetary field in
src/models/liquidity.tsis a JavaScriptnumber— an IEEE-754 double:src/services/quoteService.tsdoes arithmetic on them:Two distinct problems.
1. Precision loss above 2^53. Doubles represent integers exactly only up to 9,007,199,254,740,991. Stellar amounts are commonly held in stroops (1 XLM = 10,000,000 stroops), so 900,719,925 XLM exceeds the safe range. Past it, addition silently stops being exact.
2. Accumulated error in the routing loop. The service selects liquidity largest-first, subtracting
takenfromremainingeach iteration. Repeated float subtraction accumulates representation error, so the sum of the route portions can fail to equal the requested amount by a small residue — andMath.ceilon the fee then rounds a value that is already slightly wrong.The service's own doc comment calls the quote "deterministic". Float arithmetic across a variable number of iterations, in a variable order, is not reliably so.
🎯 Design decision required
Post your approach in a comment on this issue before writing code — this touches the models, the services and the API boundary. State and defend:
bigintfor integer minor units, or a decimal library?biginthas no dependency and is exact, but does not serialise to JSON natively and cannot express fractions. Argue one, and state the unit you standardise on.Math.ceilon the fee rounds in the protocol's favour. Whatever representation you choose, state the rounding rule explicitly, apply it consistently, and prove the route portions sum exactly to the requested amount.🧩 Requirements and context
Number.MAX_SAFE_INTEGER, and a multi-anchor route whose portions do not sum exactly. Those tests are the evidence the change is warranted.src/models/settlement.tsand the settlement path use the same amounts — check and include them, or scope explicitly and say what you left.src/openapi.tsto reflect any serialisation change.🛠️ Suggested execution
✅ Acceptance criteria
src/openapi.tsand documented as breaking.🚫 Out of scope
🧪 Verification
📤 What your PR must include
Closes #<n>.🔒 Security notes
Float arithmetic on money is a correctness failure with financial consequences: a route whose portions do not sum to the requested amount means the system's accounting does not balance, and the discrepancy is small enough to escape notice while being systematic rather than random.
Math.ceilon the fee compounds this by always rounding the same direction. Any residue that repeatedly favours one side of a settlement is a value leak, whether or not anyone is deliberately exploiting it.📋 Guidelines
💬 Join our community
Working on this, or want to sanity-check your approach before you start? Come and ask — the maintainers are there and happy to help.
Telegram: https://t.me/Grainlify