Skip to content

fix(xrpl): return string from dropsToXrp to preserve precision (#3316)#3341

Open
achowdhry-ripple wants to merge 2 commits into
mainfrom
fix/dropsToXrp-precision-loss-3316
Open

fix(xrpl): return string from dropsToXrp to preserve precision (#3316)#3341
achowdhry-ripple wants to merge 2 commits into
mainfrom
fix/dropsToXrp-precision-loss-3316

Conversation

@achowdhry-ripple

Copy link
Copy Markdown
Contributor

High Level Overview of Change

Fixes xrpl.js#3316.

  • dropsToXrp() now returns a base-10 decimal string instead of a JavaScript number. The internal .toNumber() call is replaced with .toString(BASE_TEN).

  • Client.getXrpBalance() (which forwards the value) now returns Promise<string> instead of Promise<number>.

  • Client.getBalances() is updated for the new return type (the branch that pushes the XRP balance no longer needs a redundant .toString(), and the !== 0 sentinel check becomes !== '0').

  • getBalanceChanges() drops a now-redundant .toString() on the already-string XRP value.

  • Existing dropsToXrp tests updated for the new return type; new regression test added per the issue:

    it('round-trips large amounts without precision loss (issue #3316)', function () {
      const drops = '9999999999999999'
      const xrp = dropsToXrp(drops)
      assert.strictEqual(xrp, '9999999999.999999')
      assert.strictEqual(xrpToDrops(xrp), drops)
    })
  • HISTORY.md updated under Unreleased > BREAKING CHANGES.

Context of Change

dropsToXrp converts a drops integer (≤ ~10^17 for the XRP total supply) into an XRP amount by dividing by 1,000,000. The old implementation called .toNumber() on the BigNumber result, coercing to IEEE-754 double. For amounts above ~9 billion XRP, the double cannot represent every drop exactly, so the conversion silently loses precision.

Repro from the issue:

dropsToXrp('9999999999999999')              // returned 9999999999.999998 (should be 9999999999.999999)
xrpToDrops(dropsToXrp('9999999999999999'))  // returned '9999999999999998' — 1 drop silently lost on round-trip

The fix is to return the BigNumber's exact base-10 string representation. Drops are at most 6 fractional digits of XRP, so the division terminates exactly within BigNumber's default precision and never produces exponential notation.

This is a breaking API change (return type numberstring), which is why it is grouped with the other Unreleased breaking changes in HISTORY.md. Callers that rely on a JS number can wrap the result in Number(...) — this is already done at the two internal call sites in Wallet/fundWallet.ts.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)
  • Documentation Updates
  • Release

Did you update HISTORY.md?

  • Yes
  • No, this change does not impact library users

Test Plan

  • New unit test dropsToXrp > round-trips large amounts without precision loss (issue #3316) directly exercises the regression case from the issue.
  • Existing dropsToXrp tests were updated to assert against the new string return type (mechanical change — same values, now quoted).
  • getXrpBalance mock test updated to assert the string return.
  • getBalanceChanges fixture-driven tests are unchanged (the fixture already expected string values; the new code path produces the same strings).
  • Did not re-run the full suite locally — npm install is currently hitting the npm "Exit handler never called!" bug on my machine. Relying on CI; happy to push fixups if anything fails.

`dropsToXrp` previously returned a JavaScript `number` via `.toNumber()`,
which silently lost precision for amounts approaching the XRP supply
(~10^17 drops). `xrpToDrops(dropsToXrp('9999999999999999'))` returned
`'9999999999999998'`, dropping one drop on the round-trip.

Return a base-10 decimal string from `dropsToXrp` (and from
`Client.getXrpBalance`, which forwards the value) so the full precision
of the input is preserved. Callers that need a JS number can wrap the
result in `Number(...)`.

BREAKING CHANGE: `dropsToXrp()` now returns `string` instead of `number`;
`Client.getXrpBalance()` now returns `Promise<string>`.
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 855d79af-ade5-4a51-bc2b-2415b990e285

📥 Commits

Reviewing files that changed from the base of the PR and between 70d5790 and ceb12d7.

📒 Files selected for processing (6)
  • packages/xrpl/HISTORY.md
  • packages/xrpl/src/Wallet/fundWallet.ts
  • packages/xrpl/src/client/index.ts
  • packages/xrpl/src/utils/xrpConversion.ts
  • packages/xrpl/test/client/getXrpBalance.test.ts
  • packages/xrpl/test/utils/dropsToXrp.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/xrpl/HISTORY.md

Walkthrough

This PR changes dropsToXrp() and Client.getXrpBalance() to return BigNumber instead of JavaScript numbers, eliminating precision loss for large XRP amounts. The conversion function, client APIs, wallet functions, and tests are updated to use the new type throughout.

Changes

XRP Conversion BigNumber Return Type

Layer / File(s) Summary
dropsToXrp contract and implementation
packages/xrpl/src/utils/xrpConversion.ts
dropsToXrp() now returns BigNumber instead of calling .toNumber(), preserving full precision of drops values without loss of significant digits for large XRP amounts. JSDoc and TypeScript signature updated to reflect the new return type and usage guidance (.toString() for decimal strings, .toNumber() for JS numbers).
Client XRP balance methods
packages/xrpl/src/client/index.ts
Client.getXrpBalance() return type updated to Promise<BigNumber> with revised JSDoc. Client.getBalances() initializes XRP balance as BigNumber.from(0), uses .isZero() for non-zero check, and stores XRP values via .toString() for the response object. BigNumber import added.
Wallet balance operations
packages/xrpl/src/Wallet/fundWallet.ts
fundWallet, getStartingBalance, and getUpdatedBalance call .toNumber() on the BigNumber returned from client.getXrpBalance() for numeric balance arithmetic, with error handling and comparison logic preserved.
Test assertions and documentation
packages/xrpl/test/utils/dropsToXrp.test.ts, packages/xrpl/test/client/getXrpBalance.test.ts, packages/xrpl/HISTORY.md
dropsToXrp test assertions updated to use BigNumber.isEqualTo() string comparisons across all input scenarios, including new regression test for large drops round-trip precision preservation. getXrpBalance test assertion updated to assert BigNumber values. Breaking change documented in HISTORY.md.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Possibly related issues


Suggested reviewers

  • Patel-Raj11
  • kuan121
  • ckeshava
  • pdp2121

Poem

🐰 Precision blooms where numbers dwelled,
Big integers now gently held,
No more drops upon the floor—
XRP's truths restored once more! 🎯✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: fixing precision loss in dropsToXrp by changing the return type, with reference to the GitHub issue.
Description check ✅ Passed The description follows the template structure with High Level Overview, Context of Change, Type of Change selections, and HISTORY.md confirmation. All required sections are complete with detailed implementation details and test coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dropsToXrp-precision-loss-3316

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/xrpl/src/Wallet/fundWallet.ts

Parsing error: The keyword 'import' is reserved

packages/xrpl/src/client/index.ts

Parsing error: The keyword 'import' is reserved

packages/xrpl/src/utils/xrpConversion.ts

Parsing error: The keyword 'import' is reserved

  • 2 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/xrpl/src/client/index.ts Outdated
Address PR #3341 review feedback (kuan121): returning BigNumber is
symmetric with the BigNumber.Value input type and avoids the silent
string-concat foot-gun where `dropsToXrp(a) + dropsToXrp(b)` would have
concatenated rather than added.

- `dropsToXrp()` returns `BigNumber` (was `string`, originally `number`).
- `Client.getXrpBalance()` returns `Promise<BigNumber>` (was
  `Promise<string>`).
- `Client.getBalances()` initialises `xrpPromise` with `new BigNumber(0)`,
  uses `.isZero()` for the sentinel check, and `.toString()` when pushing
  the XRP balance.
- `getBalanceChanges()` calls `.toString()` on the result of `dropsToXrp`
  to populate the string-typed `Balance.value`.
- `fundWallet`/`Client.fundWallet` convert via `.toNumber()` (replacing
  `Number(...)` coercion on the previous string return).
- Tests updated to assert via `BigNumber.isEqualTo` / `.isZero()`.
- `HISTORY.md` breaking-change entry updated to describe BigNumber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
formatBalances(response.result.lines),
)
if (xrpBalance !== 0) {
if (!xrpBalance.isZero()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add test coverage for this change as well to be thorough

@@ -1,6 +1,7 @@
/* eslint-disable jsdoc/require-jsdoc -- Request has many aliases, but they don't need unique docs */

@kuan121 kuan121 May 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you update the PR title and description?

@kuan121

kuan121 commented May 22, 2026

Copy link
Copy Markdown
Collaborator

Browser tests are still failing.

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.

dropsToXrp() returns number, causing precision loss for large amounts

3 participants