diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2656432d2a58..784e2f6fee66 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -635,6 +635,9 @@ "newRPC": { "message": "New RPC URL" }, + "optionalChainId": { + "message": "ChainId (optional)" + }, "next": { "message": "Next" }, diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index d5bc5fe2bb9a..b80e1344b190 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -25,6 +25,7 @@ class CurrencyController { */ constructor (opts = {}) { const initState = extend({ + fromCurrency: 'ETH', currentCurrency: 'usd', conversionRate: 0, conversionDate: 'N/A', @@ -36,6 +37,26 @@ class CurrencyController { // PUBLIC METHODS // + /** + * A getter for the fromCurrency property + * + * @returns {string} A 2-4 character shorthand that describes the specific currency + * + */ + getFromCurrency () { + return this.store.getState().fromCurrency + } + + /** + * A setter for the fromCurrency property + * + * @param {string} fromCurrency The new currency to set as the fromCurrency in the store + * + */ + setFromCurrency (fromCurrency) { + this.store.updateState({ ticker: fromCurrency, fromCurrency }) + } + /** * A getter for the currentCurrency property * @@ -104,15 +125,16 @@ class CurrencyController { * */ async updateConversionRate () { - let currentCurrency + let currentCurrency, fromCurrency try { currentCurrency = this.getCurrentCurrency() - const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) + fromCurrency = this.getFromCurrency() + const response = await fetch(`https://min-api.cryptocompare.com/data/pricehistorical?fsym=${fromCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`) const parsedResponse = await response.json() - this.setConversionRate(Number(parsedResponse.bid)) - this.setConversionDate(Number(parsedResponse.timestamp)) + this.setConversionRate(Number(parsedResponse[fromCurrency.toUpperCase()][currentCurrency.toUpperCase()])) + this.setConversionDate(parseInt(new Date().getTime() / 1000)) } catch (err) { - log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err) + log.warn(`MetaMask - Failed to query currency conversion:`, fromCurrency, currentCurrency, err) this.setConversionRate(0) this.setConversionDate('N/A') } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c1667d9a677d..5d6d60f3a89f 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -11,6 +11,8 @@ const createInfuraClient = require('./createInfuraClient') const createJsonRpcClient = require('./createJsonRpcClient') const createLocalhostClient = require('./createLocalhostClient') const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') +const networks = require('./networks') +const extend = require('xtend') const { ROPSTEN, @@ -20,6 +22,7 @@ const { LOCALHOST, } = require('./enums') const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] +const ALL_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET] const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG @@ -29,6 +32,10 @@ const defaultProviderConfig = { type: testMode ? RINKEBY : MAINNET, } +const defaultNetworkConfig = { + ticker: 'ETH', +} + module.exports = class NetworkController extends EventEmitter { constructor (opts = {}) { @@ -39,7 +46,8 @@ module.exports = class NetworkController extends EventEmitter { // create stores this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') - this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) + this.networkConfig = new ObservableStore(defaultNetworkConfig) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig }) this.on('networkDidChange', this.lookupNetwork) // provider and block tracker this._provider = null @@ -51,8 +59,8 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (providerParams) { this._baseProviderParams = providerParams - const { type, rpcTarget } = this.providerStore.getState() - this._configureProvider({ type, rpcTarget }) + const { type, rpcTarget, chainId } = this.providerStore.getState() + this._configureProvider({ type, rpcTarget, chainId }) this.lookupNetwork() } @@ -72,7 +80,20 @@ module.exports = class NetworkController extends EventEmitter { return this.networkStore.getState() } - setNetworkState (network) { + getNetworkConfig () { + return this.networkConfig.getState() + } + + setNetworkState (network, type) { + if (network === 'loading') { + return this.networkStore.putState(network) + } + + // type must be defined + if (!type) { + return + } + network = networks.networkList[type] && networks.networkList[type].chainId ? networks.networkList[type].chainId : network return this.networkStore.putState(network) } @@ -85,25 +106,27 @@ module.exports = class NetworkController extends EventEmitter { if (!this._provider) { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } + var { type } = this.providerStore.getState() const ethQuery = new EthQuery(this._provider) ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) - this.setNetworkState(network) + this.setNetworkState(network, type) }) } - setRpcTarget (rpcTarget) { + setRpcTarget (rpcTarget, chainId) { const providerConfig = { type: 'rpc', rpcTarget, + chainId, } this.providerConfig = providerConfig } async setProviderType (type) { assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) - assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) + assert(ALL_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) const providerConfig = { type } this.providerConfig = providerConfig } @@ -132,7 +155,7 @@ module.exports = class NetworkController extends EventEmitter { } _configureProvider (opts) { - const { type, rpcTarget } = opts + const { type, rpcTarget, chainId } = opts // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type) if (isInfura) { @@ -142,7 +165,7 @@ module.exports = class NetworkController extends EventEmitter { this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { - this._configureStandardProvider({ rpcUrl: rpcTarget }) + this._configureStandardProvider({ rpcUrl: rpcTarget, chainId }) } else { throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`) } @@ -152,6 +175,11 @@ module.exports = class NetworkController extends EventEmitter { log.info('NetworkController - configureInfuraProvider', type) const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) + // setup networkConfig + var settings = { + ticker: 'ETH', + } + this.networkConfig.putState(settings) } _configureLocalhostProvider () { @@ -160,9 +188,21 @@ module.exports = class NetworkController extends EventEmitter { this._setNetworkClient(networkClient) } - _configureStandardProvider ({ rpcUrl }) { + _configureStandardProvider ({ rpcUrl, chainId }) { log.info('NetworkController - configureStandardProvider', rpcUrl) const networkClient = createJsonRpcClient({ rpcUrl }) + // hack to add a 'rpc' network with chainId + networks.networkList['rpc'] = { + chainId: chainId, + rpcUrl, + ticker: 'ETH', + } + // setup networkConfig + var settings = { + network: chainId, + } + settings = extend(settings, networks.networkList['rpc']) + this.networkConfig.putState(settings) this._setNetworkClient(networkClient) } diff --git a/app/scripts/controllers/network/networks.js b/app/scripts/controllers/network/networks.js new file mode 100644 index 000000000000..06a161070d0c --- /dev/null +++ b/app/scripts/controllers/network/networks.js @@ -0,0 +1,8 @@ +'use strict' +var networks = function() {} + +networks.networkList = { + +} + +module.exports = networks diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 1b85e4fd152c..92157f30e716 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -23,7 +23,7 @@ class PreferencesController { */ constructor (opts = {}) { const initState = extend({ - frequentRpcList: [], + frequentRpcListDetail: [], currentAccountTab: 'history', accountTokens: {}, tokens: [], @@ -298,10 +298,10 @@ class PreferencesController { * @returns {Promise} Promise resolves with undefined * */ - updateFrequentRpcList (_url) { - return this.addToFrequentRpcList(_url) + updateFrequentRpcList (_url, chainId) { + return this.addToFrequentRpcList(_url, chainId) .then((rpcList) => { - this.store.updateState({ frequentRpcList: rpcList }) + this.store.updateState({ frequentRpcListDetail: rpcList }) return Promise.resolve() }) } @@ -329,14 +329,14 @@ class PreferencesController { * @returns {Promise} The updated frequentRpcList. * */ - addToFrequentRpcList (_url) { - const rpcList = this.getFrequentRpcList() - const index = rpcList.findIndex((element) => { return element === _url }) + addToFrequentRpcList (_url, chainId) { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === _url }) if (index !== -1) { rpcList.splice(index, 1) } if (_url !== 'http://localhost:8545') { - rpcList.push(_url) + rpcList.push({rpcUrl : _url, chainId }) } if (rpcList.length > 3) { rpcList.shift() @@ -345,13 +345,13 @@ class PreferencesController { } /** - * Getter for the `frequentRpcList` property. + * Getter for the `frequentRpcListDetail` property. * - * @returns {array} An array of one or two rpc urls. + * @returns {array} An array of rpc urls. * */ - getFrequentRpcList () { - return this.store.getState().frequentRpcList + getFrequentRpcListDetail () { + return this.store.getState().frequentRpcListDetail } /** diff --git a/app/scripts/lib/buy-eth-url.js b/app/scripts/lib/buy-eth-url.js index 4e2d0bc79429..a1df018e8d89 100644 --- a/app/scripts/lib/buy-eth-url.js +++ b/app/scripts/lib/buy-eth-url.js @@ -11,7 +11,7 @@ module.exports = getBuyEthUrl * network does not match any of the specified cases, or if no network is given, returns undefined. * */ -function getBuyEthUrl ({ network, amount, address }) { +function getBuyEthUrl ({ network, amount, address, link }) { let url switch (network) { case '1': @@ -29,6 +29,14 @@ function getBuyEthUrl ({ network, amount, address }) { case '42': url = 'https://github.com/kovan-testnet/faucet' break + + default: + if (link) { + url = link.replace('[[amount]]', amount).replace('[[address]]', address) + } else { + url = '' + } + break } return url } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a6215d51be2e..4039af72504e 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -193,6 +193,8 @@ module.exports = class MetamaskController extends EventEmitter { }) this.networkController.on('networkDidChange', () => { this.balancesController.updateAllBalances() + var currentCurrency = this.currencyController.getCurrentCurrency() + this.setCurrentCurrency(currentCurrency, function() {}) }) this.balancesController.updateAllBalances() @@ -1368,10 +1370,13 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function returning currency info. */ setCurrentCurrency (currencyCode, cb) { + const { ticker } = this.networkController.getNetworkConfig() try { + this.currencyController.setFromCurrency(ticker) this.currencyController.setCurrentCurrency(currencyCode) this.currencyController.updateConversionRate() const data = { + fromCurrency: ticker || 'ETH', conversionRate: this.currencyController.getConversionRate(), currentCurrency: this.currencyController.getCurrentCurrency(), conversionDate: this.currencyController.getConversionDate(), @@ -1392,7 +1397,8 @@ module.exports = class MetamaskController extends EventEmitter { buyEth (address, amount) { if (!amount) amount = '5' const network = this.networkController.getNetworkState() - const url = getBuyEthUrl({ network, address, amount }) + const link = this.networkController.getNetworkConfig().buyUrl + const url = getBuyEthUrl({ network, address, amount, link }) if (url) this.platform.openWindow({ url }) } @@ -1412,9 +1418,9 @@ module.exports = class MetamaskController extends EventEmitter { * @param {string} rpcTarget - A URL for a valid Ethereum RPC API. * @returns {Promise} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget) { - this.networkController.setRpcTarget(rpcTarget) - await this.preferencesController.updateFrequentRpcList(rpcTarget) + async setCustomRpc (rpcTarget, chainId) { + this.networkController.setRpcTarget(rpcTarget, chainId) + await this.preferencesController.updateFrequentRpcList(rpcTarget, chainId) return rpcTarget } diff --git a/old-ui/app/app.js b/old-ui/app/app.js index d3e9e823b0d5..58b065850e6f 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -72,7 +72,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice: state.metamask.nextUnreadNotice, lostAccounts: state.metamask.lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], featureFlags, // state needed to get account dropdown temporarily rendering from app bar diff --git a/old-ui/app/components/account-dropdowns.js b/old-ui/app/components/account-dropdowns.js index 262de66019aa..2b9284278b0d 100644 --- a/old-ui/app/components/account-dropdowns.js +++ b/old-ui/app/components/account-dropdowns.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../../../ui/app/actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../../ui/lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem @@ -189,7 +189,11 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(selected, network, url) global.platform.openWindow({ url }) }, }, @@ -298,6 +302,7 @@ AccountDropdowns.propTypes = { keyrings: PropTypes.array, actions: PropTypes.objectOf(PropTypes.func), network: PropTypes.string, + settings: PropTypes.object, style: PropTypes.object, enableAccountOptions: PropTypes.bool, enableAccountsSelector: PropTypes.bool, @@ -316,6 +321,12 @@ const mapDispatchToProps = (dispatch) => { } } +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), + AccountDropdowns: connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns), } diff --git a/old-ui/app/components/app-bar.js b/old-ui/app/components/app-bar.js index 234c06a01791..2642ffca23cb 100644 --- a/old-ui/app/components/app-bar.js +++ b/old-ui/app/components/app-bar.js @@ -17,7 +17,7 @@ module.exports = class AppBar extends Component { static propTypes = { dispatch: PropTypes.func.isRequired, - frequentRpcList: PropTypes.array.isRequired, + frequentRpcListDetail: PropTypes.array.isRequired, isMascara: PropTypes.bool.isRequired, isOnboarding: PropTypes.bool.isRequired, identities: PropTypes.any.isRequired, @@ -196,7 +196,7 @@ module.exports = class AppBar extends Component { renderNetworkDropdown () { const { dispatch, - frequentRpcList: rpcList, + frequentRpcListDetail: rpcList, provider, } = this.props const { @@ -322,7 +322,7 @@ module.exports = class AppBar extends Component { } renderCustomOption ({ rpcTarget, type }) { - const {dispatch} = this.props + const {dispatch, network} = this.props if (type !== 'rpc') { return null @@ -340,7 +340,7 @@ module.exports = class AppBar extends Component { default: return h(DropdownMenuItem, { key: rpcTarget, - onClick: () => dispatch(actions.setRpcTarget(rpcTarget)), + onClick: () => dispatch(actions.setRpcTarget(rpcTarget, network)), closeMenu: () => this.setState({ isNetworkMenuOpen: false }), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), @@ -354,7 +354,8 @@ module.exports = class AppBar extends Component { const {dispatch} = this.props const reversedRpcList = rpcList.slice().reverse() - return reversedRpcList.map((rpc) => { + return reversedRpcList.map((entry) => { + const rpc = entry.rpcUrl const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) { @@ -363,7 +364,7 @@ module.exports = class AppBar extends Component { return h(DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.setState({ isNetworkMenuOpen: false }), - onClick: () => dispatch(actions.setRpcTarget(rpc)), + onClick: () => dispatch(actions.setRpcTarget(rpc, entry.chainId)), }, [ h('i.fa.fa-question-circle.fa-lg.menu-icon'), rpc, diff --git a/old-ui/app/components/balance.js b/old-ui/app/components/balance.js index 57ca845649ee..d1fbc2c78b7a 100644 --- a/old-ui/app/components/balance.js +++ b/old-ui/app/components/balance.js @@ -1,12 +1,18 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -16,11 +22,16 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props + const { ticker } = props var style = props.style var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' var width = props.width + if (ticker !== 'ETH') { + value = value.replace(/ETH/, ticker) + } + return ( h('.ether-balance.ether-balance-amount', { diff --git a/old-ui/app/components/buy-button-subview.js b/old-ui/app/components/buy-button-subview.js index 8bb73ae3e9c3..a7e29859c569 100644 --- a/old-ui/app/components/buy-button-subview.js +++ b/old-ui/app/components/buy-button-subview.js @@ -20,6 +20,8 @@ function mapStateToProps (state) { buyView: state.appState.buyView, network: state.metamask.network, provider: state.metamask.provider, + ticker: state.metamask.ticker, + settings: state.metamask.settings, context: state.appState.currentView.context, isSubLoading: state.appState.isSubLoading, } @@ -170,15 +172,39 @@ BuyButtonSubview.prototype.primarySubview = function () { ) default: - return ( - h('h2.error', 'Unknown network ID') - ) - + return this.mainnetSubview() } } BuyButtonSubview.prototype.mainnetSubview = function () { const props = this.props + const network = parseInt(props.network) + + let selected + if (network === 1) { + selected = [ + 'Coinbase', + 'ShapeShift', + ] + } else { + selected = this.props.settings.exchanges || [] + } + + const subtext = { + 'Coinbase': 'Crypto/FIAT (USA only)', + 'ShapeShift': 'Crypto', + } + + let texts = {} + selected.forEach(ex => { + texts[ex] = subtext[ex] + }) + + if (selected.length === 0) { + return ( + h('h2.error', 'No exchange supported') + ) + } return ( @@ -198,14 +224,8 @@ BuyButtonSubview.prototype.mainnetSubview = function () { }, [ h(RadioList, { defaultFocus: props.buyView.subview, - labels: [ - 'Coinbase', - 'ShapeShift', - ], - subtext: { - 'Coinbase': 'Crypto/FIAT (USA only)', - 'ShapeShift': 'Crypto', - }, + labels: selected, + subtext: texts, onClick: this.radioHandler.bind(this), }), ]), @@ -229,13 +249,10 @@ BuyButtonSubview.prototype.mainnetSubview = function () { } BuyButtonSubview.prototype.formVersionSubview = function () { - const network = this.props.network - if (network === '1') { - if (this.props.buyView.formView.coinbase) { - return h(CoinbaseForm, this.props) - } else if (this.props.buyView.formView.shapeshift) { - return h(ShapeshiftForm, this.props) - } + if (this.props.buyView.formView.coinbase) { + return h(CoinbaseForm, this.props) + } else if (this.props.buyView.formView.shapeshift) { + return h(ShapeshiftForm, this.props) } } @@ -252,10 +269,11 @@ BuyButtonSubview.prototype.backButtonContext = function () { } BuyButtonSubview.prototype.radioHandler = function (event) { + const ticker = this.props.ticker switch (event.target.title) { case 'Coinbase': return this.props.dispatch(actions.coinBaseSubview()) case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type, ticker)) } } diff --git a/old-ui/app/components/eth-balance.js b/old-ui/app/components/eth-balance.js index 4f538fd31f31..38219d00ebea 100644 --- a/old-ui/app/components/eth-balance.js +++ b/old-ui/app/components/eth-balance.js @@ -1,12 +1,18 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const formatBalance = require('../util').formatBalance const generateBalanceObject = require('../util').generateBalanceObject const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -16,10 +22,14 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { var props = this.props let { value } = props - const { style, width } = props + const { ticker, style, width } = props var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true value = value ? formatBalance(value, 6, needsParse) : '...' + if (ticker !== 'ETH') { + value = value.replace(/ETH/, ticker) + } + return ( h('.ether-balance.ether-balance-amount', { diff --git a/old-ui/app/components/shapeshift-form.js b/old-ui/app/components/shapeshift-form.js index 14de309aba74..e17f53ff9c93 100644 --- a/old-ui/app/components/shapeshift-form.js +++ b/old-ui/app/components/shapeshift-form.js @@ -10,6 +10,7 @@ function mapStateToProps (state) { return { warning: state.appState.warning, isSubLoading: state.appState.isSubLoading, + ticker: state.metamask.ticker, } } @@ -237,7 +238,7 @@ ShapeshiftForm.prototype.updateCoin = function (event) { var message = 'Not a valid coin' return props.dispatch(actions.displayWarning(message)) } else { - return props.dispatch(actions.pairUpdate(coin)) + return props.dispatch(actions.pairUpdate(coin, props.ticker)) } } @@ -249,7 +250,7 @@ ShapeshiftForm.prototype.handleLiveInput = function () { if (!coinOptions[coin.toUpperCase()] || coin.toUpperCase() === 'ETH') { return null } else { - return props.dispatch(actions.pairUpdate(coin)) + return props.dispatch(actions.pairUpdate(coin, props.ticker)) } } diff --git a/old-ui/app/components/shift-list-item.js b/old-ui/app/components/shift-list-item.js index 5454a90bc438..517219d23bc7 100644 --- a/old-ui/app/components/shift-list-item.js +++ b/old-ui/app/components/shift-list-item.js @@ -3,7 +3,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../../ui/lib/explorer-link.js') const actions = require('../../../ui/app/actions') const addressSummary = require('../util').addressSummary @@ -18,6 +18,7 @@ function mapStateToProps (state) { return { conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + ticker: state.metamask.ticker, } } @@ -79,7 +80,7 @@ ShiftListItem.prototype.renderUtilComponents = function () { title: 'QR Code', }, [ h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType, props.ticker)), style: { margin: '5px', marginLeft: '23px', diff --git a/old-ui/app/components/token-cell.js b/old-ui/app/components/token-cell.js index 19d7139bb892..5c224c2a00c0 100644 --- a/old-ui/app/components/token-cell.js +++ b/old-ui/app/components/token-cell.js @@ -1,10 +1,16 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const Identicon = require('./identicon') const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') -module.exports = TokenCell +module.exports = connect(mapStateToProps)(TokenCell) +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} inherits(TokenCell, Component) function TokenCell () { @@ -44,14 +50,22 @@ TokenCell.prototype.render = function () { TokenCell.prototype.send = function (address, event) { event.preventDefault() event.stopPropagation() - const url = tokenFactoryFor(address) + let url + if (this.props.settings && this.props.settings.blockExplorerTokenFactory) { + url = this.props.settings.blockExplorerTokenFactory + } + url = tokenFactoryFor(address, url) if (url) { navigateTo(url) } } TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) + let url + if (this.props.settings && this.props.settings.blockExplorerToken) { + url = this.props.settings.blockExplorerToken + } + url = etherscanLinkFor(address, userAddress, network, url) if (url) { navigateTo(url) } @@ -61,12 +75,20 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (tokenAddress, address, network) { +function etherscanLinkFor (tokenAddress, address, network, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress).replace('[[address]]', address) + } + const prefix = prefixForNetwork(network) return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } -function tokenFactoryFor (tokenAddress) { +function tokenFactoryFor (tokenAddress, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress) + } + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` } diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js index 6ecf7d193c08..d9b8257bccb4 100644 --- a/old-ui/app/components/transaction-list-item.js +++ b/old-ui/app/components/transaction-list-item.js @@ -5,7 +5,7 @@ const connect = require('react-redux').connect const EthBalance = require('./eth-balance') const addressSummary = require('../util').addressSummary -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../../ui/lib/explorer-link.js') const CopyButton = require('./copyButton') const vreme = new (require('vreme'))() const Tooltip = require('./tooltip') @@ -15,13 +15,19 @@ const actions = require('../../../ui/app/actions') const TransactionIcon = require('./transaction-list-item-icon') const ShiftListItem = require('./shift-list-item') +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + const mapDispatchToProps = dispatch => { return { retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)), } } -module.exports = connect(null, mapDispatchToProps)(TransactionListItem) +module.exports = connect(mapStateToProps, mapDispatchToProps)(TransactionListItem) inherits(TransactionListItem, Component) function TransactionListItem () { @@ -88,7 +94,11 @@ TransactionListItem.prototype.render = function () { } event.stopPropagation() if (!transaction.hash || !isLinkable) return - var url = explorerLink(transaction.hash, parseInt(network)) + let url + if (this.props.settings && this.props.settings.blockExplorerTx) { + url = this.props.settings.blockExplorerTx + } + url = explorerLink(transaction.hash, parseInt(network), url) global.platform.openWindow({ url }) }, style: { diff --git a/old-ui/app/config.js b/old-ui/app/config.js index 392a6dba7c1b..1203524ea6d6 100644 --- a/old-ui/app/config.js +++ b/old-ui/app/config.js @@ -68,7 +68,7 @@ ConfigScreen.prototype.render = function () { currentProviderDisplay(metamaskState), - h('div', { style: {display: 'flex'} }, [ + h('div', { style: {display: 'block'} }, [ h('input#new_rpc', { placeholder: 'New RPC URL', style: { @@ -81,7 +81,26 @@ ConfigScreen.prototype.render = function () { if (event.key === 'Enter') { var element = event.target var newRpc = element.value - rpcValidation(newRpc, state) + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) + } + }, + }), + h('br'), + h('input#chainid', { + placeholder: 'ChainId (optional)', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onKeyPress (event) { + if (event.key === 'Enter') { + var element = document.querySelector('input#new_rpc') + var newRpc = element.value + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) } }, }), @@ -93,7 +112,8 @@ ConfigScreen.prototype.render = function () { event.preventDefault() var element = document.querySelector('input#new_rpc') var newRpc = element.value - rpcValidation(newRpc, state) + var chainid = document.querySelector('input#chainid') + rpcValidation(newRpc, chainid.value, state) }, }, 'Save'), ]), @@ -189,9 +209,9 @@ ConfigScreen.prototype.render = function () { ) } -function rpcValidation (newRpc, state) { +function rpcValidation (newRpc, chainid, state) { if (validUrl.isWebUri(newRpc)) { - state.dispatch(actions.setRpcTarget(newRpc)) + state.dispatch(actions.setRpcTarget(newRpc, chainid)) } else { var appendedRpc = `http://${newRpc}` if (validUrl.isWebUri(appendedRpc)) { diff --git a/test/unit/app/controllers/network-contoller-test.js b/test/unit/app/controllers/network-contoller-test.js index 822311931e67..7959e6cc1265 100644 --- a/test/unit/app/controllers/network-contoller-test.js +++ b/test/unit/app/controllers/network-contoller-test.js @@ -47,7 +47,7 @@ describe('# Network Controller', function () { describe('#setNetworkState', function () { it('should update the network', function () { - networkController.setNetworkState(1) + networkController.setNetworkState(1, 'rpc') const networkState = networkController.getNetworkState() assert.equal(networkState, 1, 'network is 1') }) diff --git a/test/unit/components/balance-component-test.js b/test/unit/components/balance-component-test.js index 81e6fdf9eb37..0b3fe7c61dd7 100644 --- a/test/unit/components/balance-component-test.js +++ b/test/unit/components/balance-component-test.js @@ -8,6 +8,7 @@ const mockState = { accounts: { abc: {} }, network: 1, selectedAddress: 'abc', + ticker: 'ETH', }, } diff --git a/ui/app/actions.js b/ui/app/actions.js index 6bcc64e17aae..638395bc8a91 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1751,10 +1751,10 @@ function updateProviderType (type) { } } -function setRpcTarget (newRpc) { +function setRpcTarget (newRpc, chainId) { return (dispatch) => { log.debug(`background.setRpcTarget: ${newRpc}`) - background.setCustomRpc(newRpc, (err, result) => { + background.setCustomRpc(newRpc, chainId, (err, result) => { if (err) { log.error(err) return dispatch(self.displayWarning('Had a problem changing networks!')) @@ -2015,11 +2015,17 @@ function coinBaseSubview () { } } -function pairUpdate (coin) { +function pairUpdate (coin, ticker) { + if (!ticker) { + ticker = 'eth' + } else { + ticker = ticker.toLowerCase() + } + return (dispatch) => { dispatch(actions.showSubLoadingIndication()) dispatch(actions.hideWarning()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_${ticker}`}, (mktResponse) => { dispatch(actions.hideSubLoadingIndication()) if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) dispatch({ @@ -2032,8 +2038,11 @@ function pairUpdate (coin) { } } -function shapeShiftSubview (network) { +function shapeShiftSubview (network, ticker) { var pair = 'btc_eth' + if (ticker) { + pair = `btc_${ticker.toLowerCase()}` + } return (dispatch) => { dispatch(actions.showSubLoadingIndication()) shapeShiftRequest('marketinfo', {pair}, (mktResponse) => { @@ -2088,10 +2097,10 @@ function showQrView (data, message) { }, } } -function reshowQrCode (data, coin) { +function reshowQrCode (data, coin, ticker) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => { + shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_${ticker.toLowerCase()}`}, (mktResponse) => { if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error)) var message = [ diff --git a/ui/app/app.js b/ui/app/app.js index 4fcf092caf77..642ad86e3f25 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -99,7 +99,7 @@ class App extends Component { network, isMouseUser, provider, - frequentRpcList, + frequentRpcListDetail, currentView, setMouseUserState, } = this.props @@ -139,7 +139,7 @@ class App extends Component { // network dropdown h(NetworkDropdown, { provider, - frequentRpcList, + frequentRpcListDetail, }, []), h(AccountMenu), @@ -265,7 +265,7 @@ App.propTypes = { alertMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebarOpen: PropTypes.bool, alertOpen: PropTypes.bool, @@ -357,7 +357,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, betaUI: state.metamask.featureFlags.betaUI, diff --git a/ui/app/components/account-dropdowns.js b/ui/app/components/account-dropdowns.js index 043008a36ef1..05300cd91e20 100644 --- a/ui/app/components/account-dropdowns.js +++ b/ui/app/components/account-dropdowns.js @@ -2,7 +2,7 @@ const Component = require('react').Component const PropTypes = require('prop-types') const h = require('react-hyperscript') const actions = require('../actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../lib/account-link.js') const connect = require('react-redux').connect const Dropdown = require('./dropdown').Dropdown const DropdownMenuItem = require('./dropdown').DropdownMenuItem @@ -188,7 +188,11 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(selected, network, url) global.platform.openWindow({ url }) }, }, @@ -297,6 +301,7 @@ AccountDropdowns.propTypes = { actions: PropTypes.objectOf(PropTypes.func), network: PropTypes.string, style: PropTypes.object, + settings: PropTypes.object, enableAccountOptions: PropTypes.bool, enableAccountsSelector: PropTypes.bool, t: PropTypes.func, @@ -315,10 +320,16 @@ const mapDispatchToProps = (dispatch) => { } } +function mapStateToProps (state) { + return { + settings: state.metamask.settings, + } +} + AccountDropdowns.contextTypes = { t: PropTypes.func, } module.exports = { - AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns), + AccountDropdowns: connect(mapStateToProps, mapDispatchToProps)(AccountDropdowns), } diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index bcada41e3887..3f1e9c9871a0 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -40,6 +40,7 @@ function mapStateToProps (state) { selectedAddress: state.metamask.selectedAddress, isAccountMenuOpen: state.metamask.isAccountMenuOpen, keyrings: state.metamask.keyrings, + ticker: state.metamask.ticker, identities: state.metamask.identities, accounts: state.metamask.accounts, } @@ -152,6 +153,7 @@ AccountMenu.prototype.renderAccounts = function () { identities, accounts, selectedAddress, + ticker, keyrings, showAccountDetail, } = this.props @@ -163,8 +165,11 @@ AccountMenu.prototype.renderAccounts = function () { const isSelected = identity.address === selectedAddress const balanceValue = accounts[address] ? accounts[address].balance : '' - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() + let formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + if (ticker !== 'ETH') { + formattedBalance = formattedBalance.replace(/ETH/, ticker) + } const keyring = keyrings.find((kr) => { return kr.accounts.includes(simpleAddress) || diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index e31552f2d8f5..6472ea58cac1 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -20,6 +20,7 @@ function mapStateToProps (state) { return { account, network, + ticker: state.metamask.ticker, conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, } @@ -61,11 +62,15 @@ BalanceComponent.prototype.renderTokenBalance = function () { BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { shorten, account } = props + const { shorten, account, ticker } = props const balanceValue = account && account.balance const needsParse = 'needsParse' in props ? props.needsParse : true - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' const showFiat = 'showFiat' in props ? props.showFiat : true + let formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' + + if (ticker !== 'ETH') { + formattedBalance = formattedBalance.replace(/ETH/, ticker) + } if (formattedBalance === 'None' || formattedBalance === '...') { return h('div.flex-column.balance-display', {}, [ diff --git a/ui/app/components/buy-button-subview.js b/ui/app/components/buy-button-subview.js index c6957d2aaa3e..b1a2ad0b94f1 100644 --- a/ui/app/components/buy-button-subview.js +++ b/ui/app/components/buy-button-subview.js @@ -27,6 +27,7 @@ function mapStateToProps (state) { network: state.metamask.network, provider: state.metamask.provider, context: state.appState.currentView.context, + ticker: state.metamask.ticker, isSubLoading: state.appState.isSubLoading, } } @@ -258,10 +259,11 @@ BuyButtonSubview.prototype.backButtonContext = function () { } BuyButtonSubview.prototype.radioHandler = function (event) { + const ticker = this.props.ticker switch (event.target.title) { case 'Coinbase': - return this.props.dispatch(actions.coinBaseSubview()) + return this.props.dispatch(actions.coinBaseSubview(ticker)) case 'ShapeShift': - return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type)) + return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type, ticker)) } } diff --git a/ui/app/components/dropdowns/components/account-dropdowns.js b/ui/app/components/dropdowns/components/account-dropdowns.js index 179b6617f96f..8c8a53d67b91 100644 --- a/ui/app/components/dropdowns/components/account-dropdowns.js +++ b/ui/app/components/dropdowns/components/account-dropdowns.js @@ -26,15 +26,18 @@ class AccountDropdowns extends Component { } renderAccounts () { - const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props + const { identities, accounts, selected, menuItemStyles, actions, keyrings, ticker } = this.props return Object.keys(identities).map((key, index) => { const identity = identities[key] const isSelected = identity.address === selected const balanceValue = accounts[key].balance - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' const simpleAddress = identity.address.substring(2).toLowerCase() + let formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...' + if (ticker !== 'ETH') { + formattedBalance = formattedBalance.replace(/ETH/, ticker) + } const keyring = keyrings.find((kr) => { return kr.accounts.includes(simpleAddress) || @@ -253,6 +256,11 @@ class AccountDropdowns extends Component { padding: '8px', } + let link + if (this.props.settings && this.props.settings.blockExplorerAddr) { + link = this.props.settings.blockExplorerAddr + } + return h( Dropdown, { @@ -295,7 +303,7 @@ class AccountDropdowns extends Component { closeMenu: () => {}, onClick: () => { const { selected, network } = this.props - const url = genAccountLink(selected, network) + const url = genAccountLink(selected, network, link) global.platform.openWindow({ url }) }, style: Object.assign( @@ -421,6 +429,8 @@ AccountDropdowns.propTypes = { network: PropTypes.number, // actions.showExportPrivateKeyModal: , style: PropTypes.object, + settings: PropTypes.object, + ticker: PropTypes.string, enableAccountsSelector: PropTypes.bool, enableAccountOption: PropTypes.bool, enableAccountOptions: PropTypes.bool, @@ -458,8 +468,10 @@ const mapDispatchToProps = (dispatch) => { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, keyrings: state.metamask.keyrings, sidebarOpen: state.appState.sidebarOpen, + settings: state.metamask.settings, } } diff --git a/ui/app/components/dropdowns/network-dropdown.js b/ui/app/components/dropdowns/network-dropdown.js index 63a30dd82f7d..7bef435874a0 100644 --- a/ui/app/components/dropdowns/network-dropdown.js +++ b/ui/app/components/dropdowns/network-dropdown.js @@ -24,8 +24,9 @@ const notToggleElementClassnames = [ function mapStateToProps (state) { return { provider: state.metamask.provider, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, + network: state.metamask.network, } } @@ -40,8 +41,8 @@ function mapDispatchToProps (dispatch) { setDefaultRpcTarget: type => { dispatch(actions.setDefaultRpcTarget(type)) }, - setRpcTarget: (target) => { - dispatch(actions.setRpcTarget(target)) + setRpcTarget: (target, network) => { + dispatch(actions.setRpcTarget(target, network)) }, showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()), hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()), @@ -68,7 +69,7 @@ module.exports = compose( NetworkDropdown.prototype.render = function () { const props = this.props const { provider: { type: providerType, rpcTarget: activeNetwork } } = props - const rpcList = props.frequentRpcList + const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { fontSize: '16px', @@ -222,7 +223,7 @@ NetworkDropdown.prototype.render = function () { ), this.renderCustomOption(props.provider), - this.renderCommonRpc(rpcList, props.provider), + this.renderCommonRpc(rpcListDetail, props.provider), h( DropdownMenuItem, @@ -273,19 +274,22 @@ NetworkDropdown.prototype.getNetworkName = function () { NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { const props = this.props const reversedRpcList = rpcList.slice().reverse() + const network = props.network - return reversedRpcList.map((rpc) => { + return reversedRpcList.map((entry) => { + const rpc = entry.rpcUrl const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget if ((rpc === 'http://localhost:8545') || currentRpcTarget) { return null } else { + const chainId = entry.chainId || network return h( DropdownMenuItem, { key: `common${rpc}`, closeMenu: () => this.props.hideNetworkDropdown(), - onClick: () => props.setRpcTarget(rpc), + onClick: () => props.setRpcTarget(rpc, chainId), style: { fontSize: '16px', lineHeight: '20px', @@ -309,6 +313,7 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) { NetworkDropdown.prototype.renderCustomOption = function (provider) { const { rpcTarget, type } = provider const props = this.props + const network = props.network if (type !== 'rpc') return null @@ -322,7 +327,7 @@ NetworkDropdown.prototype.renderCustomOption = function (provider) { DropdownMenuItem, { key: rpcTarget, - onClick: () => props.setRpcTarget(rpcTarget), + onClick: () => props.setRpcTarget(rpcTarget, network), closeMenu: () => this.props.hideNetworkDropdown(), style: { fontSize: '16px', diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index 5a794c7c186e..4ffab823d2ec 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -4,7 +4,7 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') -const genAccountLink = require('etherscan-link').createAccountLink +const genAccountLink = require('../../../lib/account-link.js') const copyToClipboard = require('copy-to-clipboard') const { Menu, Item, CloseArea } = require('./components/menu') @@ -17,6 +17,7 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown) function mapStateToProps (state) { return { network: state.metamask.network, + settings: state.metamask.settings, } } @@ -67,7 +68,11 @@ TokenMenuDropdown.prototype.render = function () { h(Item, { onClick: (e) => { e.stopPropagation() - const url = genAccountLink(this.props.token.address, this.props.network) + let url + if (this.props.settings && this.props.settings.blockExplorerAddr) { + url = this.props.settings.blockExplorerAddr + } + url = genAccountLink(this.props.token.address, this.props.network, url) global.platform.openWindow({ url }) this.props.onClose() }, diff --git a/ui/app/components/eth-balance.js b/ui/app/components/eth-balance.js index c3d084bdcb22..dcf865535c1a 100644 --- a/ui/app/components/eth-balance.js +++ b/ui/app/components/eth-balance.js @@ -1,5 +1,6 @@ const { Component } = require('react') const h = require('react-hyperscript') +const connect = require('react-redux').connect const { inherits } = require('util') const { formatBalance, @@ -8,7 +9,12 @@ const { const Tooltip = require('./tooltip.js') const FiatValue = require('./fiat-value.js') -module.exports = EthBalanceComponent +module.exports = connect(mapStateToProps)(EthBalanceComponent) +function mapStateToProps (state) { + return { + ticker: state.metamask.ticker, + } +} inherits(EthBalanceComponent, Component) function EthBalanceComponent () { @@ -17,9 +23,12 @@ function EthBalanceComponent () { EthBalanceComponent.prototype.render = function () { const props = this.props - const { value, style, width, needsParse = true } = props + const { ticker, value, style, width, needsParse = true } = props - const formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + let formattedValue = value ? formatBalance(value, 6, needsParse) : '...' + if (ticker !== 'ETH') { + formattedValue = formattedValue.replace(/ETH/, ticker) + } return ( diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 4240487451ed..bfbeb110942e 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -20,15 +20,22 @@ function IdenticonComponent () { function mapStateToProps (state) { return { + ticker: state.metamask.ticker, useBlockie: state.metamask.useBlockie, } } IdenticonComponent.prototype.render = function () { var props = this.props - const { className = '', address } = props + const { className = '', address, ticker } = props var diameter = props.diameter || this.defaultDiameter + // default logo + var logo = './images/eth_logo.svg' + if (ticker && ticker !== 'ETH') { + logo = `./images/${ticker.toLowerCase()}_logo.svg` + } + return address ? ( h('div', { @@ -48,7 +55,7 @@ IdenticonComponent.prototype.render = function () { ) : ( h('img.balance-icon', { - src: './images/eth_logo.svg', + src: logo, style: { height: diameter, width: diameter, diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js index bc577fda05de..1f56f89e5066 100644 --- a/ui/app/components/modals/account-details-modal.js +++ b/ui/app/components/modals/account-details-modal.js @@ -15,6 +15,7 @@ function mapStateToProps (state) { network: state.metamask.network, selectedIdentity: getSelectedIdentity(state), keyrings: state.metamask.keyrings, + settings: state.metamask.settings, } } @@ -65,6 +66,11 @@ AccountDetailsModal.prototype.render = function () { exportPrivateKeyFeatureEnabled = false } + let link + if (this.props.settings && this.props.settings.blockExplorerAddr) { + link = this.props.settings.blockExplorerAddr + } + return h(AccountModalContainer, {}, [ h(EditableLabel, { className: 'account-modal__name', @@ -81,7 +87,7 @@ AccountDetailsModal.prototype.render = function () { h('div.account-modal-divider'), h('button.btn-primary.account-modal__button', { - onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }), + onClick: () => global.platform.openWindow({ url: genAccountLink(address, network, link) }), }, this.context.t('etherscanView')), // Holding on redesign for Export Private Key functionality diff --git a/ui/app/components/modals/buy-options-modal.js b/ui/app/components/modals/buy-options-modal.js index c70510b5fcde..a4b8458faadf 100644 --- a/ui/app/components/modals/buy-options-modal.js +++ b/ui/app/components/modals/buy-options-modal.js @@ -10,6 +10,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, + settings: state.metamask.settings, } } @@ -24,7 +25,7 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - toFaucet: network => dispatch(actions.buyEth({ network })), + toFaucet: (network, address, link) => dispatch(actions.buyEth({ network, address, amount: 0, link })), } } @@ -51,9 +52,15 @@ BuyOptions.prototype.renderModalContentOption = function (title, header, onClick BuyOptions.prototype.render = function () { const { network, toCoinbase, address, toFaucet } = this.props - const isTestNetwork = ['3', '4', '42'].find(n => n === network) + let isTestNetwork = ['3', '4', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) + let link + if (this.props.settings.isTestNet) { + isTestNetwork = true + link = this.props.settings.buyUrl + } + return h('div', {}, [ h('div.buy-modal-content.transfers-subview', { }, [ @@ -69,7 +76,7 @@ BuyOptions.prototype.render = function () { h('div.buy-modal-content-options.flex-column.flex-center', {}, [ isTestNetwork - ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network)) + ? this.renderModalContentOption(networkName, this.context.t('testFaucet'), () => toFaucet(network, address, link)) : this.renderModalContentOption('Coinbase', this.context.t('depositFiat'), () => toCoinbase(address)), // h('div.buy-modal-content-option', {}, [ diff --git a/ui/app/components/modals/deposit-ether-modal.js b/ui/app/components/modals/deposit-ether-modal.js index 2daa7fa1d75a..d24a326a5a27 100644 --- a/ui/app/components/modals/deposit-ether-modal.js +++ b/ui/app/components/modals/deposit-ether-modal.js @@ -19,6 +19,7 @@ function mapStateToProps (state) { return { network: state.metamask.network, address: state.metamask.selectedAddress, + settings: state.metamask.settings, } } @@ -36,7 +37,7 @@ function mapDispatchToProps (dispatch) { showAccountDetailModal: () => { dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })) }, - toFaucet: network => dispatch(actions.buyEth({ network })), + toFaucet: (network, address, link) => dispatch(actions.buyEth({ network, address, amount: 0, link })), } } @@ -119,9 +120,25 @@ DepositEtherModal.prototype.renderRow = function ({ DepositEtherModal.prototype.render = function () { const { network, toCoinbase, address, toFaucet } = this.props - const { buyingWithShapeshift } = this.state + let { buyingWithShapeshift } = this.state + let isTestNetwork = ['3', '4', '42'].find(n => n === network) + let noCoinbase = false + let noShapeShift = false + + if (this.props.settings.exchanges) { + if (this.props.settings.exchanges.indexOf('Coinbase') === -1) { + noCoinbase = true + } + if (this.props.settings.exchanges.indexOf('ShapeShift') === -1) { + noShapeShift = true + } + } + let link + if (this.props.settings.isTestNet) { + isTestNetwork = true + link = this.props.settings.buyUrl + } - const isTestNetwork = ['3', '4', '42'].find(n => n === network) const networkName = getNetworkDisplayName(network) return h('div.page-container.page-container--full-width.page-container--full-height', {}, [ @@ -164,7 +181,7 @@ DepositEtherModal.prototype.render = function () { title: FAUCET_ROW_TITLE, text: this.facuetRowText(networkName), buttonLabel: this.context.t('getEther'), - onButtonClick: () => toFaucet(network), + onButtonClick: () => toFaucet(network, address, link), hide: !isTestNetwork || buyingWithShapeshift, }), @@ -179,7 +196,7 @@ DepositEtherModal.prototype.render = function () { text: COINBASE_ROW_TEXT, buttonLabel: this.context.t('continueToCoinbase'), onButtonClick: () => toCoinbase(address), - hide: isTestNetwork || buyingWithShapeshift, + hide: noCoinbase || isTestNetwork || buyingWithShapeshift, }), this.renderRow({ @@ -192,7 +209,7 @@ DepositEtherModal.prototype.render = function () { text: SHAPESHIFT_ROW_TEXT, buttonLabel: this.context.t('shapeshiftBuy'), onButtonClick: () => this.setState({ buyingWithShapeshift: true }), - hide: isTestNetwork, + hide: noShapeShift || isTestNetwork, hideButton: buyingWithShapeshift, hideTitle: buyingWithShapeshift, onBackClick: () => this.setState({ buyingWithShapeshift: false }), diff --git a/ui/app/components/network-display/index.scss b/ui/app/components/network-display/index.scss index 2085cff67cc3..57c5c45d34f5 100644 --- a/ui/app/components/network-display/index.scss +++ b/ui/app/components/network-display/index.scss @@ -23,6 +23,7 @@ &--rinkeby { background-color: lighten($tulip-tree, 35%); } + } &__name { diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index 5e3fdc9af36f..0263924ef231 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -141,7 +141,7 @@ Home.propTypes = { loadingMessage: PropTypes.string, network: PropTypes.string, provider: PropTypes.object, - frequentRpcList: PropTypes.array, + frequentRpcListDetail: PropTypes.array, currentView: PropTypes.object, sidebarOpen: PropTypes.bool, isMascara: PropTypes.bool, @@ -220,7 +220,7 @@ function mapStateToProps (state) { forgottenPassword: state.appState.forgottenPassword, nextUnreadNotice, lostAccounts, - frequentRpcList: state.metamask.frequentRpcList || [], + frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], currentCurrency: state.metamask.currentCurrency, isMouseUser: state.appState.isMouseUser, isRevealingSeedWords: state.metamask.isRevealingSeedWords, diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js index ff42a13dee27..38d8729ac938 100644 --- a/ui/app/components/pages/settings/settings.js +++ b/ui/app/components/pages/settings/settings.js @@ -173,14 +173,23 @@ class Settings extends Component { onChange: event => this.setState({ newRpc: event.target.value }), onKeyPress: event => { if (event.key === 'Enter') { - this.validateRpc(this.state.newRpc) + this.validateRpc(this.state.newRpc, this.state.chainId) + } + }, + }), + h('input.settings__input', { + placeholder: this.context.t('optionalChainId'), + onChange: event => this.setState({ chainId: event.target.value }), + onKeyPress: event => { + if (event.key === 'Enter') { + this.validateRpc(this.state.newRpc, this.state.chainId) } }, }), h('div.settings__rpc-save-button', { onClick: event => { event.preventDefault() - this.validateRpc(this.state.newRpc) + this.validateRpc(this.state.newRpc, this.state.chainId) }, }, this.context.t('save')), ]), @@ -189,11 +198,11 @@ class Settings extends Component { ) } - validateRpc (newRpc) { + validateRpc (newRpc, chainId) { const { setRpcTarget, displayWarning } = this.props if (validUrl.isWebUri(newRpc)) { - setRpcTarget(newRpc) + setRpcTarget(newRpc, chainId) } else { const appendedRpc = `http://${newRpc}` @@ -341,7 +350,7 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)), - setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)), + setRpcTarget: (newRpc, chainId) => dispatch(actions.setRpcTarget(newRpc, chainId)), displayWarning: warning => dispatch(actions.displayWarning(warning)), revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()), setUseBlockie: value => dispatch(actions.setUseBlockie(value)), diff --git a/ui/app/components/send/currency-display/currency-display.js b/ui/app/components/send/currency-display/currency-display.js index 2b8eaa41f70f..b0e75cd754f1 100644 --- a/ui/app/components/send/currency-display/currency-display.js +++ b/ui/app/components/send/currency-display/currency-display.js @@ -1,5 +1,6 @@ const Component = require('react').Component const h = require('react-hyperscript') +const connect = require('react-redux').connect const inherits = require('util').inherits const { conversionUtil, multiplyCurrencies } = require('../../../conversion-util') const { removeLeadingZeroes } = require('../send.utils') @@ -12,7 +13,12 @@ CurrencyDisplay.contextTypes = { t: PropTypes.func, } -module.exports = CurrencyDisplay +module.exports = connect(mapStateToProps)(CurrencyDisplay) +function mapStateToProps (state) { + return { + fromCurrency: state.metamask.fromCurrency, + } +} inherits(CurrencyDisplay, Component) function CurrencyDisplay () { @@ -78,7 +84,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi } CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValue) { - const { primaryCurrency, convertedCurrency, conversionRate } = this.props + const { fromCurrency, primaryCurrency, convertedCurrency, conversionRate } = this.props if (conversionRate === 0 || conversionRate === null || conversionRate === undefined) { if (nonFormattedValue !== 0) { @@ -88,7 +94,7 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu let convertedValue = conversionUtil(nonFormattedValue, { fromNumericBase: 'dec', - fromCurrency: primaryCurrency, + fromCurrency: fromCurrency || primaryCurrency, toCurrency: convertedCurrency, numberOfDecimals: 2, conversionRate, @@ -132,6 +138,7 @@ CurrencyDisplay.prototype.render = function () { const { className = 'currency-display', primaryBalanceClassName = 'currency-display__input', + fromCurrency, primaryCurrency, readOnly = false, inError = false, @@ -174,7 +181,7 @@ CurrencyDisplay.prototype.render = function () { step, }), - h('span.currency-display__currency-symbol', primaryCurrency), + h('span.currency-display__currency-symbol', fromCurrency || primaryCurrency), ]), diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 2c4ba40bf576..b9cb92afd8c0 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -16,19 +16,23 @@ function mapStateToProps (state) { selectedAddress, } = state.metamask const { warning } = state.appState + const ticker = state.metamask.ticker + const provider = state.metamask.provider return { coinOptions, tokenExchangeRates, selectedAddress, + provider, + ticker, warning, } } function mapDispatchToProps (dispatch) { return { - shapeShiftSubview: () => dispatch(shapeShiftSubview()), - pairUpdate: coin => dispatch(pairUpdate(coin)), + shapeShiftSubview: (type, ticker) => dispatch(shapeShiftSubview(type, ticker)), + pairUpdate: (coin, ticker) => dispatch(pairUpdate(coin, ticker)), buyWithShapeShift: data => dispatch(buyWithShapeShift(data)), } } @@ -56,22 +60,27 @@ function ShapeshiftForm () { } ShapeshiftForm.prototype.getCoinPair = function () { - return `${this.state.depositCoin.toUpperCase()}_ETH` + const ticker = this.props.ticker + return `${this.state.depositCoin.toUpperCase()}_${ticker}` } ShapeshiftForm.prototype.componentWillMount = function () { - this.props.shapeShiftSubview() + const ticker = this.props.ticker + const type = this.props.provider.type + this.props.shapeShiftSubview(type, ticker) } ShapeshiftForm.prototype.onCoinChange = function (coin) { + const ticker = this.props.ticker this.setState({ depositCoin: coin, errorMessage: '', }) - this.props.pairUpdate(coin) + this.props.pairUpdate(coin, ticker) } ShapeshiftForm.prototype.onBuyWithShapeShift = function () { + const ticker = this.props.ticker this.setState({ isLoading: true, showQrCode: true, @@ -85,7 +94,7 @@ ShapeshiftForm.prototype.onBuyWithShapeShift = function () { refundAddress: returnAddress, depositCoin, } = this.state - const pair = `${depositCoin}_eth` + const pair = `${depositCoin}_${ticker.toLowerCase()}` const data = { withdrawal, pair, @@ -175,7 +184,7 @@ ShapeshiftForm.prototype.renderQrCode = function () { ShapeshiftForm.prototype.render = function () { const { coinOptions, btnClass, warning } = this.props const { errorMessage, showQrCode, depositAddress } = this.state - const { tokenExchangeRates } = this.props + const { tokenExchangeRates, ticker } = this.props const token = tokenExchangeRates[this.getCoinPair()] return h('div.shapeshift-form-wrapper', [ @@ -209,7 +218,7 @@ ShapeshiftForm.prototype.render = function () { this.context.t('receive'), ]), - h('div.shapeshift-form__selector-input', ['ETH']), + h('div.shapeshift-form__selector-input', [ticker]), ]), diff --git a/ui/app/components/shift-list-item.js b/ui/app/components/shift-list-item.js index 4334aacba56a..5071e6395c7b 100644 --- a/ui/app/components/shift-list-item.js +++ b/ui/app/components/shift-list-item.js @@ -4,7 +4,7 @@ const PropTypes = require('prop-types') const h = require('react-hyperscript') const connect = require('react-redux').connect const vreme = new (require('vreme'))() -const explorerLink = require('etherscan-link').createExplorerLink +const explorerLink = require('../../lib/explorer-link.js') const actions = require('../actions') const addressSummary = require('../util').addressSummary @@ -25,6 +25,7 @@ function mapStateToProps (state) { selectedAddress: state.metamask.selectedAddress, conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, + ticker: state.metamask.ticker, } } @@ -84,7 +85,7 @@ ShiftListItem.prototype.renderUtilComponents = function () { title: this.context.t('qrCode'), }, [ h('i.fa.fa-qrcode.pointer.pop-hover', { - onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType)), + onClick: () => props.dispatch(actions.reshowQrCode(props.depositAddress, props.depositType, props.ticker)), style: { margin: '5px', marginLeft: '23px', diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index 4100d76a59eb..af11f35cbbc1 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -19,6 +19,7 @@ function mapStateToProps (state) { contractExchangeRates: state.metamask.contractExchangeRates, conversionRate: state.metamask.conversionRate, sidebarOpen: state.appState.sidebarOpen, + settings: state.metamask.settings, } } @@ -143,7 +144,11 @@ TokenCell.prototype.send = function (address, event) { } TokenCell.prototype.view = function (address, userAddress, network, event) { - const url = etherscanLinkFor(address, userAddress, network) + let url + if (this.props.settings && this.props.settings.blockExplorerToken) { + url = this.props.settings.blockExplorerToken + } + url = etherscanLinkFor(address, userAddress, network, url) if (url) { navigateTo(url) } @@ -153,7 +158,11 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (tokenAddress, address, network) { +function etherscanLinkFor (tokenAddress, address, network, url) { + if (url) { + return url.replace('[[tokenAddress]]', tokenAddress).replace('[[address]]', address) + } + const prefix = prefixForNetwork(network) return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` } diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 474d62638053..46eb9ef889f3 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -32,6 +32,7 @@ module.exports = compose( function mapStateToProps (state) { return { tokens: state.metamask.tokens, + ticker: state.metamask.ticker, currentCurrency: getCurrentCurrency(state), contractExchangeRates: state.metamask.contractExchangeRates, selectedAddressTxList: state.metamask.selectedAddressTxList, @@ -110,6 +111,7 @@ TxListItem.prototype.getSendEtherTotal = function () { const { transactionAmount, conversionRate, + ticker, address, currentCurrency, } = this.props @@ -121,7 +123,7 @@ TxListItem.prototype.getSendEtherTotal = function () { const totalInFiat = conversionUtil(transactionAmount, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: 'ETH', + fromCurrency: ticker, toCurrency: currentCurrency, fromDenomination: 'WEI', numberOfDecimals: 2, @@ -130,15 +132,15 @@ TxListItem.prototype.getSendEtherTotal = function () { const totalInETH = conversionUtil(transactionAmount, { fromNumericBase: 'hex', toNumericBase: 'dec', - fromCurrency: 'ETH', - toCurrency: 'ETH', + fromCurrency: ticker, + toCurrency: ticker, fromDenomination: 'WEI', conversionRate, numberOfDecimals: 6, }) return { - total: `${totalInETH} ETH`, + total: `${totalInETH} ${ticker}`, fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`, } } diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index d8c4a9d19793..51c302641a2a 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -26,6 +26,7 @@ TxList.contextTypes = { function mapStateToProps (state) { return { + settings: state.metamask.settings, txsToRender: selectors.transactionsSelector(state), conversionRate: selectors.conversionRateSelector(state), selectedAddress: selectors.getSelectedAddress(state), @@ -155,7 +156,11 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa } TxList.prototype.view = function (txHash, network) { - const url = etherscanLinkFor(txHash, network) + let url + if (this.props.settings && this.props.settings.blockExplorerTx) { + url = this.props.settings.blockExplorerTx + } + url = etherscanLinkFor(txHash, network, url) if (url) { navigateTo(url) } @@ -165,7 +170,10 @@ function navigateTo (url) { global.platform.openWindow({ url }) } -function etherscanLinkFor (txHash, network) { +function etherscanLinkFor (txHash, network, url) { const prefix = prefixForNetwork(network) + if (url) { + return url.replace('[[txHash]]', txHash) + } return `https://${prefix}etherscan.io/tx/${txHash}` } diff --git a/ui/app/css/itcss/components/settings.scss b/ui/app/css/itcss/components/settings.scss index 0dd61ac5eb41..7a94c1cb648a 100644 --- a/ui/app/css/itcss/components/settings.scss +++ b/ui/app/css/itcss/components/settings.scss @@ -48,7 +48,6 @@ display: flex; flex-direction: column; padding: 0 5px; - height: 71px; @media screen and (max-width: 575px) { height: initial; @@ -78,10 +77,12 @@ } .settings__input { - padding-left: 10px; + padding-left: 15px; font-size: 14px; - height: 40px; + height: 56px; border: 1px solid $alto; + margin-bottom: 3px; + border-radius: 2px; } .settings__input::-webkit-input-placeholder { diff --git a/ui/lib/account-link.js b/ui/lib/account-link.js index 037d990fa223..ad6d38cfbd7d 100644 --- a/ui/lib/account-link.js +++ b/ui/lib/account-link.js @@ -1,4 +1,4 @@ -module.exports = function (address, network) { +module.exports = function (address, network, url) { const net = parseInt(network) let link switch (net) { @@ -18,6 +18,10 @@ module.exports = function (address, network) { link = `https://kovan.etherscan.io/address/${address}` break default: + if (url) { + return url.replace('[[address]]', address) + } + link = '' break } diff --git a/ui/lib/explorer-link.js b/ui/lib/explorer-link.js new file mode 100644 index 000000000000..ce38a02f3bfd --- /dev/null +++ b/ui/lib/explorer-link.js @@ -0,0 +1,10 @@ +const prefixForNetwork = require('./etherscan-prefix-for-network') + +module.exports = function (hash, network, url) { + if (url) { + return url.replace('[[txHash]]', hash) + } + + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/tx/${hash}` +}