Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[experimental] A library to make it easy to do math and work with amounts #2125

Open
steveluscher opened this issue Feb 15, 2024 · 1 comment
Assignees
Labels
enhancement New feature or request

Comments

@steveluscher
Copy link
Collaborator

steveluscher commented Feb 15, 2024

The new web3.js leans into JavaScript BigInts and opaque types for amounts like Lamports. While BigInts can prevent truncation errors (see #1116) they are in some ways more difficult to do math on. While the Lamports type gives developers a strong and typesafe signal that a given input or output is in lamports rather than SOL, it offers no way to convert between the two or to prepare one for UI display in the other.

The goal of this issue is to develop an API that allows developers to do both without introducing rounding or precision errors.

Spitballing an API

Values as basis points

Starting with values expressed as an opaque type having basis points and a decimal:

type Value<
  TDecimals extends bigint,
  TBasisPoints extends bigint = bigint
> = [basisPoints: TBasisPoints, decimal: TDecimals];
type LamportsValue = Value<0n>;
type SolValue = Value<9n>;

And coercion functions:

function lamportsValue(putativeLamportsValue: unknown): putativeValue is LamportsValue {
  assertIsValue(putativeValue);  // Array.isArray(v) && v.length === 2 && v.every(vv => typeof vv === 'bigint')
  if (putativeValue[1] !== 0n) {
    throw new SolanaError(...);
  }
  return putativeValue as LamportsValue;
}
function solValue(putativeSolValue: unknown): putativeValue is SolValue {
  assertIsValue(putativeValue);  // Array.isArray(v) && v.length === 2 && v.every(vv => typeof vv === 'bigint')
  if (putativeValue[1] !== 9n) {
    throw new SolanaError(...);
  }
  return putativeValue as SolValue;
}

Important

Decimals must be 0n or greater.

Math

TODO

Formatting

Tip

Apparently we can achieve UI display using Intl.NumberFormat v3 by leaning on scientific notation. Check it out.

const formatter = new Intl.NumberFormat("en-US", {
  currency: "USD",
  minimumFractionDigits: 2,
  roundingMode: 'halfExpand',
  style: "currency",
});
const basisPoints = 100115000n;
const decimals = 6;
formatter.format(`${basisPoints}E-${decimals}`); // -> $100.12

See also

Convert values to decimal strings for formatting.

function valueToDecimalString<TDecimals extends bigint, TBasisPoints extends bigint = bigint>(
    value: Value<TDecimals, TBasisPoints>,
): `${TBasisPoints}E-${TDecimals}` {
    return `${value[0]}E-${value[1]}`;
}

Let callers supply a formatter.

function getFormatter(locale) {
    return new Intl.NumberFormat(locale, {
        maximumFractionDigits: 2,
        roundingMode: 'halfExpand',
        notation: 'compact',
        compactDisplay: 'short',
    });
}
const decimalString = valueToDecimalString([12345671234555321n, 9n]);
`${getFormatter('ja-JP').format(decimalString)} SOL`;
> '1234.57万 SOL'
`${getFormatter('en-US').format(decimalString)} SOL`;
> '12.35M SOL'

Maybe, just maybe, we can make a passthrough:

function format(value: Value<bigint>, ...formatArgs: ConstructorParameters<typeof Intl.NumberFormat>): string {
    // Don't love this architecture though, since it means recreating the `NumberFormat` object on every pass.
    const formatter = new Intl.NumberFormat(...formatArgs);
    const decimalString = valueToDecimalString(value);
    return formatter.format(decimalString);
}
@steveluscher steveluscher added the enhancement New feature or request label Feb 15, 2024
@lorisleiva lorisleiva self-assigned this Feb 16, 2024
@steveluscher
Copy link
Collaborator Author

steveluscher commented Mar 5, 2024

Bullish on a string formatting API that looks something like:

function formatLamportsAsSol(formatter: Intl.NumberFormat, lamports: Lamports): string {
    return formatter.format(`${lamports}E-9`);
}

function formatSolAsLamports(formatter: Intl.NumberFormat, solDecimalString: string): string {
    return formatter.format(`${solDecimalString}E9`);
}

Nicely does:

console.log(formatSolAsLamports(formatter, '2.123456789'));
> "2123456789"

^ these are my half-baked thoughts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants