@metamask/utils
Version:
Various JavaScript/TypeScript utilities of wide relevance to the MetaMask codebase
209 lines (208 loc) • 7.18 kB
JavaScript
/* eslint-disable operator-assignment */
/*
Primary Attribution
Richard Moore <ricmoo@me.com>
https://github.com/ethers-io
Note, Richard is a god of ether gods. Follow and respect him, and use Ethers.io!
*/
const zero = BigInt(0);
const negative1 = BigInt(-1);
/**
* Converts a string, number, or bigint to a bigint.
*
* @param arg - The value to convert to bigint.
* @returns The bigint representation of the input.
* @throws Error if the input type cannot be converted to bigint.
*/
export function numericToBigInt(arg) {
if (typeof arg === 'string') {
return BigInt(arg);
}
if (typeof arg === 'number') {
return BigInt(arg);
}
if (typeof arg === 'bigint') {
return arg;
}
throw new Error(`Cannot convert ${typeof arg} to BigInt`);
}
// complete ethereum unit map
export const unitMap = {
noether: '0',
wei: '1',
kwei: '1000',
Kwei: '1000',
babbage: '1000',
femtoether: '1000',
mwei: '1000000',
Mwei: '1000000',
lovelace: '1000000',
picoether: '1000000',
gwei: '1000000000',
Gwei: '1000000000',
shannon: '1000000000',
nanoether: '1000000000',
nano: '1000000000',
szabo: '1000000000000',
microether: '1000000000000',
micro: '1000000000000',
finney: '1000000000000000',
milliether: '1000000000000000',
milli: '1000000000000000',
ether: '1000000000000000000',
kether: '1000000000000000000000',
grand: '1000000000000000000000',
mether: '1000000000000000000000000',
gether: '1000000000000000000000000000',
tether: '1000000000000000000000000000000',
};
// Pre-computed unit values as BigInt for performance
const unitMapBigInt = Object.fromEntries(Object.entries(unitMap).map(([key, value]) => [key, BigInt(value)]));
const unitLengths = Object.fromEntries(Object.entries(unitMap).map(([key, value]) => [key, value.length - 1 || 1]));
const NUMBER_REGEX = /^-?[0-9.]+$/u;
const FRACTION_REGEX = /^([0-9]*[1-9]|0)(0*)/u;
const COMMIFY_REGEX = /\B(?=(\d{3})+(?!\d))/gu;
/**
* Returns value of unit in Wei.
*
* @param unitInput - The unit to convert to, default ether.
* @returns Value of the unit (in Wei).
* @throws Error if the unit is not correct.
*/
export function getValueOfUnit(unitInput = 'ether') {
const unit = unitInput.toLowerCase();
const unitValue = unitMapBigInt[unit];
if (unitValue === undefined) {
throw new Error(`The unit provided ${unitInput} doesn't exist, please use the one of the following units ${JSON.stringify(unitMap, null, 2)}`);
}
return unitValue;
}
/**
* Converts a number to a string.
*
* @param arg - The number to convert to a string.
* @returns The string representation of the number.
* @throws Error if the number is invalid.
*/
export function numberToString(arg) {
if (typeof arg === 'string') {
if (!NUMBER_REGEX.test(arg)) {
throw new Error(`while converting number to string, invalid number value '${arg}', should be a number matching (^-?[0-9.]+).`);
}
return arg;
}
if (typeof arg === 'number') {
return String(arg);
}
if (typeof arg === 'bigint') {
return arg.toString();
}
throw new Error(`while converting number to string, invalid number value '${String(arg)}' type ${typeof arg}.`);
}
/**
* Converts a number from Wei to a string.
*
* @param weiInput - The number to convert from Wei.
* @param unit - The unit to convert to, default ether.
* @param optionsInput - The options to use for the conversion.
* @param optionsInput.pad - Whether to pad the fractional part with zeros.
* @param optionsInput.commify - Whether to add commas to separate thousands.
* @returns The string representation of the number.
* @throws Error if the number is invalid.
*/
export function fromWei(weiInput, unit, optionsInput) {
let wei = numericToBigInt(weiInput);
const negative = wei < zero;
const unitLower = unit.toLowerCase();
const base = unitMapBigInt[unitLower];
const baseLength = unitLengths[unitLower];
const options = optionsInput ?? {};
if (base === undefined) {
throw new Error(`The unit provided ${unit} doesn't exist, please use the one of the following units ${JSON.stringify(unitMap, null, 2)}`);
}
// Handle special case of noether (base = 0)
if (base === zero) {
return negative ? '-0' : '0';
}
if (negative) {
wei = wei * negative1;
}
let fraction = (wei % base).toString();
fraction = fraction.padStart(baseLength, '0');
if (!options.pad) {
const fractionMatch = fraction.match(FRACTION_REGEX);
// istanbul ignore next: defensive fallback that's never reachable but necessary to satisfy TS
fraction = fractionMatch?.[1] ?? '0';
}
let whole = (wei / base).toString();
if (options.commify) {
whole = whole.replace(COMMIFY_REGEX, ',');
}
let value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`;
if (negative) {
value = `-${value}`;
}
return value;
}
/**
* Converts a number to Wei.
*
* @param etherInput - The number to convert to Wei.
* @param unit - The unit to convert to, default ether.
* @returns The number in Wei.
* @throws Error if the number is invalid.
*/
export function toWei(etherInput, unit) {
const unitLower = unit.toLowerCase();
const base = unitMapBigInt[unitLower];
const baseLength = unitLengths[unitLower];
if (base === undefined) {
throw new Error(`The unit provided ${unit} doesn't exist, please use the one of the following units ${JSON.stringify(unitMap, null, 2)}`);
}
// Handle special case of noether (base = 0)
if (base === zero) {
return zero;
}
// Fast path for bigint inputs when unit is wei (no conversion needed)
if (typeof etherInput === 'bigint' && unitLower === 'wei') {
return etherInput;
}
// Fast path for bigint inputs with whole units (no fractional part)
if (typeof etherInput === 'bigint') {
return etherInput * base;
}
let ether = numberToString(etherInput);
// Is it negative?
const negative = ether.startsWith('-');
if (negative) {
ether = ether.substring(1);
}
if (ether === '.') {
throw new Error(`While converting number ${etherInput} to wei, invalid value`);
}
// Split it into a whole and fractional part
const comps = ether.split('.');
if (comps.length > 2) {
throw new Error(`While converting number ${etherInput} to wei, too many decimal points`);
}
let whole = comps[0];
let fraction = comps[1];
if (!whole) {
whole = '0';
}
if (!fraction) {
fraction = '0';
}
if (fraction.length > baseLength) {
throw new Error(`While converting number ${etherInput} to wei, too many decimal places`);
}
fraction = fraction.padEnd(baseLength, '0');
const wholeBigInt = BigInt(whole);
const fractionBigInt = BigInt(fraction);
let wei = wholeBigInt * base + fractionBigInt;
if (negative) {
wei = wei * negative1;
}
return wei;
}
//# sourceMappingURL=unitsConversion.mjs.map