/** * Currency Conversion Utility * This utility function can be used for converting currency related values within metamask. * The caller should be able to pass it a value, along with information about the value's * numeric base, denomination and currency, and the desired numeric base, denomination and * currency. It should return a single value. * * @param {(number | string | BN)} value - The value to convert. * @param {object} [options] - Options to specify details of the conversion * @param {string} [options.fromCurrency = 'ETH' | 'USD'] - The currency of the passed value * @param {string} [options.toCurrency = 'ETH' | 'USD'] - The desired currency of the result * @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] - The numeric basic of the passed value. * @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] - The desired numeric basic of the result. * @param {string} [options.fromDenomination = 'WEI'] - The denomination of the passed value * @param {string} [options.numberOfDecimals] - The desired number of decimals in the result * @param {string} [options.roundDown] - The desired number of decimals to round down to * @param {number} [options.conversionRate] - The rate to use to make the fromCurrency -> toCurrency conversion * @returns {(number | string | BN)} * * The utility passes value along with the options as a single object to the `converter` function. * `converter` conditional modifies the supplied `value` property, depending * on the accompanying options. */ import BigNumber from 'bignumber.js'; import { addHexPrefix, BN } from 'ethereumjs-util'; import { ETH, WEI } from '../../ui/helpers/constants/common'; import { stripHexPrefix } from './hexstring-utils'; // Big Number Constants const BIG_NUMBER_WEI_MULTIPLIER = new BigNumber('1000000000000000000'); const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000'); const BIG_NUMBER_ETH_MULTIPLIER = new BigNumber('1'); // Setter Maps const toBigNumber = { hex: (n) => new BigNumber(stripHexPrefix(n), 16), dec: (n) => new BigNumber(String(n), 10), BN: (n) => new BigNumber(n.toString(16), 16), }; const toNormalizedDenomination = { WEI: (bigNumber) => bigNumber.div(BIG_NUMBER_WEI_MULTIPLIER), GWEI: (bigNumber) => bigNumber.div(BIG_NUMBER_GWEI_MULTIPLIER), ETH: (bigNumber) => bigNumber.div(BIG_NUMBER_ETH_MULTIPLIER), }; const toSpecifiedDenomination = { WEI: (bigNumber) => bigNumber.times(BIG_NUMBER_WEI_MULTIPLIER).round(), GWEI: (bigNumber) => bigNumber.times(BIG_NUMBER_GWEI_MULTIPLIER).round(9), ETH: (bigNumber) => bigNumber.times(BIG_NUMBER_ETH_MULTIPLIER).round(9), }; const baseChange = { hex: (n) => n.toString(16), dec: (n) => new BigNumber(n).toString(10), BN: (n) => new BN(n.toString(16)), }; // Utility function for checking base types const isValidBase = (base) => { return Number.isInteger(base) && base > 1; }; /** * Defines the base type of numeric value * * @typedef {('hex' | 'dec' | 'BN')} NumericBase */ /** * Defines which type of denomination a value is in * * @typedef {('WEI' | 'GWEI' | 'ETH')} EthDenomination */ /** * Utility method to convert a value between denominations, formats and currencies. * * @param {object} input * @param {string | BigNumber} input.value * @param {NumericBase} input.fromNumericBase * @param {EthDenomination} [input.fromDenomination] * @param {string} [input.fromCurrency] * @param {NumericBase} input.toNumericBase * @param {EthDenomination} [input.toDenomination] * @param {string} [input.toCurrency] * @param {number} [input.numberOfDecimals] * @param {number} [input.conversionRate] * @param {boolean} [input.invertConversionRate] * @param {string} [input.roundDown] */ const converter = ({ value, fromNumericBase, fromDenomination, fromCurrency, toNumericBase, toDenomination, toCurrency, numberOfDecimals, conversionRate, invertConversionRate, roundDown, }) => { let convertedValue = fromNumericBase ? toBigNumber[fromNumericBase](value) : value; if (fromDenomination) { convertedValue = toNormalizedDenomination[fromDenomination](convertedValue); } if (fromCurrency !== toCurrency) { if (conversionRate === null || conversionRate === undefined) { throw new Error( `Converting from ${fromCurrency} to ${toCurrency} requires a conversionRate, but one was not provided`, ); } let rate = toBigNumber.dec(conversionRate); if (invertConversionRate) { rate = new BigNumber(1.0).div(conversionRate); } convertedValue = convertedValue.times(rate); } if (toDenomination) { convertedValue = toSpecifiedDenomination[toDenomination](convertedValue); } if (numberOfDecimals !== undefined && numberOfDecimals !== null) { convertedValue = convertedValue.round( numberOfDecimals, BigNumber.ROUND_HALF_DOWN, ); } if (roundDown) { convertedValue = convertedValue.round(roundDown, BigNumber.ROUND_DOWN); } if (toNumericBase) { convertedValue = baseChange[toNumericBase](convertedValue); } return convertedValue; }; const conversionUtil = ( value, { fromCurrency = null, toCurrency = fromCurrency, fromNumericBase, toNumericBase, fromDenomination, toDenomination, numberOfDecimals, conversionRate, invertConversionRate, }, ) => { if (fromCurrency !== toCurrency && !conversionRate) { return 0; } return converter({ fromCurrency, toCurrency, fromNumericBase, toNumericBase, fromDenomination, toDenomination, numberOfDecimals, conversionRate, invertConversionRate, value: value || '0', }); }; const getBigNumber = (value, base) => { if (!isValidBase(base)) { throw new Error('Must specify valid base'); } // We don't include 'number' here, because BigNumber will throw if passed // a number primitive it considers unsafe. if (typeof value === 'string' || value instanceof BigNumber) { return new BigNumber(value, base); } return new BigNumber(String(value), base); }; const addCurrencies = (a, b, options = {}) => { const { aBase, bBase, ...conversionOptions } = options; if (!isValidBase(aBase) || !isValidBase(bBase)) { throw new Error('Must specify valid aBase and bBase'); } const value = getBigNumber(a, aBase).add(getBigNumber(b, bBase)); return converter({ value, ...conversionOptions, }); }; const subtractCurrencies = (a, b, options = {}) => { const { aBase, bBase, ...conversionOptions } = options; if (!isValidBase(aBase) || !isValidBase(bBase)) { throw new Error('Must specify valid aBase and bBase'); } const value = getBigNumber(a, aBase).minus(getBigNumber(b, bBase)); return converter({ value, ...conversionOptions, }); }; const multiplyCurrencies = (a, b, options = {}) => { const { multiplicandBase, multiplierBase, ...conversionOptions } = options; if (!isValidBase(multiplicandBase) || !isValidBase(multiplierBase)) { throw new Error('Must specify valid multiplicandBase and multiplierBase'); } const value = getBigNumber(a, multiplicandBase).times( getBigNumber(b, multiplierBase), ); return converter({ value, ...conversionOptions, }); }; const divideCurrencies = (a, b, options = {}) => { const { dividendBase, divisorBase, ...conversionOptions } = options; if (!isValidBase(dividendBase) || !isValidBase(divisorBase)) { throw new Error('Must specify valid dividendBase and divisorBase'); } const value = getBigNumber(a, dividendBase).div(getBigNumber(b, divisorBase)); return converter({ value, ...conversionOptions, }); }; const conversionGreaterThan = ({ ...firstProps }, { ...secondProps }) => { const firstValue = converter({ ...firstProps }); const secondValue = converter({ ...secondProps }); return firstValue.gt(secondValue); }; const conversionLessThan = ({ ...firstProps }, { ...secondProps }) => { const firstValue = converter({ ...firstProps }); const secondValue = converter({ ...secondProps }); return firstValue.lt(secondValue); }; const conversionMax = ({ ...firstProps }, { ...secondProps }) => { const firstIsGreater = conversionGreaterThan( { ...firstProps }, { ...secondProps }, ); return firstIsGreater ? firstProps.value : secondProps.value; }; const conversionGTE = ({ ...firstProps }, { ...secondProps }) => { const firstValue = converter({ ...firstProps }); const secondValue = converter({ ...secondProps }); return firstValue.greaterThanOrEqualTo(secondValue); }; const conversionLTE = ({ ...firstProps }, { ...secondProps }) => { const firstValue = converter({ ...firstProps }); const secondValue = converter({ ...secondProps }); return firstValue.lessThanOrEqualTo(secondValue); }; const toNegative = (n, options = {}) => { return multiplyCurrencies(n, -1, options); }; function decGWEIToHexWEI(decGWEI) { return conversionUtil(decGWEI, { fromNumericBase: 'dec', toNumericBase: 'hex', fromDenomination: 'GWEI', toDenomination: 'WEI', }); } export function subtractHexes(aHexWEI, bHexWEI) { return subtractCurrencies(aHexWEI, bHexWEI, { aBase: 16, bBase: 16, toNumericBase: 'hex', numberOfDecimals: 6, }); } export function addHexes(aHexWEI, bHexWEI) { return addCurrencies(aHexWEI, bHexWEI, { aBase: 16, bBase: 16, toNumericBase: 'hex', numberOfDecimals: 6, }); } export function decWEIToDecETH(hexWEI) { return conversionUtil(hexWEI, { fromNumericBase: 'dec', toNumericBase: 'dec', fromDenomination: 'WEI', toDenomination: 'ETH', }); } export function hexWEIToDecETH(hexWEI) { return conversionUtil(hexWEI, { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', toDenomination: 'ETH', }); } export function decEthToConvertedCurrency( ethTotal, convertedCurrency, conversionRate, ) { return conversionUtil(ethTotal, { fromNumericBase: 'dec', toNumericBase: 'dec', fromCurrency: 'ETH', toCurrency: convertedCurrency, numberOfDecimals: 2, conversionRate, }); } export function getWeiHexFromDecimalValue({ value, fromCurrency, conversionRate, fromDenomination, invertConversionRate, }) { return conversionUtil(value, { fromNumericBase: 'dec', toNumericBase: 'hex', toCurrency: ETH, fromCurrency, conversionRate, invertConversionRate, fromDenomination, toDenomination: WEI, }); } /** * Converts a BN object to a hex string with a '0x' prefix * * @param {BN} inputBn - The BN to convert to a hex string * @returns {string} A '0x' prefixed hex string */ export function bnToHex(inputBn) { return addHexPrefix(inputBn.toString(16)); } export function getValueFromWeiHex({ value, fromCurrency = ETH, toCurrency, conversionRate, numberOfDecimals, toDenomination, }) { return conversionUtil(value, { fromNumericBase: 'hex', toNumericBase: 'dec', fromCurrency, toCurrency, numberOfDecimals, fromDenomination: WEI, toDenomination, conversionRate, }); } export function sumHexes(...args) { const total = args.reduce((acc, hexAmount) => { return addCurrencies(acc, hexAmount, { toNumericBase: 'hex', aBase: 16, bBase: 16, }); }); return addHexPrefix(total); } export function hexWEIToDecGWEI(decGWEI) { return conversionUtil(decGWEI, { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', toDenomination: 'GWEI', }); } export function decimalToHex(decimal) { return conversionUtil(decimal, { fromNumericBase: 'dec', toNumericBase: 'hex', }); } export function hexToDecimal(hexValue) { return conversionUtil(hexValue, { fromNumericBase: 'hex', toNumericBase: 'dec', }); } export { conversionUtil, addCurrencies, multiplyCurrencies, conversionGreaterThan, conversionLessThan, conversionGTE, conversionLTE, conversionMax, toNegative, subtractCurrencies, decGWEIToHexWEI, toBigNumber, toNormalizedDenomination, divideCurrencies, };