diff options
Diffstat (limited to 'ui/app/helpers')
34 files changed, 2202 insertions, 9 deletions
diff --git a/ui/app/helpers/constants/common.js b/ui/app/helpers/constants/common.js new file mode 100644 index 000000000..58fae5e5f --- /dev/null +++ b/ui/app/helpers/constants/common.js @@ -0,0 +1,13 @@ +export const ETH = 'ETH' +export const GWEI = 'GWEI' +export const WEI = 'WEI' + +export const PRIMARY = 'PRIMARY' +export const SECONDARY = 'SECONDARY' + +export const NETWORK_TYPES = { + KOVAN: 'kovan', + MAINNET: 'mainnet', + RINKEBY: 'rinkeby', + ROPSTEN: 'ropsten', +} 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..c15027ff4 --- /dev/null +++ b/ui/app/helpers/constants/routes.js @@ -0,0 +1,93 @@ +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' +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, + ADVANCED_ROUTE, + SECURITY_ROUTE, + COMPANY_ROUTE, + GENERAL_ROUTE, + ABOUT_US_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/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 <Route { ...props } /> + case !completedOnboarding: + return <Redirect to={{ pathname: INITIALIZE_ROUTE }} /> + default: + return <Redirect to={{ pathname: UNLOCK_ROUTE }} /> + } +} + +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 + ? <Route { ...props } /> + : <Redirect to={{ pathname: INITIALIZE_ROUTE }} /> +} + +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 ( + <WrappedComponent + { ...this.props } + methodData={{ data: methodData, done, error }} + /> + ) + } + } +} 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 => ( + <div className="test">Testing</div> + ) + const WrappedComponent = withModalProps(TestComponent) + const store = configureMockStore()(mockState) + const wrapper = mount( + <WrappedComponent store={store} /> + ) + + 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 ( + <WrappedComponent + { ...this.props } + string={string} + symbol={symbol} + error={error} + /> + ) + } + } +} diff --git a/ui/app/helpers/common.util.js b/ui/app/helpers/utils/common.util.js index 0c02481e6..0c02481e6 100644 --- a/ui/app/helpers/common.util.js +++ b/ui/app/helpers/utils/common.util.js diff --git a/ui/app/helpers/tests/common.util.test.js b/ui/app/helpers/utils/common.util.test.js index a52b91a10..6259f4a89 100644 --- a/ui/app/helpers/tests/common.util.test.js +++ b/ui/app/helpers/utils/common.util.test.js @@ -1,4 +1,4 @@ -import * as utils from '../common.util' +import * as utils from './common.util' import assert from 'assert' describe('Common utils', () => { diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/utils/confirm-tx.util.js index 0451824e8..224560f5a 100644 --- a/ui/app/helpers/confirm-transaction/util.js +++ b/ui/app/helpers/utils/confirm-tx.util.js @@ -8,11 +8,11 @@ import { addCurrencies, multiplyCurrencies, conversionGreaterThan, -} from '../../conversion-util' +} from './conversion-util' import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction' -export function increaseLastGasPrice (lastGasPrice) { +export function increaseLastGasPrice (lastGasPrice = '0x0') { return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, { multiplicandBase: 16, multiplierBase: 10, @@ -27,7 +27,7 @@ export function hexGreaterThan (a, b) { ) } -export function getHexGasTotal ({ gasLimit, gasPrice }) { +export function getHexGasTotal ({ gasLimit = '0x0', gasPrice = '0x0' }) { return ethUtil.addHexPrefix(multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex', multiplicandBase: 16, diff --git a/ui/app/helpers/confirm-transaction/util.test.js b/ui/app/helpers/utils/confirm-tx.util.test.js index 4c1a3e16b..e818601ca 100644 --- a/ui/app/helpers/confirm-transaction/util.test.js +++ b/ui/app/helpers/utils/confirm-tx.util.test.js @@ -1,4 +1,4 @@ -import * as utils from './util' +import * as utils from './confirm-tx.util' import assert from 'assert' describe('Confirm Transaction utils', () => { 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/conversions.util.js b/ui/app/helpers/utils/conversions.util.js index 065d67e8e..b4ec50626 100644 --- a/ui/app/helpers/conversions.util.js +++ b/ui/app/helpers/utils/conversions.util.js @@ -1,6 +1,6 @@ import ethUtil from 'ethereumjs-util' import { ETH, GWEI, WEI } from '../constants/common' -import { conversionUtil, addCurrencies } from '../conversion-util' +import { conversionUtil, addCurrencies } from './conversion-util' export function bnToHex (inputBn) { return ethUtil.addHexPrefix(inputBn.toString(16)) diff --git a/ui/app/helpers/formatters.js b/ui/app/helpers/utils/formatters.js index 106a2520d..106a2520d 100644 --- a/ui/app/helpers/formatters.js +++ b/ui/app/helpers/utils/formatters.js 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/transactions.util.js b/ui/app/helpers/utils/transactions.util.js index d5b7f4958..edf2e24f6 100644 --- a/ui/app/helpers/transactions.util.js +++ b/ui/app/helpers/utils/transactions.util.js @@ -5,7 +5,7 @@ import abiDecoder from 'abi-decoder' import { TRANSACTION_TYPE_CANCEL, TRANSACTION_STATUS_CONFIRMED, -} from '../../../app/scripts/controllers/transactions/enums' +} from '../../../../app/scripts/controllers/transactions/enums' import { TOKEN_METHOD_TRANSFER, @@ -21,7 +21,7 @@ import { CANCEL_ATTEMPT_ACTION_KEY, } from '../constants/transactions' -import { addCurrencies } from '../conversion-util' +import { addCurrencies } from './conversion-util' abiDecoder.addABI(abi) diff --git a/ui/app/helpers/tests/transactions.util.test.js b/ui/app/helpers/utils/transactions.util.test.js index 838522e35..4a8ca5c9d 100644 --- a/ui/app/helpers/tests/transactions.util.test.js +++ b/ui/app/helpers/utils/transactions.util.test.js @@ -1,4 +1,4 @@ -import * as utils from '../transactions.util' +import * as utils from './transactions.util' import assert from 'assert' describe('Transactions utils', () => { diff --git a/ui/app/helpers/utils/util.js b/ui/app/helpers/utils/util.js new file mode 100644 index 000000000..c50d7cbe5 --- /dev/null +++ b/ui/app/helpers/utils/util.js @@ -0,0 +1,326 @@ +const abi = require('human-standard-token-abi') +const ethUtil = require('ethereumjs-util') +const hexToBn = require('../../../../app/scripts/lib/hex-to-bn') +import { DateTime } from 'luxon' + +const MIN_GAS_PRICE_GWEI_BN = new ethUtil.BN(1) +const GWEI_FACTOR = new ethUtil.BN(1e9) +const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR) + +// formatData :: ( date: <Unix Timestamp> ) -> String +function formatDate (date, format = 'M/d/y \'at\' T') { + return DateTime.fromMillis(date).toFormat(format) +} + +var valueTable = { + wei: '1000000000000000000', + kwei: '1000000000000000', + mwei: '1000000000000', + gwei: '1000000000', + szabo: '1000000', + finney: '1000', + ether: '1', + kether: '0.001', + mether: '0.000001', + gether: '0.000000001', + tether: '0.000000000001', +} +var bnTable = {} +for (var currency in valueTable) { + bnTable[currency] = new ethUtil.BN(valueTable[currency], 10) +} + +module.exports = { + valuesFor: valuesFor, + addressSummary: addressSummary, + miniAddressSummary: miniAddressSummary, + isAllOneCase: isAllOneCase, + isValidAddress: isValidAddress, + isValidENSAddress, + numericBalance: numericBalance, + parseBalance: parseBalance, + formatBalance: formatBalance, + generateBalanceObject: generateBalanceObject, + dataSize: dataSize, + readableDate: readableDate, + normalizeToWei: normalizeToWei, + normalizeEthStringToWei: normalizeEthStringToWei, + normalizeNumberToWei: normalizeNumberToWei, + valueTable: valueTable, + bnTable: bnTable, + isHex: isHex, + formatDate, + bnMultiplyByFraction, + getTxFeeBn, + shortenBalance, + getContractAtAddress, + exportAsFile: exportAsFile, + isInvalidChecksumAddress, + allNull, + getTokenAddressFromTokenObject, + checksumAddress, + addressSlicer, + isEthNetwork, +} + +function isEthNetwork (netId) { + if (!netId || netId === '1' || netId === '3' || netId === '4' || netId === '42' || netId === '5777') { + return true + } + + return false +} + +function valuesFor (obj) { + if (!obj) return [] + return Object.keys(obj) + .map(function (key) { return obj[key] }) +} + +function addressSummary (address, firstSegLength = 10, lastSegLength = 4, includeHex = true) { + if (!address) return '' + let checked = checksumAddress(address) + if (!includeHex) { + checked = ethUtil.stripHexPrefix(checked) + } + return checked ? checked.slice(0, firstSegLength) + '...' + checked.slice(checked.length - lastSegLength) : '...' +} + +function miniAddressSummary (address) { + if (!address) return '' + var checked = checksumAddress(address) + return checked ? checked.slice(0, 4) + '...' + checked.slice(-4) : '...' +} + +function isValidAddress (address, network) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) +} + +function isValidENSAddress (address) { + return address.match(/^.{7,}\.(eth|test)$/) +} + +function isInvalidChecksumAddress (address) { + var prefixed = ethUtil.addHexPrefix(address) + if (address === '0x0000000000000000000000000000000000000000') return false + return !isAllOneCase(prefixed) && !ethUtil.isValidChecksumAddress(prefixed) && ethUtil.isValidAddress(prefixed) +} + +function isAllOneCase (address) { + if (!address) return true + var lower = address.toLowerCase() + var upper = address.toUpperCase() + return address === lower || address === upper +} + +// Takes wei Hex, returns wei BN, even if input is null +function numericBalance (balance) { + if (!balance) return new ethUtil.BN(0, 16) + var stripped = ethUtil.stripHexPrefix(balance) + return new ethUtil.BN(stripped, 16) +} + +// Takes hex, returns [beforeDecimal, afterDecimal] +function parseBalance (balance) { + var beforeDecimal, afterDecimal + const wei = numericBalance(balance) + var weiString = wei.toString() + const trailingZeros = /0+$/ + + beforeDecimal = weiString.length > 18 ? weiString.slice(0, weiString.length - 18) : '0' + afterDecimal = ('000000000000000000' + wei).slice(-18).replace(trailingZeros, '') + if (afterDecimal === '') { afterDecimal = '0' } + return [beforeDecimal, afterDecimal] +} + +// Takes wei hex, returns an object with three properties. +// Its "formatted" property is what we generally use to render values. +function formatBalance (balance, decimalsToKeep, needsParse = true, ticker = 'ETH') { + var parsed = needsParse ? parseBalance(balance) : balance.split('.') + var beforeDecimal = parsed[0] + var afterDecimal = parsed[1] + var formatted = 'None' + if (decimalsToKeep === undefined) { + if (beforeDecimal === '0') { + if (afterDecimal !== '0') { + var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits + if (sigFigs) { afterDecimal = sigFigs[0] } + formatted = '0.' + afterDecimal + ` ${ticker}` + } + } else { + formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${ticker}` + } + } else { + afterDecimal += Array(decimalsToKeep).join('0') + formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${ticker}` + } + return formatted +} + + +function generateBalanceObject (formattedBalance, decimalsToKeep = 1) { + var balance = formattedBalance.split(' ')[0] + var label = formattedBalance.split(' ')[1] + var beforeDecimal = balance.split('.')[0] + var afterDecimal = balance.split('.')[1] + var shortBalance = shortenBalance(balance, decimalsToKeep) + + if (beforeDecimal === '0' && afterDecimal.substr(0, 5) === '00000') { + // eslint-disable-next-line eqeqeq + if (afterDecimal == 0) { + balance = '0' + } else { + balance = '<1.0e-5' + } + } else if (beforeDecimal !== '0') { + balance = `${beforeDecimal}.${afterDecimal.slice(0, decimalsToKeep)}` + } + + return { balance, label, shortBalance } +} + +function shortenBalance (balance, decimalsToKeep = 1) { + var truncatedValue + var convertedBalance = parseFloat(balance) + if (convertedBalance > 1000000) { + truncatedValue = (balance / 1000000).toFixed(decimalsToKeep) + return `${truncatedValue}m` + } else if (convertedBalance > 1000) { + truncatedValue = (balance / 1000).toFixed(decimalsToKeep) + return `${truncatedValue}k` + } else if (convertedBalance === 0) { + return '0' + } else if (convertedBalance < 0.001) { + return '<0.001' + } else if (convertedBalance < 1) { + var stringBalance = convertedBalance.toString() + if (stringBalance.split('.')[1].length > 3) { + return convertedBalance.toFixed(3) + } else { + return stringBalance + } + } else { + return convertedBalance.toFixed(decimalsToKeep) + } +} + +function dataSize (data) { + var size = data ? ethUtil.stripHexPrefix(data).length : 0 + return size + ' bytes' +} + +// Takes a BN and an ethereum currency name, +// returns a BN in wei +function normalizeToWei (amount, currency) { + try { + return amount.mul(bnTable.wei).div(bnTable[currency]) + } catch (e) {} + return amount +} + +function normalizeEthStringToWei (str) { + const parts = str.split('.') + let eth = new ethUtil.BN(parts[0], 10).mul(bnTable.wei) + if (parts[1]) { + var decimal = parts[1] + while (decimal.length < 18) { + decimal += '0' + } + if (decimal.length > 18) { + decimal = decimal.slice(0, 18) + } + const decimalBN = new ethUtil.BN(decimal, 10) + eth = eth.add(decimalBN) + } + return eth +} + +var multiple = new ethUtil.BN('10000', 10) +function normalizeNumberToWei (n, currency) { + var enlarged = n * 10000 + var amount = new ethUtil.BN(String(enlarged), 10) + return normalizeToWei(amount, currency).div(multiple) +} + +function readableDate (ms) { + var date = new Date(ms) + var month = date.getMonth() + var day = date.getDate() + var year = date.getFullYear() + var hours = date.getHours() + var minutes = '0' + date.getMinutes() + var seconds = '0' + date.getSeconds() + + var dateStr = `${month}/${day}/${year}` + var time = `${hours}:${minutes.substr(-2)}:${seconds.substr(-2)}` + return `${dateStr} ${time}` +} + +function isHex (str) { + return Boolean(str.match(/^(0x)?[0-9a-fA-F]+$/)) +} + +function bnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new ethUtil.BN(numerator) + const denomBN = new ethUtil.BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} + +function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimit) { + const gasBn = hexToBn(gas) + const gasPriceBn = hexToBn(gasPrice) + const txFeeBn = gasBn.mul(gasPriceBn) + + return txFeeBn.toString(16) +} + +function getContractAtAddress (tokenAddress) { + return global.eth.contract(abi).at(tokenAddress) +} + +function exportAsFile (filename, data, type = 'text/csv') { + // source: https://stackoverflow.com/a/33542499 by Ludovic Feltz + const blob = new Blob([data], {type}) + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename) + } else { + const elem = window.document.createElement('a') + elem.target = '_blank' + elem.href = window.URL.createObjectURL(blob) + elem.download = filename + document.body.appendChild(elem) + elem.click() + document.body.removeChild(elem) + } +} + +function allNull (obj) { + return Object.entries(obj).every(([key, value]) => value === null) +} + +function getTokenAddressFromTokenObject (token) { + return Object.values(token)[0].address.toLowerCase() +} + +/** + * Safely checksumms a potentially-null address + * + * @param {String} [address] - address to checksum + * @param {String} [network] - network id + * @returns {String} - checksummed address + * + */ +function checksumAddress (address, network) { + const checksummed = address ? ethUtil.toChecksumAddress(address) : '' + return checksummed +} + +function addressSlicer (address = '') { + if (address.length < 11) { + return address + } + + return `${address.slice(0, 6)}...${address.slice(-4)}` +} |