diff --git a/app/scripts/background.js b/app/scripts/background.js index 3c46f4693..0f9ecc1c9 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -10,6 +10,7 @@ const IdentityStore = require('./lib/idStore') const createTxNotification = require('./lib/notifications.js').createTxNotification const createMsgNotification = require('./lib/notifications.js').createMsgNotification const configManager = require('./lib/config-manager-singleton') +const messageManager = require('./lib/message-manager') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const HostStore = require('./lib/remote-store.js').HostStore const Web3 = require('web3') @@ -175,6 +176,8 @@ function setupControllerConnection(stream){ setSelectedAddress: idStore.setSelectedAddress.bind(idStore), approveTransaction: idStore.approveTransaction.bind(idStore), cancelTransaction: idStore.cancelTransaction.bind(idStore), + signMessage: idStore.signMessage.bind(idStore), + cancelMessage: idStore.cancelMessage.bind(idStore), setLocked: idStore.setLocked.bind(idStore), clearSeedWordCache: idStore.clearSeedWordCache.bind(idStore), exportAccount: idStore.exportAccount.bind(idStore), @@ -206,7 +209,10 @@ idStore.on('update', updateBadge) function updateBadge(state){ var label = '' 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) { label = String(count) } diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 0e7454dfd..5bfb8befe 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -211,73 +211,6 @@ ConfigManager.prototype.updateTx = function(tx) { 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 diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index c25d83c9d..b8d825d8b 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -9,6 +9,7 @@ const extend = require('xtend') const createId = require('web3-provider-engine/util/random-id') const autoFaucet = require('./auto-faucet') const configManager = require('./config-manager-singleton') +const messageManager = require('./message-manager') const DEFAULT_RPC = 'https://testrpc.metamask.io/' @@ -32,6 +33,7 @@ function IdentityStore(opts = {}) { selectedAddress: null, identities: {}, } + // not part of serilized metamask state - only kept in memory this._unconfTxCbs = {} this._unconfMsgCbs = {} @@ -85,6 +87,8 @@ IdentityStore.prototype.getState = function(){ seedWords: seedWords, unconfTxs: configManager.unconfirmedTxs(), transactions: configManager.getTxList(), + unconfMsgs: messageManager.unconfirmedMsgs(), + messages: messageManager.getMsgList(), selectedAddress: configManager.getSelectedAccount(), })) } @@ -226,7 +230,7 @@ IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){ time: time, status: 'unconfirmed', } - configManager.addMsg(msgData) + messageManager.addMsg(msgData) console.log('addUnconfirmedMessage:', msgData) // keep the cb around for after approval (requires user interaction) @@ -241,27 +245,27 @@ IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){ // comes from metamask ui IdentityStore.prototype.approveMessage = function(msgId, cb){ - var msgData = configManager.getMsg(msgId) + var msgData = messageManager.getMsg(msgId) var approvalCb = this._unconfMsgCbs[msgId] || noop // accept msg cb() approvalCb(null, true) // clean up - configManager.confirmMsg(msgId) + messageManager.confirmMsg(msgId) delete this._unconfMsgCbs[msgId] this._didUpdate() } // comes from metamask ui IdentityStore.prototype.cancelMessage = function(msgId){ - var txData = configManager.getMsg(msgId) + var txData = messageManager.getMsg(msgId) var approvalCb = this._unconfMsgCbs[msgId] || noop // reject tx approvalCb(null, false) // clean up - configManager.rejectMsg(msgId) + messageManager.rejectMsg(msgId) delete this._unconfTxCbs[msgId] this._didUpdate() } @@ -271,7 +275,14 @@ IdentityStore.prototype.signMessage = function(msgParams, cb){ try { console.log('signing msg...', 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) { cb(err) } @@ -426,7 +437,7 @@ function IdManagement(opts) { var privKeyHex = this.exportPrivateKey(txParams.from) var privKey = ethUtil.toBuffer(privKeyHex) tx.sign(privKey) - + // Add the tx hash to the persisted meta-tx object var txHash = ethUtil.bufferToHex(tx.hash()) var metaTx = configManager.getTx(txParams.metamaskId) @@ -472,4 +483,4 @@ function concatSig(v, r, s) { s = ethUtil.toUnsigned(s).toString('hex') v = ethUtil.stripHexPrefix(ethUtil.intToHex(v)) return ethUtil.addHexPrefix(r.concat(s, v).toString("hex")) -} \ No newline at end of file +} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js new file mode 100644 index 000000000..91edb7759 --- /dev/null +++ b/app/scripts/lib/message-manager.js @@ -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) +} + diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 250e7318c..a876b275f 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -13,7 +13,7 @@ function mapStateToProps(state) { return { identities: state.metamask.identities, accounts: state.metamask.accounts, - address: state.appState.currentView.context, + address: state.metamask.selectedAccount, accountDetail: state.appState.accountDetail, transactions: state.metamask.transactions, networkVersion: state.metamask.network, @@ -27,6 +27,7 @@ function AccountDetailScreen() { AccountDetailScreen.prototype.render = function() { var state = this.props + var selected = state.address || Object.keys(state.accounts[0]).address var identity = state.identities[state.address] var account = state.accounts[state.address] var accountDetail = state.accountDetail diff --git a/ui/app/actions.js b/ui/app/actions.js index 12e20e0cf..72550ff15 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -42,6 +42,7 @@ var actions = { SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', + SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE', // account detail screen SHOW_SEND_PAGE: 'SHOW_SEND_PAGE', showSendPage: showSendPage, @@ -57,7 +58,8 @@ var actions = { NEXT_TX: 'NEXT_TX', PREVIOUS_TX: 'PREV_TX', setSelectedAddress: setSelectedAddress, - signTx: signTx, + signMsg: signMsg, + cancelMsg: cancelMsg, sendTx: sendTx, cancelTx: cancelTx, completedTx: completedTx, @@ -152,16 +154,15 @@ function setSelectedAddress(address) { } } -function signTx(txData) { +function signMsg(msgData) { return (dispatch) => { dispatch(this.showLoadingIndication()) - web3.eth.sendTransaction(txData, (err, data) => { + _accountManager.signMessage(msgData, (err) => { dispatch(this.hideLoadingIndication()) if (err) return dispatch(this.displayWarning(err.message)) - dispatch(this.hideWarning()) - dispatch(this.goHome()) + dispatch(this.completedTx(msgData.metamaskId)) }) } } @@ -193,9 +194,14 @@ function txError(err) { } } +function cancelMsg(msgData){ + _accountManager.cancelMessage(msgData.id) + return this.completedTx(msgData.id) +} + function cancelTx(txData){ _accountManager.cancelTransaction(txData.id) - return this.goHome() + return this.completedTx(txData.id) } // diff --git a/ui/app/app.js b/ui/app/app.js index fa375fb7f..fce98d8e1 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -202,7 +202,7 @@ App.prototype.renderPrimary = function(state){ return h(CreateVaultScreen, {key: 'createVault'}) default: - return h(AccountsScreen, {key: 'accounts'}) + return h(AccountDetailScreen, {key: 'account-detail'}) } } diff --git a/ui/app/components/pending-msg.js b/ui/app/components/pending-msg.js new file mode 100644 index 000000000..cb6df2a3d --- /dev/null +++ b/ui/app/components/pending-msg.js @@ -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'), + ]), + ]) + ) +} + diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js new file mode 100644 index 000000000..2519998a5 --- /dev/null +++ b/ui/app/components/pending-tx.js @@ -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'), + ]), + ]) + ) +} + diff --git a/ui/app/components/template.js b/ui/app/components/template.js new file mode 100644 index 000000000..9e4eca20f --- /dev/null +++ b/ui/app/components/template.js @@ -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') + ) +} diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 983070013..28f2db39c 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -7,10 +7,10 @@ const copyToClipboard = require('copy-to-clipboard') const actions = require('./actions') const AccountPanel = require('./components/account-panel') const valuesFor = require('./util').valuesFor -const addressSummary = require('./util').addressSummary -const readableDate = require('./util').readableDate -const formatBalance = require('./util').formatBalance -const dataSize = require('./util').dataSize +const txHelper = require('../lib/tx-helper') + +const ConfirmTx = require('./components/pending-tx') +const PendingMsg = require('./components/pending-msg') module.exports = connect(mapStateToProps)(ConfirmTxScreen) @@ -20,7 +20,9 @@ function mapStateToProps(state) { accounts: state.metamask.accounts, selectedAddress: state.metamask.selectedAddress, unconfTxs: state.metamask.unconfTxs, + unconfMsgs: state.metamask.unconfMsgs, index: state.appState.currentView.context, + warning: state.appState.warning, } } @@ -32,12 +34,11 @@ function ConfirmTxScreen() { ConfirmTxScreen.prototype.render = function() { 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 txParams = txData.txParams || {} - var address = txParams.from || state.selectedAddress - var identity = state.identities[address] || { address: address } - var account = state.accounts[address] || { address: address } return ( @@ -46,7 +47,7 @@ ConfirmTxScreen.prototype.render = function() { // subtitle and nav h('.section-title.flex-row.flex-center', [ 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'), ]), @@ -72,58 +73,44 @@ ConfirmTxScreen.prototype.render = function() { }), ]), + warningIfExists(state.warning), + h(ReactCSSTransitionGroup, { transitionName: "main", transitionEnterTimeout: 300, transitionLeaveTimeout: 300, }, [ - h('.transaction', { + currentTxView({ + // Properties + txData: txData, 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){ event.stopPropagation() this.props.dispatch(actions.sendTx(txData)) @@ -134,7 +121,25 @@ ConfirmTxScreen.prototype.cancelTransaction = function(txData, event){ 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() - 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) + } } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 57cdccbac..fb03ffeee 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -1,6 +1,7 @@ const extend = require('xtend') const actions = require('../actions') const valuesFor = require('../util').valuesFor +const txHelper = require('../../lib/tx-helper') module.exports = reduceApp @@ -127,10 +128,7 @@ function reduceApp(state, action) { case actions.GO_HOME: return extend(appState, { - currentView: { - name: 'accountDetail', - context: appState.currentView.context, - }, + currentView: {}, accountDetail: { accountExport: 'none', privateKey: '', @@ -185,9 +183,24 @@ function reduceApp(state, action) { warning: null, }) + case actions.SHOW_CONF_MSG_PAGE: + return extend(appState, { + currentView: { + name: 'confTx', + context: 0, + }, + transForward: true, + warning: null, + }) + case actions.COMPLETED_TX: - var unconfTxs = Object.keys(state.metamask.unconfTxs).filter(tx => tx !== tx.id) - if (unconfTxs && unconfTxs.length > 0) { + var unconfTxs = state.metamask.unconfTxs + var unconfMsgs = state.metamask.unconfMsgs + + var unconfTxList = txHelper(unconfTxs, unconfMsgs) + .filter(tx => tx !== tx.id) + + if (unconfTxList && unconfTxList.length > 0) { return extend(appState, { transForward: false, currentView: { @@ -202,7 +215,7 @@ function reduceApp(state, action) { warning: null, currentView: { name: 'accountDetail', - context: appState.currentView.context, + context: state.metamask.selectedAddress, }, }) } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 43bb3f761..2fe96c453 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -44,13 +44,19 @@ function reduceMetamask(state, action) { case actions.COMPLETED_TX: var stringId = String(action.id) var newState = extend(metamaskState, { - unconfTxs: {} + unconfTxs: {}, + unconfMsgs: {}, }) for (var id in metamaskState.unconfTxs) { if (id !== stringId) { newState.unconfTxs[id] = metamaskState.unconfTxs[id] } } + for (var id in metamaskState.unconfMsgs) { + if (id !== stringId) { + newState.unconfMsgs[id] = metamaskState.unconfMsgs[id] + } + } return newState case actions.CLEAR_SEED_WORD_CACHE: diff --git a/ui/index.js b/ui/index.js index 4ecce2fbe..d67c6f096 100644 --- a/ui/index.js +++ b/ui/index.js @@ -43,6 +43,11 @@ function startApp(metamaskState, accountManager, opts){ 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){ store.dispatch(actions.updateMetamaskState(metamaskState)) }) diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js new file mode 100644 index 000000000..49845b01a --- /dev/null +++ b/ui/lib/tx-helper.js @@ -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) +}