mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-21 17:37:01 +01:00
New settings custom rpc form (#6490)
* Add networks tab to settings, with header. * Adds network list to settings network tab. * Adds form to settings networks tab and connects it to network list. * Network tab: form adding and editing working * Settings network form properly handles input errors * Add translations for settings network form * Clean up styles of settings network tab. * Add popup-view styles and behaviour to settings network tab. * Fix save button on settings network form * Adds 'Add Network' button and addMode to settings networks tab * Lint fix for settings networks tab addition * Fix navigation in settings networks tab. * Editing an rpcurl in networks tab does not create new network, just changes rpc of old * Fix layout of settings tabs other than network * Networks dropdown 'Custom Rpc' item links to networks tab in settings. * Update settings sidebar networks subheader. * Make networks tab buttons width consistent with input widths in extension view. * Fix settings screen subheader height in popup view * Fix height of add networks button in popup view * Add optional label to chainId and symbol form labels in networks setting tab * Style fixes for networks tab headers * Add ability to customize block explorer used by custom rpc * Stylistic improvements+fixes to custom rpc form. * Hide cancel button. * Highlight and show network form of provider by default. * Standardize network subheader name to 'Networks' * Update e2e tests for new settings network form * Update unit tests for new rpcPrefs prop * Extract blockexplorer url construction into method. * Fix broken styles on non-network tabs in popup mode * Fix block explorer url links for cases when provider in state has not been updated. * Fix vertical spacing of network form * Don't allow click of save button on network form if nothing has changed * Ensure add network button is shown in popup view * Lint fix for networks tab * Fix block explorer url preference setting. * Fix e2e tests for custom blockexplorer in account details modal changes. * Update integration test states to include frequentRpcList property * Fix some capitalizations in en/messages.json * Remove some console.logs added during custom rpc form work * Fix external account link text and url for modal and dropdown. * Documentation, url validation, proptype required additions and lint fixes on network tab and form.
This commit is contained in:
parent
094e4cf555
commit
13be683701
@ -83,6 +83,9 @@
|
||||
"address": {
|
||||
"message": "Address"
|
||||
},
|
||||
"addNetwork": {
|
||||
"message": "Add Network"
|
||||
},
|
||||
"advanced": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
@ -191,6 +194,13 @@
|
||||
"message": "must be greater than or equal to $1 and less than or equal to $2.",
|
||||
"description": "helper for inputting hex as decimal input"
|
||||
},
|
||||
"blockExplorerUrl": {
|
||||
"message": "Block Explorer"
|
||||
},
|
||||
"blockExplorerView": {
|
||||
"message": "View account at $1",
|
||||
"description": "$1 replaced by URL for custom block explorer"
|
||||
},
|
||||
"blockiesIdenticon": {
|
||||
"message": "Use Blockies Identicon"
|
||||
},
|
||||
@ -230,6 +240,9 @@
|
||||
"ok": {
|
||||
"message": "Ok"
|
||||
},
|
||||
"optionalBlockExplorerUrl": {
|
||||
"message": "Block Explorer URL (optional)"
|
||||
},
|
||||
"cancel": {
|
||||
"message": "Cancel"
|
||||
},
|
||||
@ -245,6 +258,9 @@
|
||||
"cancelN": {
|
||||
"message": "Cancel all $1 transactions"
|
||||
},
|
||||
"chainId": {
|
||||
"message": "Chain ID"
|
||||
},
|
||||
"classicInterface": {
|
||||
"message": "Use classic interface"
|
||||
},
|
||||
@ -502,6 +518,9 @@
|
||||
"edit": {
|
||||
"message": "Edit"
|
||||
},
|
||||
"editNetwork": {
|
||||
"message": "Edit Network"
|
||||
},
|
||||
"editAccountName": {
|
||||
"message": "Edit Account Name"
|
||||
},
|
||||
@ -934,9 +953,15 @@
|
||||
"negativeETH": {
|
||||
"message": "Can not send negative amounts of ETH."
|
||||
},
|
||||
"networkName": {
|
||||
"message": "Network Name"
|
||||
},
|
||||
"networks": {
|
||||
"message": "Networks"
|
||||
},
|
||||
"networkSettingsDescription": {
|
||||
"message": "Add and edit custom RPC networks"
|
||||
},
|
||||
"nevermind": {
|
||||
"message": "Nevermind"
|
||||
},
|
||||
@ -977,7 +1002,7 @@
|
||||
"protectYourKeysMessage2": {
|
||||
"message": "Keep your phrase safe. If you see something fishy, or you’re uncertain about a website, email support@metamask.io"
|
||||
},
|
||||
"rpcURL": {
|
||||
"rpcUrl": {
|
||||
"message": "New RPC URL"
|
||||
},
|
||||
"showAdvancedOptions": {
|
||||
@ -1492,6 +1517,9 @@
|
||||
"supportCenter": {
|
||||
"message": "Visit our Support Center"
|
||||
},
|
||||
"symbol": {
|
||||
"message": "Symbol"
|
||||
},
|
||||
"symbolBetweenZeroTwelve": {
|
||||
"message": "Symbol must be between 0 and 12 characters."
|
||||
},
|
||||
@ -1714,9 +1742,15 @@
|
||||
"viewAccount": {
|
||||
"message": "View Account"
|
||||
},
|
||||
"viewOnCustomBlockExplorer": {
|
||||
"message": "View at $1"
|
||||
},
|
||||
"viewOnEtherscan": {
|
||||
"message": "View on Etherscan"
|
||||
},
|
||||
"viewNetworkInfo": {
|
||||
"message": "View Network Info"
|
||||
},
|
||||
"visitWebSite": {
|
||||
"message": "Visit our web site"
|
||||
},
|
||||
|
@ -129,13 +129,14 @@ module.exports = class NetworkController extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
|
||||
setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) {
|
||||
const providerConfig = {
|
||||
type: 'rpc',
|
||||
rpcTarget,
|
||||
chainId,
|
||||
ticker,
|
||||
nickname,
|
||||
rpcPrefs,
|
||||
}
|
||||
this.providerConfig = providerConfig
|
||||
}
|
||||
|
@ -488,8 +488,8 @@ class PreferencesController {
|
||||
rpcList[index] = updatedRpc
|
||||
this.store.updateState({ frequentRpcListDetail: rpcList })
|
||||
} else {
|
||||
const { rpcUrl, chainId, ticker, nickname } = newRpcDetails
|
||||
return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname)
|
||||
const { rpcUrl, chainId, ticker, nickname, rpcPrefs = {} } = newRpcDetails
|
||||
return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname, rpcPrefs)
|
||||
}
|
||||
return Promise.resolve(rpcList)
|
||||
}
|
||||
@ -503,22 +503,22 @@ class PreferencesController {
|
||||
* @returns {Promise<array>} Promise resolving to updated frequentRpcList.
|
||||
*
|
||||
*/
|
||||
addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') {
|
||||
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') {
|
||||
let checkedChainId
|
||||
if (!!chainId && !Number.isNaN(parseInt(chainId))) {
|
||||
checkedChainId = chainId
|
||||
addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) {
|
||||
const rpcList = this.getFrequentRpcListDetail()
|
||||
const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
|
||||
if (index !== -1) {
|
||||
rpcList.splice(index, 1)
|
||||
}
|
||||
rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname })
|
||||
if (url !== 'http://localhost:8545') {
|
||||
let checkedChainId
|
||||
if (!!chainId && !Number.isNaN(parseInt(chainId))) {
|
||||
checkedChainId = chainId
|
||||
}
|
||||
rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname, rpcPrefs })
|
||||
}
|
||||
this.store.updateState({ frequentRpcListDetail: rpcList })
|
||||
return Promise.resolve(rpcList)
|
||||
}
|
||||
this.store.updateState({ frequentRpcListDetail: rpcList })
|
||||
return Promise.resolve(rpcList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes custom RPC url from state.
|
||||
|
@ -1633,9 +1633,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
* @returns {Promise<String>} - The RPC Target URL confirmed.
|
||||
*/
|
||||
|
||||
async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname) {
|
||||
await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname })
|
||||
this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname)
|
||||
async updateAndSetCustomRpc (rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs) {
|
||||
await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs })
|
||||
this.networkController.setRpcTarget(rpcUrl, chainId, ticker, nickname, rpcPrefs)
|
||||
return rpcUrl
|
||||
}
|
||||
|
||||
@ -1648,15 +1648,15 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
* @param {string} nickname - Optional nickname of the selected network.
|
||||
* @returns {Promise<String>} - The RPC Target URL confirmed.
|
||||
*/
|
||||
async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
|
||||
async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) {
|
||||
const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail()
|
||||
const rpcSettings = frequentRpcListDetail.find((rpc) => rpcTarget === rpc.rpcUrl)
|
||||
|
||||
if (rpcSettings) {
|
||||
this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname)
|
||||
this.networkController.setRpcTarget(rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs)
|
||||
} else {
|
||||
this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname)
|
||||
await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname)
|
||||
this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname, rpcPrefs)
|
||||
await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname, rpcPrefs)
|
||||
}
|
||||
return rpcTarget
|
||||
}
|
||||
|
@ -192,7 +192,8 @@
|
||||
"type": "testnet"
|
||||
},
|
||||
"shapeShiftTxList": [],
|
||||
"lostAccounts": []
|
||||
"lostAccounts": [],
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -134,7 +134,8 @@
|
||||
"useNativeCurrencyAsPrimaryCurrency": true,
|
||||
"showFiatInTestnets": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -157,7 +157,8 @@
|
||||
"preferences": {
|
||||
"useNativeCurrencyAsPrimaryCurrency": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -116,7 +116,8 @@
|
||||
"useNativeCurrencyAsPrimaryCurrency": true,
|
||||
"showFiatInTestnets": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -138,7 +138,8 @@
|
||||
"useNativeCurrencyAsPrimaryCurrency": true,
|
||||
"showFiatInTestnets": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -117,7 +117,8 @@
|
||||
"useNativeCurrencyAsPrimaryCurrency": true,
|
||||
"showFiatInTestnets": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -87,7 +87,8 @@
|
||||
"type": "testnet"
|
||||
},
|
||||
"shapeShiftTxList": [],
|
||||
"lostAccounts": []
|
||||
"lostAccounts": [],
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -1059,7 +1059,8 @@
|
||||
"preferences": {
|
||||
"useNativeCurrencyAsPrimaryCurrency": true
|
||||
},
|
||||
"completedUiMigration": true
|
||||
"completedUiMigration": true,
|
||||
"frequentRpcListDetail": []
|
||||
},
|
||||
"appState": {
|
||||
"menuOpen": false,
|
||||
|
@ -1341,11 +1341,14 @@ describe('MetaMask', function () {
|
||||
await customRpcButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
|
||||
await findElement(driver, By.css('.settings-page__sub-header-text'))
|
||||
|
||||
const customRpcInputs = await findElements(driver, By.css('input[type="text"]'))
|
||||
const customRpcInput = customRpcInputs[1]
|
||||
await customRpcInput.clear()
|
||||
await customRpcInput.sendKeys(customRpcUrl)
|
||||
|
||||
const customRpcSave = await findElement(driver, By.css('.settings-tab__rpc-save-button'))
|
||||
const customRpcSave = await findElement(driver, By.css('.page-container__footer-button'))
|
||||
await customRpcSave.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
|
@ -527,14 +527,14 @@ describe('preferences controller', function () {
|
||||
it('should add custom RPC url to state', function () {
|
||||
preferencesController.addToFrequentRpcList('rpc_url', 1)
|
||||
preferencesController.addToFrequentRpcList('http://localhost:8545', 1)
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }])
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
|
||||
preferencesController.addToFrequentRpcList('rpc_url', 1)
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }])
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
|
||||
})
|
||||
|
||||
it('should remove custom RPC url from state', function () {
|
||||
preferencesController.addToFrequentRpcList('rpc_url', 1)
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '' }])
|
||||
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: 1, ticker: 'ETH', nickname: '', rpcPrefs: {} }])
|
||||
preferencesController.removeFromFrequentRpcList('other_rpc_url')
|
||||
preferencesController.removeFromFrequentRpcList('http://localhost:8545')
|
||||
preferencesController.removeFromFrequentRpcList('rpc_url')
|
||||
|
@ -4,7 +4,7 @@ const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../../store/actions')
|
||||
const { getSelectedIdentity } = require('../../../selectors/selectors')
|
||||
const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors')
|
||||
const genAccountLink = require('../../../../lib/account-link.js')
|
||||
const { Menu, Item, CloseArea } = require('./components/menu')
|
||||
|
||||
@ -20,6 +20,7 @@ function mapStateToProps (state) {
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
network: state.metamask.network,
|
||||
keyrings: state.metamask.keyrings,
|
||||
rpcPrefs: getRpcPrefsForCurrentProvider(state),
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,8 +29,8 @@ function mapDispatchToProps (dispatch) {
|
||||
showAccountDetailModal: () => {
|
||||
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
|
||||
},
|
||||
viewOnEtherscan: (address, network) => {
|
||||
global.platform.openWindow({ url: genAccountLink(address, network) })
|
||||
viewOnEtherscan: (address, network, rpcPrefs) => {
|
||||
global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) })
|
||||
},
|
||||
showRemoveAccountConfirmationModal: (identity) => {
|
||||
return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
|
||||
@ -56,7 +57,9 @@ AccountDetailsDropdown.prototype.render = function () {
|
||||
keyrings,
|
||||
showAccountDetailModal,
|
||||
viewOnEtherscan,
|
||||
showRemoveAccountConfirmationModal } = this.props
|
||||
showRemoveAccountConfirmationModal,
|
||||
rpcPrefs,
|
||||
} = this.props
|
||||
|
||||
const address = selectedIdentity.address
|
||||
|
||||
@ -112,10 +115,12 @@ AccountDetailsDropdown.prototype.render = function () {
|
||||
name: 'Clicked View on Etherscan',
|
||||
},
|
||||
})
|
||||
viewOnEtherscan(address, network)
|
||||
viewOnEtherscan(address, network, rpcPrefs)
|
||||
this.props.onClose()
|
||||
},
|
||||
text: this.context.t('viewOnEtherscan'),
|
||||
text: (rpcPrefs.blockExplorerUrl
|
||||
? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]])
|
||||
: this.context.t('viewOnEtherscan')),
|
||||
icon: h(`img`, { src: 'images/open-etherscan.svg', style: { height: '15px' } }),
|
||||
}),
|
||||
isRemovable ? h(Item, {
|
||||
|
@ -10,7 +10,7 @@ const Dropdown = require('./components/dropdown').Dropdown
|
||||
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
|
||||
const NetworkDropdownIcon = require('./components/network-dropdown-icon')
|
||||
const R = require('ramda')
|
||||
const { ADVANCED_ROUTE } = require('../../../helpers/constants/routes')
|
||||
const { NETWORKS_ROUTE } = require('../../../helpers/constants/routes')
|
||||
|
||||
// classes from nodes of the toggle element.
|
||||
const notToggleElementClassnames = [
|
||||
@ -49,6 +49,7 @@ function mapDispatchToProps (dispatch) {
|
||||
},
|
||||
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
|
||||
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
|
||||
setNetworksTabAddMode: isInAddMode => dispatch(actions.setNetworksTabAddMode(isInAddMode)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +73,7 @@ module.exports = compose(
|
||||
// TODO: specify default props and proptypes
|
||||
NetworkDropdown.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { provider: { type: providerType, rpcTarget: activeNetwork } } = props
|
||||
const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = props
|
||||
const rpcListDetail = props.frequentRpcListDetail
|
||||
const isOpen = this.props.networkDropdownOpen
|
||||
const dropdownMenuItemStyle = {
|
||||
@ -255,7 +256,10 @@ NetworkDropdown.prototype.render = function () {
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => this.props.hideNetworkDropdown(),
|
||||
onClick: () => this.props.history.push(ADVANCED_ROUTE),
|
||||
onClick: () => {
|
||||
setNetworksTabAddMode(true)
|
||||
this.props.history.push(NETWORKS_ROUTE)
|
||||
},
|
||||
style: dropdownMenuItemStyle,
|
||||
},
|
||||
[
|
||||
|
@ -5,7 +5,7 @@ const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../../store/actions')
|
||||
const AccountModalContainer = require('./account-modal-container')
|
||||
const { getSelectedIdentity } = require('../../../selectors/selectors')
|
||||
const { getSelectedIdentity, getRpcPrefsForCurrentProvider } = require('../../../selectors/selectors')
|
||||
const genAccountLink = require('../../../../lib/account-link.js')
|
||||
const QrView = require('../../ui/qr-code')
|
||||
const EditableLabel = require('../../ui/editable-label')
|
||||
@ -17,6 +17,7 @@ function mapStateToProps (state) {
|
||||
network: state.metamask.network,
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
keyrings: state.metamask.keyrings,
|
||||
rpcPrefs: getRpcPrefsForCurrentProvider(state),
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +55,7 @@ AccountDetailsModal.prototype.render = function () {
|
||||
showExportPrivateKeyModal,
|
||||
setAccountLabel,
|
||||
keyrings,
|
||||
rpcPrefs,
|
||||
} = this.props
|
||||
const { name, address } = selectedIdentity
|
||||
|
||||
@ -86,8 +88,12 @@ AccountDetailsModal.prototype.render = function () {
|
||||
h(Button, {
|
||||
type: 'secondary',
|
||||
className: 'account-modal__button',
|
||||
onClick: () => global.platform.openWindow({ url: genAccountLink(address, network) }),
|
||||
}, this.context.t('etherscanView')),
|
||||
onClick: () => {
|
||||
global.platform.openWindow({ url: genAccountLink(address, network, rpcPrefs) })
|
||||
},
|
||||
}, (rpcPrefs.blockExplorerUrl
|
||||
? this.context.t('blockExplorerView', [rpcPrefs.blockExplorerUrl.match(/^https?:\/\/(.+)/)[1]])
|
||||
: this.context.t('viewOnEtherscan'))),
|
||||
|
||||
// Holding on redesign for Export Private Key functionality
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import {
|
||||
getBlockExplorerUrlForTx,
|
||||
} from '../../../helpers/utils/transactions.util'
|
||||
import SenderToRecipient from '../../ui/sender-to-recipient'
|
||||
import { FLAT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'
|
||||
import TransactionActivityLog from '../transaction-activity-log'
|
||||
import TransactionBreakdown from '../transaction-breakdown'
|
||||
import Button from '../../ui/button'
|
||||
import Tooltip from '../../ui/tooltip'
|
||||
import prefixForNetwork from '../../../../lib/etherscan-prefix-for-network'
|
||||
|
||||
export default class TransactionListItemDetails extends PureComponent {
|
||||
static contextTypes = {
|
||||
@ -22,6 +24,7 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
showRetry: PropTypes.bool,
|
||||
cancelDisabled: PropTypes.bool,
|
||||
transactionGroup: PropTypes.object,
|
||||
rpcPrefs: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -30,12 +33,9 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
}
|
||||
|
||||
handleEtherscanClick = () => {
|
||||
const { transactionGroup: { primaryTransaction } } = this.props
|
||||
const { transactionGroup: { primaryTransaction }, rpcPrefs } = this.props
|
||||
const { hash, metamaskNetworkId } = primaryTransaction
|
||||
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
|
||||
this.context.metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Navigation',
|
||||
@ -44,7 +44,7 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
},
|
||||
})
|
||||
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
global.platform.openWindow({ url: getBlockExplorerUrlForTx(metamaskNetworkId, hash, rpcPrefs) })
|
||||
}
|
||||
|
||||
handleCancel = event => {
|
||||
@ -125,6 +125,7 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
showRetry,
|
||||
onCancel,
|
||||
onRetry,
|
||||
rpcPrefs: { blockExplorerUrl } = {},
|
||||
} = this.props
|
||||
const { primaryTransaction: transaction } = transactionGroup
|
||||
const { txParams: { to, from } = {} } = transaction
|
||||
@ -158,7 +159,7 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('viewOnEtherscan')}>
|
||||
<Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}>
|
||||
<Button
|
||||
type="raised"
|
||||
onClick={this.handleEtherscanClick}
|
||||
|
@ -33,6 +33,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
value: PropTypes.string,
|
||||
fetchBasicGasAndTimeEstimates: PropTypes.func,
|
||||
fetchGasEstimates: PropTypes.func,
|
||||
rpcPrefs: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@ -161,6 +162,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
showRetry,
|
||||
tokenData,
|
||||
transactionGroup,
|
||||
rpcPrefs,
|
||||
} = this.props
|
||||
const { txParams = {} } = transaction
|
||||
const { showTransactionDetails } = this.state
|
||||
@ -216,6 +218,7 @@ export default class TransactionListItem extends PureComponent {
|
||||
onCancel={this.handleCancel}
|
||||
showCancel={showCancel}
|
||||
cancelDisabled={!hasEnoughCancelGas}
|
||||
rpcPrefs={rpcPrefs}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -18,12 +18,14 @@ import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSe
|
||||
import { isBalanceSufficient } from '../../../pages/send/send.utils'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { metamask: { knownMethodData, accounts } } = state
|
||||
const { metamask: { knownMethodData, accounts, provider, frequentRpcListDetail } } = state
|
||||
const { showFiatInTestnets } = preferencesSelector(state)
|
||||
const isMainnet = getIsMainnet(state)
|
||||
const { transactionGroup: { primaryTransaction } = {} } = ownProps
|
||||
const { txParams: { gas: gasLimit, gasPrice } = {} } = primaryTransaction
|
||||
const selectedAccountBalance = accounts[getSelectedAddress(state)].balance
|
||||
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
|
||||
const { rpcPrefs } = selectRpcInfo || {}
|
||||
|
||||
const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({
|
||||
amount: '0x0',
|
||||
@ -40,6 +42,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
showFiat: (isMainnet || !!showFiatInTestnets),
|
||||
selectedAccountBalance,
|
||||
hasEnoughCancelGas,
|
||||
rpcPrefs,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,11 +41,11 @@ const styles = {
|
||||
inputFocused: {},
|
||||
inputRoot: {
|
||||
'label + &': {
|
||||
marginTop: '8px',
|
||||
marginTop: '9px',
|
||||
},
|
||||
border: '1px solid #d2d8dd',
|
||||
border: '2px solid #BBC0C5',
|
||||
height: '48px',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '6px',
|
||||
padding: '0 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
@ -77,6 +77,8 @@ function reduceApp (state, action) {
|
||||
ledger: `m/44'/60'/0'/0/0`,
|
||||
},
|
||||
lastSelectedProvider: null,
|
||||
networksTabSelectedRpcUrl: '',
|
||||
networksTabIsInAddMode: false,
|
||||
}, state.appState)
|
||||
|
||||
switch (action.type) {
|
||||
@ -751,6 +753,16 @@ function reduceApp (state, action) {
|
||||
lastSelectedProvider: action.value,
|
||||
})
|
||||
|
||||
case actions.SET_SELECTED_SETTINGS_RPC_URL:
|
||||
return extend(appState, {
|
||||
networksTabSelectedRpcUrl: action.value,
|
||||
})
|
||||
|
||||
case actions.SET_NETWORKS_TAB_ADD_MODE:
|
||||
return extend(appState, {
|
||||
networksTabIsInAddMode: action.value,
|
||||
})
|
||||
|
||||
default:
|
||||
return appState
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ const ADVANCED_ROUTE = '/settings/advanced'
|
||||
const SECURITY_ROUTE = '/settings/security'
|
||||
const COMPANY_ROUTE = '/settings/company'
|
||||
const ABOUT_US_ROUTE = '/settings/about-us'
|
||||
const NETWORKS_ROUTE = '/settings/networks'
|
||||
const REVEAL_SEED_ROUTE = '/seed'
|
||||
const MOBILE_SYNC_ROUTE = '/mobile-sync'
|
||||
const CONFIRM_SEED_ROUTE = '/confirm-seed'
|
||||
@ -86,4 +87,5 @@ module.exports = {
|
||||
COMPANY_ROUTE,
|
||||
GENERAL_ROUTE,
|
||||
ABOUT_US_ROUTE,
|
||||
NETWORKS_ROUTE,
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import {
|
||||
TRANSACTION_TYPE_CANCEL,
|
||||
TRANSACTION_STATUS_CONFIRMED,
|
||||
} from '../../../../app/scripts/controllers/transactions/enums'
|
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
|
||||
|
||||
|
||||
import {
|
||||
TOKEN_METHOD_TRANSFER,
|
||||
@ -188,3 +190,17 @@ export function getStatusKey (transaction) {
|
||||
|
||||
return transaction.status
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an external block explorer URL at which a transaction can be viewed.
|
||||
* @param {number} networkId
|
||||
* @param {string} hash
|
||||
* @param {Object} rpcPrefs
|
||||
*/
|
||||
export function getBlockExplorerUrlForTx (networkId, hash, rpcPrefs = {}) {
|
||||
if (rpcPrefs.blockExplorerUrl) {
|
||||
return `${rpcPrefs.blockExplorerUrl}/tx/${hash}`
|
||||
}
|
||||
const prefix = prefixForNetwork(networkId)
|
||||
return `https://${prefix}etherscan.io/tx/${hash}`
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
@import 'info-tab/index';
|
||||
|
||||
@import 'networks-tab/index';
|
||||
|
||||
@import 'settings-tab/index';
|
||||
|
||||
.settings-page {
|
||||
@ -13,7 +15,6 @@
|
||||
flex-flow: row nowrap;
|
||||
padding: 12px 24px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $alto;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&__title {
|
||||
@ -33,6 +34,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__sub-header {
|
||||
height: 72px;
|
||||
border-bottom: 1px solid #D8D8D8;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
height: 69px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub-header-text {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
color: black;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
display: none;
|
||||
|
||||
@ -60,8 +89,9 @@
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
border-top: 1px solid #D8D8D8;
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
@ -93,7 +123,7 @@
|
||||
|
||||
&__body {
|
||||
padding: 12px 24px;
|
||||
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
1
ui/app/pages/settings/networks-tab/index.js
Normal file
1
ui/app/pages/settings/networks-tab/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './networks-tab.container'
|
200
ui/app/pages/settings/networks-tab/index.scss
Normal file
200
ui/app/pages/settings/networks-tab/index.scss
Normal file
@ -0,0 +1,200 @@
|
||||
.networks-tab {
|
||||
&__content {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-width: 739px;
|
||||
justify-content: space-between;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 12px 24px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: block;
|
||||
background-image: url('/images/caret-left-black.svg');
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: .5;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__network-form {
|
||||
flex: 0.5 0 auto;
|
||||
max-width: 343px;
|
||||
max-height: 465px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.page-container__footer {
|
||||
border-top: none;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
width: 93%;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__network-form-row {
|
||||
@media screen and (max-width: 575px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 93%;
|
||||
}
|
||||
}
|
||||
|
||||
&__network-form-label {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
&__networks-list {
|
||||
flex: 0.5 0 auto;
|
||||
max-width: 343px;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
max-width: 100vw;
|
||||
width: 100vw;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-network-button-wrapper {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: flex;
|
||||
padding-top: 19px;
|
||||
padding-bottom: 23px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid #D8D8D8;
|
||||
|
||||
.button {
|
||||
width: 178px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__add-network-header-button-wrapper {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 21px;
|
||||
justify-content: center;
|
||||
|
||||
.button {
|
||||
width: 178px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list--selection {
|
||||
@media screen and (max-width: 575px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-item {
|
||||
display: flex;
|
||||
padding: 13px 0px 13px 17px;
|
||||
position: relative;
|
||||
|
||||
.menu-icon-circle {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
padding: 20px 23px 21px 17px;
|
||||
border-bottom: 1px solid #D8D8D8;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-item:last-of-type {
|
||||
@media screen and (max-width: 575px) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-name {
|
||||
margin-left: 11px;
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
line-height: 23px;
|
||||
color: #6A737D;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-arrow {
|
||||
display: none;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: block;
|
||||
background-image: url('/images/caret-right.svg');
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__networks-list-name--selected {
|
||||
font-weight: bold;
|
||||
color: #000000;
|
||||
}
|
||||
}
|
1
ui/app/pages/settings/networks-tab/network-form/index.js
Normal file
1
ui/app/pages/settings/networks-tab/network-form/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './network-form.component'
|
@ -0,0 +1,225 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import validUrl from 'valid-url'
|
||||
import PageContainerFooter from '../../../../components/ui/page-container/page-container-footer'
|
||||
import TextField from '../../../../components/ui/text-field'
|
||||
|
||||
export default class NetworksTab extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
metricsEvent: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
editRpc: PropTypes.func.isRequired,
|
||||
rpcUrl: PropTypes.string,
|
||||
chainId: PropTypes.string,
|
||||
ticker: PropTypes.string,
|
||||
viewOnly: PropTypes.bool,
|
||||
networkName: PropTypes.string,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
setRpcTarget: PropTypes.func.isRequired,
|
||||
networksTabIsInAddMode: PropTypes.bool,
|
||||
blockExplorerUrl: PropTypes.string,
|
||||
rpcPrefs: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
rpcUrl: this.props.rpcUrl,
|
||||
chainId: this.props.chainId,
|
||||
ticker: this.props.ticker,
|
||||
networkName: this.props.networkName,
|
||||
blockExplorerUrl: this.props.blockExplorerUrl,
|
||||
errors: {},
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { rpcUrl: prevRpcUrl, networksTabIsInAddMode: prevAddMode } = prevProps
|
||||
const {
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
networkName,
|
||||
networksTabIsInAddMode,
|
||||
blockExplorerUrl,
|
||||
} = this.props
|
||||
|
||||
if (!prevAddMode && networksTabIsInAddMode) {
|
||||
this.setState({
|
||||
rpcUrl: '',
|
||||
chainId: '',
|
||||
ticker: '',
|
||||
networkName: '',
|
||||
blockExplorerUrl: '',
|
||||
errors: {},
|
||||
})
|
||||
} else if (prevRpcUrl !== rpcUrl) {
|
||||
this.setState({ rpcUrl, chainId, ticker, networkName, blockExplorerUrl, errors: {} })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.onClear()
|
||||
this.setState({
|
||||
rpcUrl: '',
|
||||
chainId: '',
|
||||
ticker: '',
|
||||
networkName: '',
|
||||
blockExplorerUrl: '',
|
||||
errors: {},
|
||||
})
|
||||
}
|
||||
|
||||
stateIsUnchanged () {
|
||||
const {
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
networkName,
|
||||
blockExplorerUrl,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
rpcUrl: stateRpcUrl,
|
||||
chainId: stateChainId,
|
||||
ticker: stateTicker,
|
||||
networkName: stateNetworkName,
|
||||
blockExplorerUrl: stateBlockExplorerUrl,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
stateRpcUrl === rpcUrl &&
|
||||
stateChainId === chainId &&
|
||||
stateTicker === ticker &&
|
||||
stateNetworkName === networkName &&
|
||||
stateBlockExplorerUrl === blockExplorerUrl
|
||||
)
|
||||
}
|
||||
|
||||
renderFormTextField (fieldKey, textFieldId, onChange, value, optionalTextFieldKey) {
|
||||
const { errors } = this.state
|
||||
const { viewOnly } = this.props
|
||||
|
||||
return (
|
||||
<div className="networks-tab__network-form-row">
|
||||
<div className="networks-tab__network-form-label">{this.context.t(optionalTextFieldKey || fieldKey)}</div>
|
||||
<TextField
|
||||
type="text"
|
||||
id={textFieldId}
|
||||
onChange={onChange}
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={value}
|
||||
disabled={viewOnly}
|
||||
error={errors[fieldKey]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
setStateWithValue = (stateKey, validator) => {
|
||||
return (e) => {
|
||||
validator && validator(e.target.value, stateKey)
|
||||
this.setState({ [stateKey]: e.target.value })
|
||||
}
|
||||
}
|
||||
|
||||
setErrorTo = (errorKey, errorVal) => {
|
||||
this.setState({
|
||||
errors: {
|
||||
...this.state.errors,
|
||||
[errorKey]: errorVal,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
validateChainId = (chainId) => {
|
||||
this.setErrorTo('chainId', !!chainId && Number.isNaN(parseInt(chainId))
|
||||
? `${this.context.t('invalidInput')} chainId`
|
||||
: ''
|
||||
)
|
||||
}
|
||||
|
||||
validateUrl = (url, stateKey) => {
|
||||
if (validUrl.isWebUri(url)) {
|
||||
this.setErrorTo(stateKey, '')
|
||||
} else {
|
||||
const appendedRpc = `http://${url}`
|
||||
const validWhenAppended = validUrl.isWebUri(appendedRpc) && !url.match(/^https?:\/\/$/)
|
||||
|
||||
this.setErrorTo(stateKey, this.context.t(validWhenAppended ? 'uriErrorMsg' : 'invalidRPC'))
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { setRpcTarget, viewOnly, rpcUrl: propsRpcUrl, editRpc, rpcPrefs = {} } = this.props
|
||||
const {
|
||||
networkName,
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
blockExplorerUrl,
|
||||
errors,
|
||||
} = this.state
|
||||
|
||||
|
||||
return (
|
||||
<div className="networks-tab__network-form">
|
||||
{this.renderFormTextField(
|
||||
'networkName',
|
||||
'network-name',
|
||||
this.setStateWithValue('networkName'),
|
||||
networkName,
|
||||
)}
|
||||
{this.renderFormTextField(
|
||||
'rpcUrl',
|
||||
'rpc-url',
|
||||
this.setStateWithValue('rpcUrl', this.validateUrl),
|
||||
rpcUrl,
|
||||
)}
|
||||
{this.renderFormTextField(
|
||||
'chainId',
|
||||
'chainId',
|
||||
this.setStateWithValue('chainId', this.validateChainId),
|
||||
chainId,
|
||||
'optionalChainId',
|
||||
)}
|
||||
{this.renderFormTextField(
|
||||
'symbol',
|
||||
'network-ticker',
|
||||
this.setStateWithValue('ticker'),
|
||||
ticker,
|
||||
'optionalSymbol',
|
||||
)}
|
||||
{this.renderFormTextField(
|
||||
'blockExplorerUrl',
|
||||
'block-explorer-url',
|
||||
this.setStateWithValue('blockExplorerUrl', this.validateUrl),
|
||||
blockExplorerUrl,
|
||||
'optionalBlockExplorerUrl',
|
||||
)}
|
||||
<PageContainerFooter
|
||||
cancelText={this.context.t('cancel')}
|
||||
hideCancel={true}
|
||||
onSubmit={() => {
|
||||
if (propsRpcUrl && rpcUrl !== propsRpcUrl) {
|
||||
editRpc(propsRpcUrl, rpcUrl, chainId, ticker, networkName, {
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||
...rpcPrefs,
|
||||
})
|
||||
} else {
|
||||
setRpcTarget(rpcUrl, chainId, ticker, networkName, {
|
||||
blockExplorerUrl: blockExplorerUrl || rpcPrefs.blockExplorerUrl,
|
||||
...rpcPrefs,
|
||||
})
|
||||
}
|
||||
}}
|
||||
submitText={this.context.t('save')}
|
||||
submitButtonType={'confirm'}
|
||||
disabled={viewOnly || this.stateIsUnchanged() || Object.values(errors).some(x => x) || !rpcUrl}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
214
ui/app/pages/settings/networks-tab/networks-tab.component.js
Normal file
214
ui/app/pages/settings/networks-tab/networks-tab.component.js
Normal file
@ -0,0 +1,214 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { SETTINGS_ROUTE } from '../../../helpers/constants/routes'
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
|
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
|
||||
import classnames from 'classnames'
|
||||
import Button from '../../../components/ui/button'
|
||||
import NetworkForm from './network-form'
|
||||
import NetworkDropdownIcon from '../../../components/app/dropdowns/components/network-dropdown-icon'
|
||||
|
||||
export default class NetworksTab extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
metricsEvent: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
editRpc: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
networkIsSelected: PropTypes.bool,
|
||||
networksTabIsInAddMode: PropTypes.bool,
|
||||
networksToRender: PropTypes.array.isRequired,
|
||||
selectedNetwork: PropTypes.object,
|
||||
setNetworksTabAddMode: PropTypes.func.isRequired,
|
||||
setRpcTarget: PropTypes.func.isRequired,
|
||||
setSelectedSettingsRpcUrl: PropTypes.func.isRequired,
|
||||
providerUrl: PropTypes.string,
|
||||
providerType: PropTypes.string,
|
||||
networkDefaultedToProvider: PropTypes.bool,
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.setSelectedSettingsRpcUrl(null)
|
||||
}
|
||||
|
||||
isCurrentPath (pathname) {
|
||||
return this.props.location.pathname === pathname
|
||||
}
|
||||
|
||||
renderSubHeader () {
|
||||
const {
|
||||
networkIsSelected,
|
||||
setSelectedSettingsRpcUrl,
|
||||
setNetworksTabAddMode,
|
||||
networksTabIsInAddMode,
|
||||
networkDefaultedToProvider,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="settings-page__sub-header">
|
||||
<div
|
||||
className="networks-tab__back-button"
|
||||
onClick={(networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode
|
||||
? () => {
|
||||
setNetworksTabAddMode(false)
|
||||
setSelectedSettingsRpcUrl(null)
|
||||
}
|
||||
: () => this.props.history.push(SETTINGS_ROUTE)
|
||||
}
|
||||
/>
|
||||
<span className="settings-page__sub-header-text">{ this.context.t('networks') }</span>
|
||||
<div className="networks-tab__add-network-header-button-wrapper">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
setSelectedSettingsRpcUrl(null)
|
||||
setNetworksTabAddMode(true)
|
||||
}}
|
||||
>
|
||||
{ this.context.t('addNetwork') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNetworkListItem (network, selectRpcUrl) {
|
||||
const {
|
||||
setSelectedSettingsRpcUrl,
|
||||
setNetworksTabAddMode,
|
||||
networkIsSelected,
|
||||
providerUrl,
|
||||
providerType,
|
||||
networksTabIsInAddMode,
|
||||
} = this.props
|
||||
const {
|
||||
border,
|
||||
iconColor,
|
||||
label,
|
||||
labelKey,
|
||||
rpcUrl,
|
||||
providerType: currentProviderType,
|
||||
} = network
|
||||
|
||||
const listItemNetworkIsSelected = selectRpcUrl && selectRpcUrl === rpcUrl
|
||||
const listItemUrlIsProviderUrl = rpcUrl === providerUrl
|
||||
const listItemTypeIsProviderNonRpcType = providerType !== 'rpc' && currentProviderType === providerType
|
||||
const listItemNetworkIsCurrentProvider = !networkIsSelected && !networksTabIsInAddMode && (listItemUrlIsProviderUrl || listItemTypeIsProviderNonRpcType)
|
||||
const displayNetworkListItemAsSelected = listItemNetworkIsSelected || listItemNetworkIsCurrentProvider
|
||||
|
||||
return (
|
||||
<div
|
||||
key={'settings-network-list-item:' + rpcUrl}
|
||||
className="networks-tab__networks-list-item"
|
||||
onClick={ () => {
|
||||
setNetworksTabAddMode(false)
|
||||
setSelectedSettingsRpcUrl(rpcUrl)
|
||||
}}
|
||||
>
|
||||
<NetworkDropdownIcon
|
||||
backgroundColor={iconColor || 'white'}
|
||||
innerBorder={border}
|
||||
/>
|
||||
<div className={ classnames('networks-tab__networks-list-name', {
|
||||
'networks-tab__networks-list-name--selected': displayNetworkListItemAsSelected,
|
||||
}) }>
|
||||
{ label || this.context.t(labelKey) }
|
||||
</div>
|
||||
<div className="networks-tab__networks-list-arrow" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNetworksList () {
|
||||
const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames('networks-tab__networks-list', {
|
||||
'networks-tab__networks-list--selection': (networkIsSelected && !networkDefaultedToProvider) || networksTabIsInAddMode,
|
||||
})}>
|
||||
{ networksToRender.map(network => this.renderNetworkListItem(network, selectedNetwork.rpcUrl)) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderNetworksTabContent () {
|
||||
const {
|
||||
setRpcTarget,
|
||||
setSelectedSettingsRpcUrl,
|
||||
setNetworksTabAddMode,
|
||||
selectedNetwork: {
|
||||
labelKey,
|
||||
label,
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
viewOnly,
|
||||
rpcPrefs,
|
||||
blockExplorerUrl,
|
||||
},
|
||||
networksTabIsInAddMode,
|
||||
editRpc,
|
||||
networkDefaultedToProvider,
|
||||
} = this.props
|
||||
const envIsPopup = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
|
||||
return (
|
||||
<div className="networks-tab__content">
|
||||
{this.renderNetworksList()}
|
||||
{networksTabIsInAddMode || !envIsPopup || (envIsPopup && !networkDefaultedToProvider)
|
||||
? <NetworkForm
|
||||
setRpcTarget={setRpcTarget}
|
||||
editRpc={editRpc}
|
||||
networkName={label || labelKey && this.context.t(labelKey) || ''}
|
||||
rpcUrl={rpcUrl}
|
||||
chainId={chainId}
|
||||
ticker={ticker}
|
||||
onClear={() => {
|
||||
setNetworksTabAddMode(false)
|
||||
setSelectedSettingsRpcUrl(null)
|
||||
}}
|
||||
viewOnly={viewOnly}
|
||||
networksTabIsInAddMode={networksTabIsInAddMode}
|
||||
rpcPrefs={rpcPrefs}
|
||||
blockExplorerUrl={blockExplorerUrl}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { setNetworksTabAddMode, setSelectedSettingsRpcUrl, networkIsSelected, networksTabIsInAddMode } = this.props
|
||||
|
||||
return (
|
||||
<div className="networks-tab__body">
|
||||
{this.renderSubHeader()}
|
||||
{this.renderNetworksTabContent()}
|
||||
{!networkIsSelected && !networksTabIsInAddMode
|
||||
? <div className="networks-tab__add-network-button-wrapper">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
setSelectedSettingsRpcUrl(null)
|
||||
setNetworksTabAddMode(true)
|
||||
}}
|
||||
>
|
||||
{ this.context.t('addNetwork') }
|
||||
</Button>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.renderContent()
|
||||
}
|
||||
}
|
50
ui/app/pages/settings/networks-tab/networks-tab.constants.js
Normal file
50
ui/app/pages/settings/networks-tab/networks-tab.constants.js
Normal file
@ -0,0 +1,50 @@
|
||||
const defaultNetworksData = [
|
||||
{
|
||||
labelKey: 'mainnet',
|
||||
iconColor: '#29B6AF',
|
||||
providerType: 'mainnet',
|
||||
rpcUrl: 'https://api.infura.io/v1/jsonrpc/mainnet',
|
||||
chainId: '1',
|
||||
ticker: 'ETH',
|
||||
blockExplorerUrl: 'https://etherscan.io',
|
||||
},
|
||||
{
|
||||
labelKey: 'ropsten',
|
||||
iconColor: '#FF4A8D',
|
||||
providerType: 'ropsten',
|
||||
rpcUrl: 'https://api.infura.io/v1/jsonrpc/ropsten',
|
||||
chainId: '3',
|
||||
ticker: 'ETH',
|
||||
blockExplorerUrl: 'https://ropsten.etherscan.io',
|
||||
},
|
||||
{
|
||||
labelKey: 'kovan',
|
||||
iconColor: '#9064FF',
|
||||
providerType: 'kovan',
|
||||
rpcUrl: 'https://api.infura.io/v1/jsonrpc/kovan',
|
||||
chainId: '4',
|
||||
ticker: 'ETH',
|
||||
blockExplorerUrl: 'https://etherscan.io',
|
||||
},
|
||||
{
|
||||
labelKey: 'rinkeby',
|
||||
iconColor: '#F6C343',
|
||||
providerType: 'rinkeby',
|
||||
rpcUrl: 'https://api.infura.io/v1/jsonrpc/rinkeby',
|
||||
chainId: '42',
|
||||
ticker: 'ETH',
|
||||
blockExplorerUrl: 'https://rinkeby.etherscan.io',
|
||||
},
|
||||
{
|
||||
labelKey: 'localhost',
|
||||
iconColor: 'white',
|
||||
border: '1px solid #6A737D',
|
||||
providerType: 'localhost',
|
||||
rpcUrl: 'http://localhost:8545/',
|
||||
blockExplorerUrl: 'https://etherscan.io',
|
||||
},
|
||||
]
|
||||
|
||||
export {
|
||||
defaultNetworksData,
|
||||
}
|
77
ui/app/pages/settings/networks-tab/networks-tab.container.js
Normal file
77
ui/app/pages/settings/networks-tab/networks-tab.container.js
Normal file
@ -0,0 +1,77 @@
|
||||
import NetworksTab from './networks-tab.component'
|
||||
import { compose } from 'recompose'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
setSelectedSettingsRpcUrl,
|
||||
updateAndSetCustomRpc,
|
||||
displayWarning,
|
||||
setNetworksTabAddMode,
|
||||
editRpc,
|
||||
} from '../../../store/actions'
|
||||
import { defaultNetworksData } from './networks-tab.constants'
|
||||
const defaultNetworks = defaultNetworksData.map(network => ({ ...network, viewOnly: true }))
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
frequentRpcListDetail,
|
||||
provider,
|
||||
} = state.metamask
|
||||
const {
|
||||
networksTabSelectedRpcUrl,
|
||||
networksTabIsInAddMode,
|
||||
} = state.appState
|
||||
|
||||
const frequentRpcNetworkListDetails = frequentRpcListDetail.map(rpc => {
|
||||
return {
|
||||
label: rpc.nickname,
|
||||
iconColor: '#6A737D',
|
||||
providerType: 'rpc',
|
||||
rpcUrl: rpc.rpcUrl,
|
||||
chainId: rpc.chainId,
|
||||
ticker: rpc.ticker,
|
||||
blockExplorerUrl: rpc.rpcPrefs && rpc.rpcPrefs.blockExplorerUrl || '',
|
||||
}
|
||||
})
|
||||
|
||||
const networksToRender = [ ...defaultNetworks, ...frequentRpcNetworkListDetails ]
|
||||
let selectedNetwork = networksToRender.find(network => network.rpcUrl === networksTabSelectedRpcUrl) || {}
|
||||
const networkIsSelected = Boolean(selectedNetwork.rpcUrl)
|
||||
|
||||
let networkDefaultedToProvider = false
|
||||
if (!networkIsSelected && !networksTabIsInAddMode) {
|
||||
selectedNetwork = networksToRender.find(network => {
|
||||
return network.rpcUrl === provider.rpcTarget || network.providerType !== 'rpc' && network.providerType === provider.type
|
||||
}) || {}
|
||||
networkDefaultedToProvider = true
|
||||
}
|
||||
|
||||
return {
|
||||
selectedNetwork,
|
||||
networksToRender,
|
||||
networkIsSelected,
|
||||
networksTabIsInAddMode,
|
||||
providerType: provider.type,
|
||||
providerUrl: provider.rpcTarget,
|
||||
networkDefaultedToProvider,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setSelectedSettingsRpcUrl: newRpcUrl => dispatch(setSelectedSettingsRpcUrl(newRpcUrl)),
|
||||
setRpcTarget: (newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
||||
dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname, rpcPrefs))
|
||||
},
|
||||
displayWarning: warning => dispatch(displayWarning(warning)),
|
||||
setNetworksTabAddMode: isInAddMode => dispatch(setNetworksTabAddMode(isInAddMode)),
|
||||
editRpc: (oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs) => {
|
||||
dispatch(editRpc(oldRpc, newRpc, chainId, ticker, nickname, rpcPrefs))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(NetworksTab)
|
@ -6,6 +6,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util'
|
||||
import TabBar from '../../components/app/tab-bar'
|
||||
import c from 'classnames'
|
||||
import SettingsTab from './settings-tab'
|
||||
import NetworksTab from './networks-tab'
|
||||
import AdvancedTab from './advanced-tab'
|
||||
import InfoTab from './info-tab'
|
||||
import SecurityTab from './security-tab'
|
||||
@ -16,6 +17,7 @@ import {
|
||||
GENERAL_ROUTE,
|
||||
ABOUT_US_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
NETWORKS_ROUTE,
|
||||
} from '../../helpers/constants/routes'
|
||||
|
||||
const ROUTES_TO_I18N_KEYS = {
|
||||
@ -55,7 +57,7 @@ class SettingsPage extends PureComponent {
|
||||
>
|
||||
<div className="settings-page__header">
|
||||
{
|
||||
!this.isCurrentPath(SETTINGS_ROUTE) && (
|
||||
!this.isCurrentPath(SETTINGS_ROUTE) && !this.isCurrentPath(NETWORKS_ROUTE) && (
|
||||
<div
|
||||
className="settings-page__back-button"
|
||||
onClick={() => history.push(SETTINGS_ROUTE)}
|
||||
@ -104,6 +106,7 @@ class SettingsPage extends PureComponent {
|
||||
{ content: t('general'), description: t('generalSettingsDescription'), key: GENERAL_ROUTE },
|
||||
{ content: t('advanced'), description: t('advancedSettingsDescription'), key: ADVANCED_ROUTE },
|
||||
{ content: t('securityAndPrivacy'), description: t('securitySettingsDescription'), key: SECURITY_ROUTE },
|
||||
{ content: t('networks'), description: t('networkSettingsDescription'), key: NETWORKS_ROUTE },
|
||||
{ content: t('about'), description: t('aboutSettingsDescription'), key: ABOUT_US_ROUTE },
|
||||
]}
|
||||
isActive={key => {
|
||||
@ -135,6 +138,11 @@ class SettingsPage extends PureComponent {
|
||||
path={ADVANCED_ROUTE}
|
||||
component={AdvancedTab}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={NETWORKS_ROUTE}
|
||||
component={NetworksTab}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={SECURITY_ROUTE}
|
||||
|
@ -49,6 +49,7 @@ const selectors = {
|
||||
getNumberOfTokens,
|
||||
isEthereumNetwork,
|
||||
getMetaMetricState,
|
||||
getRpcPrefsForCurrentProvider,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
@ -327,3 +328,10 @@ function getMetaMetricState (state) {
|
||||
participateInMetaMetrics: state.metamask.participateInMetaMetrics,
|
||||
}
|
||||
}
|
||||
|
||||
function getRpcPrefsForCurrentProvider (state) {
|
||||
const { frequentRpcListDetail, provider } = state.metamask
|
||||
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
|
||||
const { rpcPrefs = {} } = selectRpcInfo || {}
|
||||
return rpcPrefs
|
||||
}
|
||||
|
@ -239,6 +239,7 @@ var actions = {
|
||||
updateAndSetCustomRpc: updateAndSetCustomRpc,
|
||||
setRpcTarget: setRpcTarget,
|
||||
delRpcTarget: delRpcTarget,
|
||||
editRpc: editRpc,
|
||||
setProviderType: setProviderType,
|
||||
SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
|
||||
setHardwareWalletDefaultHdPath,
|
||||
@ -350,6 +351,11 @@ var actions = {
|
||||
|
||||
setFirstTimeFlowType,
|
||||
SET_FIRST_TIME_FLOW_TYPE: 'SET_FIRST_TIME_FLOW_TYPE',
|
||||
|
||||
SET_SELECTED_SETTINGS_RPC_URL: 'SET_SELECTED_SETTINGS_RPC_URL',
|
||||
setSelectedSettingsRpcUrl,
|
||||
SET_NETWORKS_TAB_ADD_MODE: 'SET_NETWORKS_TAB_ADD_MODE',
|
||||
setNetworksTabAddMode,
|
||||
}
|
||||
|
||||
module.exports = actions
|
||||
@ -1958,10 +1964,10 @@ function setPreviousProvider (type) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) {
|
||||
function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) {
|
||||
return (dispatch) => {
|
||||
log.debug(`background.updateAndSetCustomRpc: ${newRpc} ${chainId} ${ticker} ${nickname}`)
|
||||
background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, (err) => {
|
||||
background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
return dispatch(actions.displayWarning('Had a problem changing networks!'))
|
||||
@ -1974,6 +1980,29 @@ function updateAndSetCustomRpc (newRpc, chainId, ticker = 'ETH', nickname) {
|
||||
}
|
||||
}
|
||||
|
||||
function editRpc (oldRpc, newRpc, chainId, ticker = 'ETH', nickname, rpcPrefs) {
|
||||
return (dispatch) => {
|
||||
log.debug(`background.delRpcTarget: ${oldRpc}`)
|
||||
background.delCustomRpc(oldRpc, (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
return dispatch(self.displayWarning('Had a problem removing network!'))
|
||||
}
|
||||
dispatch(actions.setSelectedToken())
|
||||
background.updateAndSetCustomRpc(newRpc, chainId, ticker, nickname || newRpc, rpcPrefs, (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
return dispatch(actions.displayWarning('Had a problem changing networks!'))
|
||||
}
|
||||
dispatch({
|
||||
type: actions.SET_RPC_TARGET,
|
||||
value: newRpc,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setRpcTarget (newRpc, chainId, ticker = 'ETH', nickname) {
|
||||
return (dispatch) => {
|
||||
log.debug(`background.setRpcTarget: ${newRpc} ${chainId} ${ticker} ${nickname}`)
|
||||
@ -2000,6 +2029,7 @@ function delRpcTarget (oldRpc) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calls the addressBookController to add a new address.
|
||||
function addToAddressBook (recipient, nickname = '') {
|
||||
log.debug(`background.addToAddressBook`)
|
||||
@ -2716,3 +2746,17 @@ function setFirstTimeFlowType (type) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedSettingsRpcUrl (newRpcUrl) {
|
||||
return {
|
||||
type: actions.SET_SELECTED_SETTINGS_RPC_URL,
|
||||
value: newRpcUrl,
|
||||
}
|
||||
}
|
||||
|
||||
function setNetworksTabAddMode (isInAddMode) {
|
||||
return {
|
||||
type: actions.SET_NETWORKS_TAB_ADD_MODE,
|
||||
value: isInAddMode,
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
module.exports = function (address, network) {
|
||||
module.exports = function (address, network, rpcPrefs) {
|
||||
if (rpcPrefs.blockExplorerUrl) {
|
||||
return `${rpcPrefs.blockExplorerUrl}/address/${address}`
|
||||
}
|
||||
|
||||
const net = parseInt(network)
|
||||
let link
|
||||
switch (net) {
|
||||
|
Loading…
Reference in New Issue
Block a user