1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Add UI for Signing Messages

Calls to `eth.sign` are now transiently persisted in memory, and displayed in a chronological stack with pending transactions (which are still persisted to disk).

This allows the user a method to sign/cancel transactions even if they miss the Chrome notification.

Improved a lot of the view routing, to avoid cases where routes would show an empty account view, or transition to the accounts list when it shouldn't.

Broke the transaction approval view into a couple components so messages and transactions could have their own templates.
This commit is contained in:
Dan Finlay 2016-05-03 14:32:22 -07:00
parent dcbf17af2d
commit e6c4d63ccd
15 changed files with 354 additions and 144 deletions

View File

@ -10,6 +10,7 @@ const IdentityStore = require('./lib/idStore')
const createTxNotification = require('./lib/notifications.js').createTxNotification const createTxNotification = require('./lib/notifications.js').createTxNotification
const createMsgNotification = require('./lib/notifications.js').createMsgNotification const createMsgNotification = require('./lib/notifications.js').createMsgNotification
const configManager = require('./lib/config-manager-singleton') const configManager = require('./lib/config-manager-singleton')
const messageManager = require('./lib/message-manager')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const HostStore = require('./lib/remote-store.js').HostStore const HostStore = require('./lib/remote-store.js').HostStore
const Web3 = require('web3') const Web3 = require('web3')
@ -175,6 +176,8 @@ function setupControllerConnection(stream){
setSelectedAddress: idStore.setSelectedAddress.bind(idStore), setSelectedAddress: idStore.setSelectedAddress.bind(idStore),
approveTransaction: idStore.approveTransaction.bind(idStore), approveTransaction: idStore.approveTransaction.bind(idStore),
cancelTransaction: idStore.cancelTransaction.bind(idStore), cancelTransaction: idStore.cancelTransaction.bind(idStore),
signMessage: idStore.signMessage.bind(idStore),
cancelMessage: idStore.cancelMessage.bind(idStore),
setLocked: idStore.setLocked.bind(idStore), setLocked: idStore.setLocked.bind(idStore),
clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore), clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore),
exportAccount: idStore.exportAccount.bind(idStore), exportAccount: idStore.exportAccount.bind(idStore),
@ -206,7 +209,10 @@ idStore.on('update', updateBadge)
function updateBadge(state){ function updateBadge(state){
var label = '' var label = ''
var unconfTxs = configManager.unconfirmedTxs() var unconfTxs = configManager.unconfirmedTxs()
var count = Object.keys(unconfTxs).length var unconfTxLen = Object.keys(unconfTxs).length
var unconfMsgs = messageManager.unconfirmedMsgs()
var unconfMsgLen = Object.keys(unconfMsgs).length
var count = unconfTxLen + unconfMsgLen
if (count) { if (count) {
label = String(count) label = String(count)
} }

View File

@ -211,73 +211,6 @@ ConfigManager.prototype.updateTx = function(tx) {
this._saveTxList(transactions) this._saveTxList(transactions)
} }
//
// Msg
//
ConfigManager.prototype.getMsgList = function() {
var data = this.migrator.getData()
if (data.messages !== undefined) {
return data.messages
} else {
return []
}
}
ConfigManager.prototype.unconfirmedMsgs = function() {
var messages = this.getMsgList()
return messages.filter(msg => msg.status === 'unconfirmed')
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
}
ConfigManager.prototype._saveMsgList = function(msgList) {
var data = this.migrator.getData()
data.messages = msgList
this.setData(data)
}
ConfigManager.prototype.addMsg = function(msg) {
var messages = this.getMsgList()
messages.push(msg)
this._saveMsgList(messages)
}
ConfigManager.prototype.getMsg = function(msgId) {
var messages = this.getMsgList()
var matching = messages.filter(msg => msg.id === msgId)
return matching.length > 0 ? matching[0] : null
}
ConfigManager.prototype.confirmMsg = function(msgId) {
this._setMsgStatus(msgId, 'confirmed')
}
ConfigManager.prototype.rejectMsg = function(msgId) {
this._setMsgStatus(msgId, 'rejected')
}
ConfigManager.prototype._setMsgStatus = function(msgId, status) {
var msg = this.getMsg(msgId)
msg.status = status
this.updateMsg(msg)
}
ConfigManager.prototype.updateMsg = function(msg) {
var messages = this.getMsgList()
var found, index
messages.forEach((otherMsg, i) => {
if (otherMsg.id === msg.id) {
found = true
index = i
}
})
if (found) {
messages[index] = msg
}
this._saveMsgList(messages)
}
// observable // observable

View File

@ -9,6 +9,7 @@ const extend = require('xtend')
const createId = require('web3-provider-engine/util/random-id') const createId = require('web3-provider-engine/util/random-id')
const autoFaucet = require('./auto-faucet') const autoFaucet = require('./auto-faucet')
const configManager = require('./config-manager-singleton') const configManager = require('./config-manager-singleton')
const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/' const DEFAULT_RPC = 'https://testrpc.metamask.io/'
@ -32,6 +33,7 @@ function IdentityStore(opts = {}) {
selectedAddress: null, selectedAddress: null,
identities: {}, identities: {},
} }
// not part of serilized metamask state - only kept in memory // not part of serilized metamask state - only kept in memory
this._unconfTxCbs = {} this._unconfTxCbs = {}
this._unconfMsgCbs = {} this._unconfMsgCbs = {}
@ -85,6 +87,8 @@ IdentityStore.prototype.getState = function(){
seedWords: seedWords, seedWords: seedWords,
unconfTxs: configManager.unconfirmedTxs(), unconfTxs: configManager.unconfirmedTxs(),
transactions: configManager.getTxList(), transactions: configManager.getTxList(),
unconfMsgs: messageManager.unconfirmedMsgs(),
messages: messageManager.getMsgList(),
selectedAddress: configManager.getSelectedAccount(), selectedAddress: configManager.getSelectedAccount(),
})) }))
} }
@ -226,7 +230,7 @@ IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){
time: time, time: time,
status: 'unconfirmed', status: 'unconfirmed',
} }
configManager.addMsg(msgData) messageManager.addMsg(msgData)
console.log('addUnconfirmedMessage:', msgData) console.log('addUnconfirmedMessage:', msgData)
// keep the cb around for after approval (requires user interaction) // keep the cb around for after approval (requires user interaction)
@ -241,27 +245,27 @@ IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){
// comes from metamask ui // comes from metamask ui
IdentityStore.prototype.approveMessage = function(msgId, cb){ IdentityStore.prototype.approveMessage = function(msgId, cb){
var msgData = configManager.getMsg(msgId) var msgData = messageManager.getMsg(msgId)
var approvalCb = this._unconfMsgCbs[msgId] || noop var approvalCb = this._unconfMsgCbs[msgId] || noop
// accept msg // accept msg
cb() cb()
approvalCb(null, true) approvalCb(null, true)
// clean up // clean up
configManager.confirmMsg(msgId) messageManager.confirmMsg(msgId)
delete this._unconfMsgCbs[msgId] delete this._unconfMsgCbs[msgId]
this._didUpdate() this._didUpdate()
} }
// comes from metamask ui // comes from metamask ui
IdentityStore.prototype.cancelMessage = function(msgId){ IdentityStore.prototype.cancelMessage = function(msgId){
var txData = configManager.getMsg(msgId) var txData = messageManager.getMsg(msgId)
var approvalCb = this._unconfMsgCbs[msgId] || noop var approvalCb = this._unconfMsgCbs[msgId] || noop
// reject tx // reject tx
approvalCb(null, false) approvalCb(null, false)
// clean up // clean up
configManager.rejectMsg(msgId) messageManager.rejectMsg(msgId)
delete this._unconfTxCbs[msgId] delete this._unconfTxCbs[msgId]
this._didUpdate() this._didUpdate()
} }
@ -271,7 +275,14 @@ IdentityStore.prototype.signMessage = function(msgParams, cb){
try { try {
console.log('signing msg...', msgParams.data) console.log('signing msg...', msgParams.data)
var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data) var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
cb(null, rawMsg) if ('metamaskId' in msgParams) {
var id = msgParams.metamaskId
delete msgParams.metamaskId
this.approveMessage(id, cb)
} else {
cb(null, rawMsg)
}
} catch (err) { } catch (err) {
cb(err) cb(err)
} }
@ -426,7 +437,7 @@ function IdManagement(opts) {
var privKeyHex = this.exportPrivateKey(txParams.from) var privKeyHex = this.exportPrivateKey(txParams.from)
var privKey = ethUtil.toBuffer(privKeyHex) var privKey = ethUtil.toBuffer(privKeyHex)
tx.sign(privKey) tx.sign(privKey)
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
var txHash = ethUtil.bufferToHex(tx.hash()) var txHash = ethUtil.bufferToHex(tx.hash())
var metaTx = configManager.getTx(txParams.metamaskId) var metaTx = configManager.getTx(txParams.metamaskId)
@ -472,4 +483,4 @@ function concatSig(v, r, s) {
s = ethUtil.toUnsigned(s).toString('hex') s = ethUtil.toUnsigned(s).toString('hex')
v = ethUtil.stripHexPrefix(ethUtil.intToHex(v)) v = ethUtil.stripHexPrefix(ethUtil.intToHex(v))
return ethUtil.addHexPrefix(r.concat(s, v).toString("hex")) return ethUtil.addHexPrefix(r.concat(s, v).toString("hex"))
} }

View File

@ -0,0 +1,61 @@
module.exports = new MessageManager()
function MessageManager(opts) {
this.messages = []
}
MessageManager.prototype.getMsgList = function() {
return this.messages
}
MessageManager.prototype.unconfirmedMsgs = function() {
var messages = this.getMsgList()
return messages.filter(msg => msg.status === 'unconfirmed')
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
}
MessageManager.prototype._saveMsgList = function(msgList) {
this.messages = msgList
}
MessageManager.prototype.addMsg = function(msg) {
var messages = this.getMsgList()
messages.push(msg)
this._saveMsgList(messages)
}
MessageManager.prototype.getMsg = function(msgId) {
var messages = this.getMsgList()
var matching = messages.filter(msg => msg.id === msgId)
return matching.length > 0 ? matching[0] : null
}
MessageManager.prototype.confirmMsg = function(msgId) {
this._setMsgStatus(msgId, 'confirmed')
}
MessageManager.prototype.rejectMsg = function(msgId) {
this._setMsgStatus(msgId, 'rejected')
}
MessageManager.prototype._setMsgStatus = function(msgId, status) {
var msg = this.getMsg(msgId)
if (msg) msg.status = status
this.updateMsg(msg)
}
MessageManager.prototype.updateMsg = function(msg) {
var messages = this.getMsgList()
var found, index
messages.forEach((otherMsg, i) => {
if (otherMsg.id === msg.id) {
found = true
index = i
}
})
if (found) {
messages[index] = msg
}
this._saveMsgList(messages)
}

View File

@ -13,7 +13,7 @@ function mapStateToProps(state) {
return { return {
identities: state.metamask.identities, identities: state.metamask.identities,
accounts: state.metamask.accounts, accounts: state.metamask.accounts,
address: state.appState.currentView.context, address: state.metamask.selectedAccount,
accountDetail: state.appState.accountDetail, accountDetail: state.appState.accountDetail,
transactions: state.metamask.transactions, transactions: state.metamask.transactions,
networkVersion: state.metamask.network, networkVersion: state.metamask.network,
@ -27,6 +27,7 @@ function AccountDetailScreen() {
AccountDetailScreen.prototype.render = function() { AccountDetailScreen.prototype.render = function() {
var state = this.props var state = this.props
var selected = state.address || Object.keys(state.accounts[0]).address
var identity = state.identities[state.address] var identity = state.identities[state.address]
var account = state.accounts[state.address] var account = state.accounts[state.address]
var accountDetail = state.accountDetail var accountDetail = state.accountDetail

View File

@ -42,6 +42,7 @@ var actions = {
SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL',
SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE',
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
// account detail screen // account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
showSendPage: showSendPage, showSendPage: showSendPage,
@ -57,7 +58,8 @@ var actions = {
NEXT_TX: 'NEXT_TX', NEXT_TX: 'NEXT_TX',
PREVIOUS_TX: 'PREV_TX', PREVIOUS_TX: 'PREV_TX',
setSelectedAddress: setSelectedAddress, setSelectedAddress: setSelectedAddress,
signTx: signTx, signMsg: signMsg,
cancelMsg: cancelMsg,
sendTx: sendTx, sendTx: sendTx,
cancelTx: cancelTx, cancelTx: cancelTx,
completedTx: completedTx, completedTx: completedTx,
@ -152,16 +154,15 @@ function setSelectedAddress(address) {
} }
} }
function signTx(txData) { function signMsg(msgData) {
return (dispatch) => { return (dispatch) => {
dispatch(this.showLoadingIndication()) dispatch(this.showLoadingIndication())
web3.eth.sendTransaction(txData, (err, data) => { _accountManager.signMessage(msgData, (err) => {
dispatch(this.hideLoadingIndication()) dispatch(this.hideLoadingIndication())
if (err) return dispatch(this.displayWarning(err.message)) if (err) return dispatch(this.displayWarning(err.message))
dispatch(this.hideWarning()) dispatch(this.completedTx(msgData.metamaskId))
dispatch(this.goHome())
}) })
} }
} }
@ -193,9 +194,14 @@ function txError(err) {
} }
} }
function cancelMsg(msgData){
_accountManager.cancelMessage(msgData.id)
return this.completedTx(msgData.id)
}
function cancelTx(txData){ function cancelTx(txData){
_accountManager.cancelTransaction(txData.id) _accountManager.cancelTransaction(txData.id)
return this.goHome() return this.completedTx(txData.id)
} }
// //

View File

@ -202,7 +202,7 @@ App.prototype.renderPrimary = function(state){
return h(CreateVaultScreen, {key: 'createVault'}) return h(CreateVaultScreen, {key: 'createVault'})
default: default:
return h(AccountsScreen, {key: 'accounts'}) return h(AccountDetailScreen, {key: 'account-detail'})
} }
} }

View File

@ -0,0 +1,65 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const addressSummary = require('../util').addressSummary
const readableDate = require('../util').readableDate
const formatBalance = require('../util').formatBalance
const dataSize = require('../util').dataSize
module.exports = PendingMsg
inherits(PendingMsg, Component)
function PendingMsg() {
Component.call(this)
}
PendingMsg.prototype.render = function() {
var state = this.props
var msgData = state.txData
var msgParams = msgData.msgParams || {}
var address = msgParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('.transaction', {
key: msgData.id,
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
// tx data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(msgData.time)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'MESSAGE'),
h('span.font-small', msgParams.data),
]),
]),
// send + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelMessage,
}, 'Cancel'),
h('button', {
onClick: state.signMessage,
}, 'Sign'),
]),
])
)
}

View File

@ -0,0 +1,71 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountPanel = require('./account-panel')
const addressSummary = require('../util').addressSummary
const readableDate = require('../util').readableDate
const formatBalance = require('../util').formatBalance
const dataSize = require('../util').dataSize
module.exports = PendingTx
inherits(PendingTx, Component)
function PendingTx() {
Component.call(this)
}
PendingTx.prototype.render = function() {
var state = this.props
var txData = state.txData
var txParams = txData.txParams || {}
var address = txParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return (
h('.transaction', {
key: txData.id,
}, [
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
// tx data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'TO ADDRESS'),
h('span.font-small', addressSummary(txParams.to)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(txData.time)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'AMOUNT'),
h('span.font-small', formatBalance(txParams.value)),
]),
]),
// send + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelTransaction,
}, 'Cancel'),
h('button', {
onClick: state.sendTransaction,
}, 'Send'),
]),
])
)
}

View File

@ -0,0 +1,19 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
module.exports = NewComponent
inherits(NewComponent, Component)
function NewComponent() {
Component.call(this)
}
NewComponent.prototype.render = function() {
var state = this.props
return (
h('span', 'Placeholder component')
)
}

View File

@ -7,10 +7,10 @@ const copyToClipboard = require('copy-to-clipboard')
const actions = require('./actions') const actions = require('./actions')
const AccountPanel = require('./components/account-panel') const AccountPanel = require('./components/account-panel')
const valuesFor = require('./util').valuesFor const valuesFor = require('./util').valuesFor
const addressSummary = require('./util').addressSummary const txHelper = require('../lib/tx-helper')
const readableDate = require('./util').readableDate
const formatBalance = require('./util').formatBalance const ConfirmTx = require('./components/pending-tx')
const dataSize = require('./util').dataSize const PendingMsg = require('./components/pending-msg')
module.exports = connect(mapStateToProps)(ConfirmTxScreen) module.exports = connect(mapStateToProps)(ConfirmTxScreen)
@ -20,7 +20,9 @@ function mapStateToProps(state) {
accounts: state.metamask.accounts, accounts: state.metamask.accounts,
selectedAddress: state.metamask.selectedAddress, selectedAddress: state.metamask.selectedAddress,
unconfTxs: state.metamask.unconfTxs, unconfTxs: state.metamask.unconfTxs,
unconfMsgs: state.metamask.unconfMsgs,
index: state.appState.currentView.context, index: state.appState.currentView.context,
warning: state.appState.warning,
} }
} }
@ -32,12 +34,11 @@ function ConfirmTxScreen() {
ConfirmTxScreen.prototype.render = function() { ConfirmTxScreen.prototype.render = function() {
var state = this.props var state = this.props
var unconfTxList = valuesFor(state.unconfTxs).sort(tx => tx.time)
var unconfTxs = state.unconfTxs
var unconfMsgs = state.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs)
var txData = unconfTxList[state.index] || {} var txData = unconfTxList[state.index] || {}
var txParams = txData.txParams || {}
var address = txParams.from || state.selectedAddress
var identity = state.identities[address] || { address: address }
var account = state.accounts[address] || { address: address }
return ( return (
@ -46,7 +47,7 @@ ConfirmTxScreen.prototype.render = function() {
// subtitle and nav // subtitle and nav
h('.section-title.flex-row.flex-center', [ h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.navigateToAccounts.bind(this), onClick: this.goHome.bind(this),
}), }),
h('h2.page-subtitle', 'Confirm Transaction'), h('h2.page-subtitle', 'Confirm Transaction'),
]), ]),
@ -72,58 +73,44 @@ ConfirmTxScreen.prototype.render = function() {
}), }),
]), ]),
warningIfExists(state.warning),
h(ReactCSSTransitionGroup, { h(ReactCSSTransitionGroup, {
transitionName: "main", transitionName: "main",
transitionEnterTimeout: 300, transitionEnterTimeout: 300,
transitionLeaveTimeout: 300, transitionLeaveTimeout: 300,
}, [ }, [
h('.transaction', { currentTxView({
// Properties
txData: txData,
key: txData.id, key: txData.id,
}, [ selectedAddress: state.selectedAddress,
accounts: state.accounts,
identities: state.identities,
// Actions
sendTransaction: this.sendTransaction.bind(this, txData),
cancelTransaction: this.cancelTransaction.bind(this, txData),
signMessage: this.signMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
}),
// account that will sign
h(AccountPanel, {
showFullAddress: true,
identity: identity,
account: account,
}),
// tx data
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-row.flex-space-between', [
h('label.font-small', 'TO ADDRESS'),
h('span.font-small', addressSummary(txParams.to)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'DATE'),
h('span.font-small', readableDate(txData.time)),
]),
h('.flex-row.flex-space-between', [
h('label.font-small', 'AMOUNT'),
h('span.font-small', formatBalance(txParams.value)),
]),
]),
// send + cancel
h('.flex-row.flex-space-around', [
h('button', {
onClick: this.cancelTransaction.bind(this, txData),
}, 'Cancel'),
h('button', {
onClick: this.sendTransaction.bind(this, txData),
}, 'Send'),
]),
]),
]), ]),
]) // No comma or semicolon can go here ])
) )
} }
function currentTxView (opts) {
if ('txParams' in opts.txData) {
// This is a pending transaction
return h(ConfirmTx, opts)
} else if ('msgParams' in opts.txData) {
// This is a pending message to sign
return h(PendingMsg, opts)
}
}
ConfirmTxScreen.prototype.sendTransaction = function(txData, event){ ConfirmTxScreen.prototype.sendTransaction = function(txData, event){
event.stopPropagation() event.stopPropagation()
this.props.dispatch(actions.sendTx(txData)) this.props.dispatch(actions.sendTx(txData))
@ -134,7 +121,25 @@ ConfirmTxScreen.prototype.cancelTransaction = function(txData, event){
this.props.dispatch(actions.cancelTx(txData)) this.props.dispatch(actions.cancelTx(txData))
} }
ConfirmTxScreen.prototype.navigateToAccounts = function(event){ ConfirmTxScreen.prototype.signMessage = function(msgData, event){
var params = msgData.msgParams
params.metamaskId = msgData.id
event.stopPropagation() event.stopPropagation()
this.props.dispatch(actions.showAccountsPage()) this.props.dispatch(actions.signMsg(params))
}
ConfirmTxScreen.prototype.cancelMessage = function(msgData, event){
event.stopPropagation()
this.props.dispatch(actions.cancelMsg(msgData))
}
ConfirmTxScreen.prototype.goHome = function(event){
event.stopPropagation()
this.props.dispatch(actions.goHome())
}
function warningIfExists(warning) {
if (warning) {
return h('span.error', { style: { margin: 'auto' } }, warning)
}
} }

View File

@ -1,6 +1,7 @@
const extend = require('xtend') const extend = require('xtend')
const actions = require('../actions') const actions = require('../actions')
const valuesFor = require('../util').valuesFor const valuesFor = require('../util').valuesFor
const txHelper = require('../../lib/tx-helper')
module.exports = reduceApp module.exports = reduceApp
@ -127,10 +128,7 @@ function reduceApp(state, action) {
case actions.GO_HOME: case actions.GO_HOME:
return extend(appState, { return extend(appState, {
currentView: { currentView: {},
name: 'accountDetail',
context: appState.currentView.context,
},
accountDetail: { accountDetail: {
accountExport: 'none', accountExport: 'none',
privateKey: '', privateKey: '',
@ -185,9 +183,24 @@ function reduceApp(state, action) {
warning: null, warning: null,
}) })
case actions.SHOW_CONF_MSG_PAGE:
return extend(appState, {
currentView: {
name: 'confTx',
context: 0,
},
transForward: true,
warning: null,
})
case actions.COMPLETED_TX: case actions.COMPLETED_TX:
var unconfTxs = Object.keys(state.metamask.unconfTxs).filter(tx => tx !== tx.id) var unconfTxs = state.metamask.unconfTxs
if (unconfTxs && unconfTxs.length > 0) { var unconfMsgs = state.metamask.unconfMsgs
var unconfTxList = txHelper(unconfTxs, unconfMsgs)
.filter(tx => tx !== tx.id)
if (unconfTxList && unconfTxList.length > 0) {
return extend(appState, { return extend(appState, {
transForward: false, transForward: false,
currentView: { currentView: {
@ -202,7 +215,7 @@ function reduceApp(state, action) {
warning: null, warning: null,
currentView: { currentView: {
name: 'accountDetail', name: 'accountDetail',
context: appState.currentView.context, context: state.metamask.selectedAddress,
}, },
}) })
} }

View File

@ -44,13 +44,19 @@ function reduceMetamask(state, action) {
case actions.COMPLETED_TX: case actions.COMPLETED_TX:
var stringId = String(action.id) var stringId = String(action.id)
var newState = extend(metamaskState, { var newState = extend(metamaskState, {
unconfTxs: {} unconfTxs: {},
unconfMsgs: {},
}) })
for (var id in metamaskState.unconfTxs) { for (var id in metamaskState.unconfTxs) {
if (id !== stringId) { if (id !== stringId) {
newState.unconfTxs[id] = metamaskState.unconfTxs[id] newState.unconfTxs[id] = metamaskState.unconfTxs[id]
} }
} }
for (var id in metamaskState.unconfMsgs) {
if (id !== stringId) {
newState.unconfMsgs[id] = metamaskState.unconfMsgs[id]
}
}
return newState return newState
case actions.CLEAR_SEED_WORD_CACHE: case actions.CLEAR_SEED_WORD_CACHE:

View File

@ -43,6 +43,11 @@ function startApp(metamaskState, accountManager, opts){
store.dispatch(actions.showConfTxPage()) store.dispatch(actions.showConfTxPage())
} }
// if unconfirmed messages, start on msgConf page
if (Object.keys(metamaskState.unconfMsgs || {}).length) {
store.dispatch(actions.showConfTxPage())
}
accountManager.on('update', function(metamaskState){ accountManager.on('update', function(metamaskState){
store.dispatch(actions.updateMetamaskState(metamaskState)) store.dispatch(actions.updateMetamaskState(metamaskState))
}) })

8
ui/lib/tx-helper.js Normal file
View File

@ -0,0 +1,8 @@
const valuesFor = require('../app/util').valuesFor
module.exports = function(unconfTxs, unconfMsgs) {
var txValues = valuesFor(unconfTxs)
var msgValues = valuesFor(unconfMsgs)
var allValues = txValues.concat(msgValues)
return allValues.sort(tx => tx.time)
}