solana-web3.js
solana-web3.js copied to clipboard
[experimental] A library to make it easy to do math and work with amounts
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.NumberFormatv3 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.12See also
- https://github.com/tc39/proposal-intl-numberformat-v3#interpret-strings-as-decimals-ecma-402-334
- https://caniuse.com/mdn-javascript_builtins_intl_numberformat_format_number_parameter-string_decimal
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);
}