diff --git a/src/utils/bn.util.ts b/src/utils/bn.util.ts new file mode 100644 index 0000000..cd5733f --- /dev/null +++ b/src/utils/bn.util.ts @@ -0,0 +1,218 @@ +export declare type HexString = string +export declare type Numbers = number | bigint | string | HexString + +const isHexStrict = hex => typeof hex === 'string' && /^((-)?0x[0-9a-f]+|(0x))$/i.test(hex) +export declare type ValidInputTypes = Uint8Array | bigint | string | number | boolean +export const isHex = (hex: ValidInputTypes): boolean => + typeof hex === 'number' || + typeof hex === 'bigint' || + (typeof hex === 'string' && /^((-0x|0x|-)?[0-9a-f]+|(0x))$/i.test(hex)) +const base = BigInt(10) +const expo10 = (expo: number) => base ** BigInt(expo) + +export const ethUnitMap = { + noether: BigInt('0'), + wei: BigInt(1), + kwei: expo10(3), + Kwei: expo10(3), + babbage: expo10(3), + femtoether: expo10(3), + mwei: expo10(6), + Mwei: expo10(6), + lovelace: expo10(6), + picoether: expo10(6), + gwei: expo10(9), + Gwei: expo10(9), + shannon: expo10(9), + nanoether: expo10(9), + nano: expo10(9), + szabo: expo10(12), + microether: expo10(12), + micro: expo10(12), + finney: expo10(15), + milliether: expo10(15), + milli: expo10(15), + ether: expo10(18), + kether: expo10(21), + grand: expo10(21), + mether: expo10(24), + gether: expo10(27), + tether: expo10(30), +} + +export type EtherUnits = keyof typeof ethUnitMap + +/** + * Converts value to it's number representation + */ +export const hexToNumber = (value: string): bigint | number => { + if (!isHexStrict(value)) { + throw new Error('Invalid hex string') + } + + const [negative, hexValue] = value.startsWith('-') ? [true, value.slice(1)] : [false, value] + const num = BigInt(hexValue) + + if (num > Number.MAX_SAFE_INTEGER) { + return negative ? -num : num + } + + if (num < Number.MIN_SAFE_INTEGER) { + return num + } + + return negative ? -1 * Number(num) : Number(num) +} + +export const toNumber = (value: Numbers): number | bigint => { + if (typeof value === 'number') { + return value + } + + if (typeof value === 'bigint') { + return value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER ? Number(value) : value + } + + if (typeof value === 'string' && isHexStrict(value)) { + return hexToNumber(value) + } + + try { + return toNumber(BigInt(value)) + } catch { + throw new Error('ivalid number: ' + value) + } +} + +/** + * Auto converts any given value into it's bigint representation + * + * @param value - The value to convert + * @returns - Returns the value in bigint representation + + * @example + * ```ts + * console.log(web3.utils.toBigInt(1)); + * > 1n + * ``` + */ +export const toBigInt = (value: unknown): bigint => { + if (typeof value === 'number') { + return BigInt(value) + } + + if (typeof value === 'bigint') { + return value + } + + // isHex passes for dec, too + if (typeof value === 'string' && isHex(value)) { + return BigInt(value) + } + + if (typeof value === 'string' && value.indexOf(',') >= 0) { + return BigInt(value.replace(/,/g, '')) + } + + throw new Error('invalid number' + value) +} + +export const toBigWei = (number: Numbers, unit: EtherUnits = 'ether'): bigint => { + return toBigInt(toWei(number, unit)) +} + +export const toWei = (number: Numbers, unit: EtherUnits = 'ether'): string => { + const denomination = ethUnitMap[unit] + + if (!denomination) { + throw new Error('error unit: ' + unit) + } + + // if value is decimal e.g. 24.56 extract `integer` and `fraction` part + // to avoid `fraction` to be null use `concat` with empty string + typeof number === 'string' && number.indexOf(',') >= 0 && (number = number.replace(/,/g, '')) + const [integer, fraction] = String(typeof number === 'string' && !isHexStrict(number) ? number : toNumber(number)) + .split('.') + .concat('') + + // join the value removing `.` from + // 24.56 -> 2456 + const value = BigInt(`${integer}${fraction}`) + + // multiply value with denomination + // 2456 * 1000000 -> 2456000000 + const updatedValue = value * denomination + + // count number of zeros in denomination + const numberOfZerosInDenomination = denomination.toString().length - 1 + + // check which either `fraction` or `denomination` have lower number of zeros + const decimals = Math.min(fraction.length, numberOfZerosInDenomination) + + if (decimals === 0) { + return updatedValue.toString() + } + + // Add zeros to make length equal to required decimal points + // If string is larger than decimal points required then remove last zeros + return updatedValue.toString().padStart(decimals, '0').slice(0, -decimals) +} + +/** + * Takes a number of wei and converts it to any other ether unit. + * @param number - The value in wei + * @param unit - The unit to convert to + * @returns - Returns the converted value in the given unit + * + * @example + * ```ts + * console.log(web3.utils.fromWei("1", "ether")); + * > 0.000000000000000001 + * + * console.log(web3.utils.fromWei("1", "shannon")); + * > 0.000000001 + * ``` + */ +export const fromWei = (number: Numbers, unit: EtherUnits = 'ether'): string => { + const denomination = ethUnitMap[unit] + + if (!denomination) { + throw new Error('invalid unit: ' + unit) + } + + // value in wei would always be integer + // 13456789, 1234 + const value = String(toNumber(number)) + + // count number of zeros in denomination + // 1000000 -> 6 + const numberOfZerosInDenomination = denomination.toString().length - 1 + + if (numberOfZerosInDenomination <= 0) { + return value.toString() + } + + // pad the value with required zeros + // 13456789 -> 13456789, 1234 -> 001234 + const zeroPaddedValue = value.padStart(numberOfZerosInDenomination, '0') + + // get the integer part of value by counting number of zeros from start + // 13456789 -> '13' + // 001234 -> '' + const integer = zeroPaddedValue.slice(0, -numberOfZerosInDenomination) + + // get the fraction part of value by counting number of zeros backward + // 13456789 -> '456789' + // 001234 -> '001234' + const fraction = zeroPaddedValue.slice(-numberOfZerosInDenomination).replace(/\.?0+$/, '') + + if (integer === '') { + return `0.${fraction}` + } + + if (fraction === '') { + return integer + } + + return `${integer}.${fraction}` +}