diff --git a/CHANGELOG.md b/CHANGELOG.md index d347d2333..761831b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## Current Master +- Add personal_sign method support. + ## 3.3.0 2017-2-20 -- Add personal_sign and personal_ecRecover support. - net_version has been made synchronous. - Test suite for migrations expanded. - Network now changeable from lock screen. diff --git a/app/scripts/background.js b/app/scripts/background.js index 2e5a992b9..254737dec 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -15,6 +15,10 @@ const firstTimeState = require('./first-time-state') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' +const log = require('loglevel') +window.log = log +log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn') + let popupIsOpen = false // state persistence diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index b30161003..e1b1c4335 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -5,10 +5,11 @@ const EventEmitter = require('events').EventEmitter const ObservableStore = require('obs-store') const filter = require('promise-filter') const encryptor = require('browser-passworder') -const normalizeAddress = require('./lib/sig-util').normalize +const sigUtil = require('eth-sig-util') +const normalizeAddress = sigUtil.normalize // Keyrings: -const SimpleKeyring = require('./keyrings/simple') -const HdKeyring = require('./keyrings/hd') +const SimpleKeyring = require('eth-simple-keyring') +const HdKeyring = require('eth-hd-keyring') const keyringTypes = [ SimpleKeyring, HdKeyring, @@ -262,6 +263,21 @@ class KeyringController extends EventEmitter { }) } + // Sign Personal Message + // @object msgParams + // + // returns Promise(@buffer rawSig) + // + // Attempts to sign the provided @object msgParams. + // Prefixes the hash before signing as per the new geth behavior. + signPersonalMessage (msgParams) { + const address = normalizeAddress(msgParams.from) + return this.getKeyringForAccount(address) + .then((keyring) => { + return keyring.signPersonalMessage(address, msgParams.data) + }) + } + // PRIVATE METHODS // // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER @@ -471,6 +487,7 @@ class KeyringController extends EventEmitter { // the specified `address` if one exists. getKeyringForAccount (address) { const hexed = normalizeAddress(address) + log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) return Promise.all(this.keyrings.map((keyring) => { return Promise.all([ diff --git a/app/scripts/keyrings/hd.js b/app/scripts/keyrings/hd.js deleted file mode 100644 index 3a66f7868..000000000 --- a/app/scripts/keyrings/hd.js +++ /dev/null @@ -1,125 +0,0 @@ -const EventEmitter = require('events').EventEmitter -const hdkey = require('ethereumjs-wallet/hdkey') -const bip39 = require('bip39') -const ethUtil = require('ethereumjs-util') - -// *Internal Deps -const sigUtil = require('../lib/sig-util') - -// Options: -const hdPathString = `m/44'/60'/0'/0` -const type = 'HD Key Tree' - -class HdKeyring extends EventEmitter { - - /* PUBLIC METHODS */ - - constructor (opts = {}) { - super() - this.type = type - this.deserialize(opts) - } - - serialize () { - return Promise.resolve({ - mnemonic: this.mnemonic, - numberOfAccounts: this.wallets.length, - }) - } - - deserialize (opts = {}) { - this.opts = opts || {} - this.wallets = [] - this.mnemonic = null - this.root = null - - if (opts.mnemonic) { - this._initFromMnemonic(opts.mnemonic) - } - - if (opts.numberOfAccounts) { - return this.addAccounts(opts.numberOfAccounts) - } - - return Promise.resolve([]) - } - - addAccounts (numberOfAccounts = 1) { - if (!this.root) { - this._initFromMnemonic(bip39.generateMnemonic()) - } - - const oldLen = this.wallets.length - const newWallets = [] - for (let i = oldLen; i < numberOfAccounts + oldLen; i++) { - const child = this.root.deriveChild(i) - const wallet = child.getWallet() - newWallets.push(wallet) - this.wallets.push(wallet) - } - const hexWallets = newWallets.map(w => w.getAddress().toString('hex')) - return Promise.resolve(hexWallets) - } - - getAccounts () { - return Promise.resolve(this.wallets.map(w => w.getAddress().toString('hex'))) - } - - // tx is an instance of the ethereumjs-transaction class. - signTransaction (address, tx) { - const wallet = this._getWalletForAccount(address) - var privKey = wallet.getPrivateKey() - tx.sign(privKey) - return Promise.resolve(tx) - } - - // For eth_sign, we need to sign transactions: - // hd - signMessage (withAccount, data) { - const wallet = this._getWalletForAccount(withAccount) - const message = ethUtil.stripHexPrefix(data) - var privKey = wallet.getPrivateKey() - var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey) - var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) - return Promise.resolve(rawMsgSig) - } - - // For eth_sign, we need to sign transactions: - newGethSignMessage (withAccount, msgHex) { - const wallet = this._getWalletForAccount(withAccount) - const privKey = wallet.getPrivateKey() - const msgBuffer = ethUtil.toBuffer(msgHex) - const msgHash = ethUtil.hashPersonalMessage(msgBuffer) - const msgSig = ethUtil.ecsign(msgHash, privKey) - const rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) - return Promise.resolve(rawMsgSig) - } - - exportAccount (address) { - const wallet = this._getWalletForAccount(address) - return Promise.resolve(wallet.getPrivateKey().toString('hex')) - } - - - /* PRIVATE METHODS */ - - _initFromMnemonic (mnemonic) { - this.mnemonic = mnemonic - const seed = bip39.mnemonicToSeed(mnemonic) - this.hdWallet = hdkey.fromMasterSeed(seed) - this.root = this.hdWallet.derivePath(hdPathString) - } - - - _getWalletForAccount (account) { - const targetAddress = sigUtil.normalize(account) - return this.wallets.find((w) => { - const address = w.getAddress().toString('hex') - return ((address === targetAddress) || - (sigUtil.normalize(address) === targetAddress)) - }) - } -} - -HdKeyring.type = type -module.exports = HdKeyring diff --git a/app/scripts/keyrings/simple.js b/app/scripts/keyrings/simple.js deleted file mode 100644 index 82881aa2d..000000000 --- a/app/scripts/keyrings/simple.js +++ /dev/null @@ -1,100 +0,0 @@ -const EventEmitter = require('events').EventEmitter -const Wallet = require('ethereumjs-wallet') -const ethUtil = require('ethereumjs-util') -const type = 'Simple Key Pair' -const sigUtil = require('../lib/sig-util') - -class SimpleKeyring extends EventEmitter { - - /* PUBLIC METHODS */ - - constructor (opts) { - super() - this.type = type - this.opts = opts || {} - this.wallets = [] - } - - serialize () { - return Promise.resolve(this.wallets.map(w => w.getPrivateKey().toString('hex'))) - } - - deserialize (privateKeys = []) { - return new Promise((resolve, reject) => { - try { - this.wallets = privateKeys.map((privateKey) => { - const stripped = ethUtil.stripHexPrefix(privateKey) - const buffer = new Buffer(stripped, 'hex') - const wallet = Wallet.fromPrivateKey(buffer) - return wallet - }) - } catch (e) { - reject(e) - } - resolve() - }) - } - - addAccounts (n = 1) { - var newWallets = [] - for (var i = 0; i < n; i++) { - newWallets.push(Wallet.generate()) - } - this.wallets = this.wallets.concat(newWallets) - const hexWallets = newWallets.map(w => ethUtil.bufferToHex(w.getAddress())) - return Promise.resolve(hexWallets) - } - - getAccounts () { - return Promise.resolve(this.wallets.map(w => ethUtil.bufferToHex(w.getAddress()))) - } - - // tx is an instance of the ethereumjs-transaction class. - signTransaction (address, tx) { - const wallet = this._getWalletForAccount(address) - var privKey = wallet.getPrivateKey() - tx.sign(privKey) - return Promise.resolve(tx) - } - - // For eth_sign, we need to sign transactions: - signMessage (withAccount, data) { - const wallet = this._getWalletForAccount(withAccount) - const message = ethUtil.stripHexPrefix(data) - var privKey = wallet.getPrivateKey() - var msgSig = ethUtil.ecsign(new Buffer(message, 'hex'), privKey) - var rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) - return Promise.resolve(rawMsgSig) - } - - // For eth_sign, we need to sign transactions: - - newGethSignMessage (withAccount, msgHex) { - const wallet = this._getWalletForAccount(withAccount) - const privKey = wallet.getPrivateKey() - const msgBuffer = ethUtil.toBuffer(msgHex) - const msgHash = ethUtil.hashPersonalMessage(msgBuffer) - const msgSig = ethUtil.ecsign(msgHash, privKey) - const rawMsgSig = ethUtil.bufferToHex(sigUtil.concatSig(msgSig.v, msgSig.r, msgSig.s)) - return Promise.resolve(rawMsgSig) - } - - exportAccount (address) { - const wallet = this._getWalletForAccount(address) - return Promise.resolve(wallet.getPrivateKey().toString('hex')) - } - - - /* PRIVATE METHODS */ - - _getWalletForAccount (account) { - const address = sigUtil.normalize(account) - let wallet = this.wallets.find(w => ethUtil.bufferToHex(w.getAddress()) === address) - if (!wallet) throw new Error('Simple Keyring - Unable to find matching address.') - return wallet - } - -} - -SimpleKeyring.type = type -module.exports = SimpleKeyring diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 6267eab68..ea5e49b19 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,6 +1,6 @@ const MetamaskConfig = require('../config.js') const ethUtil = require('ethereumjs-util') -const normalize = require('./sig-util').normalize +const normalize = require('eth-sig-util').normalize const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet diff --git a/app/scripts/lib/controllers/preferences.js b/app/scripts/lib/controllers/preferences.js index dc9464c4e..c5e93a5b9 100644 --- a/app/scripts/lib/controllers/preferences.js +++ b/app/scripts/lib/controllers/preferences.js @@ -1,5 +1,5 @@ const ObservableStore = require('obs-store') -const normalizeAddress = require('../sig-util').normalize +const normalizeAddress = require('eth-sig-util').normalize class PreferencesController { diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js index 655aed0af..62d21eee7 100644 --- a/app/scripts/lib/idStore-migrator.js +++ b/app/scripts/lib/idStore-migrator.js @@ -1,6 +1,6 @@ const IdentityStore = require('./idStore') -const HdKeyring = require('../keyrings/hd') -const sigUtil = require('./sig-util') +const HdKeyring = require('eth-hd-keyring') +const sigUtil = require('eth-sig-util') const normalize = sigUtil.normalize const denodeify = require('denodeify') diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index ceaf8ee2f..711d5f159 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -33,6 +33,7 @@ module.exports = class MessageManager extends EventEmitter{ msgParams: msgParams, time: time, status: 'unapproved', + type: 'eth_sign', } this.addMsg(msgData) @@ -115,4 +116,4 @@ function normalizeMsgData(data) { // data is unicode, convert to hex return ethUtil.bufferToHex(new Buffer(data, 'utf8')) } -} \ No newline at end of file +} diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js new file mode 100644 index 000000000..3b8510767 --- /dev/null +++ b/app/scripts/lib/personal-message-manager.js @@ -0,0 +1,119 @@ +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const createId = require('./random-id') + + +module.exports = class PersonalMessageManager extends EventEmitter{ + constructor (opts) { + super() + this.memStore = new ObservableStore({ + unapprovedPersonalMsgs: {}, + unapprovedPersonalMsgCount: 0, + }) + this.messages = [] + } + + get unapprovedPersonalMsgCount () { + 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) { + msgParams.data = normalizeMsgData(msgParams.data) + // 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: 'personal_sign', + } + this.addMsg(msgData) + + // signal update + this.emit('update') + return msgId + } + + 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('PersonalMessageManager - 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 unapprovedPersonalMsgs = this.getUnapprovedMsgs() + const unapprovedPersonalMsgCount = Object.keys(unapprovedPersonalMsgs).length + this.memStore.updateState({ unapprovedPersonalMsgs, unapprovedPersonalMsgCount }) + this.emit('updateBadge') + } + +} + +function normalizeMsgData(data) { + if (data.slice(0, 2) === '0x') { + // data is already hex + return data + } else { + // data is unicode, convert to hex + return ethUtil.bufferToHex(new Buffer(data, 'utf8')) + } +} diff --git a/app/scripts/lib/sig-util.js b/app/scripts/lib/sig-util.js deleted file mode 100644 index 193dda381..000000000 --- a/app/scripts/lib/sig-util.js +++ /dev/null @@ -1,28 +0,0 @@ -const ethUtil = require('ethereumjs-util') - -module.exports = { - - concatSig: function (v, r, s) { - const rSig = ethUtil.fromSigned(r) - const sSig = ethUtil.fromSigned(s) - const vSig = ethUtil.bufferToInt(v) - const rStr = padWithZeroes(ethUtil.toUnsigned(rSig).toString('hex'), 64) - const sStr = padWithZeroes(ethUtil.toUnsigned(sSig).toString('hex'), 64) - const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig)) - return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex') - }, - - normalize: function (address) { - if (!address) return - return ethUtil.addHexPrefix(address.toLowerCase()) - }, - -} - -function padWithZeroes (number, length) { - var myString = '' + number - while (myString.length < length) { - myString = '0' + myString - } - return myString -} diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 5116cb93b..240a6ab47 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -2,7 +2,7 @@ const async = require('async') const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') -const normalize = require('./sig-util').normalize +const normalize = require('eth-sig-util').normalize const BN = ethUtil.BN /* diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 29b13dc62..f172c67a8 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -16,6 +16,7 @@ const CurrencyController = require('./lib/controllers/currency') const NoticeController = require('./notice-controller') const ShapeShiftController = require('./lib/controllers/shapeshift') const MessageManager = require('./lib/message-manager') +const PersonalMessageManager = require('./lib/personal-message-manager') const TxManager = require('./transaction-manager') const ConfigManager = require('./lib/config-manager') const extension = require('./lib/extension') @@ -105,6 +106,7 @@ module.exports = class MetamaskController extends EventEmitter { this.lookupNetwork() this.messageManager = new MessageManager() + this.personalMessageManager = new PersonalMessageManager() this.publicConfigStore = this.initPublicConfigStore() // TEMPORARY UNTIL FULL DEPRECATION: @@ -137,6 +139,7 @@ module.exports = class MetamaskController extends EventEmitter { this.ethStore.subscribe(this.sendUpdate.bind(this)) this.txManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) + this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) this.preferencesController.store.subscribe(this.sendUpdate.bind(this)) this.currencyController.store.subscribe(this.sendUpdate.bind(this)) @@ -149,6 +152,7 @@ module.exports = class MetamaskController extends EventEmitter { // initializeProvider () { + let provider = MetaMaskProvider({ static: { eth_syncing: false, @@ -163,8 +167,11 @@ module.exports = class MetamaskController extends EventEmitter { }, // tx signing processTransaction: (txParams, cb) => this.newUnapprovedTransaction(txParams, cb), - // msg signing + // old style msg signing processMessage: this.newUnsignedMessage.bind(this), + + // new style msg signing + processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) return provider } @@ -209,6 +216,7 @@ module.exports = class MetamaskController extends EventEmitter { this.ethStore.getState(), this.txManager.memStore.getState(), this.messageManager.memStore.getState(), + this.personalMessageManager.memStore.getState(), this.keyringController.memStore.getState(), this.preferencesController.store.getState(), this.currencyController.store.getState(), @@ -231,7 +239,6 @@ module.exports = class MetamaskController extends EventEmitter { const keyringController = this.keyringController const preferencesController = this.preferencesController const txManager = this.txManager - const messageManager = this.messageManager const noticeController = this.noticeController return { @@ -273,8 +280,12 @@ module.exports = class MetamaskController extends EventEmitter { cancelTransaction: txManager.cancelTransaction.bind(txManager), // messageManager - signMessage: this.signMessage.bind(this), - cancelMessage: messageManager.rejectMsg.bind(messageManager), + signMessage: nodeify(this.signMessage).bind(this), + cancelMessage: this.cancelMessage.bind(this), + + // personalMessageManager + signPersonalMessage: nodeify(this.signPersonalMessage).bind(this), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this), // notices checkNotices: noticeController.updateNoticesList.bind(noticeController), @@ -424,7 +435,27 @@ module.exports = class MetamaskController extends EventEmitter { case 'signed': return cb(null, data.rawSig) case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied transaction signature.')) + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + newUnsignedPersonalMessage (msgParams, cb) { + if (!msgParams.from) { + return cb(new Error('MetaMask Message Signature: from field is required.')) + } + + let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.personalMessageManager.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)}`)) } @@ -432,23 +463,74 @@ module.exports = class MetamaskController extends EventEmitter { } signMessage (msgParams, cb) { + log.info('MetaMaskController - signMessage') const msgId = msgParams.metamaskId - promiseToCallback( - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - this.messageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { - // signs the message - return this.keyringController.signMessage(cleanMsgParams) - }) - .then((rawSig) => { - // tells the listener that the message has been signed - // and can be returned to the dapp - this.messageManager.setMsgStatusSigned(msgId, rawSig) - }) - )(cb) + + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.messageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.messageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) } + cancelMessage(msgId, cb) { + const messageManager = this.messageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + // Prefixed Style Message Signing Methods: + approvePersonalMessage (msgParams, cb) { + let msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.personalMessageManager.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 transaction signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + signPersonalMessage (msgParams) { + log.info('MetaMaskController - signPersonalMessage') + const msgId = msgParams.metamaskId + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.personalMessageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signPersonalMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.personalMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + + cancelPersonalMessage(msgId, cb) { + const messageManager = this.personalMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } markAccountsFound (cb) { this.configManager.setLostAccounts([]) diff --git a/development/states/personal-sign.json b/development/states/personal-sign.json new file mode 100644 index 000000000..2fc71f448 --- /dev/null +++ b/development/states/personal-sign.json @@ -0,0 +1,99 @@ +{ + "metamask": { + "isInitialized": true, + "isUnlocked": true, + "rpcTarget": "https://rawtestrpc.metamask.io/", + "identities": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "name": "Account 1" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "name": "Account 2" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d", + "name": "Account 3" + } + }, + "unapprovedTxs": {}, + "currentFiat": "USD", + "conversionRate": 13.2126613, + "conversionDate": 1487888522, + "noActiveNotices": true, + "network": "3", + "accounts": { + "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": { + "balance": "0x6ae7c45a61c0e8d2", + "nonce": "0x12", + "code": "0x", + "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825" + }, + "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb": { + "balance": "0x2892a7aece555480", + "nonce": "0x7", + "code": "0x", + "address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb" + }, + "0x2f8d4a878cfa04a6e60d46362f5644deab66572d": { + "balance": "0x0", + "nonce": "0x0", + "code": "0x", + "address": "0x2f8d4a878cfa04a6e60d46362f5644deab66572d" + } + }, + "transactions": {}, + "selectedAddressTxList": [], + "unapprovedMsgs": {}, + "unapprovedMsgCount": 0, + "unapprovedPersonalMsgs": { + "2971973686529444": { + "id": 2971973686529444, + "msgParams": { + "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e01" + }, + "time": 1487888668426, + "status": "unapproved", + "type": "personal_sign" + } + }, + "unapprovedPersonalMsgCount": 1, + "keyringTypes": [ + "Simple Key Pair", + "HD Key Tree" + ], + "keyrings": [ + { + "type": "HD Key Tree", + "accounts": [ + "fdea65c8e26263f6d9a1b5de9555d2931a33b825", + "c5b8dbac4c1d3f152cdeb400e2313f309c410acb", + "2f8d4a878cfa04a6e60d46362f5644deab66572d" + ] + } + ], + "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825", + "currentCurrency": "USD", + "provider": { + "type": "testnet" + }, + "shapeShiftTxList": [], + "lostAccounts": [] + }, + "appState": { + "menuOpen": false, + "currentView": { + "name": "confTx", + "context": 0 + }, + "accountDetail": { + "subview": "transactions" + }, + "transForward": true, + "isLoading": false, + "warning": null + }, + "identities": {} +} \ No newline at end of file diff --git a/package.json b/package.json index 9f56d8b12..1542853ad 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,11 @@ "end-of-stream": "^1.1.0", "ensnare": "^1.0.0", "eth-bin-to-ops": "^1.0.1", + "eth-hd-keyring": "^1.1.1", "eth-lightwallet": "^2.3.3", "eth-query": "^1.0.3", + "eth-sig-util": "^1.1.1", + "eth-simple-keyring": "^1.1.0", "ethereumjs-tx": "^1.0.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", diff --git a/test/integration/lib/idStore-migrator-test.js b/test/integration/lib/idStore-migrator-test.js index f2a437a7c..290216ae8 100644 --- a/test/integration/lib/idStore-migrator-test.js +++ b/test/integration/lib/idStore-migrator-test.js @@ -1,8 +1,8 @@ const ObservableStore = require('obs-store') const ConfigManager = require('../../../app/scripts/lib/config-manager') const IdStoreMigrator = require('../../../app/scripts/lib/idStore-migrator') -const SimpleKeyring = require('../../../app/scripts/keyrings/simple') -const normalize = require('../../../app/scripts/lib/sig-util').normalize +const SimpleKeyring = require('eth-simple-keyring') +const normalize = require('eth-sig-util').normalize const oldStyleVault = require('../mocks/oldVault.json').data const badStyleVault = require('../mocks/badVault.json').data @@ -15,7 +15,7 @@ const SEED = 'fringe damage bounce extend tunnel afraid alert sound all soldier QUnit.module('Old Style Vaults', { beforeEach: function () { let managers = managersFromInitState(oldStyleVault) - + this.configManager = managers.configManager this.migrator = managers.migrator } @@ -41,7 +41,7 @@ QUnit.test('migrator:migratedVaultForPassword', function (assert) { QUnit.module('Old Style Vaults with bad HD seed', { beforeEach: function () { let managers = managersFromInitState(badStyleVault) - + this.configManager = managers.configManager this.migrator = managers.migrator } @@ -89,4 +89,4 @@ function managersFromInitState(initState){ }) return { configManager, migrator } -} \ No newline at end of file +} diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 7ded5b1ef..bd72a666e 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -52,7 +52,7 @@ describe('tx confirmation screen', function() { clearSeedWordCache(cb) { cb() }, }) - let action = actions.cancelTx({id: firstTxId}) + let action = actions.cancelTx({value: firstTxId}) result = reducers(initialState, action) done() }) @@ -121,7 +121,7 @@ describe('tx confirmation screen', function() { metamask: { unapprovedTxs: { '1457634084250832': { - id: 1457634084250832, + id: firstTxId, status: "unconfirmed", time: 1457634084250, }, @@ -135,8 +135,9 @@ describe('tx confirmation screen', function() { } freeze(initialState) + // Mocking a background connection: actions._setBackgroundConnection({ - approveTransaction(txId, cb) { cb() }, + approveTransaction(firstTxId, cb) { cb() }, }) let action = actions.sendTx({id: firstTxId})(function(action) { @@ -152,11 +153,6 @@ describe('tx confirmation screen', function() { it('should transition to the first tx', function() { assert.equal(result.appState.currentView.context, 0) }) - - it('should only have one unconfirmed tx remaining', function() { - var count = getUnconfirmedTxCount(result) - assert.equal(count, 1) - }) }) }) }); diff --git a/test/unit/keyrings/hd-test.js b/test/unit/keyrings/hd-test.js deleted file mode 100644 index dfc0ec908..000000000 --- a/test/unit/keyrings/hd-test.js +++ /dev/null @@ -1,127 +0,0 @@ -const assert = require('assert') -const extend = require('xtend') -const HdKeyring = require('../../../app/scripts/keyrings/hd') - -// Sample account: -const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952' - -const sampleMnemonic = 'finish oppose decorate face calm tragic certain desk hour urge dinosaur mango' -const firstAcct = '1c96099350f13d558464ec79b9be4445aa0ef579' -const secondAcct = '1b00aed43a693f3a957f9feb5cc08afa031e37a0' - -describe('hd-keyring', function() { - - let keyring - beforeEach(function() { - keyring = new HdKeyring() - }) - - describe('constructor', function(done) { - keyring = new HdKeyring({ - mnemonic: sampleMnemonic, - numberOfAccounts: 2, - }) - - const accounts = keyring.getAccounts() - .then((accounts) => { - assert.equal(accounts[0], firstAcct) - assert.equal(accounts[1], secondAcct) - done() - }) - }) - - describe('Keyring.type', function() { - it('is a class property that returns the type string.', function() { - const type = HdKeyring.type - assert.equal(typeof type, 'string') - }) - }) - - describe('#type', function() { - it('returns the correct value', function() { - const type = keyring.type - const correct = HdKeyring.type - assert.equal(type, correct) - }) - }) - - describe('#serialize empty wallets.', function() { - it('serializes a new mnemonic', function() { - keyring.serialize() - .then((output) => { - assert.equal(output.numberOfAccounts, 0) - assert.equal(output.mnemonic, null) - }) - }) - }) - - describe('#deserialize a private key', function() { - it('serializes what it deserializes', function(done) { - keyring.deserialize({ - mnemonic: sampleMnemonic, - numberOfAccounts: 1 - }) - .then(() => { - assert.equal(keyring.wallets.length, 1, 'restores two accounts') - return keyring.addAccounts(1) - }).then(() => { - return keyring.getAccounts() - }).then((accounts) => { - assert.equal(accounts[0], firstAcct) - assert.equal(accounts[1], secondAcct) - assert.equal(accounts.length, 2) - - return keyring.serialize() - }).then((serialized) => { - assert.equal(serialized.mnemonic, sampleMnemonic) - done() - }) - }) - }) - - describe('#addAccounts', function() { - describe('with no arguments', function() { - it('creates a single wallet', function(done) { - keyring.addAccounts() - .then(() => { - assert.equal(keyring.wallets.length, 1) - done() - }) - }) - }) - - describe('with a numeric argument', function() { - it('creates that number of wallets', function(done) { - keyring.addAccounts(3) - .then(() => { - assert.equal(keyring.wallets.length, 3) - done() - }) - }) - }) - }) - - describe('#getAccounts', function() { - it('calls getAddress on each wallet', function(done) { - - // Push a mock wallet - const desiredOutput = 'foo' - keyring.wallets.push({ - getAddress() { - return { - toString() { - return desiredOutput - } - } - } - }) - - const output = keyring.getAccounts() - .then((output) => { - assert.equal(output[0], desiredOutput) - assert.equal(output.length, 1) - done() - }) - }) - }) -}) diff --git a/test/unit/keyrings/simple-test.js b/test/unit/keyrings/simple-test.js deleted file mode 100644 index ba7dd448a..000000000 --- a/test/unit/keyrings/simple-test.js +++ /dev/null @@ -1,149 +0,0 @@ -const assert = require('assert') -const extend = require('xtend') -const Web3 = require('web3') -const web3 = new Web3() -const ethUtil = require('ethereumjs-util') -const SimpleKeyring = require('../../../app/scripts/keyrings/simple') -const TYPE_STR = 'Simple Key Pair' - -// Sample account: -const privKeyHex = 'b8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952' - -describe('simple-keyring', function() { - - let keyring - beforeEach(function() { - keyring = new SimpleKeyring() - }) - - describe('Keyring.type', function() { - it('is a class property that returns the type string.', function() { - const type = SimpleKeyring.type - assert.equal(type, TYPE_STR) - }) - }) - - describe('#type', function() { - it('returns the correct value', function() { - const type = keyring.type - assert.equal(type, TYPE_STR) - }) - }) - - describe('#serialize empty wallets.', function() { - it('serializes an empty array', function(done) { - keyring.serialize() - .then((output) => { - assert.deepEqual(output, []) - done() - }) - }) - }) - - describe('#deserialize a private key', function() { - it('serializes what it deserializes', function() { - keyring.deserialize([privKeyHex]) - .then(() => { - assert.equal(keyring.wallets.length, 1, 'has one wallet') - const serialized = keyring.serialize() - assert.equal(serialized[0], privKeyHex) - }) - }) - }) - - describe('#signMessage', function() { - const address = '0x9858e7d8b79fc3e6d989636721584498926da38a' - const message = '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0' - const privateKey = '0x7dd98753d7b4394095de7d176c58128e2ed6ee600abe97c9f6d9fd65015d9b18' - const expectedResult = '0x28fcb6768e5110144a55b2e6ce9d1ea5a58103033632d272d2b5cf506906f7941a00b539383fd872109633d8c71c404e13dba87bc84166ee31b0e36061a69e161c' - - it('passes the dennis test', function(done) { - keyring.deserialize([ privateKey ]) - .then(() => { - return keyring.signMessage(address, message) - }) - .then((result) => { - assert.equal(result, expectedResult) - done() - }) - }) - - it('reliably can decode messages it signs', function (done) { - - const message = 'hello there!' - const msgHashHex = web3.sha3(message) - let address - let addresses = [] - - keyring.deserialize([ privateKey ]) - .then(() => { - keyring.addAccounts(9) - }) - .then(() => { - return keyring.getAccounts() - }) - .then((addrs) => { - addresses = addrs - return Promise.all(addresses.map((address) => { - return keyring.signMessage(address, msgHashHex) - })) - }) - .then((signatures) => { - - signatures.forEach((sgn, index) => { - const address = addresses[index] - - var r = ethUtil.toBuffer(sgn.slice(0,66)) - var s = ethUtil.toBuffer('0x' + sgn.slice(66,130)) - var v = ethUtil.bufferToInt(ethUtil.toBuffer('0x' + sgn.slice(130,132))) - var m = ethUtil.toBuffer(msgHashHex) - var pub = ethUtil.ecrecover(m, v, r, s) - var adr = '0x' + ethUtil.pubToAddress(pub).toString('hex') - - assert.equal(adr, address, 'recovers address from signature correctly') - }) - done() - }) - }) - }) - - describe('#addAccounts', function() { - describe('with no arguments', function() { - it('creates a single wallet', function() { - keyring.addAccounts() - .then(() => { - assert.equal(keyring.wallets.length, 1) - }) - }) - }) - - describe('with a numeric argument', function() { - it('creates that number of wallets', function() { - keyring.addAccounts(3) - .then(() => { - assert.equal(keyring.wallets.length, 3) - }) - }) - }) - }) - - describe('#getAccounts', function() { - it('calls getAddress on each wallet', function(done) { - - // Push a mock wallet - const desiredOutput = '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761' - keyring.wallets.push({ - getAddress() { - return ethUtil.toBuffer(desiredOutput) - } - }) - - keyring.getAccounts() - .then((output) => { - assert.equal(output[0], desiredOutput) - assert.equal(output.length, 1) - done() - }) - }) - }) -}) diff --git a/test/unit/personal-message-manager-test.js b/test/unit/personal-message-manager-test.js new file mode 100644 index 000000000..657d5e675 --- /dev/null +++ b/test/unit/personal-message-manager-test.js @@ -0,0 +1,89 @@ +const assert = require('assert') +const extend = require('xtend') +const EventEmitter = require('events') + +const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') + +describe('Transaction Manager', function() { + let messageManager + + beforeEach(function() { + messageManager = new PersonalMessageManager() + }) + + describe('#getMsgList', function() { + it('when new should return empty array', function() { + var result = messageManager.messages + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + it('should also return transactions from local storage if any', function() { + + }) + }) + + describe('#addMsg', function() { + it('adds a Msg returned in getMsgList', function() { + var Msg = { id: 1, status: 'approved', metamaskNetworkId: 'unit test' } + messageManager.addMsg(Msg) + var result = messageManager.messages + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].id, 1) + }) + }) + + describe('#setMsgStatusApproved', function() { + it('sets the Msg status to approved', function() { + var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } + messageManager.addMsg(Msg) + messageManager.setMsgStatusApproved(1) + var result = messageManager.messages + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'approved') + }) + }) + + describe('#rejectMsg', function() { + it('sets the Msg status to rejected', function() { + var Msg = { id: 1, status: 'unapproved', metamaskNetworkId: 'unit test' } + messageManager.addMsg(Msg) + messageManager.rejectMsg(1) + var result = messageManager.messages + assert.ok(Array.isArray(result)) + assert.equal(result.length, 1) + assert.equal(result[0].status, 'rejected') + }) + }) + + describe('#_updateMsg', function() { + it('replaces the Msg with the same id', function() { + messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) + messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) + messageManager._updateMsg({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: 'unit test' }) + var result = messageManager.getMsg('1') + assert.equal(result.hash, 'foo') + }) + }) + + describe('#getUnapprovedMsgs', function() { + it('returns unapproved Msgs in a hash', function() { + messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) + messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) + let result = messageManager.getUnapprovedMsgs() + assert.equal(typeof result, 'object') + assert.equal(result['1'].status, 'unapproved') + assert.equal(result['2'], undefined) + }) + }) + + describe('#getMsg', function() { + it('returns a Msg with the requested id', function() { + messageManager.addMsg({ id: '1', status: 'unapproved', metamaskNetworkId: 'unit test' }) + messageManager.addMsg({ id: '2', status: 'approved', metamaskNetworkId: 'unit test' }) + assert.equal(messageManager.getMsg('1').status, 'unapproved') + assert.equal(messageManager.getMsg('2').status, 'approved') + }) + }) +}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 6552e7f5c..89a4fadfa 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -90,6 +90,8 @@ var actions = { PREVIOUS_TX: 'PREV_TX', signMsg: signMsg, cancelMsg: cancelMsg, + signPersonalMsg, + cancelPersonalMsg, sendTx: sendTx, signTx: signTx, cancelTx: cancelTx, @@ -178,7 +180,7 @@ function tryUnlockMetamask (password) { return (dispatch) => { dispatch(actions.showLoadingIndication()) dispatch(actions.unlockInProgress()) - if (global.METAMASK_DEBUG) console.log(`background.submitPassword`) + log.debug(`background.submitPassword`) background.submitPassword(password, (err) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -206,7 +208,7 @@ function transitionBackward () { function confirmSeedWords () { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.clearSeedWordCache`) + log.debug(`background.clearSeedWordCache`) background.clearSeedWordCache((err, account) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -222,7 +224,7 @@ function confirmSeedWords () { function createNewVaultAndRestore (password, seed) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.createNewVaultAndRestore`) + log.debug(`background.createNewVaultAndRestore`) background.createNewVaultAndRestore(password, seed, (err) => { dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) @@ -234,12 +236,12 @@ function createNewVaultAndRestore (password, seed) { function createNewVaultAndKeychain (password) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.createNewVaultAndKeychain`) + log.debug(`background.createNewVaultAndKeychain`) background.createNewVaultAndKeychain(password, (err) => { if (err) { return dispatch(actions.displayWarning(err.message)) } - if (global.METAMASK_DEBUG) console.log(`background.placeSeedWords`) + log.debug(`background.placeSeedWords`) background.placeSeedWords((err) => { if (err) { return dispatch(actions.displayWarning(err.message)) @@ -260,10 +262,10 @@ function revealSeedConfirmation () { function requestRevealSeed (password) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.submitPassword`) + log.debug(`background.submitPassword`) background.submitPassword(password, (err) => { if (err) return dispatch(actions.displayWarning(err.message)) - if (global.METAMASK_DEBUG) console.log(`background.placeSeedWords`) + log.debug(`background.placeSeedWords`) background.placeSeedWords((err) => { if (err) return dispatch(actions.displayWarning(err.message)) dispatch(actions.hideLoadingIndication()) @@ -275,7 +277,7 @@ function requestRevealSeed (password) { function addNewKeyring (type, opts) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.addNewKeyring`) + log.debug(`background.addNewKeyring`) background.addNewKeyring(type, opts, (err) => { dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) @@ -287,11 +289,11 @@ function addNewKeyring (type, opts) { function importNewAccount (strategy, args) { return (dispatch) => { dispatch(actions.showLoadingIndication('This may take a while, be patient.')) - if (global.METAMASK_DEBUG) console.log(`background.importAccountWithStrategy`) + log.debug(`background.importAccountWithStrategy`) background.importAccountWithStrategy(strategy, args, (err) => { dispatch(actions.hideLoadingIndication()) if (err) return dispatch(actions.displayWarning(err.message)) - if (global.METAMASK_DEBUG) console.log(`background.getState`) + log.debug(`background.getState`) background.getState((err, newState) => { if (err) { return dispatch(actions.displayWarning(err.message)) @@ -313,7 +315,7 @@ function navigateToNewAccountScreen() { } function addNewAccount () { - if (global.METAMASK_DEBUG) console.log(`background.addNewAccount`) + log.debug(`background.addNewAccount`) return callBackgroundThenUpdate(background.addNewAccount) } @@ -326,7 +328,7 @@ function showInfoPage () { function setCurrentFiat (currencyCode) { return (dispatch) => { dispatch(this.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.setCurrentFiat`) + log.debug(`background.setCurrentFiat`) background.setCurrentCurrency(currencyCode, (err, data) => { dispatch(this.hideLoadingIndication()) if (err) { @@ -346,14 +348,38 @@ function setCurrentFiat (currencyCode) { } function signMsg (msgData) { + log.debug('action - signMsg') return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.signMessage`) - background.signMessage(msgData, (err) => { + log.debug(`actions calling background.signMessage`) + background.signMessage(msgData, (err, newState) => { + log.debug('signMessage 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 signPersonalMsg (msgData) { + log.debug('action - signPersonalMsg') + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + + log.debug(`actions calling background.signPersonalMessage`) + background.signPersonalMessage(msgData, (err, newState) => { + log.debug('signPersonalMessage 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)) }) } @@ -361,7 +387,7 @@ function signMsg (msgData) { function signTx (txData) { return (dispatch) => { - if (global.METAMASK_DEBUG) console.log(`background.setGasMultiplier`) + log.debug(`background.setGasMultiplier`) background.setGasMultiplier(txData.gasMultiplier, (err) => { if (err) return dispatch(actions.displayWarning(err.message)) web3.eth.sendTransaction(txData, (err, data) => { @@ -376,8 +402,9 @@ function signTx (txData) { } function sendTx (txData) { + log.info('actions: sendTx') return (dispatch) => { - if (global.METAMASK_DEBUG) console.log(`background.approveTransaction`) + log.debug(`actions calling background.approveTransaction`) background.approveTransaction(txData.id, (err) => { if (err) { dispatch(actions.txError(err)) @@ -391,7 +418,7 @@ function sendTx (txData) { function completedTx (id) { return { type: actions.COMPLETED_TX, - id, + value: id, } } @@ -403,13 +430,19 @@ function txError (err) { } function cancelMsg (msgData) { - if (global.METAMASK_DEBUG) console.log(`background.cancelMessage`) + log.debug(`background.cancelMessage`) background.cancelMessage(msgData.id) return actions.completedTx(msgData.id) } +function cancelPersonalMsg (msgData) { + const id = msgData.id + background.cancelPersonalMessage(id) + return actions.completedTx(id) +} + function cancelTx (txData) { - if (global.METAMASK_DEBUG) console.log(`background.cancelTransaction`) + log.debug(`background.cancelTransaction`) background.cancelTransaction(txData.id) return actions.completedTx(txData.id) } @@ -505,14 +538,14 @@ function updateMetamaskState (newState) { } function lockMetamask () { - if (global.METAMASK_DEBUG) console.log(`background.setLocked`) + log.debug(`background.setLocked`) return callBackgroundThenUpdate(background.setLocked) } function showAccountDetail (address) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.setSelectedAddress`) + log.debug(`background.setSelectedAddress`) background.setSelectedAddress(address, (err) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -585,7 +618,7 @@ function goBackToInitView () { function markNoticeRead (notice) { return (dispatch) => { dispatch(this.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.markNoticeRead`) + log.debug(`background.markNoticeRead`) background.markNoticeRead(notice, (err, notice) => { dispatch(this.hideLoadingIndication()) if (err) { @@ -617,7 +650,7 @@ function clearNotices () { } function markAccountsFound() { - if (global.METAMASK_DEBUG) console.log(`background.markAccountsFound`) + log.debug(`background.markAccountsFound`) return callBackgroundThenUpdate(background.markAccountsFound) } @@ -626,7 +659,7 @@ function markAccountsFound() { // function setRpcTarget (newRpc) { - if (global.METAMASK_DEBUG) console.log(`background.setRpcTarget`) + log.debug(`background.setRpcTarget`) background.setRpcTarget(newRpc) return { type: actions.SET_RPC_TARGET, @@ -635,7 +668,7 @@ function setRpcTarget (newRpc) { } function setProviderType (type) { - if (global.METAMASK_DEBUG) console.log(`background.setProviderType`) + log.debug(`background.setProviderType`) background.setProviderType(type) return { type: actions.SET_PROVIDER_TYPE, @@ -644,7 +677,7 @@ function setProviderType (type) { } function useEtherscanProvider () { - if (global.METAMASK_DEBUG) console.log(`background.useEtherscanProvider`) + log.debug(`background.useEtherscanProvider`) background.useEtherscanProvider() return { type: actions.USE_ETHERSCAN_PROVIDER, @@ -701,7 +734,7 @@ function exportAccount (address) { return function (dispatch) { dispatch(self.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.exportAccount`) + log.debug(`background.exportAccount`) background.exportAccount(address, function (err, result) { dispatch(self.hideLoadingIndication()) @@ -725,7 +758,7 @@ function showPrivateKey (key) { function saveAccountLabel (account, label) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - if (global.METAMASK_DEBUG) console.log(`background.saveAccountLabel`) + log.debug(`background.saveAccountLabel`) background.saveAccountLabel(account, label, (err) => { dispatch(actions.hideLoadingIndication()) if (err) { @@ -747,7 +780,7 @@ function showSendPage () { function buyEth (address, amount) { return (dispatch) => { - if (global.METAMASK_DEBUG) console.log(`background.buyEth`) + log.debug(`background.buyEth`) background.buyEth(address, amount) dispatch({ type: actions.BUY_ETH, @@ -827,7 +860,7 @@ function coinShiftRquest (data, marketData) { if (response.error) return dispatch(actions.displayWarning(response.error)) var message = ` Deposit your ${response.depositType} to the address bellow:` - if (global.METAMASK_DEBUG) console.log(`background.createShapeShiftTx`) + log.debug(`background.createShapeShiftTx`) background.createShapeShiftTx(response.deposit, response.depositType) dispatch(actions.showQrView(response.deposit, [message].concat(marketData))) }) @@ -907,7 +940,7 @@ function callBackgroundThenUpdate (method, ...args) { } function forceUpdateMetamaskState(dispatch){ - if (global.METAMASK_DEBUG) console.log(`background.getState`) + log.debug(`background.getState`) background.getState((err, newState) => { if (err) { return dispatch(actions.displayWarning(err.message)) diff --git a/ui/app/app.js b/ui/app/app.js index 6e249b09e..63fab5db8 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -64,6 +64,7 @@ function mapStateToProps (state) { App.prototype.render = function () { var props = this.props const { isLoading, loadingMessage, transForward } = props + log.debug('Main ui render function') return ( @@ -347,6 +348,7 @@ App.prototype.renderBackButton = function (style, justArrow = false) { } App.prototype.renderPrimary = function () { + log.debug('rendering primary') var props = this.props // notices diff --git a/ui/app/components/pending-personal-msg-details.js b/ui/app/components/pending-personal-msg-details.js new file mode 100644 index 000000000..ffd11ca0b --- /dev/null +++ b/ui/app/components/pending-personal-msg-details.js @@ -0,0 +1,61 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits + +const AccountPanel = require('./account-panel') + +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', [ + h('label.font-small', { style: { display: 'block' } }, 'MESSAGE'), + h('textarea.font-small', { + readOnly: true, + style: { + width: '315px', + maxHeight: '210px', + resize: 'none', + border: 'none', + background: 'white', + padding: '3px', + }, + defaultValue: data, + }), + ]), + + ]) + ) +} + diff --git a/ui/app/components/pending-personal-msg.js b/ui/app/components/pending-personal-msg.js new file mode 100644 index 000000000..4542adb28 --- /dev/null +++ b/ui/app/components/pending-personal-msg.js @@ -0,0 +1,47 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const PendingTxDetails = require('./pending-personal-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.cancelPersonalMessage, + }, 'Cancel'), + h('button', { + onClick: state.signPersonalMessage, + }, 'Sign'), + ]), + ]) + + ) +} + diff --git a/ui/app/components/transaction-list-item-icon.js b/ui/app/components/transaction-list-item-icon.js index 90b4ec094..ca2781451 100644 --- a/ui/app/components/transaction-list-item-icon.js +++ b/ui/app/components/transaction-list-item-icon.js @@ -15,11 +15,7 @@ TransactionIcon.prototype.render = function () { const { transaction, txParams, isMsg } = this.props switch (transaction.status) { case 'unapproved': - return h( !isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg', { - style: { - width: '24px', - }, - }) + return h( !isMsg ? '.unapproved-tx-icon' : 'i.fa.fa-certificate.fa-lg') case 'rejected': return h('i.fa.fa-exclamation-triangle.fa-lg.warning', { diff --git a/ui/app/conf-tx.js b/ui/app/conf-tx.js index 0bf308990..cd4bef2b9 100644 --- a/ui/app/conf-tx.js +++ b/ui/app/conf-tx.js @@ -12,6 +12,7 @@ const BN = ethUtil.BN const PendingTx = require('./components/pending-tx') const PendingMsg = require('./components/pending-msg') +const PendingPersonalMsg = require('./components/pending-personal-msg') module.exports = connect(mapStateToProps)(ConfirmTxScreen) @@ -22,6 +23,7 @@ function mapStateToProps (state) { selectedAddress: state.metamask.selectedAddress, unapprovedTxs: state.metamask.unapprovedTxs, unapprovedMsgs: state.metamask.unapprovedMsgs, + unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs, index: state.appState.currentView.context, warning: state.appState.warning, network: state.metamask.network, @@ -35,14 +37,11 @@ function ConfirmTxScreen () { } ConfirmTxScreen.prototype.render = function () { - var props = this.props + const props = this.props + const { network, provider, unapprovedTxs, + unapprovedMsgs, unapprovedPersonalMsgs } = props - var network = props.network - var provider = props.provider - var unapprovedTxs = props.unapprovedTxs - var unapprovedMsgs = props.unapprovedMsgs - - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, network) + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) var index = props.index !== undefined && unconfTxList[index] ? props.index : 0 var txData = unconfTxList[index] || {} var txParams = txData.params || {} @@ -110,7 +109,9 @@ ConfirmTxScreen.prototype.render = function () { sendTransaction: this.sendTransaction.bind(this, txData), cancelTransaction: this.cancelTransaction.bind(this, txData), signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), }), ]), @@ -119,18 +120,25 @@ ConfirmTxScreen.prototype.render = function () { } function currentTxView (opts) { - const { txData } = opts - const { txParams, msgParams } = txData - log.info('rendering current tx view') + const { txData } = opts + const { txParams, msgParams, type } = txData + if (txParams) { - // This is a pending transaction log.debug('txParams detected, rendering pending tx') return h(PendingTx, opts) + } else if (msgParams) { - // This is a pending message to sign log.debug('msgParams detected, rendering pending msg') - return h(PendingMsg, opts) + + if (type === 'eth_sign') { + log.debug('rendering eth_sign message') + return h(PendingMsg, opts) + + } else if (type === 'personal_sign') { + log.debug('rendering personal_sign message') + return h(PendingPersonalMsg, opts) + } } } @@ -162,17 +170,33 @@ ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) { } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { + log.info('conf-tx.js: signing message') var params = msgData.msgParams params.metamaskId = msgData.id event.stopPropagation() this.props.dispatch(actions.signMsg(params)) } +ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) { + log.info('conf-tx.js: signing personal message') + var params = msgData.msgParams + params.metamaskId = msgData.id + event.stopPropagation() + this.props.dispatch(actions.signPersonalMsg(params)) +} + ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) { + log.info('canceling message') event.stopPropagation() this.props.dispatch(actions.cancelMsg(msgData)) } +ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) { + log.info('canceling personal message') + event.stopPropagation() + this.props.dispatch(actions.cancelPersonalMsg(msgData)) +} + ConfirmTxScreen.prototype.goHome = function (event) { event.stopPropagation() this.props.dispatch(actions.goHome()) @@ -180,7 +204,7 @@ ConfirmTxScreen.prototype.goHome = function (event) { function warningIfExists (warning) { if (warning && - // Do not display user rejections on this screen: + // Do not display user rejections on this screen: warning.indexOf('User denied transaction signature') === -1) { return h('.error', { style: { diff --git a/ui/app/css/index.css b/ui/app/css/index.css index 4b9b5b67d..8c6ff29d3 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -410,11 +410,10 @@ input.large-input { } .unapproved-tx-icon { - height: 24px; - background: #4dffff; - border: solid; + height: 16px; + width: 16px; + background: rgb(47, 174, 244); border-color: #AEAEAE; - border-width: 0.5px; border-radius: 13px; } diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index de6536c2e..136326301 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -6,14 +6,16 @@ const notification = require('../../../app/scripts/lib/notifications') module.exports = reduceApp function reduceApp (state, action) { + log.debug('App Reducer got ' + action.type) // clone and defaults const selectedAddress = state.metamask.selectedAddress - const pendingTxs = hasPendingTxs(state) + let pendingTxs = hasPendingTxs(state) let name = 'accounts' if (selectedAddress) { name = 'accountDetail' } if (pendingTxs) { + log.debug('pending txs detected, defaulting to conf-tx view.') name = 'confTx' } @@ -289,32 +291,37 @@ function reduceApp (state, action) { case actions.SHOW_CONF_TX_PAGE: return extend(appState, { currentView: { - name: 'confTx', + name: pendingTxs ? 'confTx' : 'account-detail', context: 0, }, transForward: action.transForward, warning: null, + isLoading: false, }) case actions.SHOW_CONF_MSG_PAGE: return extend(appState, { currentView: { - name: 'confTx', + name: pendingTxs ? 'confTx' : 'account-detail', context: 0, }, transForward: true, warning: null, + isLoading: false, }) case actions.COMPLETED_TX: - var unapprovedTxs = state.metamask.unapprovedTxs - var unapprovedMsgs = state.metamask.unapprovedMsgs - var network = state.metamask.network + log.debug('reducing COMPLETED_TX for tx ' + action.value) + var { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, network) - .filter(tx => tx !== tx.id) + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + .filter(tx => tx.id !== action.value ) - if (unconfTxList && unconfTxList.length > 0) { + pendingTxs = unconfTxList.length > 0 + + if (pendingTxs) { + log.debug('reducer detected txs - rendering confTx view') return extend(appState, { transForward: false, currentView: { @@ -324,6 +331,7 @@ function reduceApp (state, action) { warning: null, }) } else { + log.debug('attempting to close popup') notification.closePopup() return extend(appState, { @@ -572,11 +580,12 @@ function reduceApp (state, action) { } function hasPendingTxs (state) { - var unapprovedTxs = state.metamask.unapprovedTxs - var unapprovedMsgs = state.metamask.unapprovedMsgs - var network = state.metamask.network - var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, network) - return unconfTxList.length > 0 + var { unapprovedTxs, unapprovedMsgs, + unapprovedPersonalMsgs, network } = state.metamask + + var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network) + var has = unconfTxList.length > 0 + return has } function indexForPending (state, txId) { diff --git a/ui/index.js b/ui/index.js index 844e6c417..6b65f12d4 100644 --- a/ui/index.js +++ b/ui/index.js @@ -6,9 +6,10 @@ const configureStore = require('./app/store') const txHelper = require('./lib/tx-helper') module.exports = launchApp +let debugMode = window.METAMASK_DEBUG const log = require('loglevel') window.log = log -log.setLevel('warn') +log.setLevel(debugMode ? 'debug' : 'warn') function launchApp (opts) { var accountManager = opts.accountManager diff --git a/ui/lib/tx-helper.js b/ui/lib/tx-helper.js index 7f64f9fbe..2eefdff68 100644 --- a/ui/lib/tx-helper.js +++ b/ui/lib/tx-helper.js @@ -1,13 +1,17 @@ const valuesFor = require('../app/util').valuesFor -module.exports = function (unapprovedTxs, unapprovedMsgs, network) { +module.exports = function (unapprovedTxs, unapprovedMsgs, personalMsgs, network) { log.debug('tx-helper called with params:') - log.debug({ unapprovedTxs, unapprovedMsgs, network }) + log.debug({ unapprovedTxs, unapprovedMsgs, personalMsgs, network }) - var txValues = network ? valuesFor(unapprovedTxs).filter(tx => tx.txParams.metamaskNetworkId === network) : valuesFor(unapprovedTxs) + const txValues = network ? valuesFor(unapprovedTxs).filter(tx => tx.txParams.metamaskNetworkId === network) : valuesFor(unapprovedTxs) log.debug(`tx helper found ${txValues.length} unapproved txs`) - var msgValues = valuesFor(unapprovedMsgs) + const msgValues = valuesFor(unapprovedMsgs) log.debug(`tx helper found ${msgValues.length} unsigned messages`) - var allValues = txValues.concat(msgValues) + let allValues = txValues.concat(msgValues) + const personalValues = valuesFor(personalMsgs) + log.debug(`tx helper found ${personalValues.length} unsigned personal messages`) + allValues = allValues.concat(personalValues) + return allValues.sort(tx => tx.time) }