diff options
Diffstat (limited to 'ui/app/helpers/utils')
-rw-r--r-- | ui/app/helpers/utils/common.util.js | 5 | ||||
-rw-r--r-- | ui/app/helpers/utils/common.util.test.js | 27 | ||||
-rw-r--r-- | ui/app/helpers/utils/confirm-tx.util.js | 131 | ||||
-rw-r--r-- | ui/app/helpers/utils/confirm-tx.util.test.js | 137 | ||||
-rw-r--r-- | ui/app/helpers/utils/conversion-util.js | 251 | ||||
-rw-r--r-- | ui/app/helpers/utils/conversion-util.test.js | 22 | ||||
-rw-r--r-- | ui/app/helpers/utils/conversions.util.js | 122 | ||||
-rw-r--r-- | ui/app/helpers/utils/formatters.js | 3 | ||||
-rw-r--r-- | ui/app/helpers/utils/i18n-helper.js | 44 | ||||
-rw-r--r-- | ui/app/helpers/utils/metametrics.util.js | 184 | ||||
-rw-r--r-- | ui/app/helpers/utils/token-util.js | 118 | ||||
-rw-r--r-- | ui/app/helpers/utils/transactions.util.js | 179 | ||||
-rw-r--r-- | ui/app/helpers/utils/transactions.util.test.js | 57 | ||||
-rw-r--r-- | ui/app/helpers/utils/util.js | 326 |
14 files changed, 1606 insertions, 0 deletions
diff --git a/ui/app/helpers/utils/common.util.js b/ui/app/helpers/utils/common.util.js new file mode 100644 index 000000000..0c02481e6 --- /dev/null +++ b/ui/app/helpers/utils/common.util.js @@ -0,0 +1,5 @@ +export function camelCaseToCapitalize (str = '') { + return str + .replace(/([A-Z])/g, ' $1') + .replace(/^./, str => str.toUpperCase()) +} diff --git a/ui/app/helpers/utils/common.util.test.js b/ui/app/helpers/utils/common.util.test.js new file mode 100644 index 000000000..6259f4a89 --- /dev/null +++ b/ui/app/helpers/utils/common.util.test.js @@ -0,0 +1,27 @@ +import * as utils from './common.util' +import assert from 'assert' + +describe('Common utils', () => { + describe('camelCaseToCapitalize', () => { + it('should return a capitalized string from a camel-cased string', () => { + const tests = [ + { + test: undefined, + expected: '', + }, + { + test: '', + expected: '', + }, + { + test: 'thisIsATest', + expected: 'This Is A Test', + }, + ] + + tests.forEach(({ test, expected }) => { + assert.equal(utils.camelCaseToCapitalize(test), expected) + }) + }) + }) +}) diff --git a/ui/app/helpers/utils/confirm-tx.util.js b/ui/app/helpers/utils/confirm-tx.util.js new file mode 100644 index 000000000..f843db118 --- /dev/null +++ b/ui/app/helpers/utils/confirm-tx.util.js @@ -0,0 +1,131 @@ +import currencyFormatter from 'currency-formatter' +import currencies from 'currency-formatter/currencies' +import ethUtil from 'ethereumjs-util' +import BigNumber from 'bignumber.js' + +import { + conversionUtil, + addCurrencies, + multiplyCurrencies, + conversionGreaterThan, +} from './conversion-util' + +import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' + +export function increaseLastGasPrice (lastGasPrice) { + return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { + multiplicandBase: 16, + multiplierBase: 10, + toNumericBase: 'hex', + })) +} + +export function hexGreaterThan (a, b) { + return conversionGreaterThan( + { value: a, fromNumericBase: 'hex' }, + { value: b, fromNumericBase: 'hex' }, + ) +} + +export function getHexGasTotal ({ gasLimit, gasPrice }) { + return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 16, + })) +} + +export function addEth (...args) { + return args.reduce((acc, base) => { + return addCurrencies(acc, base, { + toNumericBase: 'dec', + numberOfDecimals: 6, + }) + }) +} + +export function addFiat (...args) { + return args.reduce((acc, base) => { + return addCurrencies(acc, base, { + toNumericBase: 'dec', + numberOfDecimals: 2, + }) + }) +} + +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 getTransactionFee ({ + value, + fromCurrency = 'ETH', + toCurrency, + conversionRate, + numberOfDecimals, +}) { + return conversionUtil(value, { + fromNumericBase: 'BN', + toNumericBase: 'dec', + fromDenomination: 'WEI', + fromCurrency, + toCurrency, + numberOfDecimals, + conversionRate, + }) +} + +export function formatCurrency (value, currencyCode) { + const upperCaseCurrencyCode = currencyCode.toUpperCase() + + return currencies.find(currency => currency.code === upperCaseCurrencyCode) + ? currencyFormatter.format(Number(value), { code: upperCaseCurrencyCode, style: 'currency' }) + : value +} + +export function convertTokenToFiat ({ + value, + fromCurrency = 'ETH', + toCurrency, + conversionRate, + contractExchangeRate, +}) { + const totalExchangeRate = conversionRate * contractExchangeRate + + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromCurrency, + toCurrency, + numberOfDecimals: 2, + conversionRate: totalExchangeRate, + }) +} + +export function hasUnconfirmedTransactions (state) { + return unconfirmedTransactionsCountSelector(state) > 0 +} + +export function roundExponential (value) { + const PRECISION = 4 + const bigNumberValue = new BigNumber(String(value)) + + // In JS, numbers with exponentials greater than 20 get displayed as an exponential. + return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value +} diff --git a/ui/app/helpers/utils/confirm-tx.util.test.js b/ui/app/helpers/utils/confirm-tx.util.test.js new file mode 100644 index 000000000..e818601ca --- /dev/null +++ b/ui/app/helpers/utils/confirm-tx.util.test.js @@ -0,0 +1,137 @@ +import * as utils from './confirm-tx.util' +import assert from 'assert' + +describe('Confirm Transaction utils', () => { + describe('increaseLastGasPrice', () => { + it('should increase the gasPrice by 10%', () => { + const increasedGasPrice = utils.increaseLastGasPrice('0xa') + assert.equal(increasedGasPrice, '0xb') + }) + + it('should prefix the result with 0x', () => { + const increasedGasPrice = utils.increaseLastGasPrice('a') + assert.equal(increasedGasPrice, '0xb') + }) + }) + + describe('hexGreaterThan', () => { + it('should return true if the first value is greater than the second value', () => { + assert.equal( + utils.hexGreaterThan('0xb', '0xa'), + true + ) + }) + + it('should return false if the first value is less than the second value', () => { + assert.equal( + utils.hexGreaterThan('0xa', '0xb'), + false + ) + }) + + it('should return false if the first value is equal to the second value', () => { + assert.equal( + utils.hexGreaterThan('0xa', '0xa'), + false + ) + }) + + it('should correctly compare prefixed and non-prefixed hex values', () => { + assert.equal( + utils.hexGreaterThan('0xb', 'a'), + true + ) + }) + }) + + describe('getHexGasTotal', () => { + it('should multiply the hex gasLimit and hex gasPrice values together', () => { + assert.equal( + utils.getHexGasTotal({ gasLimit: '0x5208', gasPrice: '0x3b9aca00' }), + '0x1319718a5000' + ) + }) + + it('should prefix the result with 0x', () => { + assert.equal( + utils.getHexGasTotal({ gasLimit: '5208', gasPrice: '3b9aca00' }), + '0x1319718a5000' + ) + }) + }) + + describe('addEth', () => { + it('should add two values together rounding to 6 decimal places', () => { + assert.equal( + utils.addEth('0.12345678', '0'), + '0.123457' + ) + }) + + it('should add any number of values together rounding to 6 decimal places', () => { + assert.equal( + utils.addEth('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'), + '0.123457' + ) + }) + }) + + describe('addFiat', () => { + it('should add two values together rounding to 2 decimal places', () => { + assert.equal( + utils.addFiat('0.12345678', '0'), + '0.12' + ) + }) + + it('should add any number of values together rounding to 2 decimal places', () => { + assert.equal( + utils.addFiat('0.1', '0.02', '0.003', '0.0004', '0.00005', '0.000006', '0.0000007'), + '0.12' + ) + }) + }) + + describe('getValueFromWeiHex', () => { + it('should get the transaction amount in ETH', () => { + const ethTransactionAmount = utils.getValueFromWeiHex({ + value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6, + }) + + assert.equal(ethTransactionAmount, '1') + }) + + it('should get the transaction amount in fiat', () => { + const fiatTransactionAmount = utils.getValueFromWeiHex({ + value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2, + }) + + assert.equal(fiatTransactionAmount, '468.58') + }) + }) + + describe('getTransactionFee', () => { + it('should get the transaction fee in ETH', () => { + const ethTransactionFee = utils.getTransactionFee({ + value: '0x1319718a5000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6, + }) + + assert.equal(ethTransactionFee, '0.000021') + }) + + it('should get the transaction fee in fiat', () => { + const fiatTransactionFee = utils.getTransactionFee({ + value: '0x1319718a5000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2, + }) + + assert.equal(fiatTransactionFee, '0.01') + }) + }) + + describe('formatCurrency', () => { + it('should format USD values', () => { + const value = utils.formatCurrency('123.45', 'usd') + assert.equal(value, '$123.45') + }) + }) +}) diff --git a/ui/app/helpers/utils/conversion-util.js b/ui/app/helpers/utils/conversion-util.js new file mode 100644 index 000000000..8cc531773 --- /dev/null +++ b/ui/app/helpers/utils/conversion-util.js @@ -0,0 +1,251 @@ +/* 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` uses Ramda.js to apply a composition of conditional setters to the `value` property, depending +* on the accompanying options. Some of these conditional setters are selected via key-value maps, where +* the keys are specified in the options parameters and the values are setter functions. +*/ + +const BigNumber = require('bignumber.js') +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN +const R = require('ramda') +const { stripHexPrefix } = require('ethereumjs-util') + +BigNumber.config({ + ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN, +}) + +// 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') + +// Individual Setters +const convert = R.invoker(1, 'times') +const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN) +const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN) +const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) +const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) + +// 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)), +} + +// Predicates +const fromAndToCurrencyPropsNotEqual = R.compose( + R.not, + R.eqBy(R.__, 'fromCurrency', 'toCurrency'), + R.flip(R.prop) +) + +// Lens +const valuePropertyLens = R.over(R.lensProp('value')) +const conversionRateLens = R.over(R.lensProp('conversionRate')) + +// conditional conversionRate setting wrapper +const whenPredSetCRWithPropAndSetter = (pred, prop, setter) => R.when( + pred, + R.converge( + conversionRateLens, + [R.pipe(R.prop(prop), setter), R.identity] + ) +) + +// conditional 'value' setting wrappers +const whenPredSetWithPropAndSetter = (pred, prop, setter) => R.when( + pred, + R.converge( + valuePropertyLens, + [R.pipe(R.prop(prop), setter), R.identity] + ) +) +const whenPropApplySetterMap = (prop, setterMap) => whenPredSetWithPropAndSetter( + R.prop(prop), + prop, + R.prop(R.__, setterMap) +) + +// Conversion utility function +const converter = R.pipe( + whenPredSetCRWithPropAndSetter(R.prop('conversionRate'), 'conversionRate', decToBigNumberViaString), + whenPredSetCRWithPropAndSetter(R.prop('invertConversionRate'), 'conversionRate', invertConversionRate), + whenPropApplySetterMap('fromNumericBase', toBigNumber), + whenPropApplySetterMap('fromDenomination', toNormalizedDenomination), + whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), + whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), + whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round), + whenPredSetWithPropAndSetter(R.prop('roundDown'), 'roundDown', roundDown), + whenPropApplySetterMap('toNumericBase', baseChange), + R.view(R.lensProp('value')) +) + +const conversionUtil = (value, { + fromCurrency = null, + toCurrency = fromCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + invertConversionRate, +}) => converter({ + fromCurrency, + toCurrency, + fromNumericBase, + toNumericBase, + fromDenomination, + toDenomination, + numberOfDecimals, + conversionRate, + invertConversionRate, + value: value || '0', +}) + +const addCurrencies = (a, b, options = {}) => { + const { + aBase, + bBase, + ...conversionOptions + } = options + const value = (new BigNumber(a.toString(), aBase)).add(b.toString(), bBase) + + return converter({ + value, + ...conversionOptions, + }) +} + +const subtractCurrencies = (a, b, options = {}) => { + const { + aBase, + bBase, + ...conversionOptions + } = options + const value = (new BigNumber(String(a), aBase)).minus(b, bBase) + + return converter({ + value, + ...conversionOptions, + }) +} + +const multiplyCurrencies = (a, b, options = {}) => { + const { + multiplicandBase, + multiplierBase, + ...conversionOptions + } = options + + const bigNumberA = new BigNumber(String(a), multiplicandBase) + const bigNumberB = new BigNumber(String(b), multiplierBase) + + const value = bigNumberA.times(bigNumberB) + + 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) +} + +module.exports = { + conversionUtil, + addCurrencies, + multiplyCurrencies, + conversionGreaterThan, + conversionLessThan, + conversionGTE, + conversionLTE, + conversionMax, + toNegative, + subtractCurrencies, +} diff --git a/ui/app/helpers/utils/conversion-util.test.js b/ui/app/helpers/utils/conversion-util.test.js new file mode 100644 index 000000000..368ce3bba --- /dev/null +++ b/ui/app/helpers/utils/conversion-util.test.js @@ -0,0 +1,22 @@ +import assert from 'assert' +import {addCurrencies} from './conversion-util' + + +describe('conversion utils', () => { + describe('addCurrencies()', () => { + it('add whole numbers', () => { + const result = addCurrencies(3, 9) + assert.equal(result.toNumber(), 12) + }) + + it('add decimals', () => { + const result = addCurrencies(1.3, 1.9) + assert.equal(result.toNumber(), 3.2) + }) + + it('add repeating decimals', () => { + const result = addCurrencies(1 / 3, 1 / 9) + assert.equal(result.toNumber(), 0.4444444444444444) + }) + }) +}) diff --git a/ui/app/helpers/utils/conversions.util.js b/ui/app/helpers/utils/conversions.util.js new file mode 100644 index 000000000..b4ec50626 --- /dev/null +++ b/ui/app/helpers/utils/conversions.util.js @@ -0,0 +1,122 @@ +import ethUtil from 'ethereumjs-util' +import { ETH, GWEI, WEI } from '../constants/common' +import { conversionUtil, addCurrencies } from './conversion-util' + +export function bnToHex (inputBn) { + return ethUtil.addHexPrefix(inputBn.toString(16)) +} + +export function hexToDecimal (hexValue) { + return conversionUtil(hexValue, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) +} + +export function decimalToHex (decimal) { + return conversionUtil(decimal, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + }) +} + +export function getEthConversionFromWeiHex ({ value, fromCurrency = ETH, conversionRate, numberOfDecimals = 6 }) { + const denominations = [fromCurrency, GWEI, WEI] + + let nonZeroDenomination + + for (let i = 0; i < denominations.length; i++) { + const convertedValue = getValueFromWeiHex({ + value, + conversionRate, + fromCurrency, + toCurrency: fromCurrency, + numberOfDecimals, + toDenomination: denominations[i], + }) + + if (convertedValue !== '0' || i === denominations.length - 1) { + nonZeroDenomination = `${convertedValue} ${denominations[i]}` + break + } + } + + return nonZeroDenomination +} + +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 getWeiHexFromDecimalValue ({ + value, + fromCurrency, + conversionRate, + fromDenomination, + invertConversionRate, +}) { + return conversionUtil(value, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + toCurrency: ETH, + fromCurrency, + conversionRate, + invertConversionRate, + fromDenomination, + toDenomination: WEI, + }) +} + +export function addHexWEIsToDec (aHexWEI, bHexWEI) { + return addCurrencies(aHexWEI, bHexWEI, { + aBase: 16, + bBase: 16, + fromDenomination: 'WEI', + numberOfDecimals: 6, + }) +} + +export function decEthToConvertedCurrency (ethTotal, convertedCurrency, conversionRate) { + return conversionUtil(ethTotal, { + fromNumericBase: 'dec', + toNumericBase: 'dec', + fromCurrency: 'ETH', + toCurrency: convertedCurrency, + numberOfDecimals: 2, + conversionRate, + }) +} + +export function decGWEIToHexWEI (decGWEI) { + return conversionUtil(decGWEI, { + fromNumericBase: 'dec', + toNumericBase: 'hex', + fromDenomination: 'GWEI', + toDenomination: 'WEI', + }) +} + +export function hexWEIToDecGWEI (decGWEI) { + return conversionUtil(decGWEI, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + }) +} diff --git a/ui/app/helpers/utils/formatters.js b/ui/app/helpers/utils/formatters.js new file mode 100644 index 000000000..106a2520d --- /dev/null +++ b/ui/app/helpers/utils/formatters.js @@ -0,0 +1,3 @@ +export function formatETHFee (ethFee) { + return ethFee + ' ETH' +} diff --git a/ui/app/helpers/utils/i18n-helper.js b/ui/app/helpers/utils/i18n-helper.js new file mode 100644 index 000000000..db07049e1 --- /dev/null +++ b/ui/app/helpers/utils/i18n-helper.js @@ -0,0 +1,44 @@ +// cross-browser connection to extension i18n API +const log = require('loglevel') + +/** + * Returns a localized message for the given key + * @param {object} locale The locale + * @param {string} key The message key + * @param {string[]} substitutions A list of message substitution replacements + * @return {null|string} The localized message + */ +const getMessage = (locale, key, substitutions) => { + if (!locale) { + return null + } + if (!locale[key]) { + log.warn(`Translator - Unable to find value for key "${key}"`) + return null + } + const entry = locale[key] + let phrase = entry.message + // perform substitutions + if (substitutions && substitutions.length) { + substitutions.forEach((substitution, index) => { + const regex = new RegExp(`\\$${index + 1}`, 'g') + phrase = phrase.replace(regex, substitution) + }) + } + return phrase +} + +async function fetchLocale (localeName) { + try { + const response = await fetch(`./_locales/${localeName}/messages.json`) + return await response.json() + } catch (error) { + log.error(`failed to fetch ${localeName} locale because of ${error}`) + return {} + } +} + +module.exports = { + getMessage, + fetchLocale, +} diff --git a/ui/app/helpers/utils/metametrics.util.js b/ui/app/helpers/utils/metametrics.util.js new file mode 100644 index 000000000..01984bd5e --- /dev/null +++ b/ui/app/helpers/utils/metametrics.util.js @@ -0,0 +1,184 @@ +/* eslint camelcase: 0 */ + +const ethUtil = require('ethereumjs-util') + +const inDevelopment = process.env.NODE_ENV === 'development' + +const METAMETRICS_BASE_URL = 'https://chromeextensionmm.innocraft.cloud/piwik.php' +const METAMETRICS_REQUIRED_PARAMS = `?idsite=${inDevelopment ? 1 : 2}&rec=1&apiv=1` +const METAMETRICS_BASE_FULL = METAMETRICS_BASE_URL + METAMETRICS_REQUIRED_PARAMS + +const METAMETRICS_TRACKING_URL = inDevelopment + ? 'http://www.metamask.io/metametrics' + : 'http://www.metamask.io/metametrics-prod' + +const METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE = 'gasLimitChange' +const METAMETRICS_CUSTOM_GAS_PRICE_CHANGE = 'gasPriceChange' +const METAMETRICS_CUSTOM_FUNCTION_TYPE = 'functionType' +const METAMETRICS_CUSTOM_RECIPIENT_KNOWN = 'recipientKnown' +const METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN = 'origin' +const METAMETRICS_CUSTOM_FROM_NETWORK = 'fromNetwork' +const METAMETRICS_CUSTOM_TO_NETWORK = 'toNetwork' +const METAMETRICS_CUSTOM_ERROR_FIELD = 'errorField' +const METAMETRICS_CUSTOM_ERROR_MESSAGE = 'errorMessage' +const METAMETRICS_CUSTOM_RPC_NETWORK_ID = 'networkId' +const METAMETRICS_CUSTOM_RPC_CHAIN_ID = 'chainId' + +const METAMETRICS_CUSTOM_NETWORK = 'network' +const METAMETRICS_CUSTOM_ENVIRONMENT_TYPE = 'environmentType' +const METAMETRICS_CUSTOM_ACTIVE_CURRENCY = 'activeCurrency' +const METAMETRICS_CUSTOM_ACCOUNT_TYPE = 'accountType' +const METAMETRICS_CUSTOM_NUMBER_OF_TOKENS = 'numberOfTokens' +const METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS = 'numberOfAccounts' + +const customVariableNameIdMap = { + [METAMETRICS_CUSTOM_FUNCTION_TYPE]: 1, + [METAMETRICS_CUSTOM_RECIPIENT_KNOWN]: 2, + [METAMETRICS_CUSTOM_CONFIRM_SCREEN_ORIGIN]: 3, + [METAMETRICS_CUSTOM_GAS_LIMIT_CHANGE]: 4, + [METAMETRICS_CUSTOM_GAS_PRICE_CHANGE]: 5, + [METAMETRICS_CUSTOM_FROM_NETWORK]: 1, + [METAMETRICS_CUSTOM_TO_NETWORK]: 2, + [METAMETRICS_CUSTOM_RPC_NETWORK_ID]: 1, + [METAMETRICS_CUSTOM_RPC_CHAIN_ID]: 2, + [METAMETRICS_CUSTOM_ERROR_FIELD]: 1, + [METAMETRICS_CUSTOM_ERROR_MESSAGE]: 2, +} + +const customDimensionsNameIdMap = { + [METAMETRICS_CUSTOM_NETWORK]: 5, + [METAMETRICS_CUSTOM_ENVIRONMENT_TYPE]: 6, + [METAMETRICS_CUSTOM_ACTIVE_CURRENCY]: 7, + [METAMETRICS_CUSTOM_ACCOUNT_TYPE]: 8, + [METAMETRICS_CUSTOM_NUMBER_OF_TOKENS]: 9, + [METAMETRICS_CUSTOM_NUMBER_OF_ACCOUNTS]: 10, +} + +function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) { + const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'MetaMask' + return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(previousPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` +} + +function composeCustomDimensionParamAddition (customDimensions) { + const customDimensionParamStrings = Object.keys(customDimensions).reduce((acc, name) => { + return [...acc, `dimension${customDimensionsNameIdMap[name]}=${customDimensions[name]}`] + }, []) + return `&${customDimensionParamStrings.join('&')}` +} + +function composeCustomVarParamAddition (customVariables) { + const customVariableIdValuePairs = Object.keys(customVariables).reduce((acc, name) => { + return { + [customVariableNameIdMap[name]]: [name, customVariables[name]], + ...acc, + } + }, {}) + return `&cvar=${encodeURIComponent(JSON.stringify(customVariableIdValuePairs))}` +} + +function composeParamAddition (paramValue, paramName) { + return paramValue !== 0 && !paramValue + ? '' + : `&${paramName}=${paramValue}` +} + +function composeUrl (config, permissionPreferences = {}) { + const { + eventOpts = {}, + customVariables = '', + pageOpts = '', + network, + environmentType, + activeCurrency, + accountType, + numberOfTokens, + numberOfAccounts, + previousPath = '', + currentPath, + metaMetricsId, + confirmTransactionOrigin, + url: configUrl, + excludeMetaMetricsId, + isNewVisit, + } = config + const base = METAMETRICS_BASE_FULL + + const e_c = composeParamAddition(eventOpts.category, 'e_c') + const e_a = composeParamAddition(eventOpts.action, 'e_a') + const e_n = composeParamAddition(eventOpts.name, 'e_n') + const new_visit = isNewVisit ? `&new_visit=1` : '' + + const cvar = customVariables && composeCustomVarParamAddition(customVariables) || '' + + const action_name = '' + + const urlref = previousPath && composeUrlRefParamAddition(previousPath, confirmTransactionOrigin) + + const dimensions = !pageOpts.hideDimensions ? composeCustomDimensionParamAddition({ + network, + environmentType, + activeCurrency, + accountType, + numberOfTokens: customVariables && customVariables.numberOfTokens || numberOfTokens, + numberOfAccounts: customVariables && customVariables.numberOfAccounts || numberOfAccounts, + }) : '' + const url = configUrl || `&url=${encodeURIComponent(currentPath.replace(/chrome-extension:\/\/\w+/, METAMETRICS_TRACKING_URL))}` + const _id = metaMetricsId && !excludeMetaMetricsId ? `&_id=${metaMetricsId.slice(2, 18)}` : '' + const rand = `&rand=${String(Math.random()).slice(2)}` + const pv_id = `&pv_id=${ethUtil.bufferToHex(ethUtil.sha3(url || currentPath.match(/chrome-extension:\/\/\w+\/(.+)/)[0])).slice(2, 8)}` + const uid = metaMetricsId && !excludeMetaMetricsId + ? `&uid=${metaMetricsId.slice(2, 18)}` + : excludeMetaMetricsId + ? '&uid=0000000000000000' + : '' + + return [ base, e_c, e_a, e_n, cvar, action_name, urlref, dimensions, url, _id, rand, pv_id, uid, new_visit ].join('') +} + +export function sendMetaMetricsEvent (config, permissionPreferences) { + return fetch(composeUrl(config, permissionPreferences), { + 'headers': {}, + 'method': 'GET', + }) +} + +export function verifyUserPermission (config, props) { + const { + eventOpts = {}, + } = config + const { userPermissionPreferences } = props + const { + allowAll, + allowNone, + allowSendMetrics, + } = userPermissionPreferences + + if (allowNone) { + return false + } else if (allowAll) { + return true + } else if (allowSendMetrics && eventOpts.name === 'send') { + return true + } else { + return false + } +} + +const trackableSendCounts = { + 1: true, + 10: true, + 30: true, + 50: true, + 100: true, + 250: true, + 500: true, + 1000: true, + 2500: true, + 5000: true, + 10000: true, + 25000: true, +} + +export function sendCountIsTrackable (sendCount) { + return Boolean(trackableSendCounts[sendCount]) +} diff --git a/ui/app/helpers/utils/token-util.js b/ui/app/helpers/utils/token-util.js new file mode 100644 index 000000000..35a19a69f --- /dev/null +++ b/ui/app/helpers/utils/token-util.js @@ -0,0 +1,118 @@ +const log = require('loglevel') +const util = require('./util') +const BigNumber = require('bignumber.js') +import contractMap from 'eth-contract-metadata' + +const casedContractMap = Object.keys(contractMap).reduce((acc, base) => { + return { + ...acc, + [base.toLowerCase()]: contractMap[base], + } +}, {}) + +const DEFAULT_SYMBOL = '' +const DEFAULT_DECIMALS = '0' + +async function getSymbolFromContract (tokenAddress) { + const token = util.getContractAtAddress(tokenAddress) + + try { + const result = await token.symbol() + return result[0] + } catch (error) { + log.warn(`symbol() call for token at address ${tokenAddress} resulted in error:`, error) + } +} + +async function getDecimalsFromContract (tokenAddress) { + const token = util.getContractAtAddress(tokenAddress) + + try { + const result = await token.decimals() + const decimalsBN = result[0] + return decimalsBN && decimalsBN.toString() + } catch (error) { + log.warn(`decimals() call for token at address ${tokenAddress} resulted in error:`, error) + } +} + +function getContractMetadata (tokenAddress) { + return tokenAddress && casedContractMap[tokenAddress.toLowerCase()] +} + +async function getSymbol (tokenAddress) { + let symbol = await getSymbolFromContract(tokenAddress) + + if (!symbol) { + const contractMetadataInfo = getContractMetadata(tokenAddress) + + if (contractMetadataInfo) { + symbol = contractMetadataInfo.symbol + } + } + + return symbol +} + +async function getDecimals (tokenAddress) { + let decimals = await getDecimalsFromContract(tokenAddress) + + if (!decimals || decimals === '0') { + const contractMetadataInfo = getContractMetadata(tokenAddress) + + if (contractMetadataInfo) { + decimals = contractMetadataInfo.decimals + } + } + + return decimals +} + +export async function getSymbolAndDecimals (tokenAddress, existingTokens = []) { + const existingToken = existingTokens.find(({ address }) => tokenAddress === address) + + if (existingToken) { + return { + symbol: existingToken.symbol, + decimals: existingToken.decimals, + } + } + + let symbol, decimals + + try { + symbol = await getSymbol(tokenAddress) + decimals = await getDecimals(tokenAddress) + } catch (error) { + log.warn(`symbol() and decimal() calls for token at address ${tokenAddress} resulted in error:`, error) + } + + return { + symbol: symbol || DEFAULT_SYMBOL, + decimals: decimals || DEFAULT_DECIMALS, + } +} + +export function tokenInfoGetter () { + const tokens = {} + + return async (address) => { + if (tokens[address]) { + return tokens[address] + } + + tokens[address] = await getSymbolAndDecimals(address) + + return tokens[address] + } +} + +export function calcTokenAmount (value, decimals) { + const multiplier = Math.pow(10, Number(decimals || 0)) + return new BigNumber(String(value)).div(multiplier) +} + +export function getTokenValue (tokenParams = []) { + const valueData = tokenParams.find(param => param.name === '_value') + return valueData && valueData.value +} diff --git a/ui/app/helpers/utils/transactions.util.js b/ui/app/helpers/utils/transactions.util.js new file mode 100644 index 000000000..edf2e24f6 --- /dev/null +++ b/ui/app/helpers/utils/transactions.util.js @@ -0,0 +1,179 @@ +import ethUtil from 'ethereumjs-util' +import MethodRegistry from 'eth-method-registry' +import abi from 'human-standard-token-abi' +import abiDecoder from 'abi-decoder' +import { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_STATUS_CONFIRMED, +} from '../../../../app/scripts/controllers/transactions/enums' + +import { + TOKEN_METHOD_TRANSFER, + TOKEN_METHOD_APPROVE, + TOKEN_METHOD_TRANSFER_FROM, + SEND_ETHER_ACTION_KEY, + DEPLOY_CONTRACT_ACTION_KEY, + APPROVE_ACTION_KEY, + SEND_TOKEN_ACTION_KEY, + TRANSFER_FROM_ACTION_KEY, + SIGNATURE_REQUEST_KEY, + CONTRACT_INTERACTION_KEY, + CANCEL_ATTEMPT_ACTION_KEY, +} from '../constants/transactions' + +import { addCurrencies } from './conversion-util' + +abiDecoder.addABI(abi) + +export function getTokenData (data = '') { + return abiDecoder.decodeMethod(data) +} + +const registry = new MethodRegistry({ provider: global.ethereumProvider }) + +/** + * Attempts to return the method data from the MethodRegistry library, if the method exists in the + * registry. Otherwise, returns an empty object. + * @param {string} data - The hex data (@code txParams.data) of a transaction + * @returns {Object} + */ +export async function getMethodData (data = '') { + const prefixedData = ethUtil.addHexPrefix(data) + const fourBytePrefix = prefixedData.slice(0, 10) + const sig = await registry.lookup(fourBytePrefix) + + if (!sig) { + return {} + } + + const parsedResult = registry.parse(sig) + + return { + name: parsedResult.name, + params: parsedResult.args, + } +} + +export function isConfirmDeployContract (txData = {}) { + const { txParams = {} } = txData + return !txParams.to +} + +/** + * Returns four-byte method signature from data + * + * @param {string} data - The hex data (@code txParams.data) of a transaction + * @returns {string} - The four-byte method signature + */ +export function getFourBytePrefix (data = '') { + const prefixedData = ethUtil.addHexPrefix(data) + const fourBytePrefix = prefixedData.slice(0, 10) + return fourBytePrefix +} + +/** + * Returns the action of a transaction as a key to be passed into the translator. + * @param {Object} transaction - txData object + * @param {Object} methodData - Data returned from eth-method-registry + * @returns {string|undefined} + */ +export async function getTransactionActionKey (transaction, methodData) { + const { txParams: { data, to } = {}, msgParams, type } = transaction + + if (type === 'cancel') { + return CANCEL_ATTEMPT_ACTION_KEY + } + + if (msgParams) { + return SIGNATURE_REQUEST_KEY + } + + if (isConfirmDeployContract(transaction)) { + return DEPLOY_CONTRACT_ACTION_KEY + } + + if (data) { + const toSmartContract = await isSmartContractAddress(to) + + if (!toSmartContract) { + return SEND_ETHER_ACTION_KEY + } + + const { name } = methodData + const methodName = name && name.toLowerCase() + + if (!methodName) { + return CONTRACT_INTERACTION_KEY + } + + switch (methodName) { + case TOKEN_METHOD_TRANSFER: + return SEND_TOKEN_ACTION_KEY + case TOKEN_METHOD_APPROVE: + return APPROVE_ACTION_KEY + case TOKEN_METHOD_TRANSFER_FROM: + return TRANSFER_FROM_ACTION_KEY + default: + return undefined + } + } else { + return SEND_ETHER_ACTION_KEY + } +} + +export function getLatestSubmittedTxWithNonce (transactions = [], nonce = '0x0') { + if (!transactions.length) { + return {} + } + + return transactions.reduce((acc, current) => { + const { submittedTime, txParams: { nonce: currentNonce } = {} } = current + + if (currentNonce === nonce) { + return acc.submittedTime + ? submittedTime > acc.submittedTime ? current : acc + : current + } else { + return acc + } + }, {}) +} + +export async function isSmartContractAddress (address) { + const code = await global.eth.getCode(address) + // Geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + return !codeIsEmpty +} + +export function sumHexes (...args) { + const total = args.reduce((acc, base) => { + return addCurrencies(acc, base, { + toNumericBase: 'hex', + }) + }) + + return ethUtil.addHexPrefix(total) +} + +/** + * Returns a status key for a transaction. Requires parsing the txMeta.txReceipt on top of + * txMeta.status because txMeta.status does not reflect on-chain errors. + * @param {Object} transaction - The txMeta object of a transaction. + * @param {Object} transaction.txReceipt - The transaction receipt. + * @returns {string} + */ +export function getStatusKey (transaction) { + const { txReceipt: { status: receiptStatus } = {}, type, status } = transaction + + // There was an on-chain failure + if (receiptStatus === '0x0') { + return 'failed' + } + + if (status === TRANSACTION_STATUS_CONFIRMED && type === TRANSACTION_TYPE_CANCEL) { + return 'cancelled' + } + + return transaction.status +} diff --git a/ui/app/helpers/utils/transactions.util.test.js b/ui/app/helpers/utils/transactions.util.test.js new file mode 100644 index 000000000..4a8ca5c9d --- /dev/null +++ b/ui/app/helpers/utils/transactions.util.test.js @@ -0,0 +1,57 @@ +import * as utils from './transactions.util' +import assert from 'assert' + +describe('Transactions utils', () => { + describe('getTokenData', () => { + it('should return token data', () => { + const tokenData = utils.getTokenData('0xa9059cbb00000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000004e20') + assert.ok(tokenData) + const { name, params } = tokenData + assert.equal(name, 'transfer') + const [to, value] = params + assert.equal(to.name, '_to') + assert.equal(to.type, 'address') + assert.equal(value.name, '_value') + assert.equal(value.type, 'uint256') + }) + + it('should not throw errors when called without arguments', () => { + assert.doesNotThrow(() => utils.getTokenData()) + }) + }) + + describe('getStatusKey', () => { + it('should return the correct status', () => { + const tests = [ + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x0', + }, + }, + expected: 'failed', + }, + { + transaction: { + status: 'confirmed', + txReceipt: { + status: '0x1', + }, + }, + expected: 'confirmed', + }, + { + transaction: { + status: 'pending', + }, + expected: 'pending', + }, + ] + + tests.forEach(({ transaction, expected }) => { + assert.equal(utils.getStatusKey(transaction), expected) + }) + }) + }) +}) diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js new file mode 100644 index 000000000..c50d7cbe5 --- /dev/null +++ b/ui/app/helpers/utils/util.js @@ -0,0 +1,326 @@ +const abi = require('human-standard-token-abi') +const ethUtil = require('ethereumjs-util') +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +import { DateTime } from 'luxon' + +const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1) +const GWEI_FACTOR = new ethUtil.BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + +// formatData :: ( date: <Unix Timestamp> ) -> String +function formatDate (date, format = 'M/d/y \'at\' T') { + return DateTime.fromMillis(date).toFormat(format) +} + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + isValidENSAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, + formatDate, + bnMultiplyByFraction, + getTxFeeBn, + shortenBalance, + getContractAtAddress, + exportAsFile: exportAsFile, + isInvalidChecksumAddress, + allNull, + getTokenAddressFromTokenObject, + checksumAddress, + addressSlicer, + isEthNetwork, +} + +function isEthNetwork (netId) { + if (!netId || netId === '1' || netId === '3' || netId === '4' || netId === '42' || netId === '5777') { + return true + } + + return false +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = checksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = checksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address, network) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isValidENSAddress (address) { + return address.match(/^.{7,}\.(eth|test)$/) +} + +function isInvalidChecksumAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ` ${ticker}` + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}` + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}` + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + if (decimal.length > 18) { + decimal = decimal.slice(0, 18) + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} + +function bnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new ethUtil.BN(numerator) + const denomBN = new ethUtil.BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) { + const gasBn = hexToBn(gas) + const gasPriceBn = hexToBn(gasPrice) + const txFeeBn = gasBn.mul(gasPriceBn) + + return txFeeBn.toString(16) +} + +function getContractAtAddress (tokenAddress) { + return global.eth.contract(abi).at(tokenAddress) +} + +function exportAsFile (filename, data, type = 'text/csv') { + // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz + const blob = new Blob([data], {type}) + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename) + } else { + const elem = window.document.createElement('a') + elem.target = '_blank' + elem.href = window.URL.createObjectURL(blob) + elem.download = filename + document.body.appendChild(elem) + elem.click() + document.body.removeChild(elem) + } +} + +function allNull (obj) { + return Object.entries(obj).every(([key, value]) => value === null) +} + +function getTokenAddressFromTokenObject (token) { + return Object.values(token)[0].address.toLowerCase() +} + +/** + * Safely checksumms a potentially-null address + * + * @param {String} [address] - address to checksum + * @param {String} [network] - network id + * @returns {String} - checksummed address + * + */ +function checksumAddress (address, network) { + const checksummed = address ? ethUtil.toChecksumAddress(address) : '' + return checksummed +} + +function addressSlicer (address = '') { + if (address.length < 11) { + return address + } + + return `${address.slice(0, 6)}...${address.slice(-4)}` +} |