Merge pull request #237 from poanetwork/multiple-hardware

(Feature) Multiple Ledger accounts for one session
This commit is contained in:
Victor Baranov 2019-01-16 15:59:52 +03:00 committed by GitHub
commit c988d7cce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 861 additions and 153 deletions

View File

@ -179,7 +179,7 @@ jobs:
key: dependency-cache-{{ .Revision }}
- run:
name: Test
command: sudo npm install -g npm@6 && npm audit
command: sudo npm install -g npm@6.4.1 && npm audit
test-e2e-chrome:
docker:

View File

@ -0,0 +1,83 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
/**
* @typedef {Object} CachedBalancesOptions
* @property {Object} accountTracker An {@code AccountTracker} reference
* @property {Function} getNetwork A function to get the current network
* @property {Object} initState The initial controller state
*/
/**
* Background controller responsible for maintaining
* a cache of account balances in local storage
*/
class CachedBalancesController {
/**
* Creates a new controller instance
*
* @param {CachedBalancesOptions} [opts] Controller configuration parameters
*/
constructor (opts = {}) {
const { accountTracker, getNetwork } = opts
this.accountTracker = accountTracker
this.getNetwork = getNetwork
const initState = extend({
cachedBalances: {},
}, opts.initState)
this.store = new ObservableStore(initState)
this._registerUpdates()
}
/**
* Updates the cachedBalances property for the current network. Cached balances will be updated to those in the passed accounts
* if balances in the passed accounts are truthy.
*
* @param {Object} obj The the recently updated accounts object for the current network
* @returns {Promise<void>}
*/
async updateCachedBalances ({ accounts }) {
const network = await this.getNetwork()
const balancesToCache = await this._generateBalancesToCache(accounts, network)
this.store.updateState({
cachedBalances: balancesToCache,
})
}
_generateBalancesToCache (newAccounts, currentNetwork) {
const { cachedBalances } = this.store.getState()
const currentNetworkBalancesToCache = { ...cachedBalances[currentNetwork] }
Object.keys(newAccounts).forEach(accountID => {
const account = newAccounts[accountID]
if (account.balance) {
currentNetworkBalancesToCache[accountID] = account.balance
}
})
const balancesToCache = {
...cachedBalances,
[currentNetwork]: currentNetworkBalancesToCache,
}
return balancesToCache
}
/**
* Sets up listeners and subscriptions which should trigger an update of cached balances. These updates will
* happen when the current account changes. Which happens on block updates, as well as on network and account
* selections.
*
* @private
*
*/
_registerUpdates () {
const update = this.updateCachedBalances.bind(this)
this.accountTracker.store.subscribe(update)
}
}
module.exports = CachedBalancesController

View File

@ -29,6 +29,7 @@ const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura')
const BlacklistController = require('./controllers/blacklist')
const CachedBalancesController = require('./controllers/cached-balances')
const RecentBlocksController = require('./controllers/recent-blocks')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
@ -52,6 +53,8 @@ const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util')
const sigUtil = require('eth-sig-util')
const accountsPerPage = 5
module.exports = class MetamaskController extends EventEmitter {
/**
@ -138,6 +141,12 @@ module.exports = class MetamaskController extends EventEmitter {
}
})
this.cachedBalancesController = new CachedBalancesController({
accountTracker: this.accountTracker,
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
initState: initState.CachedBalancesController,
})
// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
@ -227,6 +236,7 @@ module.exports = class MetamaskController extends EventEmitter {
ShapeShiftController: this.shapeshiftController.store,
NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store,
})
this.memStore = new ComposableObservableStore(null, {
@ -234,6 +244,7 @@ module.exports = class MetamaskController extends EventEmitter {
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
BalancesController: this.balancesController.store,
CachedBalancesController: this.cachedBalancesController.store,
TokenRatesController: this.tokenRatesController.store,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
@ -374,6 +385,7 @@ module.exports = class MetamaskController extends EventEmitter {
// hardware wallets
connectHardware: nodeify(this.connectHardware, this),
connectHardwareAndUnlockAddress: nodeify(this.connectHardwareAndUnlockAddress, this),
forgetDevice: nodeify(this.forgetDevice, this),
checkHardwareStatus: nodeify(this.checkHardwareStatus, this),
unlockHardwareWalletAccount: nodeify(this.unlockHardwareWalletAccount, this),
@ -645,6 +657,72 @@ module.exports = class MetamaskController extends EventEmitter {
return accounts
}
connectHardwareAndUnlockAddress (deviceName, hdPath, addressToUnlock) {
return new Promise(async (resolve, reject) => {
try {
const keyring = await this.getKeyringForDevice(deviceName, hdPath)
const accountsFromFirstPage = await keyring.getFirstPage()
const initialPage = 0
let accounts = await this.findAccountInLedger({
accounts: accountsFromFirstPage,
keyring,
page: initialPage,
addressToUnlock,
hdPath,
})
accounts = accounts || accountsFromFirstPage
// Merge with existing accounts
// and make sure addresses are not repeated
const oldAccounts = await this.keyringController.getAccounts()
const accountsToTrack = [...new Set(oldAccounts.concat(accounts.map(a => a.address.toLowerCase())))]
this.accountTracker.syncWithAddresses(accountsToTrack)
resolve(accountsFromFirstPage)
} catch (e) {
reject(e)
}
})
}
async findAccountInLedger ({accounts, keyring, page, addressToUnlock, hdPath}) {
return new Promise(async (resolve, reject) => {
// to do: store pages depth in dropdown
const pagesDepth = 10
if (page >= pagesDepth) {
reject({
message: `Requested account ${addressToUnlock} is not found in ${pagesDepth} pages of ${hdPath} path of Ledger. Try to unlock this account from Ledger.`,
})
return
}
if (accounts.length) {
const accountIsFound = accounts.some((account, ind) => {
const normalizedAddress = account.address.toLowerCase()
if (normalizedAddress === addressToUnlock) {
const indToUnlock = page * accountsPerPage + ind
keyring.setAccountToUnlock(indToUnlock)
}
return normalizedAddress === addressToUnlock
})
if (!accountIsFound) {
accounts = await keyring.getNextPage()
page++
this.findAccountInLedger({accounts, keyring, page, addressToUnlock, hdPath})
.then(accounts => {
resolve(accounts)
})
.catch(e => {
reject(e)
})
} else {
resolve(accounts)
}
}
})
}
/**
* Check if the device is unlocked
*
@ -674,21 +752,45 @@ module.exports = class MetamaskController extends EventEmitter {
*/
async unlockHardwareWalletAccount (index, deviceName, hdPath) {
const keyring = await this.getKeyringForDevice(deviceName, hdPath)
let hdAccounts
let indexInPage
if (deviceName.includes('ledger')) {
hdAccounts = await keyring.getFirstPage()
const accountPosition = Number(index) + 1
const pages = Math.ceil(accountPosition / accountsPerPage)
indexInPage = index % accountsPerPage
if (pages > 1) {
for (let iterator = 0; iterator < pages; iterator++) {
hdAccounts = await keyring.getNextPage()
iterator++
}
}
}
keyring.setAccountToUnlock(index)
const oldAccounts = await this.keyringController.getAccounts()
const keyState = await this.keyringController.addNewAccount(keyring)
const newAccounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(newAccounts)
let selectedAddressChanged = false
newAccounts.forEach(address => {
if (!oldAccounts.includes(address)) {
// Set the account label to Trezor 1 / Ledger 1, etc
this.preferencesController.setAccountLabel(address, `${deviceName[0].toUpperCase()}${deviceName.slice(1)} ${parseInt(index, 10) + 1}`)
// Select the account
this.preferencesController.setSelectedAddress(address)
selectedAddressChanged = true
}
})
if (deviceName.includes('ledger')) {
if (!selectedAddressChanged) {
// Select the account
this.preferencesController.setSelectedAddress(hdAccounts[indexInPage].address)
}
}
const { identities } = this.preferencesController.store.getState()
return { ...keyState, identities }
}

View File

@ -15,15 +15,20 @@ const TabBar = require('./components/tab-bar')
const TokenList = require('./components/token-list')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
const CopyButton = require('./components/copyButton')
const ToastComponent = require('./components/toast')
import { getMetaMaskAccounts } from '../../ui/app/selectors'
module.exports = connect(mapStateToProps)(AccountDetailScreen)
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
return {
metamask: state.metamask,
identities: state.metamask.identities,
keyrings: state.metamask.keyrings,
accounts: state.metamask.accounts,
warning: state.appState.warning,
toastMsg: state.appState.toastMsg,
accounts,
address: state.metamask.selectedAddress,
accountDetail: state.appState.accountDetail,
network: state.metamask.network,
@ -62,6 +67,11 @@ AccountDetailScreen.prototype.render = function () {
h('.account-detail-section.full-flex-height', [
h(ToastComponent, {
msg: props.toastMsg,
isSuccess: false,
}),
// identicon, label, balance, etc
h('.account-data-subsection', {
style: {

View File

@ -44,6 +44,7 @@ const DeleteRpc = require('./components/delete-rpc')
const DeleteImportedAccount = require('./components/delete-imported-account')
const ConfirmChangePassword = require('./components/confirm-change-password')
const ethNetProps = require('eth-net-props')
const { getMetaMaskAccounts } = require('../../ui/app/selectors')
module.exports = compose(
withRouter,
@ -54,9 +55,11 @@ inherits(App, Component)
function App () { Component.call(this) }
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
const {
identities,
accounts,
address,
keyrings,
isInitialized,

View File

@ -10,6 +10,7 @@ const ethUtil = require('ethereumjs-util')
const copyToClipboard = require('copy-to-clipboard')
const ethNetProps = require('eth-net-props')
const { getCurrentKeyring, ifLooseAcc, ifContractAcc } = require('../util')
const { getHdPaths } = require('./connect-hardware/util')
class AccountDropdowns extends Component {
constructor (props) {
@ -62,6 +63,25 @@ class AccountDropdowns extends Component {
closeMenu: () => {},
onClick: () => {
this.props.actions.showAccountDetail(identity.address)
if (this.ifHardwareAcc(keyring)) {
const ledger = 'ledger'
if (keyring.type.toLowerCase().includes(ledger)) {
const hdPaths = getHdPaths()
return new Promise((resolve, reject) => {
this.props.actions.connectHardwareAndUnlockAddress(ledger, hdPaths[1].value, identity.address)
.then(_ => resolve())
.catch(e => {
this.props.actions.connectHardwareAndUnlockAddress(ledger, hdPaths[0].value, identity.address)
.then(_ => resolve())
.catch(e => reject(e))
})
})
.catch(e => {
this.props.actions.displayWarning((e && e.message) || e)
this.props.actions.displayToast(e)
})
}
}
},
style: {
marginTop: index === 0 ? '5px' : '',
@ -404,7 +424,16 @@ const mapDispatchToProps = (dispatch) => {
showConnectHWWalletPage: () => dispatch(actions.showConnectHWWalletPage()),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showDeleteImportedAccount: (identity) => dispatch(actions.showDeleteImportedAccount(identity)),
displayWarning: (msg) => dispatch(actions.displayWarning(msg)),
getContract: (addr) => dispatch(actions.getContract(addr)),
setHardwareWalletDefaultHdPath: ({device, path}) => {
return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
},
connectHardwareAndUnlockAddress: (deviceName, hdPath, address) => {
return dispatch(actions.connectHardwareAndUnlockAddress(deviceName, hdPath, address))
},
displayToast: (msg) => dispatch(actions.displayToast(msg)),
hideToast: () => dispatch(actions.hideToast()),
},
}
}

View File

@ -10,6 +10,7 @@ import { getNetworkDisplayName } from '../../../app/scripts/controllers/network/
import { getFaucets, getExchanges } from '../../../app/scripts/lib/buy-eth-url'
import ethNetProps from 'eth-net-props'
import PropTypes from 'prop-types'
import { getMetaMaskAccounts } from '../../../ui/app/selectors'
class BuyButtonSubview extends Component {
render () {
@ -197,9 +198,10 @@ BuyButtonSubview.propTypes = {
}
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
return {
identity: state.appState.identity,
account: state.metamask.accounts[state.appState.buyView.buyAddress],
account: accounts[state.appState.buyView.buyAddress],
warning: state.appState.warning,
buyView: state.appState.buyView,
network: state.metamask.network,

View File

@ -4,25 +4,13 @@ import ethNetProps from 'eth-net-props'
import { default as Select } from 'react-select'
import Button from '../../../../ui/app/components/button'
import { capitalizeFirstLetter } from '../../../../app/scripts/lib/util'
import { isLedger, getHdPaths } from './util'
class AccountList extends Component {
constructor (props, context) {
super(props)
}
getHdPaths = () => {
return [
{
label: `Ledger Live`,
value: `m/44'/60'/0'/0/0`,
},
{
label: `Legacy (MEW / MyCrypto)`,
value: `m/44'/60'/0'`,
},
]
}
goToNextPage = () => {
// If we have < 5 accounts, it's restricted by BIP-44
if (this.props.accounts.length === 5) {
@ -39,7 +27,7 @@ class AccountList extends Component {
renderHdPathSelector = () => {
const { onPathChange, selectedPath } = this.props
const options = this.getHdPaths()
const options = getHdPaths()
return (
<div>
<h3 className="hw-connect__hdPath__title">Select HD Path</h3>
@ -67,26 +55,46 @@ class AccountList extends Component {
<h3 className="hw-connect">
<h3 className="hw-connect__unlock-title">{`Unlock ${capitalizeFirstLetter(device)}`}</h3>
{device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null}
<p className="hw-connect__msg">Select the account to view in Nifty Wallet</p>
<p className="hw-connect__msg">Select the accounts to view in Nifty Wallet</p>
</h3>
</div>
)
}
renderInput = (a, i) => {
const { device, selectedAccount, selectedAccounts } = this.props
if (isLedger(device)) {
return (
<input
type="checkbox"
name={`selectedAccount-${i}`}
id={`address-${i}`}
value={a.index}
onChange={(e) => this.props.onAccountChange(e.target.value)}
checked={selectedAccounts.includes(a.index.toString())}
/>
)
} else {
return (
<input
type="radio"
name="selectedAccount"
id={`address-${i}`}
value={a.index}
onChange={(e) => this.props.onAccountChange(e.target.value)}
checked={selectedAccount === a.index.toString()}
/>
)
}
}
renderAccounts = () => {
const rows = []
this.props.accounts.forEach((a, i) => {
rows.push(
<div className="hw-account-list__item" key={a.address}>
<div className="hw-account-list__item__radio">
<input
type="radio"
name="selectedAccount"
id={`address-${i}`}
value={a.index}
onChange={(e) => this.props.onAccountChange(e.target.value)}
checked={this.props.selectedAccount === a.index.toString()}
/>
{this.renderInput(a, i)}
<label className="hw-account-list__item__label" htmlFor={`address-${i}`}>
{`${a.address.slice(0, 4)}...${a.address.slice(-4)}`}
<span
@ -125,7 +133,7 @@ class AccountList extends Component {
}
renderButtons = () => {
const disabled = this.props.selectedAccount === null
const disabled = !this.props.selectedAccount && this.props.selectedAccounts.length === 0
const buttonProps = {}
if (disabled) {
buttonProps.disabled = true
@ -182,6 +190,7 @@ AccountList.propTypes = {
getPage: PropTypes.func.isRequired,
network: PropTypes.string,
selectedAccount: PropTypes.string,
selectedAccounts: PropTypes.array,
history: PropTypes.object,
onUnlockAccount: PropTypes.func,
onCancel: PropTypes.func,

View File

@ -7,6 +7,8 @@ import AccountList from './account-list'
import { formatBalance } from '../../util'
import { getPlatform } from '../../../../app/scripts/lib/util'
import { PLATFORM_FIREFOX } from '../../../../app/scripts/lib/enums'
import { isLedger } from './util'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
class ConnectHardwareForm extends Component {
constructor (props, context) {
@ -14,6 +16,7 @@ class ConnectHardwareForm extends Component {
this.state = {
error: null,
selectedAccount: null,
selectedAccounts: [],
accounts: [],
browserSupported: true,
unlocked: false,
@ -69,7 +72,25 @@ class ConnectHardwareForm extends Component {
}
onAccountChange = (account) => {
this.setState({selectedAccount: account.toString(), error: null})
let selectedAcc = account.toString()
if (isLedger(this.state.device)) {
const selectedAccounts = this.state.selectedAccounts
if (!selectedAccounts.includes(selectedAcc)) {
selectedAccounts.push(selectedAcc)
} else {
const indToRemove = selectedAccounts.indexOf(selectedAcc)
selectedAccounts.splice(indToRemove, 1)
selectedAcc = selectedAccounts[selectedAccounts.length - 1]
}
const newState = {
selectedAccounts,
selectedAccount: selectedAcc,
error: null,
}
this.setState(newState)
} else {
this.setState({selectedAccount: account.toString(), error: null})
}
}
onAccountRestriction = () => {
@ -97,19 +118,20 @@ class ConnectHardwareForm extends Component {
}
const newState = { unlocked: true, device, error: null }
// Default to the first account
if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => {
if (a.address.toLowerCase() === this.props.address) {
newState.selectedAccount = a.index.toString()
}
})
// If the page doesn't contain the selected account, let's deselect it
} else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
newState.selectedAccount = null
if (!isLedger(device)) {
// Default to the first account
if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => {
if (a.address.toLowerCase() === this.props.address) {
newState.selectedAccount = a.index.toString()
}
})
// If the page doesn't contain the selected account, let's deselect it
} else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
newState.selectedAccount = null
}
}
// Map accounts with balances
newState.accounts = accounts.map(account => {
const normalizedAddress = account.address.toLowerCase()
@ -136,6 +158,7 @@ class ConnectHardwareForm extends Component {
this.setState({
error: null,
selectedAccount: null,
selectedAccounts: [],
accounts: [],
unlocked: false,
})
@ -146,16 +169,35 @@ class ConnectHardwareForm extends Component {
onUnlockAccount = (device) => {
if (this.state.selectedAccount === null) {
if (!this.state.selectedAccount && this.state.selectedAccounts.length === 0) {
this.setState({ error: 'You need to select an account!' })
}
this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
.then(_ => {
this.props.goHome()
}).catch(e => {
this.setState({ error: (e.message || e.toString()) })
})
if (this.state.selectedAccounts.length > 0) {
this.unlockHardwareWalletAccounts(this.state.selectedAccounts, device)
.then(_ => {
this.props.goHome()
})
} else {
this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
.then(_ => {
this.props.goHome()
}).catch(e => {
this.setState({ error: (e.message || e.toString()) })
})
}
}
unlockHardwareWalletAccounts = (accounts, device) => {
return accounts.reduce((promise, account) => {
return promise
.then((result) => {
return new Promise((resolve, reject) => {
resolve(this.props.unlockHardwareWalletAccount(account, device))
})
})
.catch(e => this.setState({ error: (e.message || e.toString()) }))
}, Promise.resolve())
}
onCancel = () => {
@ -185,6 +227,7 @@ class ConnectHardwareForm extends Component {
device={this.state.device}
accounts={this.state.accounts}
selectedAccount={this.state.selectedAccount}
selectedAccounts={this.state.selectedAccounts}
onAccountChange={this.onAccountChange}
network={this.props.network}
getPage={this.getPage}
@ -239,8 +282,9 @@ ConnectHardwareForm.propTypes = {
const mapStateToProps = state => {
const {
metamask: { network, selectedAddress, identities = {}, accounts = [] },
metamask: { network, selectedAddress, identities = {} },
} = state
const accounts = getMetaMaskAccounts(state)
const numberOfExistingAccounts = Object.keys(identities).length
const {
appState: { defaultHdPaths },

View File

@ -0,0 +1,22 @@
function isLedger (device) {
return device && device.toLowerCase().includes('ledger')
}
function getHdPaths () {
return [
{
label: `Ledger Live`,
value: `m/44'/60'/0'/0/0`,
},
{
label: `Legacy (MEW / MyCrypto)`,
value: `m/44'/60'/0'`,
},
]
}
module.exports = {
isLedger,
getHdPaths,
}

View File

@ -1,10 +1,12 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import actions from '../../../ui/app/actions'
class SendError extends Component {
class ErrorComponent extends Component {
static propTypes = {
error: PropTypes.string,
onClose: PropTypes.func,
hideWarning: PropTypes.func,
}
render () {
@ -29,7 +31,7 @@ class SendError extends Component {
height: '16px',
cursor: 'pointer',
}}
onClick={(e) => this.props.onClose(e)}
onClick={(e) => this.props.hideWarning()}
/>
<div style={{
marginLeft: '30px',
@ -47,4 +49,10 @@ class SendError extends Component {
}
}
module.exports = SendError
function mapDispatchToProps (dispatch) {
return {
hideWarning: () => dispatch(actions.hideWarning()),
}
}
module.exports = connect(null, mapDispatchToProps)(ErrorComponent)

View File

@ -24,6 +24,7 @@ const connect = require('react-redux').connect
const abiDecoder = require('abi-decoder')
const { tokenInfoGetter, calcTokenAmount } = require('../../../ui/app/token-util')
const BigNumber = require('bignumber.js')
import { getMetaMaskAccounts } from '../../../ui/app/selectors'
const MIN_GAS_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000')
@ -44,9 +45,10 @@ function PendingTx () {
}
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
accounts,
selectedAddress: state.metamask.selectedAddress,
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,

View File

@ -4,9 +4,10 @@ import { connect } from 'react-redux'
import SendProfile from './send-profile'
import ExecutorCell from './executor-cell'
import SendHeader from './send-header'
import SendError from './send-error'
import ErrorComponent from '../error'
import actions from '../../../../ui/app/actions'
import { ifContractAcc } from '../../util'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
import Web3 from 'web3'
const ownerABI = [{
@ -73,12 +74,7 @@ class ChooseContractExecutor extends Component {
<div className="send-screen flex-column flex-grow">
<SendProfile />
<SendHeader title="Choose contract executor" back={() => this.back()} />
<SendError
error={error}
onClose={() => {
this.props.hideWarning()
}}
/>
<ErrorComponent error={error} />
<div style={{ padding: '0 30px' }}>
<span className="hw-connect__header__msg">Contract transaction will be executed from selected account</span>
</div>
@ -237,9 +233,10 @@ class ChooseContractExecutor extends Component {
}
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
const result = {
selected: state.metamask.selectedAddress,
accounts: state.metamask.accounts,
accounts,
keyrings: state.metamask.keyrings,
identities: state.metamask.identities,
warning: state.appState.warning,

View File

@ -4,7 +4,8 @@ import { connect } from 'react-redux'
import PersistentForm from '../../../lib/persistent-form'
import SendProfile from './send-profile'
import SendHeader from './send-header'
import SendError from './send-error'
import ErrorComponent from '../error'
import ToastComponent from '../toast'
import Select from 'react-select'
import actions from '../../../../ui/app/actions'
import abiEncoder from 'web3-eth-abi'
@ -109,15 +110,9 @@ class SendTransactionScreen extends PersistentForm {
copyDisabled: true,
}
this.timerID = null
PersistentForm.call(this)
}
componentWillUnmount () {
this.props.hideToast()
clearTimeout(this.timerID)
}
componentWillMount () {
this.getContractMethods()
}
@ -132,11 +127,8 @@ class SendTransactionScreen extends PersistentForm {
<div className="send-screen flex-column flex-grow">
<SendProfile />
<SendHeader title="Execute Method" />
<SendError
error={error}
onClose={() => { this.props.hideWarning() }}
/>
{this.props.toastMsg ? <div className="toast">{this.props.toastMsg}</div> : null}
<ErrorComponent error={error} />
<ToastComponent msg={this.props.toastMsg} isSuccess={true} />
<div style={{ padding: '0 30px' }}>
<Select
clearable={false}
@ -387,7 +379,6 @@ class SendTransactionScreen extends PersistentForm {
}
setOutputValue = (val, type) => {
console.log(val)
if (!type) {
return val || ''
}
@ -423,9 +414,6 @@ class SendTransactionScreen extends PersistentForm {
if (txData) {
copyToClipboard(txData)
this.props.displayToast('Contract ABI encoded method call has been successfully copied to clipboard')
this.timerID = setTimeout(() => {
this.props.hideToast()
}, 4000)
}
}

View File

@ -4,6 +4,7 @@ import Identicon from '../identicon'
import { addressSummary } from '../../util'
import EthBalance from '../eth-balance'
import TokenBalance from '../token-balance'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
class SendProfile extends Component {
render () {
@ -77,9 +78,10 @@ class SendProfile extends Component {
}
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
var result = {
address: state.metamask.selectedAddress,
accounts: state.metamask.accounts,
accounts,
identities: state.metamask.identities,
network: state.metamask.network,
conversionRate: state.metamask.conversionRate,

View File

@ -18,14 +18,16 @@ BigNumber.config({ ERRORS: false })
const log = require('loglevel')
import SendProfile from './send-profile'
import SendHeader from './send-header'
import SendError from './send-error'
import ErrorComponent from '../error'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
var result = {
address: state.metamask.selectedAddress,
accounts: state.metamask.accounts,
accounts,
identities: state.metamask.identities,
warning: state.appState.warning,
network: state.metamask.network,
@ -95,7 +97,7 @@ SendTransactionScreen.prototype.render = function () {
}),
// error message
h(SendError, {
h(ErrorComponent, {
error,
}),

View File

@ -14,13 +14,15 @@ const EnsInput = require('../ens-input')
const ethUtil = require('ethereumjs-util')
import SendProfile from './send-profile'
import SendHeader from './send-header'
import SendError from './send-error'
import ErrorComponent from '../error'
import { getMetaMaskAccounts } from '../../../../ui/app/selectors'
module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps (state) {
const accounts = getMetaMaskAccounts(state)
var result = {
address: state.metamask.selectedAddress,
accounts: state.metamask.accounts,
accounts,
identities: state.metamask.identities,
warning: state.appState.warning,
network: state.metamask.network,
@ -69,11 +71,8 @@ SendTransactionScreen.prototype.render = function () {
}),
// error message
h(SendError, {
h(ErrorComponent, {
error,
onClose: () => {
this.props.dispatch(actions.hideWarning())
},
}),
// 'to' field

View File

@ -0,0 +1,59 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import actions from '../../../ui/app/actions'
class ToastComponent extends Component {
static propTypes = {
msg: PropTypes.string,
isSuccess: PropTypes.bool,
hideToast: PropTypes.func,
}
constructor (props) {
super(props)
this.timerID = null
}
componentDidUpdate (prevProps) {
if (!prevProps.msg && this.props.msg) {
this.timerID = setTimeout(() => {
this.props.hideToast()
clearTimeout(this.timerID)
}, 4000)
}
}
componentWillUnmount () {
this.props.hideToast()
clearTimeout(this.timerID)
}
render () {
const { msg } = this.props
return msg ? (
<div
className={classnames('toast', {
'green': this.props.isSuccess,
'red': !this.props.isSuccess,
})}
onClick={(e) => this.props.hideToast()}
>{(msg && msg.message) || msg}</div>
) : null
}
}
function mapStateToProps (state) {
return {
toastMsg: state.appState.toastMsg,
}
}
function mapDispatchToProps (dispatch) {
return {
hideToast: () => dispatch(actions.hideToast()),
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(ToastComponent)

View File

@ -13,6 +13,7 @@ import PendingMsg from './components/pending-msg'
import PendingPersonalMsg from './components/pending-personal-msg'
import PendingTypedMsg from './components/pending-typed-msg'
const Loading = require('./components/loading')
const { getMetaMaskAccounts } = require('../../ui/app/selectors')
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
@ -21,8 +22,8 @@ function mapStateToProps (state) {
const { screenParams, pendingTxIndex } = appState.currentView
return {
identities: metamask.identities,
accounts: getMetaMaskAccounts(state),
keyrings: metamask.keyrings,
accounts: metamask.accounts,
selectedAddress: metamask.selectedAddress,
unapprovedTxs: metamask.unapprovedTxs,
unapprovedMsgs: metamask.unapprovedMsgs,

View File

@ -280,10 +280,13 @@ app sections
/* unlock */
.toast {
border: 1px solid #60db97 !important;
background-image: url('../images/remove.svg');
background-size: 12px 12px;
background-repeat: no-repeat;
background-position: 5px 5px;
border: 1px solid !important;
color: #ffffff !important;
font-size: 12px;
background: #60db97;
text-align: center;
padding: 10px;
width: 357px;
@ -294,6 +297,17 @@ app sections
right: 0px;
z-index: 100;
animation: 500ms ease-out 0s move;
cursor: pointer;
}
.toast.green {
background-color: #60db97;
border-color: #60db97 !important;
}
.toast.red {
background-color: #ff1345;
border-color: #ff1345 !important;
}
@keyframes move {

93
package-lock.json generated
View File

@ -10124,8 +10124,8 @@
}
},
"eth-hd-keyring": {
"version": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-2.0.0.tgz",
"from": "eth-hd-keyring@2.0.0",
"version": "github:vbaranov/eth-hd-keyring#64d0fa741af88d5f232f9518fd150190c421b3e7",
"from": "github:vbaranov/eth-hd-keyring#2.0.1",
"requires": {
"bip39": "^2.2.0",
"eth-sig-util": "^2.0.1",
@ -10450,7 +10450,7 @@
"bip39": "^2.4.0",
"bluebird": "^3.5.0",
"browser-passworder": "^2.0.3",
"eth-hd-keyring": "https://registry.npmjs.org/eth-hd-keyring/-/eth-hd-keyring-2.0.0.tgz",
"eth-hd-keyring": "github:vbaranov/eth-hd-keyring#64d0fa741af88d5f232f9518fd150190c421b3e7",
"eth-sig-util": "^1.4.0",
"eth-simple-keyring": "^2.0.0",
"ethereumjs-util": "^5.1.2",
@ -10524,9 +10524,8 @@
}
},
"eth-ledger-bridge-keyring": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/eth-ledger-bridge-keyring/-/eth-ledger-bridge-keyring-0.1.1.tgz",
"integrity": "sha512-EhClGSy5ixcd55yHGXoA3C7I8iFFi6kgSqvKOSj+5URtg5PYpHP8kv+KemFPOT1Px6se/IFHI9OIelUS8kN3lw==",
"version": "github:vbaranov/eth-ledger-bridge-keyring#ab97ae49167ddbe8442c06c857c3f8ca5f7cbcf6",
"from": "github:vbaranov/eth-ledger-bridge-keyring#0.1.0-multiple-accounts",
"requires": {
"eth-sig-util": "^1.4.2",
"ethereumjs-tx": "^1.3.4",
@ -10540,7 +10539,18 @@
"resolved": "https://registry.npmjs.org/eth-sig-util/-/eth-sig-util-1.4.2.tgz",
"integrity": "sha1-jZWCAsftuq6Dlwf7pvCf8ydgYhA=",
"requires": {
"ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
}
}
}
},
"ethereum-common": {
@ -10963,6 +10973,16 @@
"requires": {
"ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
}
}
}
},
"ethereum-common": {
@ -14994,7 +15014,8 @@
"bindings": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz",
"integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw=="
"integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==",
"dev": true
},
"bip39": {
"version": "2.5.0",
@ -15013,6 +15034,7 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
"integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=",
"dev": true,
"requires": {
"safe-buffer": "^5.0.1"
}
@ -15047,7 +15069,8 @@
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
},
"body-parser": {
"version": "1.18.3",
@ -15090,12 +15113,14 @@
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
"dev": true
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
"buffer-xor": "^1.0.3",
"cipher-base": "^1.0.0",
@ -15252,7 +15277,8 @@
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=",
"dev": true
},
"builtin-modules": {
"version": "1.1.1",
@ -15350,6 +15376,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
@ -15509,6 +15536,7 @@
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
@ -15521,6 +15549,7 @@
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
@ -15820,6 +15849,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
"integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=",
"dev": true,
"requires": {
"browserify-aes": "^1.0.6",
"create-hash": "^1.1.2",
@ -15858,6 +15888,7 @@
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
"integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
@ -16150,17 +16181,6 @@
"requires": {
"ethereumjs-abi": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"ethereumjs-util": "^5.1.1"
},
"dependencies": {
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"dev": true,
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
}
}
}
},
"ethereum-common": {
@ -16171,7 +16191,8 @@
},
"ethereumjs-abi": {
"version": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git#2863c40e0982acfc0b7163f0285d4c56427c7799",
"from": "git+https://github.com/ethereumjs/ethereumjs-abi.git",
"dev": true,
"requires": {
"bn.js": "^4.10.0",
"ethereumjs-util": "^5.0.0"
@ -16366,6 +16387,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz",
"integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==",
"dev": true,
"requires": {
"bn.js": "^4.11.0",
"create-hash": "^1.1.2",
@ -16455,6 +16477,7 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.6.tgz",
"integrity": "sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==",
"dev": true,
"requires": {
"is-hex-prefixed": "1.0.0",
"strip-hex-prefix": "1.0.0"
@ -16476,6 +16499,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
"dev": true,
"requires": {
"md5.js": "^1.3.4",
"safe-buffer": "^5.1.1"
@ -16886,6 +16910,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
"integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
@ -16895,6 +16920,7 @@
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
"integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
@ -16922,6 +16948,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"dev": true,
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@ -17089,7 +17116,8 @@
"is-hex-prefixed": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz",
"integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ="
"integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=",
"dev": true
},
"is-natural-number": {
"version": "4.0.1",
@ -17290,6 +17318,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz",
"integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==",
"dev": true,
"requires": {
"bindings": "^1.2.1",
"inherits": "^2.0.3",
@ -17589,6 +17618,7 @@
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"dev": true,
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
@ -17748,12 +17778,14 @@
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true
},
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
"dev": true
},
"minimatch": {
"version": "3.0.4",
@ -17823,7 +17855,8 @@
"nan": {
"version": "2.10.0",
"resolved": "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
},
"nano-json-stream-parser": {
"version": "0.1.2",
@ -18490,6 +18523,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
"integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
"dev": true,
"requires": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1"
@ -18499,6 +18533,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/rlp/-/rlp-2.1.0.tgz",
"integrity": "sha512-93U7IKH5j7nmXFVg19MeNBGzQW5uXW1pmCuKY8veeKIhYTE32C2d0mOegfiIAfXcHOKJjjPlJisn8iHDF5AezA==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.1"
}
@ -18512,7 +18547,8 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"safe-event-emitter": {
"version": "1.0.1",
@ -18564,6 +18600,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.5.2.tgz",
"integrity": "sha512-iin3kojdybY6NArd+UFsoTuapOF7bnJNf2UbcWXaY3z+E1sJDipl60vtzB5hbO/uquBu7z0fd4VC4Irp+xoFVQ==",
"dev": true,
"requires": {
"bindings": "^1.2.1",
"bip66": "^1.1.3",
@ -18695,6 +18732,7 @@
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
@ -18905,6 +18943,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz",
"integrity": "sha1-DF8VX+8RUTczd96du1iNoFUA428=",
"dev": true,
"requires": {
"is-hex-prefixed": "1.0.0"
}

View File

@ -116,7 +116,7 @@
"eth-json-rpc-filters": "github:poanetwork/eth-json-rpc-filters#3.0.2",
"eth-json-rpc-infura": "^3.0.0",
"eth-keychain-controller": "github:vbaranov/KeyringController#simple-address",
"eth-ledger-bridge-keyring": "^0.1.0",
"eth-ledger-bridge-keyring": "github:vbaranov/eth-ledger-bridge-keyring#0.1.0-multiple-accounts",
"eth-method-registry": "^1.0.0",
"eth-net-props": "^1.0.10",
"eth-phishing-detect": "^1.1.4",

View File

@ -30,6 +30,7 @@ module.exports = {
info: By.css('li.dropdown-menu-item:nth-child(4)'),
},
account: {
item: By.className('dropdown-menu-item'),
account1: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(2) > span'),
account2: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'),
account3: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(4) > span'),
@ -54,6 +55,17 @@ module.exports = {
},
},
screens: {
hdWallet: {
buttonArrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
error: By.className('error'),
title: By.className('section-title flex-row flex-center'),
buttonConnect: {
enabled: By.className('hw-connect__connect-btn'),
disabled: By.className('hw-connect__connect-btn disabled'),
},
image: By.className('hw-connect__btn__img'),
imageSelected: By.className('hw-connect__btn selected'),
},
chooseContractExecutor: {
title: By.className('flex-center send-header'),
titleText: 'Choose contract executor',

View File

@ -269,6 +269,97 @@ describe('Metamask popup page', async function () {
})
})
describe('Connect Hardware Wallet', async function () {
it("Account menu contais item 'Connect HD wallet'", async function () {
const menu = await f.waitUntilShowUp(menus.account.menu)
await menu.click()
await f.waitUntilShowUp(menus.account.item)
const items = await driver.findElements(menus.account.item)
await f.delay(500)
assert.equal(await items[4].getText(), 'Connect hardware wallet', "item's text incorrect")
await items[4].click()
})
it("Opens screen 'Connect HD wallet',title is correct", async function () {
const title = await f.waitUntilShowUp(screens.hdWallet.title)
assert.equal(await title.getText(), 'Connect to hardware wallet', "item's text incorrect")
})
if (process.env.SELENIUM_BROWSER === 'chrome') {
it("Button 'Connect' disabled by default", async function () {
const button = await f.waitUntilShowUp(screens.hdWallet.buttonConnect.disabled)
assert.notEqual(button, false, "button isn't displayed")
assert.equal(await button.getText(), 'CONNECT', 'button has incorrect text')
})
it('Ledger image is displayed', async function () {
const image = await f.waitUntilShowUp(screens.hdWallet.image)
assert.notEqual(image, false, "ledger's image isn't displayed")
const src = await image.getAttribute('src')
assert.equal(src.includes('images/ledger-logo.svg'), true, 'Ledger has incorrect image')
})
it('Trezor image is displayed', async function () {
const images = await driver.findElements(screens.hdWallet.image)
assert.notEqual(images[1], false, "trezor's image isn't displayed")
const src = await images[1].getAttribute('src')
assert.equal(src.includes('images/trezor-logo.svg'), true, 'Trezor has incorrect image')
})
it("Button 'Connect' enabled if Trezor selected", async function () {
const images = await driver.findElements(screens.hdWallet.image)
await images[1].click()
const button = await f.waitUntilShowUp(screens.hdWallet.buttonConnect.enabled)
assert.equal(await button.isEnabled(), true, 'button is disabled')
})
it("Button 'Connect' enabled if Ledger selected", async function () {
const images = await driver.findElements(screens.hdWallet.image)
await images[0].click()
const button = await f.waitUntilShowUp(screens.hdWallet.buttonConnect.enabled)
assert.equal(await button.isEnabled(), true, 'button is disabled')
})
it('Only one device can be selected', async function () {
const selected = await driver.findElements(screens.hdWallet.imageSelected)
assert.equal(await selected.length, 1, 'more than one device is selected')
})
it('Error message if connect Ledger', async function () {
const button = await f.waitUntilShowUp(screens.hdWallet.buttonConnect.enabled)
await button.click()
const error = await f.waitUntilShowUp(screens.hdWallet.error)
const shouldBe = "TransportError: U2F browser support is needed for Ledger. Please use Chrome, Opera or Firefox with a U2F extension. Also make sure you're on an HTTPS connection"
assert.equal(await error.getText(), shouldBe, 'error has incorrect text')
})
it('Popup opens if connect Trezor', async function () {
const images = await driver.findElements(screens.hdWallet.image)
await images[1].click()
const button = await f.waitUntilShowUp(screens.hdWallet.buttonConnect.enabled)
await button.click()
const allHandles = await driver.getAllWindowHandles()
assert.equal(allHandles.length, 2, "popup isn't opened")
await f.switchToFirstPage()
await driver.navigate().refresh()
})
}
it('Button arrow leads to main screen', async function () {
const menu = await f.waitUntilShowUp(menus.account.menu)
await menu.click()
await f.waitUntilShowUp(menus.account.item)
const items = await driver.findElements(menus.account.item)
await f.delay(500)
await items[4].click()
const arrow = await f.waitUntilShowUp(screens.hdWallet.buttonArrow)
await arrow.click()
const ident = await f.waitUntilShowUp(screens.main.identicon, 20)
assert.notEqual(ident, false, "main screen isn't opened")
})
})
describe('Import Account', async function () {
it('Open import account menu', async function () {

View File

@ -0,0 +1,137 @@
const assert = require('assert')
const sinon = require('sinon')
const CachedBalancesController = require('../../../../app/scripts/controllers/cached-balances')
describe('CachedBalancesController', () => {
describe('updateCachedBalances', () => {
it('should update the cached balances', async () => {
const controller = new CachedBalancesController({
getNetwork: () => Promise.resolve(17),
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: 'mockCachedBalances',
},
})
controller._generateBalancesToCache = sinon.stub().callsFake(() => Promise.resolve('mockNewCachedBalances'))
await controller.updateCachedBalances({ accounts: 'mockAccounts' })
assert.equal(controller._generateBalancesToCache.callCount, 1)
assert.deepEqual(controller._generateBalancesToCache.args[0], ['mockAccounts', 17])
assert.equal(controller.store.getState().cachedBalances, 'mockNewCachedBalances')
})
})
describe('_generateBalancesToCache', () => {
it('should generate updated account balances where the current network was updated', () => {
const controller = new CachedBalancesController({
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
16: {
a: '0xa',
b: '0xb',
c: '0xc',
},
},
},
})
const result = controller._generateBalancesToCache({
a: { balance: '0x4' },
b: { balance: null },
c: { balance: '0x5' },
}, 17)
assert.deepEqual(result, {
17: {
a: '0x4',
b: '0x2',
c: '0x5',
},
16: {
a: '0xa',
b: '0xb',
c: '0xc',
},
})
})
it('should generate updated account balances where the a new network was selected', () => {
const controller = new CachedBalancesController({
accountTracker: {
store: {
subscribe: () => {},
},
},
initState: {
cachedBalances: {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
},
},
})
const result = controller._generateBalancesToCache({
a: { balance: '0x4' },
b: { balance: null },
c: { balance: '0x5' },
}, 16)
assert.deepEqual(result, {
17: {
a: '0x1',
b: '0x2',
c: '0x3',
},
16: {
a: '0x4',
c: '0x5',
},
})
})
})
describe('_registerUpdates', () => {
it('should subscribe to the account tracker with the updateCachedBalances method', async () => {
const subscribeSpy = sinon.spy()
const controller = new CachedBalancesController({
getNetwork: () => Promise.resolve(17),
accountTracker: {
store: {
subscribe: subscribeSpy,
},
},
})
subscribeSpy.resetHistory()
const updateCachedBalancesSpy = sinon.spy()
controller.updateCachedBalances = updateCachedBalancesSpy
controller._registerUpdates({ accounts: 'mockAccounts' })
assert.equal(subscribeSpy.callCount, 1)
subscribeSpy.args[0][0]()
assert.equal(updateCachedBalancesSpy.callCount, 1)
})
})
})

View File

@ -358,7 +358,8 @@ describe('MetaMaskController', function () {
let addNewAccountStub
let getAccountsStub
beforeEach(async function () {
accountToUnlock = 10
this.timeout(10000)
accountToUnlock = 4
windowOpenStub = sinon.stub(window, 'open')
windowOpenStub.returns(noop)

View File

@ -1,6 +1,6 @@
import React from 'react'
import assert from 'assert'
import SendContractError from '../../../../../../old-ui/app/components/send/send-error'
import ErrorComponent from '../../../../../old-ui/app/components/error'
import { mount } from 'enzyme'
import thunk from 'redux-thunk'
import configureMockStore from 'redux-mock-store'
@ -16,12 +16,12 @@ const mockStore = configureMockStore(middlewares)
const store = mockStore(state)
let wrapper
describe('SendContractError component', () => {
describe('renders SendContractError component', () => {
describe('ErrorComponent', () => {
describe('renders ErrorComponent', () => {
beforeEach(function () {
wrapper = mount(
<Provider store={store}>
<SendContractError error="Error!"/>
<ErrorComponent error="Error!"/>
</Provider>
)
})
@ -30,11 +30,11 @@ describe('SendContractError component', () => {
})
})
describe('doesn\'t render SendContractError component', () => {
describe('doesn\'t render ErrorComponent component', () => {
beforeEach(function () {
wrapper = mount(
<Provider store={store}>
<SendContractError/>
<ErrorComponent/>
</Provider>
)
})

View File

@ -12,6 +12,7 @@ describe('ChooseContractExecutor component', () => {
metamask: {
selectedAddress: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
accounts: ['0x99a22ce737b6a48f44cad6331432ce98693cad07'],
cachedBalances: {'0x99a22ce737b6a48f44cad6331432ce98693cad07': 1},
keyrings: [
{
'type': 'HD Key Tree',

View File

@ -11,6 +11,7 @@ const state = {
metamask: {
selectedAddress: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
accounts: ['0x99a22ce737b6a48f44cad6331432ce98693cad07'],
cachedBalances: {'0x99a22ce737b6a48f44cad6331432ce98693cad07': 1},
identities: {
'0x99a22ce737b6a48f44cad6331432ce98693cad07': {
name: 'Account 1',

View File

@ -91,6 +91,7 @@ var actions = {
importNewAccount,
addNewAccount,
connectHardware,
connectHardwareAndUnlockAddress,
checkHardwareStatus,
forgetDevice,
unlockHardwareWalletAccount,
@ -788,6 +789,23 @@ function connectHardware (deviceName, page, hdPath) {
}
}
function connectHardwareAndUnlockAddress (deviceName, hdPath, addressToUnlock) {
log.debug(`background.connectHardwareAndUnlockAddress`, deviceName, hdPath, addressToUnlock)
return (dispatch, getState) => {
return new Promise((resolve, reject) => {
background.connectHardwareAndUnlockAddress(deviceName, hdPath, addressToUnlock, (err, accounts) => {
if (err) {
log.error(err)
return reject(err)
}
forceUpdateMetamaskState(dispatch)
return resolve(accounts)
})
})
}
}
function unlockHardwareWalletAccount (index, deviceName, hdPath) {
log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
return (dispatch, getState) => {

View File

@ -7,6 +7,7 @@ const h = require('react-hyperscript')
const actions = require('./actions')
const classnames = require('classnames')
const log = require('loglevel')
const { getMetaMaskAccounts } = require('./selectors')
// init
const InitializeScreen = require('../../mascara/src/app/first-time').default
@ -279,9 +280,10 @@ function mapStateToProps (state) {
loadingMessage,
} = appState
const accounts = getMetaMaskAccounts(state)
const {
identities,
accounts,
address,
keyrings,
isInitialized,

View File

@ -13,6 +13,7 @@ const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const Tooltip = require('../tooltip')
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import { PRIMARY } from '../../constants/common'
import { getMetaMaskAccounts } from '../../selectors'
const {
SETTINGS_ROUTE,
@ -41,7 +42,7 @@ function mapStateToProps (state) {
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
keyrings: state.metamask.keyrings,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
accounts: getMetaMaskAccounts(state),
}
}

View File

@ -6,14 +6,14 @@ const TokenBalance = require('./token-balance')
const Identicon = require('./identicon')
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../constants/common'
const { getAssetImages, conversionRateSelector, getCurrentCurrency} = require('../selectors')
const { getAssetImages, conversionRateSelector, getCurrentCurrency, getMetaMaskAccounts } = require('../selectors')
const { formatBalance } = require('../util')
module.exports = connect(mapStateToProps)(BalanceComponent)
function mapStateToProps (state) {
const accounts = state.metamask.accounts
const accounts = getMetaMaskAccounts(state)
const network = state.metamask.network
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
const account = accounts[selectedAddress]

View File

@ -18,6 +18,7 @@ import { isBalanceSufficient } from '../../send/send.utils'
import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send/send.constants'
import { addressSlicer, valuesFor } from '../../../util'
import { getMetaMaskAccounts } from '../../../selectors'
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
return {
@ -47,11 +48,11 @@ const mapStateToProps = (state, props) => {
} = confirmTransaction
const { txParams = {}, lastGasPrice, id: transactionId } = txData
const { from: fromAddress, to: txParamsToAddress } = txParams
const accounts = getMetaMaskAccounts(state)
const {
conversionRate,
identities,
currentCurrency,
accounts,
selectedAddress,
selectedAddressTxList,
assetImages,

View File

@ -3,6 +3,7 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { getMetaMaskAccounts } = require('../../../../selectors')
const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../../routes')
@ -225,8 +226,9 @@ ConnectHardwareForm.propTypes = {
const mapStateToProps = state => {
const {
metamask: { network, selectedAddress, identities = {}, accounts = [] },
metamask: { network, selectedAddress, identities = {} },
} = state
const accounts = getMetaMaskAccounts(state)
const numberOfExistingAccounts = Object.keys(identities).length
const {
appState: { defaultHdPaths },

View File

@ -7,6 +7,7 @@ const connect = require('react-redux').connect
const actions = require('../../../../actions')
const FileInput = require('react-simple-file-input').default
const { DEFAULT_ROUTE } = require('../../../../routes')
const { getMetaMaskAccounts } = require('../../../../selectors')
const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
import Button from '../../../button'
@ -136,7 +137,7 @@ JsonImportSubview.propTypes = {
const mapStateToProps = state => {
return {
error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
}
}

View File

@ -7,6 +7,7 @@ const PropTypes = require('prop-types')
const connect = require('react-redux').connect
const actions = require('../../../../actions')
const { DEFAULT_ROUTE } = require('../../../../routes')
const { getMetaMaskAccounts } = require('../../../../selectors')
import Button from '../../../button'
PrivateKeyImportView.contextTypes = {
@ -22,7 +23,7 @@ module.exports = compose(
function mapStateToProps (state) {
return {
error: state.appState.warning,
firstAddress: Object.keys(state.metamask.accounts)[0],
firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
}
}

View File

@ -3,6 +3,9 @@ const abi = require('human-standard-token-abi')
const {
multiplyCurrencies,
} = require('../../conversion-util')
const {
getMetaMaskAccounts,
} = require('../../selectors')
const {
estimateGasPriceFromRecentBlocks,
} = require('./send.utils')
@ -53,10 +56,8 @@ const selectors = {
module.exports = selectors
function accountsWithSendEtherInfoSelector (state) {
const {
accounts,
identities,
} = state.metamask
const accounts = getMetaMaskAccounts(state)
const { identities } = state.metamask
const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
return Object.assign({}, account, identities[key])
@ -71,7 +72,7 @@ function accountsWithSendEtherInfoSelector (state) {
// const autoAddTokensThreshold = 1
// const numberOfTransactions = state.metamask.selectedAddressTxList.length
// const numberOfAccounts = Object.keys(state.metamask.accounts).length
// const numberOfAccounts = Object.keys(getMetaMaskAccounts(state)).length
// const numberOfTokensAdded = state.metamask.tokens.length
// const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&
@ -150,14 +151,14 @@ function getRecentBlocks (state) {
}
function getSelectedAccount (state) {
const accounts = state.metamask.accounts
const accounts = getMetaMaskAccounts(state)
const selectedAddress = getSelectedAddress(state)
return accounts[selectedAddress]
}
function getSelectedAddress (state) {
const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
return selectedAddress
}

View File

@ -2,12 +2,18 @@ import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionViewBalance from './transaction-view-balance.component'
import { getSelectedToken, getSelectedAddress, getSelectedTokenAssetImage } from '../../selectors'
import {
getSelectedToken,
getSelectedAddress,
getSelectedTokenAssetImage,
getMetaMaskAccounts,
} from '../../selectors'
import { showModal } from '../../actions'
const mapStateToProps = state => {
const selectedAddress = getSelectedAddress(state)
const { metamask: { network, accounts } } = state
const { metamask: { network } } = state
const accounts = getMetaMaskAccounts(state)
const account = accounts[selectedAddress]
const { balance } = account

View File

@ -38,7 +38,7 @@ function mapStateToProps (state) {
network: state.metamask.network,
sidebarOpen: state.appState.sidebar.isOpen,
identities: state.metamask.identities,
accounts: state.metamask.accounts,
accounts: selectors.getMetaMaskAccounts(state),
tokens: state.metamask.tokens,
keyrings: state.metamask.keyrings,
selectedAddress: selectors.getSelectedAddress(state),

View File

@ -12,6 +12,7 @@ const R = require('ramda')
const SignatureRequest = require('./components/signature-request')
const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes')
const { getMetaMaskAccounts } = require('./selectors')
module.exports = compose(
withRouter,
@ -28,7 +29,7 @@ function mapStateToProps (state) {
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
accounts: getMetaMaskAccounts(state),
selectedAddress: state.metamask.selectedAddress,
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,

View File

@ -1,9 +1,7 @@
const abi = require('human-standard-token-abi')
import {
transactionsSelector,
} from './selectors/transactions'
const {
multiplyCurrencies,
} = require('./conversion-util')
@ -34,12 +32,13 @@ const selectors = {
getCurrentViewContext,
getTotalUnapprovedCount,
preferencesSelector,
getMetaMaskAccounts,
}
module.exports = selectors
function getSelectedAddress (state) {
const selectedAddress = state.metamask.selectedAddress || Object.keys(state.metamask.accounts)[0]
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
return selectedAddress
}
@ -51,8 +50,27 @@ function getSelectedIdentity (state) {
return identities[selectedAddress]
}
function getMetaMaskAccounts (state) {
const currentAccounts = state.metamask.accounts
const cachedBalances = state.metamask.cachedBalances
const selectedAccounts = {}
Object.keys(currentAccounts).forEach(accountID => {
const account = currentAccounts[accountID]
if (account && account.balance === null || account.balance === undefined) {
selectedAccounts[accountID] = {
...account,
balance: cachedBalances[accountID],
}
} else {
selectedAccounts[accountID] = account
}
})
return selectedAccounts
}
function getSelectedAccount (state) {
const accounts = state.metamask.accounts
const accounts = getMetaMaskAccounts(state)
const selectedAddress = getSelectedAddress(state)
return accounts[selectedAddress]
@ -100,10 +118,8 @@ function getAddressBook (state) {
}
function accountsWithSendEtherInfoSelector (state) {
const {
accounts,
identities,
} = state.metamask
const accounts = getMetaMaskAccounts(state)
const { identities } = state.metamask
const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
return Object.assign({}, account, identities[key])
@ -169,7 +185,7 @@ function autoAddToBetaUI (state) {
const autoAddTokensThreshold = 1
const numberOfTransactions = state.metamask.selectedAddressTxList.length
const numberOfAccounts = Object.keys(state.metamask.accounts).length
const numberOfAccounts = Object.keys(getMetaMaskAccounts(state)).length
const numberOfTokensAdded = state.metamask.tokens.length
const userPassesThreshold = (numberOfTransactions > autoAddTransactionThreshold) &&