From 31175625b446cb5d18b17db23018bca8b14d280c Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 21 Mar 2019 16:03:30 -0700 Subject: Folder restructure (#6304) * Remove ui/app/keychains/ * Remove ui/app/img/ (unused images) * Move conversion-util to helpers/utils/ * Move token-util to helpers/utils/ * Move /helpers/*.js inside /helpers/utils/ * Move util tests inside /helpers/utils/ * Renameand move confirm-transaction/util.js to helpers/utils/ * Move higher-order-components to helpers/higher-order-components/ * Move infura-conversion.json to helpers/constants/ * Move all utility functions to helpers/utils/ * Move pages directory to top-level * Move all constants to helpers/constants/ * Move metametrics inside helpers/ * Move app and root inside pages/ * Move routes inside helpers/ * Re-organize ducks/ * Move reducers to ducks/ * Move selectors inside selectors/ * Move test out of test folder * Move action, reducer, store inside store/ * Move ui components inside ui/ * Move UI components inside ui/ * Move connected components inside components/app/ * Move i18n-helper inside helpers/ * Fix unit tests * Fix unit test * Move pages components * Rename routes component * Move reducers to ducks/index * Fix bad path in unit test --- ui/app/helpers/common.util.js | 5 - ui/app/helpers/confirm-transaction/util.js | 131 ----- ui/app/helpers/confirm-transaction/util.test.js | 137 ----- ui/app/helpers/constants/common.js | 10 + ui/app/helpers/constants/error-keys.js | 4 + ui/app/helpers/constants/infura-conversion.json | 653 +++++++++++++++++++++ ui/app/helpers/constants/routes.js | 83 +++ ui/app/helpers/constants/transactions.js | 24 + ui/app/helpers/conversions.util.js | 122 ---- ui/app/helpers/formatters.js | 3 - .../authenticated/authenticated.component.js | 22 + .../authenticated/authenticated.container.js | 12 + .../higher-order-components/authenticated/index.js | 1 + .../higher-order-components/i18n-provider.js | 55 ++ .../higher-order-components/initialized/index.js | 1 + .../initialized/initialized.component.js | 14 + .../initialized/initialized.container.js | 12 + .../metametrics/metametrics.provider.js | 106 ++++ .../with-method-data/index.js | 1 + .../with-method-data/with-method-data.component.js | 65 ++ .../with-modal-props/index.js | 1 + .../tests/with-modal-props.test.js | 43 ++ .../with-modal-props/with-modal-props.js | 21 + .../with-token-tracker/index.js | 1 + .../with-token-tracker.component.js | 106 ++++ ui/app/helpers/tests/common.util.test.js | 27 - ui/app/helpers/tests/transactions.util.test.js | 57 -- ui/app/helpers/transactions.util.js | 179 ------ ui/app/helpers/utils/common.util.js | 5 + ui/app/helpers/utils/common.util.test.js | 27 + ui/app/helpers/utils/confirm-tx.util.js | 131 +++++ ui/app/helpers/utils/confirm-tx.util.test.js | 137 +++++ ui/app/helpers/utils/conversion-util.js | 251 ++++++++ ui/app/helpers/utils/conversion-util.test.js | 22 + ui/app/helpers/utils/conversions.util.js | 122 ++++ ui/app/helpers/utils/formatters.js | 3 + ui/app/helpers/utils/i18n-helper.js | 44 ++ ui/app/helpers/utils/metametrics.util.js | 184 ++++++ ui/app/helpers/utils/token-util.js | 118 ++++ ui/app/helpers/utils/transactions.util.js | 179 ++++++ ui/app/helpers/utils/transactions.util.test.js | 57 ++ ui/app/helpers/utils/util.js | 326 ++++++++++ 42 files changed, 2841 insertions(+), 661 deletions(-) delete mode 100644 ui/app/helpers/common.util.js delete mode 100644 ui/app/helpers/confirm-transaction/util.js delete mode 100644 ui/app/helpers/confirm-transaction/util.test.js create mode 100644 ui/app/helpers/constants/common.js create mode 100644 ui/app/helpers/constants/error-keys.js create mode 100644 ui/app/helpers/constants/infura-conversion.json create mode 100644 ui/app/helpers/constants/routes.js create mode 100644 ui/app/helpers/constants/transactions.js delete mode 100644 ui/app/helpers/conversions.util.js delete mode 100644 ui/app/helpers/formatters.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/authenticated.component.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/authenticated.container.js create mode 100644 ui/app/helpers/higher-order-components/authenticated/index.js create mode 100644 ui/app/helpers/higher-order-components/i18n-provider.js create mode 100644 ui/app/helpers/higher-order-components/initialized/index.js create mode 100644 ui/app/helpers/higher-order-components/initialized/initialized.component.js create mode 100644 ui/app/helpers/higher-order-components/initialized/initialized.container.js create mode 100644 ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js create mode 100644 ui/app/helpers/higher-order-components/with-method-data/index.js create mode 100644 ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/index.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js create mode 100644 ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js create mode 100644 ui/app/helpers/higher-order-components/with-token-tracker/index.js create mode 100644 ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js delete mode 100644 ui/app/helpers/tests/common.util.test.js delete mode 100644 ui/app/helpers/tests/transactions.util.test.js delete mode 100644 ui/app/helpers/transactions.util.js create mode 100644 ui/app/helpers/utils/common.util.js create mode 100644 ui/app/helpers/utils/common.util.test.js create mode 100644 ui/app/helpers/utils/confirm-tx.util.js create mode 100644 ui/app/helpers/utils/confirm-tx.util.test.js create mode 100644 ui/app/helpers/utils/conversion-util.js create mode 100644 ui/app/helpers/utils/conversion-util.test.js create mode 100644 ui/app/helpers/utils/conversions.util.js create mode 100644 ui/app/helpers/utils/formatters.js create mode 100644 ui/app/helpers/utils/i18n-helper.js create mode 100644 ui/app/helpers/utils/metametrics.util.js create mode 100644 ui/app/helpers/utils/token-util.js create mode 100644 ui/app/helpers/utils/transactions.util.js create mode 100644 ui/app/helpers/utils/transactions.util.test.js create mode 100644 ui/app/helpers/utils/util.js (limited to 'ui/app/helpers') diff --git a/ui/app/helpers/common.util.js b/ui/app/helpers/common.util.js deleted file mode 100644 index 0c02481e6..000000000 --- a/ui/app/helpers/common.util.js +++ /dev/null @@ -1,5 +0,0 @@ -export function camelCaseToCapitalize (str = '') { - return str - .replace(/([A-Z])/g, ' $1') - .replace(/^./, str => str.toUpperCase()) -} diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js deleted file mode 100644 index 0451824e8..000000000 --- a/ui/app/helpers/confirm-transaction/util.js +++ /dev/null @@ -1,131 +0,0 @@ -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/confirm-transaction/util.test.js b/ui/app/helpers/confirm-transaction/util.test.js deleted file mode 100644 index 4c1a3e16b..000000000 --- a/ui/app/helpers/confirm-transaction/util.test.js +++ /dev/null @@ -1,137 +0,0 @@ -import * as utils from './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/constants/common.js b/ui/app/helpers/constants/common.js new file mode 100644 index 000000000..c6e566b8b --- /dev/null +++ b/ui/app/helpers/constants/common.js @@ -0,0 +1,10 @@ +export const ETH = 'ETH' +export const GWEI = 'GWEI' +export const WEI = 'WEI' + +export const PRIMARY = 'PRIMARY' +export const SECONDARY = 'SECONDARY' + +export const NETWORK_TYPES = { + MAINNET: 'mainnet', +} diff --git a/ui/app/helpers/constants/error-keys.js b/ui/app/helpers/constants/error-keys.js new file mode 100644 index 000000000..704064c96 --- /dev/null +++ b/ui/app/helpers/constants/error-keys.js @@ -0,0 +1,4 @@ +export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds' +export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow' +export const TRANSACTION_ERROR_KEY = 'transactionError' +export const TRANSACTION_NO_CONTRACT_ERROR_KEY = 'transactionErrorNoContract' diff --git a/ui/app/helpers/constants/infura-conversion.json b/ui/app/helpers/constants/infura-conversion.json new file mode 100644 index 000000000..9a96fe069 --- /dev/null +++ b/ui/app/helpers/constants/infura-conversion.json @@ -0,0 +1,653 @@ +{ + "objects": [ + { + "symbol": "ethaud", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "aud", + "name": "Australian Dollar" + } + }, + { + "symbol": "ethhkd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "hkd", + "name": "Hong Kong Dollar" + } + }, + { + "symbol": "ethsgd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "sgd", + "name": "Singapore Dollar" + } + }, + { + "symbol": "ethidr", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "idr", + "name": "Indonesian Rupiah" + } + }, + { + "symbol": "ethphp", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "php", + "name": "Philippine Peso" + } + }, + { + "symbol": "eth1st", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "1st", + "name": "FirstBlood" + } + }, + { + "symbol": "ethadt", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "adt", + "name": "adToken" + } + }, + { + "symbol": "ethadx", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "adx", + "name": "AdEx" + } + }, + { + "symbol": "ethant", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "ant", + "name": "Aragon" + } + }, + { + "symbol": "ethbat", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "bat", + "name": "Basic Attention Token" + } + }, + { + "symbol": "ethbnt", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "bnt", + "name": "Bancor" + } + }, + { + "symbol": "ethbtc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "btc", + "name": "Bitcoin" + } + }, + { + "symbol": "ethcad", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "cad", + "name": "Canadian Dollar" + } + }, + { + "symbol": "ethcfi", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "cfi", + "name": "Cofound.it" + } + }, + { + "symbol": "ethcrb", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "crb", + "name": "CreditBit" + } + }, + { + "symbol": "ethcvc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "cvc", + "name": "Civic" + } + }, + { + "symbol": "ethdash", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "dash", + "name": "Dash" + } + }, + { + "symbol": "ethdgd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "dgd", + "name": "DigixDAO" + } + }, + { + "symbol": "ethetc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "etc", + "name": "Ethereum Classic" + } + }, + { + "symbol": "etheur", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "eur", + "name": "Euro" + } + }, + { + "symbol": "ethfun", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "fun", + "name": "FunFair" + } + }, + { + "symbol": "ethgbp", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "gbp", + "name": "Pound Sterling" + } + }, + { + "symbol": "ethgno", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "gno", + "name": "Gnosis" + } + }, + { + "symbol": "ethgnt", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "gnt", + "name": "Golem" + } + }, + { + "symbol": "ethgup", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "gup", + "name": "Matchpool" + } + }, + { + "symbol": "ethhmq", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "hmq", + "name": "Humaniq" + } + }, + { + "symbol": "ethjpy", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "jpy", + "name": "Japanese Yen" + } + }, + { + "symbol": "ethlgd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "lgd", + "name": "Legends Room" + } + }, + { + "symbol": "ethlsk", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "lsk", + "name": "Lisk" + } + }, + { + "symbol": "ethltc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "ltc", + "name": "Litecoin" + } + }, + { + "symbol": "ethlun", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "lun", + "name": "Lunyr" + } + }, + { + "symbol": "ethmco", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "mco", + "name": "Monaco" + } + }, + { + "symbol": "ethmtl", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "mtl", + "name": "Metal" + } + }, + { + "symbol": "ethmyst", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "myst", + "name": "Mysterium" + } + }, + { + "symbol": "ethnmr", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "nmr", + "name": "Numeraire" + } + }, + { + "symbol": "ethomg", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "omg", + "name": "OmiseGO" + } + }, + { + "symbol": "ethpay", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "pay", + "name": "TenX" + } + }, + { + "symbol": "ethptoy", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "ptoy", + "name": "Patientory" + } + }, + { + "symbol": "ethqrl", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "qrl", + "name": "Quantum-Resistant Ledger" + } + }, + { + "symbol": "ethqtum", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "qtum", + "name": "Qtum" + } + }, + { + "symbol": "ethrep", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "rep", + "name": "Augur" + } + }, + { + "symbol": "ethrlc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "rlc", + "name": "iEx.ec" + } + }, + { + "symbol": "ethrub", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "rub", + "name": "Russian Ruble" + } + }, + { + "symbol": "ethsc", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "sc", + "name": "Siacoin" + } + }, + { + "symbol": "ethsngls", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "sngls", + "name": "SingularDTV" + } + }, + { + "symbol": "ethsnt", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "snt", + "name": "Status" + } + }, + { + "symbol": "ethsteem", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "steem", + "name": "Steem" + } + }, + { + "symbol": "ethstorj", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "storj", + "name": "Storj" + } + }, + { + "symbol": "ethtime", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "time", + "name": "ChronoBank" + } + }, + { + "symbol": "ethtkn", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "tkn", + "name": "TokenCard" + } + }, + { + "symbol": "ethtrst", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "trst", + "name": "WeTrust" + } + }, + { + "symbol": "ethuah", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "uah", + "name": "Ukrainian Hryvnia" + } + }, + { + "symbol": "ethusd", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "usd", + "name": "United States Dollar" + } + }, + { + "symbol": "ethwings", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "wings", + "name": "Wings" + } + }, + { + "symbol": "ethxem", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "xem", + "name": "NEM" + } + }, + { + "symbol": "ethxlm", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "xlm", + "name": "Stellar Lumen" + } + }, + { + "symbol": "ethxmr", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "xmr", + "name": "Monero" + } + }, + { + "symbol": "ethxrp", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "xrp", + "name": "Ripple" + } + }, + { + "symbol": "ethzec", + "base": { + "code": "eth", + "name": "Ethereum" + }, + "quote": { + "code": "zec", + "name": "Zcash" + } + } + ] +} diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js new file mode 100644 index 000000000..932dfa7df --- /dev/null +++ b/ui/app/helpers/constants/routes.js @@ -0,0 +1,83 @@ +const DEFAULT_ROUTE = '/' +const UNLOCK_ROUTE = '/unlock' +const LOCK_ROUTE = '/lock' +const SETTINGS_ROUTE = '/settings' +const INFO_ROUTE = '/settings/info' +const REVEAL_SEED_ROUTE = '/seed' +const MOBILE_SYNC_ROUTE = '/mobile-sync' +const CONFIRM_SEED_ROUTE = '/confirm-seed' +const RESTORE_VAULT_ROUTE = '/restore-vault' +const ADD_TOKEN_ROUTE = '/add-token' +const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token' +const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token' +const NEW_ACCOUNT_ROUTE = '/new-account' +const IMPORT_ACCOUNT_ROUTE = '/new-account/import' +const CONNECT_HARDWARE_ROUTE = '/new-account/connect' +const SEND_ROUTE = '/send' +const NOTICE_ROUTE = '/notice' +const WELCOME_ROUTE = '/welcome' + +const INITIALIZE_ROUTE = '/initialize' +const INITIALIZE_WELCOME_ROUTE = '/initialize/welcome' +const INITIALIZE_UNLOCK_ROUTE = '/initialize/unlock' +const INITIALIZE_CREATE_PASSWORD_ROUTE = '/initialize/create-password' +const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/create-password/import-account' +const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/create-password/import-with-seed-phrase' +const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/create-password/unique-image' +const INITIALIZE_NOTICE_ROUTE = '/initialize/notice' +const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action' +const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase' +const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow' +const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm' +const INITIALIZE_METAMETRICS_OPT_IN_ROUTE = '/initialize/metametrics-opt-in' + +const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' +const CONFIRM_SEND_ETHER_PATH = '/send-ether' +const CONFIRM_SEND_TOKEN_PATH = '/send-token' +const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract' +const CONFIRM_APPROVE_PATH = '/approve' +const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from' +const CONFIRM_TOKEN_METHOD_PATH = '/token-method' +const SIGNATURE_REQUEST_PATH = '/signature-request' + +module.exports = { + DEFAULT_ROUTE, + UNLOCK_ROUTE, + LOCK_ROUTE, + SETTINGS_ROUTE, + INFO_ROUTE, + REVEAL_SEED_ROUTE, + MOBILE_SYNC_ROUTE, + CONFIRM_SEED_ROUTE, + RESTORE_VAULT_ROUTE, + ADD_TOKEN_ROUTE, + CONFIRM_ADD_TOKEN_ROUTE, + CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, + NEW_ACCOUNT_ROUTE, + IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, + SEND_ROUTE, + NOTICE_ROUTE, + WELCOME_ROUTE, + INITIALIZE_ROUTE, + INITIALIZE_WELCOME_ROUTE, + INITIALIZE_UNLOCK_ROUTE, + INITIALIZE_CREATE_PASSWORD_ROUTE, + INITIALIZE_IMPORT_ACCOUNT_ROUTE, + INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, + INITIALIZE_UNIQUE_IMAGE_ROUTE, + INITIALIZE_NOTICE_ROUTE, + INITIALIZE_SELECT_ACTION_ROUTE, + INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, + INITIALIZE_END_OF_FLOW_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + CONFIRM_SEND_ETHER_PATH, + CONFIRM_SEND_TOKEN_PATH, + CONFIRM_DEPLOY_CONTRACT_PATH, + CONFIRM_APPROVE_PATH, + CONFIRM_TRANSFER_FROM_PATH, + CONFIRM_TOKEN_METHOD_PATH, + SIGNATURE_REQUEST_PATH, + INITIALIZE_METAMETRICS_OPT_IN_ROUTE, +} diff --git a/ui/app/helpers/constants/transactions.js b/ui/app/helpers/constants/transactions.js new file mode 100644 index 000000000..d0a819b9b --- /dev/null +++ b/ui/app/helpers/constants/transactions.js @@ -0,0 +1,24 @@ +export const UNAPPROVED_STATUS = 'unapproved' +export const REJECTED_STATUS = 'rejected' +export const APPROVED_STATUS = 'approved' +export const SIGNED_STATUS = 'signed' +export const SUBMITTED_STATUS = 'submitted' +export const CONFIRMED_STATUS = 'confirmed' +export const FAILED_STATUS = 'failed' +export const DROPPED_STATUS = 'dropped' +export const CANCELLED_STATUS = 'cancelled' + +export const TOKEN_METHOD_TRANSFER = 'transfer' +export const TOKEN_METHOD_APPROVE = 'approve' +export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom' + +export const SEND_ETHER_ACTION_KEY = 'sentEther' +export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment' +export const APPROVE_ACTION_KEY = 'approve' +export const SEND_TOKEN_ACTION_KEY = 'sentTokens' +export const TRANSFER_FROM_ACTION_KEY = 'transferFrom' +export const SIGNATURE_REQUEST_KEY = 'signatureRequest' +export const CONTRACT_INTERACTION_KEY = 'contractInteraction' +export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt' + +export const TRANSACTION_TYPE_SHAPESHIFT = 'shapeshift' diff --git a/ui/app/helpers/conversions.util.js b/ui/app/helpers/conversions.util.js deleted file mode 100644 index 065d67e8e..000000000 --- a/ui/app/helpers/conversions.util.js +++ /dev/null @@ -1,122 +0,0 @@ -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/formatters.js b/ui/app/helpers/formatters.js deleted file mode 100644 index 106a2520d..000000000 --- a/ui/app/helpers/formatters.js +++ /dev/null @@ -1,3 +0,0 @@ -export function formatETHFee (ethFee) { - return ethFee + ' ETH' -} diff --git a/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js b/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js new file mode 100644 index 000000000..c195d0e21 --- /dev/null +++ b/ui/app/helpers/higher-order-components/authenticated/authenticated.component.js @@ -0,0 +1,22 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Redirect, Route } from 'react-router-dom' +import { UNLOCK_ROUTE, INITIALIZE_ROUTE } from '../../constants/routes' + +export default function Authenticated (props) { + const { isUnlocked, completedOnboarding } = props + + switch (true) { + case isUnlocked && completedOnboarding: + return + case !completedOnboarding: + return + default: + return + } +} + +Authenticated.propTypes = { + isUnlocked: PropTypes.bool, + completedOnboarding: PropTypes.bool, +} diff --git a/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js new file mode 100644 index 000000000..6124b0fcd --- /dev/null +++ b/ui/app/helpers/higher-order-components/authenticated/authenticated.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Authenticated from './authenticated.component' + +const mapStateToProps = state => { + const { metamask: { isUnlocked, completedOnboarding } } = state + return { + isUnlocked, + completedOnboarding, + } +} + +export default connect(mapStateToProps)(Authenticated) diff --git a/ui/app/helpers/higher-order-components/authenticated/index.js b/ui/app/helpers/higher-order-components/authenticated/index.js new file mode 100644 index 000000000..05632ed21 --- /dev/null +++ b/ui/app/helpers/higher-order-components/authenticated/index.js @@ -0,0 +1 @@ +export { default } from './authenticated.container' diff --git a/ui/app/helpers/higher-order-components/i18n-provider.js b/ui/app/helpers/higher-order-components/i18n-provider.js new file mode 100644 index 000000000..0e34e17e0 --- /dev/null +++ b/ui/app/helpers/higher-order-components/i18n-provider.js @@ -0,0 +1,55 @@ +const { Component } = require('react') +const connect = require('react-redux').connect +const PropTypes = require('prop-types') +const { withRouter } = require('react-router-dom') +const { compose } = require('recompose') +const t = require('../utils/i18n-helper').getMessage + +class I18nProvider extends Component { + tOrDefault = (key, defaultValue, ...args) => { + const { localeMessages: { current, en } = {} } = this.props + return t(current, key, ...args) || t(en, key, ...args) || defaultValue + } + + getChildContext () { + const { localeMessages } = this.props + const { current, en } = localeMessages + return { + t (key, ...args) { + return t(current, key, ...args) || t(en, key, ...args) || `[${key}]` + }, + tOrDefault: this.tOrDefault, + tOrKey (key, ...args) { + return this.tOrDefault(key, key, ...args) + }, + } + } + + render () { + return this.props.children + } +} + +I18nProvider.propTypes = { + localeMessages: PropTypes.object, + children: PropTypes.object, +} + +I18nProvider.childContextTypes = { + t: PropTypes.func, + tOrDefault: PropTypes.func, + tOrKey: PropTypes.func, +} + +const mapStateToProps = state => { + const { localeMessages } = state + return { + localeMessages, + } +} + +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(I18nProvider) + diff --git a/ui/app/helpers/higher-order-components/initialized/index.js b/ui/app/helpers/higher-order-components/initialized/index.js new file mode 100644 index 000000000..863fcb389 --- /dev/null +++ b/ui/app/helpers/higher-order-components/initialized/index.js @@ -0,0 +1 @@ +export { default } from './initialized.container.js' diff --git a/ui/app/helpers/higher-order-components/initialized/initialized.component.js b/ui/app/helpers/higher-order-components/initialized/initialized.component.js new file mode 100644 index 000000000..2042c0046 --- /dev/null +++ b/ui/app/helpers/higher-order-components/initialized/initialized.component.js @@ -0,0 +1,14 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Redirect, Route } from 'react-router-dom' +import { INITIALIZE_ROUTE } from '../../constants/routes' + +export default function Initialized (props) { + return props.completedOnboarding + ? + : +} + +Initialized.propTypes = { + completedOnboarding: PropTypes.bool, +} diff --git a/ui/app/helpers/higher-order-components/initialized/initialized.container.js b/ui/app/helpers/higher-order-components/initialized/initialized.container.js new file mode 100644 index 000000000..0e7f72bcb --- /dev/null +++ b/ui/app/helpers/higher-order-components/initialized/initialized.container.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux' +import Initialized from './initialized.component' + +const mapStateToProps = state => { + const { metamask: { completedOnboarding } } = state + + return { + completedOnboarding, + } +} + +export default connect(mapStateToProps)(Initialized) diff --git a/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js new file mode 100644 index 000000000..6086e03fb --- /dev/null +++ b/ui/app/helpers/higher-order-components/metametrics/metametrics.provider.js @@ -0,0 +1,106 @@ +import { Component } from 'react' +import { connect } from 'react-redux' +import PropTypes from 'prop-types' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import { + getCurrentNetworkId, + getSelectedAsset, + getAccountType, + getNumberOfAccounts, + getNumberOfTokens, +} from '../../../selectors/selectors' +import { + txDataSelector, +} from '../../../selectors/confirm-transaction' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import { + sendMetaMetricsEvent, + sendCountIsTrackable, +} from '../../utils/metametrics.util' + +class MetaMetricsProvider extends Component { + static propTypes = { + network: PropTypes.string.isRequired, + environmentType: PropTypes.string.isRequired, + activeCurrency: PropTypes.string.isRequired, + accountType: PropTypes.string.isRequired, + metaMetricsSendCount: PropTypes.number.isRequired, + children: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + } + + static childContextTypes = { + metricsEvent: PropTypes.func, + } + + constructor (props) { + super(props) + + this.state = { + previousPath: '', + currentPath: window.location.href, + } + + props.history.listen(locationObj => { + this.setState({ + previousPath: this.state.currentPath, + currentPath: window.location.href, + }) + }) + } + + getChildContext () { + const props = this.props + const { pathname } = location + const { previousPath, currentPath } = this.state + + return { + metricsEvent: (config = {}, overrides = {}) => { + const { eventOpts = {} } = config + const { name = '' } = eventOpts + const { pathname: overRidePathName = '' } = overrides + const isSendFlow = Boolean(name.match(/^send|^confirm/) || overRidePathName.match(/send|confirm/)) + + if (props.participateInMetaMetrics || config.isOptIn) { + return sendMetaMetricsEvent({ + ...props, + ...config, + previousPath, + currentPath, + pathname, + excludeMetaMetricsId: isSendFlow && !sendCountIsTrackable(props.metaMetricsSendCount + 1), + ...overrides, + }) + } + }, + } + } + + render () { + return this.props.children + } +} + +const mapStateToProps = state => { + const txData = txDataSelector(state) || {} + + return { + network: getCurrentNetworkId(state), + environmentType: getEnvironmentType(), + activeCurrency: getSelectedAsset(state), + accountType: getAccountType(state), + confirmTransactionOrigin: txData.origin, + metaMetricsId: state.metamask.metaMetricsId, + participateInMetaMetrics: state.metamask.participateInMetaMetrics, + metaMetricsSendCount: state.metamask.metaMetricsSendCount, + numberOfTokens: getNumberOfTokens(state), + numberOfAccounts: getNumberOfAccounts(state), + } +} + +module.exports = compose( + withRouter, + connect(mapStateToProps) +)(MetaMetricsProvider) + diff --git a/ui/app/helpers/higher-order-components/with-method-data/index.js b/ui/app/helpers/higher-order-components/with-method-data/index.js new file mode 100644 index 000000000..f511e1ae7 --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-method-data/index.js @@ -0,0 +1 @@ +export { default } from './with-method-data.component' diff --git a/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js b/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js new file mode 100644 index 000000000..efa9ad0a2 --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-method-data/with-method-data.component.js @@ -0,0 +1,65 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import { getMethodData, getFourBytePrefix } from '../../utils/transactions.util' + +export default function withMethodData (WrappedComponent) { + return class MethodDataWrappedComponent extends PureComponent { + static propTypes = { + transaction: PropTypes.object, + knownMethodData: PropTypes.object, + addKnownMethodData: PropTypes.func, + } + + static defaultProps = { + transaction: {}, + knownMethodData: {}, + } + + state = { + methodData: {}, + done: false, + error: null, + } + + componentDidMount () { + this.fetchMethodData() + } + + async fetchMethodData () { + const { transaction, knownMethodData, addKnownMethodData } = this.props + const { txParams: { data = '' } = {} } = transaction + + if (data) { + try { + let methodData + const fourBytePrefix = getFourBytePrefix(data) + if (fourBytePrefix in knownMethodData) { + methodData = knownMethodData[fourBytePrefix] + } else { + methodData = await getMethodData(data) + if (!Object.entries(methodData).length === 0) { + addKnownMethodData(fourBytePrefix, methodData) + } + } + + this.setState({ methodData, done: true }) + } catch (error) { + this.setState({ done: true, error }) + } + } else { + this.setState({ done: true }) + } + } + + render () { + const { methodData, done, error } = this.state + + return ( + + ) + } + } +} diff --git a/ui/app/helpers/higher-order-components/with-modal-props/index.js b/ui/app/helpers/higher-order-components/with-modal-props/index.js new file mode 100644 index 000000000..e476b51d2 --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-modal-props/index.js @@ -0,0 +1 @@ +export { default } from './with-modal-props' diff --git a/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js new file mode 100644 index 000000000..654e7062a --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-modal-props/tests/with-modal-props.test.js @@ -0,0 +1,43 @@ + +import assert from 'assert' +import configureMockStore from 'redux-mock-store' +import { mount } from 'enzyme' +import React from 'react' +import withModalProps from '../with-modal-props' + +const mockState = { + appState: { + modal: { + modalState: { + props: { + prop1: 'prop1', + prop2: 2, + prop3: true, + }, + }, + }, + }, +} + +describe('withModalProps', () => { + it('should return a component wrapped with modal state props', () => { + const TestComponent = props => ( +
Testing
+ ) + const WrappedComponent = withModalProps(TestComponent) + const store = configureMockStore()(mockState) + const wrapper = mount( + + ) + + assert.ok(wrapper) + const testComponent = wrapper.find(TestComponent).at(0) + assert.equal(testComponent.length, 1) + assert.equal(testComponent.find('.test').text(), 'Testing') + const testComponentProps = testComponent.props() + assert.equal(testComponentProps.prop1, 'prop1') + assert.equal(testComponentProps.prop2, 2) + assert.equal(testComponentProps.prop3, true) + assert.equal(typeof testComponentProps.hideModal, 'function') + }) +}) diff --git a/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js new file mode 100644 index 000000000..aac6b5a61 --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-modal-props/with-modal-props.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux' +import { hideModal } from '../../../store/actions' + +const mapStateToProps = state => { + const { appState } = state + const { props: modalProps } = appState.modal.modalState + + return { + ...modalProps, + } +} + +const mapDispatchToProps = dispatch => { + return { + hideModal: () => dispatch(hideModal()), + } +} + +export default function withModalProps (Component) { + return connect(mapStateToProps, mapDispatchToProps)(Component) +} diff --git a/ui/app/helpers/higher-order-components/with-token-tracker/index.js b/ui/app/helpers/higher-order-components/with-token-tracker/index.js new file mode 100644 index 000000000..d401e81f1 --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-token-tracker/index.js @@ -0,0 +1 @@ +export { default } from './with-token-tracker.component' diff --git a/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js new file mode 100644 index 000000000..36f6a6efd --- /dev/null +++ b/ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import TokenTracker from 'eth-token-tracker' + +export default function withTokenTracker (WrappedComponent) { + return class TokenTrackerWrappedComponent extends Component { + static propTypes = { + userAddress: PropTypes.string.isRequired, + token: PropTypes.object.isRequired, + } + + constructor (props) { + super(props) + + this.state = { + string: '', + symbol: '', + error: null, + } + + this.tracker = null + this.updateBalance = this.updateBalance.bind(this) + this.setError = this.setError.bind(this) + } + + componentDidMount () { + this.createFreshTokenTracker() + } + + componentDidUpdate (prevProps) { + const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props + const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps + + if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) { + return + } + + if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) { + return + } + + this.createFreshTokenTracker() + } + + componentWillUnmount () { + this.removeListeners() + } + + createFreshTokenTracker () { + this.removeListeners() + + if (!global.ethereumProvider) { + return + } + + const { userAddress, token } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: [token], + pollingInterval: 8000, + }) + + this.tracker.on('update', this.updateBalance) + this.tracker.on('error', this.setError) + + this.tracker.updateBalances() + .then(() => this.updateBalance(this.tracker.serialize())) + .catch(error => this.setState({ error: error.message })) + } + + setError (error) { + this.setState({ error }) + } + + updateBalance (tokens = []) { + if (!this.tracker.running) { + return + } + const [{ string, symbol }] = tokens + this.setState({ string, symbol, error: null }) + } + + removeListeners () { + if (this.tracker) { + this.tracker.stop() + this.tracker.removeListener('update', this.updateBalance) + this.tracker.removeListener('error', this.setError) + } + } + + render () { + const { string, symbol, error } = this.state + + return ( + + ) + } + } +} diff --git a/ui/app/helpers/tests/common.util.test.js b/ui/app/helpers/tests/common.util.test.js deleted file mode 100644 index a52b91a10..000000000 --- a/ui/app/helpers/tests/common.util.test.js +++ /dev/null @@ -1,27 +0,0 @@ -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/tests/transactions.util.test.js b/ui/app/helpers/tests/transactions.util.test.js deleted file mode 100644 index 838522e35..000000000 --- a/ui/app/helpers/tests/transactions.util.test.js +++ /dev/null @@ -1,57 +0,0 @@ -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/transactions.util.js b/ui/app/helpers/transactions.util.js deleted file mode 100644 index d5b7f4958..000000000 --- a/ui/app/helpers/transactions.util.js +++ /dev/null @@ -1,179 +0,0 @@ -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/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: ) -> 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)}` +} -- cgit v1.2.3 From 468bc96bdfc08e80119b3d5b3a54a3cf84046af8 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Fri, 22 Mar 2019 12:20:12 -0230 Subject: Hide gas price chart and prevent api call when not on ethereum networks. (#6300) Add missing translations in gas customization modal --- ui/app/helpers/constants/common.js | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ui/app/helpers') diff --git a/ui/app/helpers/constants/common.js b/ui/app/helpers/constants/common.js index c6e566b8b..58fae5e5f 100644 --- a/ui/app/helpers/constants/common.js +++ b/ui/app/helpers/constants/common.js @@ -6,5 +6,8 @@ export const PRIMARY = 'PRIMARY' export const SECONDARY = 'SECONDARY' export const NETWORK_TYPES = { + KOVAN: 'kovan', MAINNET: 'mainnet', + RINKEBY: 'rinkeby', + ROPSTEN: 'ropsten', } -- cgit v1.2.3 From 961ad267df93cbb3fc61d0a999bd78f132c877b1 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Mon, 25 Mar 2019 13:43:23 -0230 Subject: New settings page rebased (#6333) * New setting tab * Add InfoTab * Add Advanced tab * Add Security Tab * Finish mobile view * Make new setting page responsive * Fix linter * Fix y scrolling * Update link in network dropdown * Fix e2e tests * Remove duplicate translation key * Resolve merge conflict * Only change settings header in popup view. * Place mobile-sync button in advanced-tab of settings --- ui/app/helpers/constants/routes.js | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'ui/app/helpers') diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 932dfa7df..c15027ff4 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -2,7 +2,12 @@ const DEFAULT_ROUTE = '/' const UNLOCK_ROUTE = '/unlock' const LOCK_ROUTE = '/lock' const SETTINGS_ROUTE = '/settings' +const GENERAL_ROUTE = '/settings/general' const INFO_ROUTE = '/settings/info' +const ADVANCED_ROUTE = '/settings/advanced' +const SECURITY_ROUTE = '/settings/security' +const COMPANY_ROUTE = '/settings/company' +const ABOUT_US_ROUTE = '/settings/about-us' const REVEAL_SEED_ROUTE = '/seed' const MOBILE_SYNC_ROUTE = '/mobile-sync' const CONFIRM_SEED_ROUTE = '/confirm-seed' @@ -80,4 +85,9 @@ module.exports = { CONFIRM_TOKEN_METHOD_PATH, SIGNATURE_REQUEST_PATH, INITIALIZE_METAMETRICS_OPT_IN_ROUTE, + ADVANCED_ROUTE, + SECURITY_ROUTE, + COMPANY_ROUTE, + GENERAL_ROUTE, + ABOUT_US_ROUTE, } -- cgit v1.2.3 From 0346089caac58f5866e770999fd0055e16c52d78 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 27 Mar 2019 15:57:44 -0230 Subject: getHexGasTotal and increaseLastGasPrice now gracefully handle null and undefined values --- ui/app/helpers/utils/confirm-tx.util.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ui/app/helpers') diff --git a/ui/app/helpers/utils/confirm-tx.util.js b/ui/app/helpers/utils/confirm-tx.util.js index f843db118..853427b31 100644 --- a/ui/app/helpers/utils/confirm-tx.util.js +++ b/ui/app/helpers/utils/confirm-tx.util.js @@ -13,7 +13,7 @@ import { import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' export function increaseLastGasPrice (lastGasPrice) { - return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { + return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice || '0x0', 1.1, { multiplicandBase: 16, multiplierBase: 10, toNumericBase: 'hex', @@ -28,7 +28,7 @@ export function hexGreaterThan (a, b) { } export function getHexGasTotal ({ gasLimit, gasPrice }) { - return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, { + return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit || '0x0', gasPrice || '0x0', { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 16, -- cgit v1.2.3 From 31d8b279e43b7ebd6fcb11752d24c67f12a6b622 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 27 Mar 2019 17:01:25 -0230 Subject: Defaults to the args list in increaseLastGasPrice and getHexGasTotal --- ui/app/helpers/utils/confirm-tx.util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ui/app/helpers') diff --git a/ui/app/helpers/utils/confirm-tx.util.js b/ui/app/helpers/utils/confirm-tx.util.js index 853427b31..224560f5a 100644 --- a/ui/app/helpers/utils/confirm-tx.util.js +++ b/ui/app/helpers/utils/confirm-tx.util.js @@ -12,8 +12,8 @@ import { import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' -export function increaseLastGasPrice (lastGasPrice) { - return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice || '0x0', 1.1, { +export function increaseLastGasPrice (lastGasPrice = '0x0') { + return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { multiplicandBase: 16, multiplierBase: 10, toNumericBase: 'hex', @@ -27,8 +27,8 @@ export function hexGreaterThan (a, b) { ) } -export function getHexGasTotal ({ gasLimit, gasPrice }) { - return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit || '0x0', gasPrice || '0x0', { +export function getHexGasTotal ({ gasLimit = '0x0', gasPrice = '0x0' }) { + return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex', multiplicandBase: 16, multiplierBase: 16, -- cgit v1.2.3