mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #2402 from chikeichan/merge
[NewUI] Merge master into NewUI-flat
This commit is contained in:
commit
776e8241f0
14
CHANGELOG.md
14
CHANGELOG.md
@ -2,7 +2,21 @@
|
|||||||
|
|
||||||
## Current Master
|
## Current Master
|
||||||
|
|
||||||
|
- Fix bug where web3 API was sometimes injected after the page loaded.
|
||||||
|
- Fix bug where imported accounts could not use new eth_signTypedData method.
|
||||||
|
|
||||||
|
## 3.11.0 2017-10-11
|
||||||
|
|
||||||
|
- Add support for new eth_signTypedData method per EIP 712.
|
||||||
|
- Fix bug where some transactions would be shown as pending forever, even after successfully mined.
|
||||||
|
- Fix bug where a transaction might be shown as pending forever if another tx with the same nonce was mined.
|
||||||
|
- Fix link to support article on token addresses.
|
||||||
|
|
||||||
|
## 3.10.9 2017-10-5
|
||||||
|
|
||||||
|
- Only rebrodcast transactions for a day not a days worth of blocks
|
||||||
- Remove Slack link from info page, since it is a big phishing target.
|
- Remove Slack link from info page, since it is a big phishing target.
|
||||||
|
- Stop computing balance based on pending transactions, to avoid edge case where users are unable to send transactions.
|
||||||
|
|
||||||
## 3.10.8 2017-9-28
|
## 3.10.8 2017-9-28
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "MetaMask",
|
"name": "MetaMask",
|
||||||
"short_name": "Metamask",
|
"short_name": "Metamask",
|
||||||
"version": "3.10.8",
|
"version": "3.11.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "https://metamask.io",
|
"author": "https://metamask.io",
|
||||||
"description": "Ethereum Browser Extension",
|
"description": "Ethereum Browser Extension",
|
||||||
|
@ -124,7 +124,8 @@ function setupController (initState) {
|
|||||||
var unapprovedTxCount = controller.txController.getUnapprovedTxCount()
|
var unapprovedTxCount = controller.txController.getUnapprovedTxCount()
|
||||||
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
|
var unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
|
||||||
var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount
|
var unapprovedPersonalMsgs = controller.personalMessageManager.unapprovedPersonalMsgCount
|
||||||
var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs
|
var unapprovedTypedMsgs = controller.typedMessageManager.unapprovedTypedMessagesCount
|
||||||
|
var count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgs + unapprovedTypedMsgs
|
||||||
if (count) {
|
if (count) {
|
||||||
label = String(count)
|
label = String(count)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,9 @@ const ObjectMultiplex = require('obj-multiplex')
|
|||||||
const extension = require('extensionizer')
|
const extension = require('extensionizer')
|
||||||
const PortStream = require('./lib/port-stream.js')
|
const PortStream = require('./lib/port-stream.js')
|
||||||
|
|
||||||
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
|
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'scripts', 'inpage.js')).toString()
|
||||||
|
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('scripts/inpage.js') + '\n'
|
||||||
|
const inpageBundle = inpageContent + inpageSuffix
|
||||||
|
|
||||||
// Eventually this streaming injection could be replaced with:
|
// Eventually this streaming injection could be replaced with:
|
||||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
|
||||||
@ -25,8 +27,7 @@ function setupInjection () {
|
|||||||
try {
|
try {
|
||||||
// inject in-page script
|
// inject in-page script
|
||||||
var scriptTag = document.createElement('script')
|
var scriptTag = document.createElement('script')
|
||||||
scriptTag.src = extension.extension.getURL('scripts/inpage.js')
|
scriptTag.textContent = inpageBundle
|
||||||
scriptTag.textContent = inpageText
|
|
||||||
scriptTag.onload = function () { this.parentNode.removeChild(this) }
|
scriptTag.onload = function () { this.parentNode.removeChild(this) }
|
||||||
var container = document.head || document.documentElement
|
var container = document.head || document.documentElement
|
||||||
// append as first child
|
// append as first child
|
||||||
|
@ -59,9 +59,10 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.pendingTxTracker = new PendingTransactionTracker({
|
this.pendingTxTracker = new PendingTransactionTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
nonceTracker: this.nonceTracker,
|
nonceTracker: this.nonceTracker,
|
||||||
retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
|
retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day.
|
||||||
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
|
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
|
||||||
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||||
|
getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
const promiseToCallback = require('promise-to-callback')
|
const promiseToCallback = require('promise-to-callback')
|
||||||
|
const noop = function(){}
|
||||||
|
|
||||||
module.exports = function nodeify (fn, context) {
|
module.exports = function nodeify (fn, context) {
|
||||||
return function(){
|
return function(){
|
||||||
const args = [].slice.call(arguments)
|
const args = [].slice.call(arguments)
|
||||||
const callback = args.pop()
|
const lastArg = args[args.length - 1]
|
||||||
if (typeof callback !== 'function') throw new Error('callback is not a function')
|
const lastArgIsCallback = typeof lastArg === 'function'
|
||||||
|
let callback
|
||||||
|
if (lastArgIsCallback) {
|
||||||
|
callback = lastArg
|
||||||
|
args.pop()
|
||||||
|
} else {
|
||||||
|
callback = noop
|
||||||
|
}
|
||||||
promiseToCallback(fn.apply(context, args))(callback)
|
promiseToCallback(fn.apply(context, args))(callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
this.query = new EthQuery(config.provider)
|
this.query = new EthQuery(config.provider)
|
||||||
this.nonceTracker = config.nonceTracker
|
this.nonceTracker = config.nonceTracker
|
||||||
this.retryLimit = config.retryLimit || Infinity
|
// default is one day
|
||||||
|
this.retryTimePeriod = config.retryTimePeriod || 86400000
|
||||||
this.getPendingTransactions = config.getPendingTransactions
|
this.getPendingTransactions = config.getPendingTransactions
|
||||||
|
this.getCompletedTransactions = config.getCompletedTransactions
|
||||||
this.publishTransaction = config.publishTransaction
|
this.publishTransaction = config.publishTransaction
|
||||||
|
this._checkPendingTxs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a signed tx is in a block and
|
// checks if a signed tx is in a block and
|
||||||
@ -99,8 +102,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _resubmitTx (txMeta) {
|
async _resubmitTx (txMeta) {
|
||||||
if (txMeta.retryCount > this.retryLimit) {
|
if (Date.now() > txMeta.time + this.retryTimePeriod) {
|
||||||
const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
|
const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1)
|
||||||
|
const err = new Error(`Gave up submitting after ${hours} hours.`)
|
||||||
return this.emit('tx:failed', txMeta.id, err)
|
return this.emit('tx:failed', txMeta.id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +122,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
async _checkPendingTx (txMeta) {
|
async _checkPendingTx (txMeta) {
|
||||||
const txHash = txMeta.hash
|
const txHash = txMeta.hash
|
||||||
const txId = txMeta.id
|
const txId = txMeta.id
|
||||||
|
|
||||||
// extra check in case there was an uncaught error during the
|
// extra check in case there was an uncaught error during the
|
||||||
// signature and submission process
|
// signature and submission process
|
||||||
if (!txHash) {
|
if (!txHash) {
|
||||||
@ -126,6 +131,15 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this.emit('tx:failed', txId, noTxHashErr)
|
this.emit('tx:failed', txId, noTxHashErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If another tx with the same nonce is mined, set as failed.
|
||||||
|
const taken = await this._checkIfNonceIsTaken(txMeta)
|
||||||
|
if (taken) {
|
||||||
|
const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
|
||||||
|
nonceTakenErr.name = 'NonceTakenErr'
|
||||||
|
return this.emit('tx:failed', txId, nonceTakenErr)
|
||||||
|
}
|
||||||
|
|
||||||
// get latest transaction status
|
// get latest transaction status
|
||||||
let txParams
|
let txParams
|
||||||
try {
|
try {
|
||||||
@ -157,4 +171,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
}
|
}
|
||||||
nonceGlobalLock.releaseLock()
|
nonceGlobalLock.releaseLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _checkIfNonceIsTaken (txMeta) {
|
||||||
|
const completed = this.getCompletedTransactions()
|
||||||
|
const sameNonce = completed.filter((otherMeta) => {
|
||||||
|
return otherMeta.txParams.nonce === txMeta.txParams.nonce
|
||||||
|
})
|
||||||
|
return sameNonce.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,12 @@ module.exports = class TransactionStateManger extends EventEmitter {
|
|||||||
return this.getFilteredTxList(opts)
|
return this.getFilteredTxList(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConfirmedTransactions (address) {
|
||||||
|
const opts = { status: 'confirmed' }
|
||||||
|
if (address) opts.from = address
|
||||||
|
return this.getFilteredTxList(opts)
|
||||||
|
}
|
||||||
|
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
this.once(`${txMeta.id}:signed`, function (txId) {
|
this.once(`${txMeta.id}:signed`, function (txId) {
|
||||||
this.removeAllListeners(`${txMeta.id}:rejected`)
|
this.removeAllListeners(`${txMeta.id}:rejected`)
|
||||||
@ -242,4 +248,4 @@ module.exports = class TransactionStateManger extends EventEmitter {
|
|||||||
_saveTxList (transactions) {
|
_saveTxList (transactions) {
|
||||||
this.store.updateState({ transactions })
|
this.store.updateState({ transactions })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
123
app/scripts/lib/typed-message-manager.js
Normal file
123
app/scripts/lib/typed-message-manager.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
const EventEmitter = require('events')
|
||||||
|
const ObservableStore = require('obs-store')
|
||||||
|
const createId = require('./random-id')
|
||||||
|
const assert = require('assert')
|
||||||
|
const sigUtil = require('eth-sig-util')
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = class TypedMessageManager extends EventEmitter {
|
||||||
|
constructor (opts) {
|
||||||
|
super()
|
||||||
|
this.memStore = new ObservableStore({
|
||||||
|
unapprovedTypedMessages: {},
|
||||||
|
unapprovedTypedMessagesCount: 0,
|
||||||
|
})
|
||||||
|
this.messages = []
|
||||||
|
}
|
||||||
|
|
||||||
|
get unapprovedTypedMessagesCount () {
|
||||||
|
return Object.keys(this.getUnapprovedMsgs()).length
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnapprovedMsgs () {
|
||||||
|
return this.messages.filter(msg => msg.status === 'unapproved')
|
||||||
|
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
addUnapprovedMessage (msgParams) {
|
||||||
|
this.validateParams(msgParams)
|
||||||
|
|
||||||
|
log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
|
||||||
|
// create txData obj with parameters and meta data
|
||||||
|
var time = (new Date()).getTime()
|
||||||
|
var msgId = createId()
|
||||||
|
var msgData = {
|
||||||
|
id: msgId,
|
||||||
|
msgParams: msgParams,
|
||||||
|
time: time,
|
||||||
|
status: 'unapproved',
|
||||||
|
type: 'eth_signTypedData',
|
||||||
|
}
|
||||||
|
this.addMsg(msgData)
|
||||||
|
|
||||||
|
// signal update
|
||||||
|
this.emit('update')
|
||||||
|
return msgId
|
||||||
|
}
|
||||||
|
|
||||||
|
validateParams (params) {
|
||||||
|
assert.equal(typeof params, 'object', 'Params should ben an object.')
|
||||||
|
assert.ok('data' in params, 'Params must include a data field.')
|
||||||
|
assert.ok('from' in params, 'Params must include a from field.')
|
||||||
|
assert.ok(Array.isArray(params.data), 'Data should be an array.')
|
||||||
|
assert.equal(typeof params.from, 'string', 'From field must be a string.')
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
sigUtil.typedSignatureHash(params.data)
|
||||||
|
}, 'Expected EIP712 typed data')
|
||||||
|
}
|
||||||
|
|
||||||
|
addMsg (msg) {
|
||||||
|
this.messages.push(msg)
|
||||||
|
this._saveMsgList()
|
||||||
|
}
|
||||||
|
|
||||||
|
getMsg (msgId) {
|
||||||
|
return this.messages.find(msg => msg.id === msgId)
|
||||||
|
}
|
||||||
|
|
||||||
|
approveMessage (msgParams) {
|
||||||
|
this.setMsgStatusApproved(msgParams.metamaskId)
|
||||||
|
return this.prepMsgForSigning(msgParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
setMsgStatusApproved (msgId) {
|
||||||
|
this._setMsgStatus(msgId, 'approved')
|
||||||
|
}
|
||||||
|
|
||||||
|
setMsgStatusSigned (msgId, rawSig) {
|
||||||
|
const msg = this.getMsg(msgId)
|
||||||
|
msg.rawSig = rawSig
|
||||||
|
this._updateMsg(msg)
|
||||||
|
this._setMsgStatus(msgId, 'signed')
|
||||||
|
}
|
||||||
|
|
||||||
|
prepMsgForSigning (msgParams) {
|
||||||
|
delete msgParams.metamaskId
|
||||||
|
return Promise.resolve(msgParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
rejectMsg (msgId) {
|
||||||
|
this._setMsgStatus(msgId, 'rejected')
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// PRIVATE METHODS
|
||||||
|
//
|
||||||
|
|
||||||
|
_setMsgStatus (msgId, status) {
|
||||||
|
const msg = this.getMsg(msgId)
|
||||||
|
if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".')
|
||||||
|
msg.status = status
|
||||||
|
this._updateMsg(msg)
|
||||||
|
this.emit(`${msgId}:${status}`, msg)
|
||||||
|
if (status === 'rejected' || status === 'signed') {
|
||||||
|
this.emit(`${msgId}:finished`, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateMsg (msg) {
|
||||||
|
const index = this.messages.findIndex((message) => message.id === msg.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.messages[index] = msg
|
||||||
|
}
|
||||||
|
this._saveMsgList()
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveMsgList () {
|
||||||
|
const unapprovedTypedMessages = this.getUnapprovedMsgs()
|
||||||
|
const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length
|
||||||
|
this.memStore.updateState({ unapprovedTypedMessages, unapprovedTypedMessagesCount })
|
||||||
|
this.emit('updateBadge')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -25,6 +25,7 @@ const InfuraController = require('./controllers/infura')
|
|||||||
const BlacklistController = require('./controllers/blacklist')
|
const BlacklistController = require('./controllers/blacklist')
|
||||||
const MessageManager = require('./lib/message-manager')
|
const MessageManager = require('./lib/message-manager')
|
||||||
const PersonalMessageManager = require('./lib/personal-message-manager')
|
const PersonalMessageManager = require('./lib/personal-message-manager')
|
||||||
|
const TypedMessageManager = require('./lib/typed-message-manager')
|
||||||
const TransactionController = require('./controllers/transactions')
|
const TransactionController = require('./controllers/transactions')
|
||||||
const BalancesController = require('./controllers/computed-balances')
|
const BalancesController = require('./controllers/computed-balances')
|
||||||
const ConfigManager = require('./lib/config-manager')
|
const ConfigManager = require('./lib/config-manager')
|
||||||
@ -161,6 +162,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
this.networkController.lookupNetwork()
|
this.networkController.lookupNetwork()
|
||||||
this.messageManager = new MessageManager()
|
this.messageManager = new MessageManager()
|
||||||
this.personalMessageManager = new PersonalMessageManager()
|
this.personalMessageManager = new PersonalMessageManager()
|
||||||
|
this.typedMessageManager = new TypedMessageManager()
|
||||||
this.publicConfigStore = this.initPublicConfigStore()
|
this.publicConfigStore = this.initPublicConfigStore()
|
||||||
|
|
||||||
// manual disk state subscriptions
|
// manual disk state subscriptions
|
||||||
@ -202,6 +204,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
this.balancesController.store.subscribe(this.sendUpdate.bind(this))
|
this.balancesController.store.subscribe(this.sendUpdate.bind(this))
|
||||||
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||||
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||||
|
this.typedMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||||
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
|
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
|
||||||
this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
|
this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
|
||||||
this.addressBookController.store.subscribe(this.sendUpdate.bind(this))
|
this.addressBookController.store.subscribe(this.sendUpdate.bind(this))
|
||||||
@ -239,6 +242,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
processMessage: this.newUnsignedMessage.bind(this),
|
processMessage: this.newUnsignedMessage.bind(this),
|
||||||
// personal_sign msg signing
|
// personal_sign msg signing
|
||||||
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
||||||
|
processTypedMessage: this.newUnsignedTypedMessage.bind(this),
|
||||||
}
|
}
|
||||||
const providerProxy = this.networkController.initializeProvider(providerOpts)
|
const providerProxy = this.networkController.initializeProvider(providerOpts)
|
||||||
return providerProxy
|
return providerProxy
|
||||||
@ -283,6 +287,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
this.txController.memStore.getState(),
|
this.txController.memStore.getState(),
|
||||||
this.messageManager.memStore.getState(),
|
this.messageManager.memStore.getState(),
|
||||||
this.personalMessageManager.memStore.getState(),
|
this.personalMessageManager.memStore.getState(),
|
||||||
|
this.typedMessageManager.memStore.getState(),
|
||||||
this.keyringController.memStore.getState(),
|
this.keyringController.memStore.getState(),
|
||||||
this.balancesController.store.getState(),
|
this.balancesController.store.getState(),
|
||||||
this.preferencesController.store.getState(),
|
this.preferencesController.store.getState(),
|
||||||
@ -365,6 +370,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
signPersonalMessage: nodeify(this.signPersonalMessage, this),
|
signPersonalMessage: nodeify(this.signPersonalMessage, this),
|
||||||
cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
|
cancelPersonalMessage: this.cancelPersonalMessage.bind(this),
|
||||||
|
|
||||||
|
// personalMessageManager
|
||||||
|
signTypedMessage: nodeify(this.signTypedMessage, this),
|
||||||
|
cancelTypedMessage: this.cancelTypedMessage.bind(this),
|
||||||
|
|
||||||
// notices
|
// notices
|
||||||
checkNotices: noticeController.updateNoticesList.bind(noticeController),
|
checkNotices: noticeController.updateNoticesList.bind(noticeController),
|
||||||
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
|
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
|
||||||
@ -557,6 +566,28 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newUnsignedTypedMessage (msgParams, cb) {
|
||||||
|
let msgId
|
||||||
|
try {
|
||||||
|
msgId = this.typedMessageManager.addUnapprovedMessage(msgParams)
|
||||||
|
this.sendUpdate()
|
||||||
|
this.opts.showUnconfirmedMessage()
|
||||||
|
} catch (e) {
|
||||||
|
return cb(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.typedMessageManager.once(`${msgId}:finished`, (data) => {
|
||||||
|
switch (data.status) {
|
||||||
|
case 'signed':
|
||||||
|
return cb(null, data.rawSig)
|
||||||
|
case 'rejected':
|
||||||
|
return cb(new Error('MetaMask Message Signature: User denied message signature.'))
|
||||||
|
default:
|
||||||
|
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
signMessage (msgParams, cb) {
|
signMessage (msgParams, cb) {
|
||||||
log.info('MetaMaskController - signMessage')
|
log.info('MetaMaskController - signMessage')
|
||||||
const msgId = msgParams.metamaskId
|
const msgId = msgParams.metamaskId
|
||||||
@ -619,6 +650,24 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signTypedMessage (msgParams) {
|
||||||
|
log.info('MetaMaskController - signTypedMessage')
|
||||||
|
const msgId = msgParams.metamaskId
|
||||||
|
// sets the status op the message to 'approved'
|
||||||
|
// and removes the metamaskId for signing
|
||||||
|
return this.typedMessageManager.approveMessage(msgParams)
|
||||||
|
.then((cleanMsgParams) => {
|
||||||
|
// signs the message
|
||||||
|
return this.keyringController.signTypedMessage(cleanMsgParams)
|
||||||
|
})
|
||||||
|
.then((rawSig) => {
|
||||||
|
// tells the listener that the message has been signed
|
||||||
|
// and can be returned to the dapp
|
||||||
|
this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
|
||||||
|
return this.getState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
cancelPersonalMessage (msgId, cb) {
|
cancelPersonalMessage (msgId, cb) {
|
||||||
const messageManager = this.personalMessageManager
|
const messageManager = this.personalMessageManager
|
||||||
messageManager.rejectMsg(msgId)
|
messageManager.rejectMsg(msgId)
|
||||||
@ -627,6 +676,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelTypedMessage (msgId, cb) {
|
||||||
|
const messageManager = this.typedMessageManager
|
||||||
|
messageManager.rejectMsg(msgId)
|
||||||
|
if (cb && typeof cb === 'function') {
|
||||||
|
cb(null, this.getState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
markAccountsFound (cb) {
|
markAccountsFound (cb) {
|
||||||
this.configManager.setLostAccounts([])
|
this.configManager.setLostAccounts([])
|
||||||
this.sendUpdate()
|
this.sendUpdate()
|
||||||
|
11
gulpfile.js
11
gulpfile.js
@ -157,7 +157,7 @@ gulp.task('copy:watch', function(){
|
|||||||
|
|
||||||
gulp.task('lint', function () {
|
gulp.task('lint', function () {
|
||||||
// Ignoring node_modules, dist/firefox, and docs folders:
|
// Ignoring node_modules, dist/firefox, and docs folders:
|
||||||
return gulp.src(['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js'])
|
return gulp.src(['app/**/*.js', 'ui/**/*.js', 'mascara/src/*.js', 'mascara/server/*.js', '!node_modules/**', '!dist/firefox/**', '!docs/**', '!app/scripts/chromereload.js', '!mascara/test/jquery-3.1.0.min.js'])
|
||||||
.pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
|
.pipe(eslint(fs.readFileSync(path.join(__dirname, '.eslintrc'))))
|
||||||
// eslint.format() outputs the lint results to the console.
|
// eslint.format() outputs the lint results to the console.
|
||||||
// Alternatively use eslint.formatEach() (see Docs).
|
// Alternatively use eslint.formatEach() (see Docs).
|
||||||
@ -217,8 +217,13 @@ jsFiles.forEach((jsFile) => {
|
|||||||
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` }))
|
gulp.task(`build:js:${jsFile}`, bundleTask({ watch: false, label: jsFile, filename: `${jsFile}.js` }))
|
||||||
})
|
})
|
||||||
|
|
||||||
gulp.task('dev:js', gulp.parallel(...jsDevStrings))
|
// inpage must be built before all other scripts:
|
||||||
gulp.task('build:js', gulp.parallel(...jsBuildStrings))
|
const firstDevString = jsDevStrings.shift()
|
||||||
|
gulp.task('dev:js', gulp.series(firstDevString, gulp.parallel(...jsDevStrings)))
|
||||||
|
|
||||||
|
// inpage must be built before all other scripts:
|
||||||
|
const firstBuildString = jsBuildStrings.shift()
|
||||||
|
gulp.task('build:js', gulp.series(firstBuildString, gulp.parallel(...jsBuildStrings)))
|
||||||
|
|
||||||
// disc bundle analyzer tasks
|
// disc bundle analyzer tasks
|
||||||
|
|
||||||
|
@ -7,20 +7,32 @@ async function loadProvider() {
|
|||||||
const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
|
const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
|
||||||
const ethQuery = new EthQuery(ethereumProvider)
|
const ethQuery = new EthQuery(ethereumProvider)
|
||||||
const accounts = await ethQuery.accounts()
|
const accounts = await ethQuery.accounts()
|
||||||
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
|
window.METAMASK_ACCOUNT = accounts[0] || 'locked'
|
||||||
setupButton(ethQuery)
|
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
|
||||||
|
setupButtons(ethQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function logToDom(message){
|
function logToDom(message, context){
|
||||||
document.getElementById('account').innerText = message
|
document.getElementById(context).innerText = message
|
||||||
console.log(message)
|
console.log(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupButton (ethQuery) {
|
function setupButtons (ethQuery) {
|
||||||
const button = document.getElementById('action-button-1')
|
const accountButton = document.getElementById('action-button-1')
|
||||||
button.addEventListener('click', async () => {
|
accountButton.addEventListener('click', async () => {
|
||||||
const accounts = await ethQuery.accounts()
|
const accounts = await ethQuery.accounts()
|
||||||
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
|
window.METAMASK_ACCOUNT = accounts[0] || 'locked'
|
||||||
|
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
|
||||||
|
})
|
||||||
|
const txButton = document.getElementById('action-button-2')
|
||||||
|
txButton.addEventListener('click', async () => {
|
||||||
|
if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return
|
||||||
|
const txHash = await ethQuery.sendTransaction({
|
||||||
|
from: window.METAMASK_ACCOUNT,
|
||||||
|
to: window.METAMASK_ACCOUNT,
|
||||||
|
data: '',
|
||||||
|
})
|
||||||
|
logToDom(txHash, 'cb-value')
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -10,6 +10,8 @@
|
|||||||
<body>
|
<body>
|
||||||
<button id="action-button-1">GET ACCOUNT</button>
|
<button id="action-button-1">GET ACCOUNT</button>
|
||||||
<div id="account"></div>
|
<div id="account"></div>
|
||||||
|
<button id="action-button-2">SEND TRANSACTION</button>
|
||||||
|
<div id="cb-value" ></div>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -5,7 +5,7 @@ const serveBundle = require('./util').serveBundle
|
|||||||
module.exports = createMetamascaraServer
|
module.exports = createMetamascaraServer
|
||||||
|
|
||||||
|
|
||||||
function createMetamascaraServer(){
|
function createMetamascaraServer () {
|
||||||
|
|
||||||
// start bundlers
|
// start bundlers
|
||||||
const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js')
|
const metamascaraBundle = createBundle(__dirname + '/../src/mascara.js')
|
||||||
@ -17,13 +17,13 @@ function createMetamascaraServer(){
|
|||||||
const server = express()
|
const server = express()
|
||||||
// ui window
|
// ui window
|
||||||
serveBundle(server, '/ui.js', uiBundle)
|
serveBundle(server, '/ui.js', uiBundle)
|
||||||
server.use(express.static(__dirname+'/../ui/'))
|
server.use(express.static(__dirname + '/../ui/'))
|
||||||
server.use(express.static(__dirname+'/../../dist/chrome'))
|
server.use(express.static(__dirname + '/../../dist/chrome'))
|
||||||
// metamascara
|
// metamascara
|
||||||
serveBundle(server, '/metamascara.js', metamascaraBundle)
|
serveBundle(server, '/metamascara.js', metamascaraBundle)
|
||||||
// proxy
|
// proxy
|
||||||
serveBundle(server, '/proxy/proxy.js', proxyBundle)
|
serveBundle(server, '/proxy/proxy.js', proxyBundle)
|
||||||
server.use('/proxy/', express.static(__dirname+'/../proxy'))
|
server.use('/proxy/', express.static(__dirname + '/../proxy'))
|
||||||
// background
|
// background
|
||||||
serveBundle(server, '/background.js', backgroundBuild)
|
serveBundle(server, '/background.js', backgroundBuild)
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function serveBundle(server, path, bundle){
|
function serveBundle (server, path, bundle) {
|
||||||
server.get(path, function(req, res){
|
server.get(path, function (req, res) {
|
||||||
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
|
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
|
||||||
res.send(bundle.latest)
|
res.send(bundle.latest)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBundle(entryPoint){
|
function createBundle (entryPoint) {
|
||||||
|
|
||||||
var bundleContainer = {}
|
var bundleContainer = {}
|
||||||
|
|
||||||
@ -30,8 +30,8 @@ function createBundle(entryPoint){
|
|||||||
|
|
||||||
return bundleContainer
|
return bundleContainer
|
||||||
|
|
||||||
function bundle() {
|
function bundle () {
|
||||||
bundler.bundle(function(err, result){
|
bundler.bundle(function (err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(`Bundle failed! (${entryPoint})`)
|
console.log(`Bundle failed! (${entryPoint})`)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@ -1,72 +1,60 @@
|
|||||||
global.window = global
|
global.window = global
|
||||||
const self = global
|
|
||||||
const pipe = require('pump')
|
|
||||||
|
|
||||||
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
|
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
|
||||||
const connectionListener = new SwGlobalListener(self)
|
const connectionListener = new SwGlobalListener(global)
|
||||||
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
|
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
|
||||||
const PortStream = require('../../app/scripts/lib/port-stream.js')
|
|
||||||
|
|
||||||
const DbController = require('idb-global')
|
const DbController = require('idb-global')
|
||||||
|
|
||||||
const SwPlatform = require('../../app/scripts/platforms/sw')
|
const SwPlatform = require('../../app/scripts/platforms/sw')
|
||||||
const MetamaskController = require('../../app/scripts/metamask-controller')
|
const MetamaskController = require('../../app/scripts/metamask-controller')
|
||||||
const extension = {} //require('../../app/scripts/lib/extension')
|
|
||||||
|
|
||||||
const storeTransform = require('obs-store/lib/transform')
|
|
||||||
const Migrator = require('../../app/scripts/lib/migrator/')
|
const Migrator = require('../../app/scripts/lib/migrator/')
|
||||||
const migrations = require('../../app/scripts/migrations/')
|
const migrations = require('../../app/scripts/migrations/')
|
||||||
const firstTimeState = require('../../app/scripts/first-time-state')
|
const firstTimeState = require('../../app/scripts/first-time-state')
|
||||||
|
|
||||||
const STORAGE_KEY = 'metamask-config'
|
const STORAGE_KEY = 'metamask-config'
|
||||||
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
|
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
|
||||||
let popupIsOpen = false
|
global.metamaskPopupIsOpen = false
|
||||||
let connectedClientCount = 0
|
|
||||||
|
|
||||||
const log = require('loglevel')
|
const log = require('loglevel')
|
||||||
global.log = log
|
global.log = log
|
||||||
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
|
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
|
||||||
|
|
||||||
self.addEventListener('install', function(event) {
|
global.addEventListener('install', function (event) {
|
||||||
event.waitUntil(self.skipWaiting())
|
event.waitUntil(global.skipWaiting())
|
||||||
})
|
})
|
||||||
self.addEventListener('activate', function(event) {
|
global.addEventListener('activate', function (event) {
|
||||||
event.waitUntil(self.clients.claim())
|
event.waitUntil(global.clients.claim())
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('inside:open')
|
log.debug('inside:open')
|
||||||
|
|
||||||
|
|
||||||
// // state persistence
|
// // state persistence
|
||||||
let diskStore
|
|
||||||
const dbController = new DbController({
|
const dbController = new DbController({
|
||||||
key: STORAGE_KEY,
|
key: STORAGE_KEY,
|
||||||
})
|
})
|
||||||
loadStateFromPersistence()
|
loadStateFromPersistence()
|
||||||
.then((initState) => setupController(initState))
|
.then((initState) => setupController(initState))
|
||||||
.then(() => console.log('MetaMask initialization complete.'))
|
.then(() => log.debug('MetaMask initialization complete.'))
|
||||||
.catch((err) => console.error('WHILE SETTING UP:', err))
|
.catch((err) => console.error('WHILE SETTING UP:', err))
|
||||||
|
|
||||||
// initialization flow
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// State and Persistence
|
// State and Persistence
|
||||||
//
|
//
|
||||||
function loadStateFromPersistence() {
|
async function loadStateFromPersistence () {
|
||||||
// migrations
|
// migrations
|
||||||
let migrator = new Migrator({ migrations })
|
const migrator = new Migrator({ migrations })
|
||||||
const initialState = migrator.generateInitialState(firstTimeState)
|
const initialState = migrator.generateInitialState(firstTimeState)
|
||||||
dbController.initialState = initialState
|
dbController.initialState = initialState
|
||||||
return dbController.open()
|
const versionedData = await dbController.open()
|
||||||
.then((versionedData) => migrator.migrateData(versionedData))
|
const migratedData = await migrator.migrateData(versionedData)
|
||||||
.then((versionedData) => {
|
await dbController.put(migratedData)
|
||||||
dbController.put(versionedData)
|
return migratedData.data
|
||||||
return Promise.resolve(versionedData)
|
|
||||||
})
|
|
||||||
.then((versionedData) => Promise.resolve(versionedData.data))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupController (initState, client) {
|
async function setupController (initState, client) {
|
||||||
|
|
||||||
//
|
//
|
||||||
// MetaMask Controller
|
// MetaMask Controller
|
||||||
@ -86,19 +74,19 @@ function setupController (initState, client) {
|
|||||||
})
|
})
|
||||||
global.metamaskController = controller
|
global.metamaskController = controller
|
||||||
|
|
||||||
controller.store.subscribe((state) => {
|
controller.store.subscribe(async (state) => {
|
||||||
versionifyData(state)
|
try {
|
||||||
.then((versionedData) => dbController.put(versionedData))
|
const versionedData = await versionifyData(state)
|
||||||
.catch((err) => {console.error(err)})
|
await dbController.put(versionedData)
|
||||||
|
} catch (e) { console.error('METAMASK Error:', e) }
|
||||||
})
|
})
|
||||||
function versionifyData(state) {
|
|
||||||
return dbController.get()
|
async function versionifyData (state) {
|
||||||
.then((rawData) => {
|
const rawData = await dbController.get()
|
||||||
return Promise.resolve({
|
return {
|
||||||
data: state,
|
data: state,
|
||||||
meta: rawData.meta,
|
meta: rawData.meta,
|
||||||
})}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@ -106,8 +94,7 @@ function setupController (initState, client) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
connectionListener.on('remote', (portStream, messageEvent) => {
|
connectionListener.on('remote', (portStream, messageEvent) => {
|
||||||
console.log('REMOTE CONECTION FOUND***********')
|
log.debug('REMOTE CONECTION FOUND***********')
|
||||||
connectedClientCount += 1
|
|
||||||
connectRemote(portStream, messageEvent.data.context)
|
connectRemote(portStream, messageEvent.data.context)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -116,7 +103,7 @@ function setupController (initState, client) {
|
|||||||
if (isMetaMaskInternalProcess) {
|
if (isMetaMaskInternalProcess) {
|
||||||
// communication with popup
|
// communication with popup
|
||||||
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
|
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
|
||||||
popupIsOpen = true
|
global.metamaskPopupIsOpen = true
|
||||||
} else {
|
} else {
|
||||||
// communication with page
|
// communication with page
|
||||||
setupUntrustedCommunication(connectionStream, context)
|
setupUntrustedCommunication(connectionStream, context)
|
||||||
@ -130,25 +117,14 @@ function setupController (initState, client) {
|
|||||||
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
|
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
|
||||||
controller.setupPublicConfig(mx.createStream('publicConfig'))
|
controller.setupPublicConfig(mx.createStream('publicConfig'))
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTrustedCommunication (connectionStream, originDomain) {
|
|
||||||
// setup multiplexing
|
|
||||||
var mx = setupMultiplex(connectionStream)
|
|
||||||
// connect features
|
|
||||||
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// User Interface setup
|
|
||||||
//
|
|
||||||
return Promise.resolve()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// // this will be useful later but commented out for linting for now (liiiinting)
|
||||||
|
// function sendMessageToAllClients (message) {
|
||||||
|
// global.clients.matchAll().then(function (clients) {
|
||||||
|
// clients.forEach(function (client) {
|
||||||
|
// client.postMessage(message)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
function sendMessageToAllClients (message) {
|
|
||||||
self.clients.matchAll().then(function(clients) {
|
|
||||||
clients.forEach(function(client) {
|
|
||||||
client.postMessage(message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function noop () {}
|
function noop () {}
|
||||||
|
@ -2,7 +2,7 @@ const createParentStream = require('iframe-stream').ParentStream
|
|||||||
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
|
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
|
||||||
const SwStream = require('sw-stream/lib/sw-stream.js')
|
const SwStream = require('sw-stream/lib/sw-stream.js')
|
||||||
|
|
||||||
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
|
const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
|
||||||
const background = new SWcontroller({
|
const background = new SWcontroller({
|
||||||
fileName: '/background.js',
|
fileName: '/background.js',
|
||||||
letBeIdle: false,
|
letBeIdle: false,
|
||||||
@ -12,7 +12,7 @@ const background = new SWcontroller({
|
|||||||
|
|
||||||
const pageStream = createParentStream()
|
const pageStream = createParentStream()
|
||||||
background.on('ready', () => {
|
background.on('ready', () => {
|
||||||
let swStream = SwStream({
|
const swStream = SwStream({
|
||||||
serviceWorker: background.controller,
|
serviceWorker: background.controller,
|
||||||
context: 'dapp',
|
context: 'dapp',
|
||||||
})
|
})
|
||||||
|
@ -17,17 +17,17 @@ var name = 'popup'
|
|||||||
window.METAMASK_UI_TYPE = name
|
window.METAMASK_UI_TYPE = name
|
||||||
window.METAMASK_PLATFORM_TYPE = 'mascara'
|
window.METAMASK_PLATFORM_TYPE = 'mascara'
|
||||||
|
|
||||||
let intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
|
const intervalDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
|
||||||
|
|
||||||
const background = new SWcontroller({
|
const background = new SWcontroller({
|
||||||
fileName: '/background.js',
|
fileName: '/background.js',
|
||||||
letBeIdle: false,
|
letBeIdle: false,
|
||||||
intervalDelay,
|
intervalDelay,
|
||||||
wakeUpInterval: 20000
|
wakeUpInterval: 20000,
|
||||||
})
|
})
|
||||||
// Setup listener for when the service worker is read
|
// Setup listener for when the service worker is read
|
||||||
const connectApp = function (readSw) {
|
const connectApp = function (readSw) {
|
||||||
let connectionStream = SwStream({
|
const connectionStream = SwStream({
|
||||||
serviceWorker: background.controller,
|
serviceWorker: background.controller,
|
||||||
context: name,
|
context: name,
|
||||||
})
|
})
|
||||||
@ -57,7 +57,7 @@ background.on('updatefound', windowReload)
|
|||||||
|
|
||||||
background.startWorker()
|
background.startWorker()
|
||||||
|
|
||||||
function windowReload() {
|
function windowReload () {
|
||||||
if (window.METAMASK_SKIP_RELOAD) return
|
if (window.METAMASK_SKIP_RELOAD) return
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
@ -66,4 +66,4 @@ function timeout (time) {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, time || 1500)
|
setTimeout(resolve, time || 1500)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
15
package.json
15
package.json
@ -75,19 +75,20 @@
|
|||||||
"ensnare": "^1.0.0",
|
"ensnare": "^1.0.0",
|
||||||
"eth-bin-to-ops": "^1.0.1",
|
"eth-bin-to-ops": "^1.0.1",
|
||||||
"eth-block-tracker": "^2.2.0",
|
"eth-block-tracker": "^2.2.0",
|
||||||
|
"eth-hd-keyring": "^1.2.1",
|
||||||
|
"eth-json-rpc-filters": "^1.2.2",
|
||||||
|
"eth-keyring-controller": "^2.1.0",
|
||||||
"eth-contract-metadata": "^1.1.5",
|
"eth-contract-metadata": "^1.1.5",
|
||||||
"eth-hd-keyring": "^1.1.1",
|
|
||||||
"eth-json-rpc-filters": "^1.2.1",
|
|
||||||
"eth-keyring-controller": "^2.0.0",
|
|
||||||
"eth-phishing-detect": "^1.1.4",
|
"eth-phishing-detect": "^1.1.4",
|
||||||
"eth-query": "^2.1.2",
|
"eth-query": "^2.1.2",
|
||||||
"eth-sig-util": "^1.2.2",
|
"eth-sig-util": "^1.4.0",
|
||||||
"eth-simple-keyring": "^1.1.1",
|
"eth-simple-keyring": "^1.2.0",
|
||||||
"eth-token-tracker": "^1.1.4",
|
"eth-token-tracker": "^1.1.4",
|
||||||
"ethereumjs-abi": "^0.6.4",
|
"ethereumjs-abi": "^0.6.4",
|
||||||
"ethereumjs-tx": "^1.3.0",
|
"ethereumjs-tx": "^1.3.0",
|
||||||
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
|
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
|
||||||
"ethereumjs-wallet": "^0.6.0",
|
"ethereumjs-wallet": "^0.6.0",
|
||||||
|
"etherscan-link": "^1.0.2",
|
||||||
"ethjs": "^0.2.8",
|
"ethjs": "^0.2.8",
|
||||||
"ethjs-contract": "^0.1.9",
|
"ethjs-contract": "^0.1.9",
|
||||||
"ethjs-ens": "^2.0.0",
|
"ethjs-ens": "^2.0.0",
|
||||||
@ -139,7 +140,7 @@
|
|||||||
"react-markdown": "^2.3.0",
|
"react-markdown": "^2.3.0",
|
||||||
"react-redux": "^5.0.5",
|
"react-redux": "^5.0.5",
|
||||||
"react-select": "^1.0.0-rc.2",
|
"react-select": "^1.0.0-rc.2",
|
||||||
"react-simple-file-input": "^1.0.0",
|
"react-simple-file-input": "^2.0.0",
|
||||||
"react-tooltip-component": "^0.3.0",
|
"react-tooltip-component": "^0.3.0",
|
||||||
"react-transition-group": "^2.2.0",
|
"react-transition-group": "^2.2.0",
|
||||||
"reactify": "^1.1.1",
|
"reactify": "^1.1.1",
|
||||||
@ -157,7 +158,7 @@
|
|||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"vreme": "^3.0.2",
|
"vreme": "^3.0.2",
|
||||||
"web3": "^0.20.1",
|
"web3": "^0.20.1",
|
||||||
"web3-provider-engine": "^13.3.1",
|
"web3-provider-engine": "^13.3.2",
|
||||||
"web3-stream-provider": "^3.0.1",
|
"web3-stream-provider": "^3.0.1",
|
||||||
"xtend": "^4.0.1"
|
"xtend": "^4.0.1"
|
||||||
},
|
},
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
var assert = require('assert')
|
|
||||||
var linkGen = require('../../ui/lib/account-link')
|
|
||||||
|
|
||||||
describe('account-link', function () {
|
|
||||||
it('adds ropsten prefix to ropsten test network', function () {
|
|
||||||
var result = linkGen('account', '3')
|
|
||||||
assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten included')
|
|
||||||
assert.notEqual(result.indexOf('account'), -1, 'account included')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds kovan prefix to kovan test network', function () {
|
|
||||||
var result = linkGen('account', '42')
|
|
||||||
assert.notEqual(result.indexOf('kovan'), -1, 'kovan included')
|
|
||||||
assert.notEqual(result.indexOf('account'), -1, 'account included')
|
|
||||||
})
|
|
||||||
})
|
|
@ -48,4 +48,42 @@ describe('BnInput', function () {
|
|||||||
checkValidity () { return true } },
|
checkValidity () { return true } },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('can tolerate wei precision', function (done) {
|
||||||
|
const renderer = ReactTestUtils.createRenderer()
|
||||||
|
|
||||||
|
let valueStr = '1000000000'
|
||||||
|
|
||||||
|
const value = new BN(valueStr, 10)
|
||||||
|
const inputStr = '1.000000001'
|
||||||
|
|
||||||
|
|
||||||
|
let targetStr = '1000000001'
|
||||||
|
|
||||||
|
const target = new BN(targetStr, 10)
|
||||||
|
|
||||||
|
const precision = 9 // gwei precision
|
||||||
|
const scale = 9
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
value,
|
||||||
|
scale,
|
||||||
|
precision,
|
||||||
|
onChange: (newBn) => {
|
||||||
|
assert.equal(newBn.toString(), target.toString(), 'should tolerate increase')
|
||||||
|
const reInput = BnInput.prototype.downsize(newBn.toString(), 9, 9)
|
||||||
|
assert.equal(reInput.toString(), inputStr, 'should tolerate increase')
|
||||||
|
done()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputComponent = h(BnInput, props)
|
||||||
|
const component = additions.renderIntoDocument(inputComponent)
|
||||||
|
renderer.render(inputComponent)
|
||||||
|
const input = additions.find(component, 'input.hex-input')[0]
|
||||||
|
ReactTestUtils.Simulate.change(input, { preventDefault () {}, target: {
|
||||||
|
value: inputStr,
|
||||||
|
checkValidity () { return true } },
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
var assert = require('assert')
|
|
||||||
var linkGen = require('../../ui/lib/explorer-link')
|
|
||||||
|
|
||||||
describe('explorer-link', function () {
|
|
||||||
it('adds ropsten prefix to ropsten test network', function () {
|
|
||||||
var result = linkGen('hash', '3')
|
|
||||||
assert.notEqual(result.indexOf('ropsten'), -1, 'ropsten injected')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds kovan prefix to kovan test network', function () {
|
|
||||||
var result = linkGen('hash', '42')
|
|
||||||
assert.notEqual(result.indexOf('kovan'), -1, 'kovan injected')
|
|
||||||
})
|
|
||||||
})
|
|
@ -18,14 +18,13 @@ describe('nodeify', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw if the last argument is not a function', function (done) {
|
it('should allow the last argument to not be a function', function (done) {
|
||||||
const nodified = nodeify(obj.promiseFunc, obj)
|
const nodified = nodeify(obj.promiseFunc, obj)
|
||||||
try {
|
try {
|
||||||
nodified('baz')
|
nodified('baz')
|
||||||
done(new Error('should have thrown if the last argument is not a function'))
|
|
||||||
} catch (err) {
|
|
||||||
assert.equal(err.message, 'callback is not a function')
|
|
||||||
done()
|
done()
|
||||||
|
} catch (err) {
|
||||||
|
done(new Error('should not have thrown if the last argument is not a function'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -5,6 +5,8 @@ const ObservableStore = require('obs-store')
|
|||||||
const clone = require('clone')
|
const clone = require('clone')
|
||||||
const { createStubedProvider } = require('../stub/provider')
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
|
||||||
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
|
const sinon = require('sinon')
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
const currentNetworkId = 42
|
const currentNetworkId = 42
|
||||||
const otherNetworkId = 36
|
const otherNetworkId = 36
|
||||||
@ -46,10 +48,60 @@ describe('PendingTransactionTracker', function () {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPendingTransactions: () => {return []},
|
getPendingTransactions: () => {return []},
|
||||||
|
getCompletedTransactions: () => {return []},
|
||||||
publishTransaction: () => {},
|
publishTransaction: () => {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('_checkPendingTx state management', function () {
|
||||||
|
let stub
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
if (stub) {
|
||||||
|
stub.restore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should become failed if another tx with the same nonce succeeds', async function () {
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
|
||||||
|
txGen.generate({
|
||||||
|
id: '456',
|
||||||
|
value: '0x01',
|
||||||
|
hash: '0xbad',
|
||||||
|
status: 'confirmed',
|
||||||
|
nonce: '0x01',
|
||||||
|
}, { count: 1 })
|
||||||
|
|
||||||
|
const pending = txGen.generate({
|
||||||
|
id: '123',
|
||||||
|
value: '0x02',
|
||||||
|
hash: '0xfad',
|
||||||
|
status: 'submitted',
|
||||||
|
nonce: '0x01',
|
||||||
|
}, { count: 1 })[0]
|
||||||
|
|
||||||
|
stub = sinon.stub(pendingTxTracker, 'getCompletedTransactions')
|
||||||
|
.returns(txGen.txs)
|
||||||
|
|
||||||
|
// THE EXPECTATION
|
||||||
|
const spy = sinon.spy()
|
||||||
|
pendingTxTracker.on('tx:failed', (txId, err) => {
|
||||||
|
assert.equal(txId, pending.id, 'should fail the pending tx')
|
||||||
|
assert.equal(err.name, 'NonceTakenErr', 'should emit a nonce taken error.')
|
||||||
|
spy(txId, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// THE METHOD
|
||||||
|
await pendingTxTracker._checkPendingTx(pending)
|
||||||
|
|
||||||
|
// THE ASSERTION
|
||||||
|
assert.ok(spy.calledWith(pending.id), 'tx failed should be emitted')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('#checkForTxInBlock', function () {
|
describe('#checkForTxInBlock', function () {
|
||||||
it('should return if no pending transactions', function () {
|
it('should return if no pending transactions', function () {
|
||||||
// throw a type error if it trys to do anything on the block
|
// throw a type error if it trys to do anything on the block
|
||||||
@ -239,4 +291,4 @@ describe('PendingTransactionTracker', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -118,6 +118,8 @@ var actions = {
|
|||||||
cancelMsg: cancelMsg,
|
cancelMsg: cancelMsg,
|
||||||
signPersonalMsg,
|
signPersonalMsg,
|
||||||
cancelPersonalMsg,
|
cancelPersonalMsg,
|
||||||
|
signTypedMsg,
|
||||||
|
cancelTypedMsg,
|
||||||
sendTx: sendTx,
|
sendTx: sendTx,
|
||||||
signTx: signTx,
|
signTx: signTx,
|
||||||
signTokenTx: signTokenTx,
|
signTokenTx: signTokenTx,
|
||||||
@ -462,6 +464,25 @@ function signPersonalMsg (msgData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signTypedMsg (msgData) {
|
||||||
|
log.debug('action - signTypedMsg')
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
|
|
||||||
|
log.debug(`actions calling background.signTypedMessage`)
|
||||||
|
background.signTypedMessage(msgData, (err, newState) => {
|
||||||
|
log.debug('signTypedMessage called back')
|
||||||
|
dispatch(actions.updateMetamaskState(newState))
|
||||||
|
dispatch(actions.hideLoadingIndication())
|
||||||
|
|
||||||
|
if (err) log.error(err)
|
||||||
|
if (err) return dispatch(actions.displayWarning(err.message))
|
||||||
|
|
||||||
|
dispatch(actions.completedTx(msgData.metamaskId))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function signTx (txData) {
|
function signTx (txData) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(actions.showLoadingIndication())
|
dispatch(actions.showLoadingIndication())
|
||||||
@ -633,6 +654,12 @@ function cancelPersonalMsg (msgData) {
|
|||||||
return actions.completedTx(id)
|
return actions.completedTx(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelTypedMsg (msgData) {
|
||||||
|
const id = msgData.id
|
||||||
|
background.cancelTypedMessage(id)
|
||||||
|
return actions.completedTx(id)
|
||||||
|
}
|
||||||
|
|
||||||
function cancelTx (txData) {
|
function cancelTx (txData) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
log.debug(`background.cancelTransaction`)
|
log.debug(`background.cancelTransaction`)
|
||||||
|
317
ui/app/components/account-dropdowns.js
Normal file
317
ui/app/components/account-dropdowns.js
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
const Component = require('react').Component
|
||||||
|
const PropTypes = require('react').PropTypes
|
||||||
|
const h = require('react-hyperscript')
|
||||||
|
const actions = require('../actions')
|
||||||
|
const genAccountLink = require('etherscan-link').createAccountLink
|
||||||
|
const connect = require('react-redux').connect
|
||||||
|
const Dropdown = require('./dropdown').Dropdown
|
||||||
|
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
|
||||||
|
const Identicon = require('./identicon')
|
||||||
|
const ethUtil = require('ethereumjs-util')
|
||||||
|
const copyToClipboard = require('copy-to-clipboard')
|
||||||
|
|
||||||
|
class AccountDropdowns extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
accountSelectorActive: false,
|
||||||
|
optionsMenuActive: false,
|
||||||
|
}
|
||||||
|
this.accountSelectorToggleClassName = 'accounts-selector'
|
||||||
|
this.optionsMenuToggleClassName = 'fa-ellipsis-h'
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAccounts () {
|
||||||
|
const { identities, selected, keyrings } = this.props
|
||||||
|
|
||||||
|
return Object.keys(identities).map((key, index) => {
|
||||||
|
const identity = identities[key]
|
||||||
|
const isSelected = identity.address === selected
|
||||||
|
|
||||||
|
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||||
|
|
||||||
|
const keyring = keyrings.find((kr) => {
|
||||||
|
return kr.accounts.includes(simpleAddress) ||
|
||||||
|
kr.accounts.includes(identity.address)
|
||||||
|
})
|
||||||
|
|
||||||
|
return h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => {
|
||||||
|
this.props.actions.showAccountDetail(identity.address)
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
marginTop: index === 0 ? '5px' : '',
|
||||||
|
fontSize: '24px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
Identicon,
|
||||||
|
{
|
||||||
|
address: identity.address,
|
||||||
|
diameter: 32,
|
||||||
|
style: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
this.indicateIfLoose(keyring),
|
||||||
|
h('span', {
|
||||||
|
style: {
|
||||||
|
marginLeft: '20px',
|
||||||
|
fontSize: '24px',
|
||||||
|
maxWidth: '145px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
}, identity.name || ''),
|
||||||
|
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
indicateIfLoose (keyring) {
|
||||||
|
try { // Sometimes keyrings aren't loaded yet:
|
||||||
|
const type = keyring.type
|
||||||
|
const isLoose = type !== 'HD Key Tree'
|
||||||
|
return isLoose ? h('.keyring-label', 'LOOSE') : null
|
||||||
|
} catch (e) { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAccountSelector () {
|
||||||
|
const { actions } = this.props
|
||||||
|
const { accountSelectorActive } = this.state
|
||||||
|
|
||||||
|
return h(
|
||||||
|
Dropdown,
|
||||||
|
{
|
||||||
|
useCssTransition: true, // Hardcoded because account selector is temporarily in app-header
|
||||||
|
style: {
|
||||||
|
marginLeft: '-238px',
|
||||||
|
marginTop: '38px',
|
||||||
|
minWidth: '180px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
maxHeight: '300px',
|
||||||
|
width: '300px',
|
||||||
|
},
|
||||||
|
innerStyle: {
|
||||||
|
padding: '8px 25px',
|
||||||
|
},
|
||||||
|
isOpen: accountSelectorActive,
|
||||||
|
onClickOutside: (event) => {
|
||||||
|
const { classList } = event.target
|
||||||
|
const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
|
||||||
|
if (accountSelectorActive && isNotToggleElement) {
|
||||||
|
this.setState({ accountSelectorActive: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
...this.renderAccounts(),
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => actions.addNewAccount(),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
Identicon,
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
diameter: 32,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, 'Create Account'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => actions.showImportPage(),
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
Identicon,
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
diameter: 32,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
h('span', {
|
||||||
|
style: {
|
||||||
|
marginLeft: '20px',
|
||||||
|
fontSize: '24px',
|
||||||
|
marginBottom: '5px',
|
||||||
|
},
|
||||||
|
}, 'Import Account'),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
renderAccountOptions () {
|
||||||
|
const { actions } = this.props
|
||||||
|
const { optionsMenuActive } = this.state
|
||||||
|
|
||||||
|
return h(
|
||||||
|
Dropdown,
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
marginLeft: '-215px',
|
||||||
|
minWidth: '180px',
|
||||||
|
},
|
||||||
|
isOpen: optionsMenuActive,
|
||||||
|
onClickOutside: () => {
|
||||||
|
const { classList } = event.target
|
||||||
|
const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
|
||||||
|
if (optionsMenuActive && isNotToggleElement) {
|
||||||
|
this.setState({ optionsMenuActive: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => {
|
||||||
|
const { selected, network } = this.props
|
||||||
|
const url = genAccountLink(selected, network)
|
||||||
|
global.platform.openWindow({ url })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'View account on Etherscan',
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => {
|
||||||
|
const { selected, identities } = this.props
|
||||||
|
var identity = identities[selected]
|
||||||
|
actions.showQrView(selected, identity ? identity.name : '')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show QR Code',
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => {
|
||||||
|
const { selected } = this.props
|
||||||
|
const checkSumAddress = selected && ethUtil.toChecksumAddress(selected)
|
||||||
|
copyToClipboard(checkSumAddress)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Copy Address to clipboard',
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
DropdownMenuItem,
|
||||||
|
{
|
||||||
|
closeMenu: () => {},
|
||||||
|
onClick: () => {
|
||||||
|
actions.requestAccountExport()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Export Private Key',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { style, enableAccountsSelector, enableAccountOptions } = this.props
|
||||||
|
const { optionsMenuActive, accountSelectorActive } = this.state
|
||||||
|
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: style,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enableAccountsSelector && h(
|
||||||
|
// 'i.fa.fa-angle-down',
|
||||||
|
'div.cursor-pointer.color-orange.accounts-selector',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
// fontSize: '1.8em',
|
||||||
|
background: 'url(images/switch_acc.svg) white center center no-repeat',
|
||||||
|
height: '25px',
|
||||||
|
width: '25px',
|
||||||
|
transform: 'scale(0.75)',
|
||||||
|
marginRight: '3px',
|
||||||
|
},
|
||||||
|
onClick: (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.setState({
|
||||||
|
accountSelectorActive: !accountSelectorActive,
|
||||||
|
optionsMenuActive: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.renderAccountSelector(),
|
||||||
|
),
|
||||||
|
enableAccountOptions && h(
|
||||||
|
'i.fa.fa-ellipsis-h',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
marginRight: '0.5em',
|
||||||
|
fontSize: '1.8em',
|
||||||
|
},
|
||||||
|
onClick: (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.setState({
|
||||||
|
accountSelectorActive: false,
|
||||||
|
optionsMenuActive: !optionsMenuActive,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.renderAccountOptions()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountDropdowns.defaultProps = {
|
||||||
|
enableAccountsSelector: false,
|
||||||
|
enableAccountOptions: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountDropdowns.propTypes = {
|
||||||
|
identities: PropTypes.objectOf(PropTypes.object),
|
||||||
|
selected: PropTypes.string,
|
||||||
|
keyrings: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
return {
|
||||||
|
actions: {
|
||||||
|
showConfigPage: () => dispatch(actions.showConfigPage()),
|
||||||
|
requestAccountExport: () => dispatch(actions.requestExportAccount()),
|
||||||
|
showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
|
||||||
|
addNewAccount: () => dispatch(actions.addNewAccount()),
|
||||||
|
showImportPage: () => dispatch(actions.showImportPage()),
|
||||||
|
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
|
||||||
|
}
|
@ -31,7 +31,7 @@ BnAsDecimalInput.prototype.render = function () {
|
|||||||
const suffix = props.suffix
|
const suffix = props.suffix
|
||||||
const style = props.style
|
const style = props.style
|
||||||
const valueString = value.toString(10)
|
const valueString = value.toString(10)
|
||||||
const newValue = this.downsize(valueString, scale, precision)
|
const newValue = this.downsize(valueString, scale)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
h('.flex-column', [
|
h('.flex-column', [
|
||||||
@ -145,14 +145,17 @@ BnAsDecimalInput.prototype.constructWarning = function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BnAsDecimalInput.prototype.downsize = function (number, scale, precision) {
|
BnAsDecimalInput.prototype.downsize = function (number, scale) {
|
||||||
// if there is no scaling, simply return the number
|
// if there is no scaling, simply return the number
|
||||||
if (scale === 0) {
|
if (scale === 0) {
|
||||||
return Number(number)
|
return Number(number)
|
||||||
} else {
|
} else {
|
||||||
// if the scale is the same as the precision, account for this edge case.
|
// if the scale is the same as the precision, account for this edge case.
|
||||||
var decimals = (scale === precision) ? -1 : scale - precision
|
var adjustedNumber = number
|
||||||
return Number(number.slice(0, -scale) + '.' + number.slice(-scale, decimals))
|
while (adjustedNumber.length < scale) {
|
||||||
|
adjustedNumber = '0' + adjustedNumber
|
||||||
|
}
|
||||||
|
return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
59
ui/app/components/pending-typed-msg-details.js
Normal file
59
ui/app/components/pending-typed-msg-details.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const Component = require('react').Component
|
||||||
|
const h = require('react-hyperscript')
|
||||||
|
const inherits = require('util').inherits
|
||||||
|
|
||||||
|
const AccountPanel = require('./account-panel')
|
||||||
|
const TypedMessageRenderer = require('./typed-message-renderer')
|
||||||
|
|
||||||
|
module.exports = PendingMsgDetails
|
||||||
|
|
||||||
|
inherits(PendingMsgDetails, Component)
|
||||||
|
function PendingMsgDetails () {
|
||||||
|
Component.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingMsgDetails.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 }
|
||||||
|
|
||||||
|
var { data } = msgParams
|
||||||
|
|
||||||
|
return (
|
||||||
|
h('div', {
|
||||||
|
key: msgData.id,
|
||||||
|
style: {
|
||||||
|
margin: '10px 20px',
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
|
||||||
|
// account that will sign
|
||||||
|
h(AccountPanel, {
|
||||||
|
showFullAddress: true,
|
||||||
|
identity: identity,
|
||||||
|
account: account,
|
||||||
|
imageifyIdenticons: state.imageifyIdenticons,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// message data
|
||||||
|
h('div', {
|
||||||
|
style: {
|
||||||
|
height: '260px',
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
h('label.font-small', { style: { display: 'block' } }, 'YOU ARE SIGNING'),
|
||||||
|
h(TypedMessageRenderer, {
|
||||||
|
value: data,
|
||||||
|
style: {
|
||||||
|
height: '215px',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
46
ui/app/components/pending-typed-msg.js
Normal file
46
ui/app/components/pending-typed-msg.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const Component = require('react').Component
|
||||||
|
const h = require('react-hyperscript')
|
||||||
|
const inherits = require('util').inherits
|
||||||
|
const PendingTxDetails = require('./pending-typed-msg-details')
|
||||||
|
|
||||||
|
module.exports = PendingMsg
|
||||||
|
|
||||||
|
inherits(PendingMsg, Component)
|
||||||
|
function PendingMsg () {
|
||||||
|
Component.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingMsg.prototype.render = function () {
|
||||||
|
var state = this.props
|
||||||
|
var msgData = state.txData
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
h('div', {
|
||||||
|
key: msgData.id,
|
||||||
|
}, [
|
||||||
|
|
||||||
|
// header
|
||||||
|
h('h3', {
|
||||||
|
style: {
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
}, 'Sign Message'),
|
||||||
|
|
||||||
|
// message details
|
||||||
|
h(PendingTxDetails, state),
|
||||||
|
|
||||||
|
// sign + cancel
|
||||||
|
h('.flex-row.flex-space-around', [
|
||||||
|
h('button', {
|
||||||
|
onClick: state.cancelTypedMessage,
|
||||||
|
}, 'Cancel'),
|
||||||
|
h('button', {
|
||||||
|
onClick: state.signTypedMessage,
|
||||||
|
}, 'Sign'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -3,7 +3,7 @@ const Component = require('react').Component
|
|||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const connect = require('react-redux').connect
|
const connect = require('react-redux').connect
|
||||||
const vreme = new (require('vreme'))()
|
const vreme = new (require('vreme'))()
|
||||||
const explorerLink = require('../../lib/explorer-link')
|
const explorerLink = require('etherscan-link').createExplorerLink
|
||||||
const actions = require('../actions')
|
const actions = require('../actions')
|
||||||
const addressSummary = require('../util').addressSummary
|
const addressSummary = require('../util').addressSummary
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const inherits = require('util').inherits
|
|||||||
|
|
||||||
const EthBalance = require('./eth-balance')
|
const EthBalance = require('./eth-balance')
|
||||||
const addressSummary = require('../util').addressSummary
|
const addressSummary = require('../util').addressSummary
|
||||||
const explorerLink = require('../../lib/explorer-link')
|
const explorerLink = require('etherscan-link').createExplorerLink
|
||||||
const CopyButton = require('./copyButton')
|
const CopyButton = require('./copyButton')
|
||||||
const vreme = new (require('vreme'))()
|
const vreme = new (require('vreme'))()
|
||||||
const Tooltip = require('./tooltip')
|
const Tooltip = require('./tooltip')
|
||||||
|
42
ui/app/components/typed-message-renderer.js
Normal file
42
ui/app/components/typed-message-renderer.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const Component = require('react').Component
|
||||||
|
const h = require('react-hyperscript')
|
||||||
|
const inherits = require('util').inherits
|
||||||
|
const extend = require('xtend')
|
||||||
|
|
||||||
|
module.exports = TypedMessageRenderer
|
||||||
|
|
||||||
|
inherits(TypedMessageRenderer, Component)
|
||||||
|
function TypedMessageRenderer () {
|
||||||
|
Component.call(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
TypedMessageRenderer.prototype.render = function () {
|
||||||
|
const props = this.props
|
||||||
|
const { value, style } = props
|
||||||
|
const text = renderTypedData(value)
|
||||||
|
|
||||||
|
const defaultStyle = extend({
|
||||||
|
width: '315px',
|
||||||
|
maxHeight: '210px',
|
||||||
|
resize: 'none',
|
||||||
|
border: 'none',
|
||||||
|
background: 'white',
|
||||||
|
padding: '3px',
|
||||||
|
overflow: 'scroll',
|
||||||
|
}, style)
|
||||||
|
|
||||||
|
return (
|
||||||
|
h('div.font-small', {
|
||||||
|
style: defaultStyle,
|
||||||
|
}, text)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTypedData(values) {
|
||||||
|
return values.map(function (value) {
|
||||||
|
return h('div', {}, [
|
||||||
|
h('strong', {style: {display: 'block', fontWeight: 'bold'}}, String(value.name) + ':'),
|
||||||
|
h('div', {}, value.value),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
@ -8,6 +8,7 @@ const txHelper = require('../lib/tx-helper')
|
|||||||
const PendingTx = require('./components/pending-tx')
|
const PendingTx = require('./components/pending-tx')
|
||||||
const PendingMsg = require('./components/pending-msg')
|
const PendingMsg = require('./components/pending-msg')
|
||||||
const PendingPersonalMsg = require('./components/pending-personal-msg')
|
const PendingPersonalMsg = require('./components/pending-personal-msg')
|
||||||
|
const PendingTypedMsg = require('./components/pending-typed-msg')
|
||||||
const Loading = require('./components/loading')
|
const Loading = require('./components/loading')
|
||||||
|
|
||||||
// const contentDivider = h('div', {
|
// const contentDivider = h('div', {
|
||||||
@ -29,6 +30,7 @@ function mapStateToProps (state) {
|
|||||||
unapprovedTxs: state.metamask.unapprovedTxs,
|
unapprovedTxs: state.metamask.unapprovedTxs,
|
||||||
unapprovedMsgs: state.metamask.unapprovedMsgs,
|
unapprovedMsgs: state.metamask.unapprovedMsgs,
|
||||||
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
|
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
|
||||||
|
unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
|
||||||
index: state.appState.currentView.context,
|
index: state.appState.currentView.context,
|
||||||
warning: state.appState.warning,
|
warning: state.appState.warning,
|
||||||
network: state.metamask.network,
|
network: state.metamask.network,
|
||||||
@ -53,13 +55,14 @@ ConfirmTxScreen.prototype.render = function () {
|
|||||||
currentCurrency,
|
currentCurrency,
|
||||||
unapprovedMsgs,
|
unapprovedMsgs,
|
||||||
unapprovedPersonalMsgs,
|
unapprovedPersonalMsgs,
|
||||||
|
unapprovedTypedMessages,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
blockGasLimit,
|
blockGasLimit,
|
||||||
// provider,
|
// provider,
|
||||||
// computedBalances,
|
// computedBalances,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network)
|
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
|
||||||
|
|
||||||
var txData = unconfTxList[props.index] || {}
|
var txData = unconfTxList[props.index] || {}
|
||||||
var txParams = txData.params || {}
|
var txParams = txData.params || {}
|
||||||
@ -122,6 +125,9 @@ function currentTxView (opts) {
|
|||||||
} else if (type === 'personal_sign') {
|
} else if (type === 'personal_sign') {
|
||||||
log.debug('rendering personal_sign message')
|
log.debug('rendering personal_sign message')
|
||||||
return h(PendingPersonalMsg, opts)
|
return h(PendingPersonalMsg, opts)
|
||||||
|
} else if (type === 'eth_signTypedData') {
|
||||||
|
log.debug('rendering eth_signTypedData message')
|
||||||
|
return h(PendingTypedMsg, opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return h(Loading, { isLoading: true })
|
return h(Loading, { isLoading: true })
|
||||||
@ -171,6 +177,14 @@ ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
|
|||||||
this.props.dispatch(actions.signPersonalMsg(params))
|
this.props.dispatch(actions.signPersonalMsg(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
|
||||||
|
log.info('conf-tx.js: signing typed message')
|
||||||
|
var params = msgData.msgParams
|
||||||
|
params.metamaskId = msgData.id
|
||||||
|
this.stopPropagation(event)
|
||||||
|
this.props.dispatch(actions.signTypedMsg(params))
|
||||||
|
}
|
||||||
|
|
||||||
ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
|
ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
|
||||||
log.info('canceling message')
|
log.info('canceling message')
|
||||||
this.stopPropagation(event)
|
this.stopPropagation(event)
|
||||||
@ -183,6 +197,12 @@ ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
|
|||||||
this.props.dispatch(actions.cancelPersonalMsg(msgData))
|
this.props.dispatch(actions.cancelPersonalMsg(msgData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
|
||||||
|
log.info('canceling typed message')
|
||||||
|
this.stopPropagation(event)
|
||||||
|
this.props.dispatch(actions.cancelTypedMsg(msgData))
|
||||||
|
}
|
||||||
|
|
||||||
ConfirmTxScreen.prototype.goHome = function (event) {
|
ConfirmTxScreen.prototype.goHome = function (event) {
|
||||||
this.stopPropagation(event)
|
this.stopPropagation(event)
|
||||||
this.props.dispatch(actions.goHome())
|
this.props.dispatch(actions.goHome())
|
||||||
|
@ -637,9 +637,9 @@ function checkUnconfActions (state) {
|
|||||||
|
|
||||||
function getUnconfActionList (state) {
|
function getUnconfActionList (state) {
|
||||||
const { unapprovedTxs, unapprovedMsgs,
|
const { unapprovedTxs, unapprovedMsgs,
|
||||||
unapprovedPersonalMsgs, network } = state.metamask
|
unapprovedPersonalMsgs, unapprovedTypedMessages, network } = state.metamask
|
||||||
|
|
||||||
const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network)
|
const unconfActionList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, unapprovedTypedMessages, network)
|
||||||
return unconfActionList
|
return unconfActionList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ function startApp (metamaskState, accountManager, opts) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// if unconfirmed txs, start on txConf page
|
// if unconfirmed txs, start on txConf page
|
||||||
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.network)
|
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
|
||||||
const numberOfUnapprivedTx = unapprovedTxsAll.length
|
const numberOfUnapprivedTx = unapprovedTxsAll.length
|
||||||
if (numberOfUnapprivedTx > 0) {
|
if (numberOfUnapprivedTx > 0) {
|
||||||
store.dispatch(actions.showConfTxPage({
|
store.dispatch(actions.showConfTxPage({
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
const prefixForNetwork = require('./etherscan-prefix-for-network')
|
|
||||||
|
|
||||||
module.exports = function (hash, network) {
|
|
||||||
const prefix = prefixForNetwork(network)
|
|
||||||
return `http://${prefix}etherscan.io/tx/${hash}`
|
|
||||||
}
|
|
@ -1,20 +1,27 @@
|
|||||||
const valuesFor = require('../app/util').valuesFor
|
const valuesFor = require('../app/util').valuesFor
|
||||||
|
|
||||||
module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) {
|
module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network) {
|
||||||
log.debug('tx-helper called with params:')
|
log.debug('tx-helper called with params:')
|
||||||
log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network })
|
log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, typedMessages, network })
|
||||||
|
|
||||||
const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs)
|
const txValues = network ? valuesFor(unapprovedTxs).filter(txMeta => txMeta.metamaskNetworkId === network) : valuesFor(unapprovedTxs)
|
||||||
log.debug(`tx helper found ${txValues.length} unapproved txs`)
|
log.debug(`tx helper found ${txValues.length} unapproved txs`)
|
||||||
|
|
||||||
const msgValues = valuesFor(unapprovedMsgs)
|
const msgValues = valuesFor(unapprovedMsgs)
|
||||||
log.debug(`tx helper found ${msgValues.length} unsigned messages`)
|
log.debug(`tx helper found ${msgValues.length} unsigned messages`)
|
||||||
let allValues = txValues.concat(msgValues)
|
let allValues = txValues.concat(msgValues)
|
||||||
|
|
||||||
const personalValues = valuesFor(personalMsgs)
|
const personalValues = valuesFor(personalMsgs)
|
||||||
log.debug(`tx helper found ${personalValues.length} unsigned personal messages`)
|
log.debug(`tx helper found ${personalValues.length} unsigned personal messages`)
|
||||||
allValues = allValues.concat(personalValues)
|
allValues = allValues.concat(personalValues)
|
||||||
|
|
||||||
|
const typedValues = valuesFor(typedMessages)
|
||||||
|
log.debug(`tx helper found ${typedValues.length} unsigned typed messages`)
|
||||||
|
allValues = allValues.concat(typedValues)
|
||||||
|
|
||||||
allValues = allValues.sort((a, b) => {
|
allValues = allValues.sort((a, b) => {
|
||||||
return a.time > b.time
|
return a.time > b.time
|
||||||
})
|
})
|
||||||
|
|
||||||
return allValues
|
return allValues
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user