Repository navigation
Replace floating-point USDC arithmetic in settlement and invoicing with bigint contract units #1512
Description
Summary
Settlement batches, payout conversion and invoice totals add USDC amounts as JavaScript numbers. RevenueSettlementService sums e.amountUsdc floats to decide payouts and records the float total. convertUsdcToStroops turns that float into on-chain i128 units with Math.round(... + Number.EPSILON). InvoiceService totals line items with sum + Number(item.amount). With many micro-charges these sums drift by stroops, so payouts, invoices and the ledger stop reconciling exactly, and amounts above 2^53 stroops lose precision. This issue moves domain arithmetic to bigint contract units end to end.
Background and evidence
- Float payout totals.
src/services/revenueSettlementService.ts,runBatchOnce(about line 131):const totalAmount = events.reduce((sum, e) => sum + e.amountUsdc, 0);. That value is compared withminPayout, stored asamount, and emitted withamount.toFixed(7)(about line 421). - Float to on-chain units.
src/services/sorobanSettlement.ts,convertUsdcToStroops(amountUsdc: number)(about lines 100-106), converts float to stroops withMath.round, anddistribute(developerAddress: string, amountUsdc: number)(line 193) takes a float. - Number-typed domain objects.
src/services/usageStore.ts,toNumber/mapUsageEventRow(about lines 182-200), map DECIMAL strings tonumberforUsageEvent.amountUsdc.settlementStore.ts(about lines 86-92) does the same forSettlement.amount. - Float invoice totals.
src/services/InvoiceService.ts(about lines 78-81):items.reduce((sum, item) => sum + Number(item.amount), 0), thenevent.total.toFixed(7)(about line 145). - Example. Summing 10,000 charges of
0.0000001in float gives0.0010000000000000002-style drift. After rounding at different stages, the payout, the ledger sum and the invoice can differ by a stroop.
Scope
In scope
- Types. Change
UsageEvent.amountUsdc,Settlement.amount, invoice totals and the settlement client'sdistributeamount tobigintcontract units (1 USDC = 10,000,000). Use a sharedsrc/lib/usdcUnits.tswithparseUsdcToUnitsandformatUnitsToUsdc, reusing or moving the helpers insrc/services/billing.ts. - Arithmetic. All sums, comparisons (
minPayout, also converted to units) and splits usebigint. RemoveconvertUsdcToStroopsin favour of passing units through unchanged. - Boundaries. Convert at the SQL boundary (DECIMAL or integer column, read as string) and at the API, event and webhook boundary, where the decimal string format is unchanged (
"12.3456789"). - Lint guard. Add an ESLint
no-restricted-syntaxrule (or a focused unit test) that flagsparseFloat,Number(andtoFixed(insrc/services/{revenueSettlementService,sorobanSettlement,InvoiceService,settlementStore,usageStore}.ts. - Docs. Update
docs/guides/SETTLEMENT_STORE_DOCUMENTATION.md.
Out of scope
- The
usage_eventscolumn type migration (a separate issue). - Invoice PDF rendering (
invoicePdf.tsformats to 2 decimals for display only).
Design constraints
- Exact conservation. For any set of events,
payout amount == Σ event amounts == Σ invoice line items, exactly. - External formats unchanged. API and webhook payloads keep the decimal-string format; snapshot tests prove it.
- Bigint-safe JSON. No
JSON.stringifyof raw bigints; convert explicitly.
Test plan
Extend src/services/revenueSettlementService.test.ts, src/services/sorobanSettlement.test.ts, src/services/InvoiceService.test.ts and src/services/usageStore.test.ts.
| # | Scenario | Expected |
|---|---|---|
| 1 | 10,000 events of 0.0000001 |
payout exactly 10000 units ("0.001") |
| 2 | Property (fast-check): random 7-decimal amounts |
Σ units equals the formatted total parsed back |
| 3 | Payout amount passed to the client | bigint, equal to the sum of claimed rows |
| 4 | minPayout boundary equal to the total |
payout happens (≥ semantics) |
| 5 | Invoice with 3 line items | total equals the exact sum; stored string matches |
| 6 | Amount above 2^53 units | exact |
| 7 | settlement_completed webhook payload |
amount is a decimal string, unchanged format |
| 8 | Lint guard | flags a reintroduced parseFloat |
Regression proof. Row 1 (or a crafted case like it) produces a non-exact total on current main.
Acceptance criteria
- No float arithmetic on USDC amounts remains in the listed files, enforced by the guard.
- Every test-plan row passes.
-
npm run lint,npm run typecheck,npm run buildandnpm testpass, and CI is green. - At least 90% line coverage on
src/lib/usdcUnits.tsand the touched functions.
Getting started
git checkout -b fix/settlement-bigint-amounts
npm ci && npx prisma generate
npm run test:unit -- src/services/revenueSettlementService src/services/InvoiceService src/services/sorobanSettlementRead first: runBatchOnce in src/services/revenueSettlementService.ts, convertUsdcToStroops in src/services/sorobanSettlement.ts, src/services/InvoiceService.ts, and parseUsdcToContractUnits / formatContractUnitsToUsdc in src/services/billing.ts.
PR requirements
- Link this issue (
Closes #<n>). - Paste test output, including the property test, and coverage.
- Example commit:
fix(settlement): use bigint contract units for payout and invoice math.
Priority rationale
Medium. It is a well-bounded refactor across a few services, but it determines whether payouts and invoices reconcile to the stroop, which is a money-correctness guarantee.
Activity
olubukolatanko209-doc commentedon Oct 19, 2026
@olubukolatanko209-doc has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @olubukolatanko209-doc to this issue.
drips-wave commentedon Oct 19, 2026
Congratulations, @olubukolatanko209-doc! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @olubukolatanko209-doc: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊
fix(settlement): use bigint contract units for payout and invoice math (
grantfox-oss commentedon Oct 24, 2026
🎉 This issue has been marked as completed on GrantFox!
@olubukolatanko209-doc's PR #1527 was approved and merged by @greatest0fallt1me.
🏆 @olubukolatanko209-doc: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (175 total points). Track your full progress on GrantFox.
👏 Great work, @olubukolatanko209-doc! Keep contributing to CalloraOrg.
drips-wave commentedon Oct 24, 2026
This issue has been completed by @olubukolatanko209-doc as part of the Stellar Wave Program's 10th Wave 🥳
😎 @olubukolatanko209-doc: You earned 100 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.
🧑💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.