ox
Version:
Ethereum Standard Library
230 lines • 7.07 kB
JavaScript
import * as Errors from './Errors.js';
/** @see https://ethereum.github.io/yellowpaper/paper.pdf */
export const exponents = {
wei: 0,
gwei: 9,
szabo: 12,
finney: 15,
ether: 18,
};
/**
* Formats a `bigint` Value to its string representation (divided by the given exponent).
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.format(420_000_000_000n, 9)
* // @log: '420'
* ```
*
* @param value - The `bigint` Value to format.
* @param decimals - The exponent to divide the `bigint` Value by.
* @returns The string representation of the Value.
*/
export function format(value, decimals = 0) {
if (!Number.isInteger(decimals) || decimals < 0)
throw new InvalidDecimalsError({ decimals });
let display = value.toString();
const negative = display.startsWith('-');
if (negative)
display = display.slice(1);
display = display.padStart(decimals, '0');
let [integer, fraction] = [
display.slice(0, display.length - decimals),
display.slice(display.length - decimals),
];
fraction = fraction.replace(/(0+)$/, '');
return `${negative ? '-' : ''}${integer || '0'}${fraction ? `.${fraction}` : ''}`;
}
/**
* Formats a `bigint` Value (default: wei) to a string representation of Ether.
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.formatEther(1_000_000_000_000_000_000n)
* // @log: '1'
* ```
*
* @param wei - The Value to format.
* @param unit - The unit to format the Value in. @default 'wei'.
* @returns The Ether string representation of the Value.
*/
export function formatEther(wei, unit = 'wei') {
return format(wei, exponents.ether - exponents[unit]);
}
/**
* Formats a `bigint` Value (default: wei) to a string representation of Gwei.
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.formatGwei(1_000_000_000n)
* // @log: '1'
* ```
*
* @param wei - The Value to format.
* @param unit - The unit to format the Value in. @default 'wei'.
* @returns The Gwei string representation of the Value.
*/
export function formatGwei(wei, unit = 'wei') {
return format(wei, exponents.gwei - exponents[unit]);
}
/**
* Parses a `string` representation of a Value to `bigint` (multiplied by the given exponent).
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.from('420', 9)
* // @log: 420000000000n
* ```
*
* @param value - The string representation of the Value.
* @param decimals - The exponent to multiply the Value by.
* @returns The `bigint` representation of the Value.
*/
export function from(value, decimals = 0) {
if (!Number.isInteger(decimals) || decimals < 0)
throw new InvalidDecimalsError({ decimals });
// Require at least one digit overall. Rejects '', '.', '-', '-.' which
// the previous regex accepted (and which then either produced garbage or
// threw a raw `SyntaxError` from `BigInt('')`).
if (!/^-?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)$/.test(value))
throw new InvalidDecimalNumberError({ value });
let [integer = '', fraction = '0'] = value.split('.');
const negative = integer.startsWith('-');
if (negative)
integer = integer.slice(1);
if (integer === '')
integer = '0';
// trim trailing zeros.
fraction = fraction.replace(/(0+)$/, '');
// round off if the fraction is larger than the number of decimals.
if (decimals === 0) {
// Round half-away-from-zero by inspecting the first fractional digit.
if (fraction.length > 0 && Number.parseInt(fraction[0], 10) >= 5)
integer = `${BigInt(integer) + 1n}`;
fraction = '';
}
else if (fraction.length > decimals) {
const left = fraction.slice(0, decimals);
const roundDigit = Number.parseInt(fraction.slice(decimals, decimals + 1), 10);
if (roundDigit >= 5) {
// Carry through the truncated fraction digits into the integer part
// without converting to a JS Number (avoids float precision loss).
const carried = carry(left);
if (carried.length > decimals) {
// Carry overflowed into the integer part.
fraction = carried.slice(1);
integer = `${BigInt(integer) + 1n}`;
}
else {
fraction = carried;
}
}
else {
fraction = left;
}
}
else {
fraction = fraction.padEnd(decimals, '0');
}
return BigInt(`${negative ? '-' : ''}${integer}${fraction}`);
}
/**
* Adds 1 to a digit string with carry, returning a string of the same length
* unless the carry overflows past the most-significant digit (in which case
* the returned string is one digit longer).
*
* @internal
*/
function carry(digits) {
const out = digits.split('');
let i = out.length - 1;
while (i >= 0) {
const d = Number.parseInt(out[i], 10) + 1;
if (d < 10) {
out[i] = String(d);
return out.join('');
}
out[i] = '0';
i--;
}
return `1${out.join('')}`;
}
/**
* Parses a string representation of Ether to a `bigint` Value (default: wei).
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.fromEther('420')
* // @log: 420000000000000000000n
* ```
*
* @param ether - String representation of Ether.
* @param unit - The unit to parse to. @default 'wei'.
* @returns A `bigint` Value.
*/
export function fromEther(ether, unit = 'wei') {
return from(ether, exponents.ether - exponents[unit]);
}
/**
* Parses a string representation of Gwei to a `bigint` Value (default: wei).
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.fromGwei('420')
* // @log: 420000000000n
* ```
*
* @param gwei - String representation of Gwei.
* @param unit - The unit to parse to. @default 'wei'.
* @returns A `bigint` Value.
*/
export function fromGwei(gwei, unit = 'wei') {
return from(gwei, exponents.gwei - exponents[unit]);
}
/**
* Thrown when a value is not a valid decimal number.
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.fromEther('123.456.789')
* // @error: Value.InvalidDecimalNumberError: Value `123.456.789` is not a valid decimal number.
* ```
*/
export class InvalidDecimalNumberError extends Errors.BaseError {
name = 'Value.InvalidDecimalNumberError';
constructor({ value }) {
super(`Value \`${value}\` is not a valid decimal number.`);
}
}
/**
* Thrown when the `decimals` argument is not a non-negative integer.
*
* @example
* ```ts twoslash
* import { Value } from 'ox'
*
* Value.from('1', -1)
* // @error: Value.InvalidDecimalsError: `decimals` must be a non-negative integer. Got `-1`.
* ```
*/
export class InvalidDecimalsError extends Errors.BaseError {
name = 'Value.InvalidDecimalsError';
constructor({ decimals }) {
super(`\`decimals\` must be a non-negative integer. Got \`${decimals}\`.`);
}
}
//# sourceMappingURL=Value.js.map