Skip to content

Represent issued-currency amounts across the full XRPL range, not just what decimal can hold #150

Description

@Platonenkov

Currency.ValueAsNumber is a decimal, and decimal cannot hold what the protocol allows. Every symptom in #148 follows from that, and none of them is fixable inside decimal — the fix there only chooses how to fail, because succeeding is not available.

The gap

From STAmount.h on rippled develop:

static constexpr int kMinOffset = -96;
static constexpr int kMaxOffset = 80;
static constexpr std::uint64_t kMinValue = 1'000'000'000'000'000ull;
static constexpr std::uint64_t kMaxValue = 9'999'999'999'999'999ull;
smallest positive largest
XRPL issued currency 1e-81 ~1e96
System.Decimal ~1e-28 ~7.9e28

67 orders of magnitude unreachable at the top, 53 at the bottom. A decimal also carries 28–29 significant digits against the protocol's 16, so precision is not the problem — range is.

What this costs today

  • GetBalanceChanges subtracts two balances. Out of range, one side is wrong or throws, so the whole call fails on metadata the caller does not control — and an indexer re-reading that transaction fails on it forever.
  • Offer.AmountEach divides two amounts and can return an exchange rate wrong by 67 orders of magnitude without throwing.
  • Amounts below 1e-28 become zero silently.

#148 makes these predictable rather than three-different-ways-wrong. It does not make them representable.

What is proposed

An exact amount type that covers the protocol range, and arithmetic performed in it rather than in decimal.

The model to copy is the one the protocol already defines and both reference implementations use: an integer mantissa with an exponent. rippled's STAmount keeps a std::uint64_t mantissa in [1e15, 1e16) with an exponent in [-96, 80]; xrpl.js reaches the same range through BigNumber with MIN_IOU_EXPONENT = -96, MAX_IOU_EXPONENT = 80, MAX_IOU_PRECISION = 16. In .NET the natural equivalent is System.Numerics.BigInteger for the mantissa alongside an int exponent, normalised the way rippled normalises.

This has been done once already in this repository and it went well: Xrpl.Sugar.AmmMath transcribes rippled's AMM equations rather than deriving them, and agrees with a live node to 15 significant digits. The same approach applies — take the protocol's own numeric model instead of approximating it with a CLR type that nearly fits.

Scope and shape

Deliberately not decided here, because it is worth deciding in the open:

  • Does ValueAsNumber stay? It is used widely and is convenient for ordinary amounts. Keeping it as a lossy convenience that throws out of range — what Currency.ValueAsNumber throws on out-of-range token amounts, taking GetBalanceChanges down with it #148 leaves it as — while adding an exact accessor beside it, is one option. Replacing it is another, and is a breaking change.
  • Where does the arithmetic live? GetBalanceChanges and Offer.AmountEach are the two places that compute rather than merely read, and both would move to the exact type.
  • Does the type surface publicly, or only internally with decimal at the boundary? Only the first actually lets a caller handle a 1e90 balance.

Why separately from #148

#148 closes two live failures and is a minor-version change. This is a public API question that deserves discussion rather than being settled in a bugfix, days after 11.0.0 shipped.

Blocked by nothing; #148 does not have to land first, but it makes the interim behaviour honest while this is decided.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions