diff options
Diffstat (limited to 'app/scripts/lib')
25 files changed, 1124 insertions, 834 deletions
diff --git a/app/scripts/lib/auto-faucet.js b/app/scripts/lib/auto-faucet.js index 59cf0ec20..1e86f735e 100644 --- a/app/scripts/lib/auto-faucet.js +++ b/app/scripts/lib/auto-faucet.js @@ -1,6 +1,9 @@ -var uri = 'https://faucet.metamask.io/' +const uri = 'https://faucet.metamask.io/' +const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' +const env = process.env.METAMASK_ENV module.exports = function (address) { + if (METAMASK_DEBUG || env === 'test') return // Don't faucet in development or test var http = new XMLHttpRequest() var data = address http.open('POST', uri, true) diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index c4c8053f0..1302df35f 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -3,7 +3,7 @@ const ensnare = require('ensnare') module.exports = setupDappAutoReload -function setupDappAutoReload (web3, controlStream) { +function setupDappAutoReload (web3) { // export web3 as a global, checking for usage var pageIsUsingWeb3 = false var resetWasRequested = false @@ -16,19 +16,18 @@ function setupDappAutoReload (web3, controlStream) { global.web3 = web3 })) - // listen for reset requests from metamask - controlStream.once('data', function () { + return handleResetRequest + + function handleResetRequest () { resetWasRequested = true // ignore if web3 was not used if (!pageIsUsingWeb3) return // reload after short timeout - triggerReset() - }) - - // reload the page - function triggerReset () { - setTimeout(function () { - global.location.reload() - }, 500) + setTimeout(triggerReset, 500) } } + +// reload the page +function triggerReset () { + global.location.reload() +} diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index 715efb42e..6267eab68 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,11 +1,10 @@ -const Migrator = require('pojo-migrator') const MetamaskConfig = require('../config.js') -const migrations = require('./migrations') -const rp = require('request-promise') +const ethUtil = require('ethereumjs-util') +const normalize = require('./sig-util').normalize const TESTNET_RPC = MetamaskConfig.network.testnet const MAINNET_RPC = MetamaskConfig.network.mainnet -const txLimit = 40 +const MORDEN_RPC = MetamaskConfig.network.morden /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -16,54 +15,21 @@ const txLimit = 40 */ module.exports = ConfigManager function ConfigManager (opts) { - this.txLimit = txLimit - // ConfigManager is observable and will emit updates this._subs = [] - - /* The migrator exported on the config-manager - * has two methods the user should be concerned with: - * - * getData(), which returns the app-consumable data object - * saveData(), which persists the app-consumable data object. - */ - this.migrator = new Migrator({ - - // Migrations must start at version 1 or later. - // They are objects with a `version` number - // and a `migrate` function. - // - // The `migrate` function receives the previous - // config data format, and returns the new one. - migrations: migrations, - - // How to load initial config. - // Includes step on migrating pre-pojo-migrator data. - loadData: opts.loadData, - - // How to persist migrated config. - setData: opts.setData, - }) + this.store = opts.store } ConfigManager.prototype.setConfig = function (config) { - var data = this.migrator.getData() + var data = this.getData() data.config = config this.setData(data) this._emitUpdates(config) } ConfigManager.prototype.getConfig = function () { - var data = this.migrator.getData() - if ('config' in data) { - return data.config - } else { - return { - provider: { - type: 'testnet', - }, - } - } + var data = this.getData() + return data.config } ConfigManager.prototype.setRpcTarget = function (rpcUrl) { @@ -97,19 +63,40 @@ ConfigManager.prototype.getProvider = function () { } ConfigManager.prototype.setData = function (data) { - this.migrator.saveData(data) + this.store.putState(data) } ConfigManager.prototype.getData = function () { - return this.migrator.getData() + return this.store.getState() } ConfigManager.prototype.setWallet = function (wallet) { - var data = this.migrator.getData() + var data = this.getData() data.wallet = wallet this.setData(data) } +ConfigManager.prototype.setVault = function (encryptedString) { + var data = this.getData() + data.vault = encryptedString + this.setData(data) +} + +ConfigManager.prototype.getVault = function () { + var data = this.getData() + return data.vault +} + +ConfigManager.prototype.getKeychains = function () { + return this.getData().keychains || [] +} + +ConfigManager.prototype.setKeychains = function (keychains) { + var data = this.getData() + data.keychains = keychains + this.setData(data) +} + ConfigManager.prototype.getSelectedAccount = function () { var config = this.getConfig() return config.selectedAccount @@ -117,26 +104,38 @@ ConfigManager.prototype.getSelectedAccount = function () { ConfigManager.prototype.setSelectedAccount = function (address) { var config = this.getConfig() - config.selectedAccount = address + config.selectedAccount = ethUtil.addHexPrefix(address) this.setConfig(config) } ConfigManager.prototype.getWallet = function () { - return this.migrator.getData().wallet + return this.getData().wallet } // Takes a boolean ConfigManager.prototype.setShowSeedWords = function (should) { - var data = this.migrator.getData() + var data = this.getData() data.showSeedWords = should this.setData(data) } + ConfigManager.prototype.getShouldShowSeedWords = function () { - var data = this.migrator.getData() + var data = this.getData() return data.showSeedWords } +ConfigManager.prototype.setSeedWords = function (words) { + var data = this.getData() + data.seedWords = words + this.setData(data) +} + +ConfigManager.prototype.getSeedWords = function () { + var data = this.getData() + return data.seedWords +} + ConfigManager.prototype.getCurrentRpcAddress = function () { var provider = this.getProvider() if (!provider) return null @@ -148,21 +147,20 @@ ConfigManager.prototype.getCurrentRpcAddress = function () { case 'testnet': return TESTNET_RPC + case 'morden': + return MORDEN_RPC + default: return provider && provider.rpcTarget ? provider.rpcTarget : TESTNET_RPC } } -ConfigManager.prototype.setData = function (data) { - this.migrator.saveData(data) -} - // // Tx // ConfigManager.prototype.getTxList = function () { - var data = this.migrator.getData() + var data = this.getData() if (data.transactions !== undefined) { return data.transactions } else { @@ -170,61 +168,12 @@ ConfigManager.prototype.getTxList = function () { } } -ConfigManager.prototype.unconfirmedTxs = function () { - var transactions = this.getTxList() - return transactions.filter(tx => tx.status === 'unconfirmed') - .reduce((result, tx) => { result[tx.id] = tx; return result }, {}) -} - -ConfigManager.prototype._saveTxList = function (txList) { - var data = this.migrator.getData() +ConfigManager.prototype.setTxList = function (txList) { + var data = this.getData() data.transactions = txList this.setData(data) } -ConfigManager.prototype.addTx = function (tx) { - var transactions = this.getTxList() - while (transactions.length > this.txLimit - 1) { - transactions.shift() - } - transactions.push(tx) - this._saveTxList(transactions) -} - -ConfigManager.prototype.getTx = function (txId) { - var transactions = this.getTxList() - var matching = transactions.filter(tx => tx.id === txId) - return matching.length > 0 ? matching[0] : null -} - -ConfigManager.prototype.confirmTx = function (txId) { - this._setTxStatus(txId, 'confirmed') -} - -ConfigManager.prototype.rejectTx = function (txId) { - this._setTxStatus(txId, 'rejected') -} - -ConfigManager.prototype._setTxStatus = function (txId, status) { - var tx = this.getTx(txId) - tx.status = status - this.updateTx(tx) -} - -ConfigManager.prototype.updateTx = function (tx) { - var transactions = this.getTxList() - var found, index - transactions.forEach((otherTx, i) => { - if (otherTx.id === tx.id) { - found = true - index = i - } - }) - if (found) { - transactions[index] = tx - } - this._saveTxList(transactions) -} // wallet nickname methods @@ -235,13 +184,15 @@ ConfigManager.prototype.getWalletNicknames = function () { } ConfigManager.prototype.nicknameForWallet = function (account) { + const address = normalize(account) const nicknames = this.getWalletNicknames() - return nicknames[account] + return nicknames[address] } ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { + const address = normalize(account) const nicknames = this.getWalletNicknames() - nicknames[account] = nickname + nicknames[address] = nickname var data = this.getData() data.walletNicknames = nicknames this.setData(data) @@ -249,6 +200,17 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { // observable +ConfigManager.prototype.getSalt = function () { + var data = this.getData() + return data.salt +} + +ConfigManager.prototype.setSalt = function (salt) { + var data = this.getData() + data.salt = salt + this.setData(data) +} + ConfigManager.prototype.subscribe = function (fn) { this._subs.push(fn) var unsubscribe = this.unsubscribe.bind(this, fn) @@ -266,110 +228,25 @@ ConfigManager.prototype._emitUpdates = function (state) { }) } -ConfigManager.prototype.setConfirmed = function (confirmed) { +ConfigManager.prototype.getGasMultiplier = function () { var data = this.getData() - data.isConfirmed = confirmed - this.setData(data) + return data.gasMultiplier } -ConfigManager.prototype.getConfirmed = function () { +ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) { var data = this.getData() - return ('isConfirmed' in data) && data.isConfirmed -} -ConfigManager.prototype.setCurrentFiat = function (currency) { - var data = this.getData() - data.fiatCurrency = currency - this.setData(data) -} - -ConfigManager.prototype.getCurrentFiat = function () { - var data = this.getData() - return ('fiatCurrency' in data) && data.fiatCurrency -} - -ConfigManager.prototype.updateConversionRate = function () { - var data = this.getData() - return rp(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`) - .then((response) => { - const parsedResponse = JSON.parse(response) - this.setConversionPrice(parsedResponse.ticker.price) - this.setConversionDate(parsedResponse.timestamp) - }).catch((err) => { - console.error('Error in conversion.', err) - this.setConversionPrice(0) - this.setConversionDate('N/A') - }) - -} - -ConfigManager.prototype.setConversionPrice = function (price) { - var data = this.getData() - data.conversionRate = Number(price) + data.gasMultiplier = gasMultiplier this.setData(data) } -ConfigManager.prototype.setConversionDate = function (datestring) { +ConfigManager.prototype.setLostAccounts = function (lostAccounts) { var data = this.getData() - data.conversionDate = datestring + data.lostAccounts = lostAccounts this.setData(data) } -ConfigManager.prototype.getConversionRate = function () { +ConfigManager.prototype.getLostAccounts = function () { var data = this.getData() - return (('conversionRate' in data) && data.conversionRate) || 0 -} - -ConfigManager.prototype.getConversionDate = function () { - var data = this.getData() - return (('conversionDate' in data) && data.conversionDate) || 'N/A' -} - -ConfigManager.prototype.setShouldntShowWarning = function () { - var data = this.getData() - if (data.isEthConfirmed) { - data.isEthConfirmed = !data.isEthConfirmed - } else { - data.isEthConfirmed = true - } - this.setData(data) -} - -ConfigManager.prototype.getShouldntShowWarning = function () { - var data = this.getData() - return ('isEthConfirmed' in data) && data.isEthConfirmed -} - -ConfigManager.prototype.getShapeShiftTxList = function () { - var data = this.getData() - var shapeShiftTxList = data.shapeShiftTxList ? data.shapeShiftTxList : [] - shapeShiftTxList.forEach((tx) => { - if (tx.response.status !== 'complete') { - var requestListner = function (request) { - tx.response = JSON.parse(this.responseText) - if (tx.response.status === 'complete') { - tx.time = new Date().getTime() - } - } - - var shapShiftReq = new XMLHttpRequest() - shapShiftReq.addEventListener('load', requestListner) - shapShiftReq.open('GET', `https://shapeshift.io/txStat/${tx.depositAddress}`, true) - shapShiftReq.send() - } - }) - this.setData(data) - return shapeShiftTxList -} - -ConfigManager.prototype.createShapeShiftTx = function (depositAddress, depositType) { - var data = this.getData() - - var shapeShiftTx = {depositAddress, depositType, key: 'shapeshift', time: new Date().getTime(), response: {}} - if (!data.shapeShiftTxList) { - data.shapeShiftTxList = [shapeShiftTx] - } else { - data.shapeShiftTxList.push(shapeShiftTx) - } - this.setData(data) + return data.lostAccounts || [] } diff --git a/app/scripts/lib/controllers/currency.js b/app/scripts/lib/controllers/currency.js new file mode 100644 index 000000000..c4904f8ac --- /dev/null +++ b/app/scripts/lib/controllers/currency.js @@ -0,0 +1,70 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +// every ten minutes +const POLLING_INTERVAL = 600000 + +class CurrencyController { + + constructor (opts = {}) { + const initState = extend({ + currentCurrency: 'USD', + conversionRate: 0, + conversionDate: 'N/A', + }, opts.initState) + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + getCurrentCurrency () { + return this.store.getState().currentCurrency + } + + setCurrentCurrency (currentCurrency) { + this.store.updateState({ currentCurrency }) + } + + getConversionRate () { + return this.store.getState().conversionRate + } + + setConversionRate (conversionRate) { + this.store.updateState({ conversionRate }) + } + + getConversionDate () { + return this.store.getState().conversionDate + } + + setConversionDate (conversionDate) { + this.store.updateState({ conversionDate }) + } + + updateConversionRate () { + const currentCurrency = this.getCurrentCurrency() + return fetch(`https://www.cryptonator.com/api/ticker/eth-${currentCurrency}`) + .then(response => response.json()) + .then((parsedResponse) => { + this.setConversionRate(Number(parsedResponse.ticker.price)) + this.setConversionDate(Number(parsedResponse.timestamp)) + }).catch((err) => { + console.warn('MetaMask - Failed to query currency conversion.') + this.setConversionRate(0) + this.setConversionDate('N/A') + }) + } + + scheduleConversionInterval () { + if (this.conversionInterval) { + clearInterval(this.conversionInterval) + } + this.conversionInterval = setInterval(() => { + this.updateConversionRate() + }, POLLING_INTERVAL) + } +} + +module.exports = CurrencyController diff --git a/app/scripts/lib/controllers/preferences.js b/app/scripts/lib/controllers/preferences.js new file mode 100644 index 000000000..dc9464c4e --- /dev/null +++ b/app/scripts/lib/controllers/preferences.js @@ -0,0 +1,33 @@ +const ObservableStore = require('obs-store') +const normalizeAddress = require('../sig-util').normalize + +class PreferencesController { + + constructor (opts = {}) { + const initState = opts.initState || {} + this.store = new ObservableStore(initState) + } + + // + // PUBLIC METHODS + // + + setSelectedAddress(_address) { + return new Promise((resolve, reject) => { + const address = normalizeAddress(_address) + this.store.updateState({ selectedAddress: address }) + resolve() + }) + } + + getSelectedAddress(_address) { + return this.store.getState().selectedAddress + } + + // + // PRIVATE METHODS + // + +} + +module.exports = PreferencesController diff --git a/app/scripts/lib/controllers/shapeshift.js b/app/scripts/lib/controllers/shapeshift.js new file mode 100644 index 000000000..3d955c01f --- /dev/null +++ b/app/scripts/lib/controllers/shapeshift.js @@ -0,0 +1,104 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +// every three seconds when an incomplete tx is waiting +const POLLING_INTERVAL = 3000 + +class ShapeshiftController { + + constructor (opts = {}) { + const initState = extend({ + shapeShiftTxList: [], + }, opts.initState) + this.store = new ObservableStore(initState) + this.pollForUpdates() + } + + // + // PUBLIC METHODS + // + + getShapeShiftTxList () { + const shapeShiftTxList = this.store.getState().shapeShiftTxList + return shapeShiftTxList + } + + getPendingTxs () { + const txs = this.getShapeShiftTxList() + const pending = txs.filter(tx => tx.response && tx.response.status !== 'complete') + return pending + } + + pollForUpdates () { + const pendingTxs = this.getPendingTxs() + + if (pendingTxs.length === 0) { + return + } + + Promise.all(pendingTxs.map((tx) => { + return this.updateTx(tx) + })) + .then((results) => { + results.forEach(tx => this.saveTx(tx)) + this.timeout = setTimeout(this.pollForUpdates.bind(this), POLLING_INTERVAL) + }) + } + + updateTx (tx) { + const url = `https://shapeshift.io/txStat/${tx.depositAddress}` + return fetch(url) + .then((response) => { + return response.json() + }).then((json) => { + tx.response = json + if (tx.response.status === 'complete') { + tx.time = new Date().getTime() + } + return tx + }) + } + + saveTx (tx) { + const { shapeShiftTxList } = this.store.getState() + const index = shapeShiftTxList.indexOf(tx) + if (index !== -1) { + shapeShiftTxList[index] = tx + this.store.updateState({ shapeShiftTxList }) + } + } + + removeShapeShiftTx (tx) { + const { shapeShiftTxList } = this.store.getState() + const index = shapeShiftTxList.indexOf(index) + if (index !== -1) { + shapeShiftTxList.splice(index, 1) + } + this.updateState({ shapeShiftTxList }) + } + + createShapeShiftTx (depositAddress, depositType) { + const state = this.store.getState() + let { shapeShiftTxList } = state + + var shapeShiftTx = { + depositAddress, + depositType, + key: 'shapeshift', + time: new Date().getTime(), + response: {}, + } + + if (!shapeShiftTxList) { + shapeShiftTxList = [shapeShiftTx] + } else { + shapeShiftTxList.push(shapeShiftTx) + } + + this.store.updateState({ shapeShiftTxList }) + this.pollForUpdates() + } + +} + +module.exports = ShapeshiftController diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js new file mode 100644 index 000000000..8812a507b --- /dev/null +++ b/app/scripts/lib/eth-store.js @@ -0,0 +1,132 @@ +/* Ethereum Store + * + * This module is responsible for tracking any number of accounts + * and caching their current balances & transaction counts. + * + * It also tracks transaction hashes, and checks their inclusion status + * on each new block. + */ + +const async = require('async') +const EthQuery = require('eth-query') +const ObservableStore = require('obs-store') +function noop() {} + + +class EthereumStore extends ObservableStore { + + constructor (opts = {}) { + super({ + accounts: {}, + transactions: {}, + }) + this._provider = opts.provider + this._query = new EthQuery(this._provider) + this._blockTracker = opts.blockTracker + // subscribe to latest block + this._blockTracker.on('block', this._updateForBlock.bind(this)) + // blockTracker.currentBlock may be null + this._currentBlockNumber = this._blockTracker.currentBlock + } + + // + // public + // + + addAccount (address) { + const accounts = this.getState().accounts + accounts[address] = {} + this.updateState({ accounts }) + if (!this._currentBlockNumber) return + this._updateAccount(address) + } + + removeAccount (address) { + const accounts = this.getState().accounts + delete accounts[address] + this.updateState({ accounts }) + } + + addTransaction (txHash) { + const transactions = this.getState().transactions + transactions[txHash] = {} + this.updateState({ transactions }) + if (!this._currentBlockNumber) return + this._updateTransaction(this._currentBlockNumber, txHash, noop) + } + + removeTransaction (txHash) { + const transactions = this.getState().transactions + delete transactions[txHash] + this.updateState({ transactions }) + } + + + // + // private + // + + _updateForBlock (block) { + const blockNumber = '0x' + block.number.toString('hex') + this._currentBlockNumber = blockNumber + async.parallel([ + this._updateAccounts.bind(this), + this._updateTransactions.bind(this, blockNumber), + ], (err) => { + if (err) return console.error(err) + this.emit('block', this.getState()) + }) + } + + _updateAccounts (cb = noop) { + const accounts = this.getState().accounts + const addresses = Object.keys(accounts) + async.each(addresses, this._updateAccount.bind(this), cb) + } + + _updateAccount (address, cb = noop) { + const accounts = this.getState().accounts + this._getAccount(address, (err, result) => { + if (err) return cb(err) + result.address = address + // only populate if the entry is still present + if (accounts[address]) { + accounts[address] = result + this.updateState({ accounts }) + } + cb(null, result) + }) + } + + _updateTransactions (block, cb = noop) { + const transactions = this.getState().transactions + const txHashes = Object.keys(transactions) + async.each(txHashes, this._updateTransaction.bind(this, block), cb) + } + + _updateTransaction (block, txHash, cb = noop) { + // would use the block here to determine how many confirmations the tx has + const transactions = this.getState().transactions + this._query.getTransaction(txHash, (err, result) => { + if (err) return cb(err) + // only populate if the entry is still present + if (transactions[txHash]) { + transactions[txHash] = result + this.updateState({ transactions }) + } + cb(null, result) + }) + } + + _getAccount (address, cb = noop) { + const query = this._query + async.parallel({ + balance: query.getBalance.bind(query, address), + nonce: query.getTransactionCount.bind(query, address), + code: query.getCode.bind(query, address), + }, cb) + } + +} + +module.exports = EthereumStore
\ No newline at end of file diff --git a/app/scripts/lib/extension-instance.js b/app/scripts/lib/extension-instance.js index eb3b8a1e9..628b62e3f 100644 --- a/app/scripts/lib/extension-instance.js +++ b/app/scripts/lib/extension-instance.js @@ -42,10 +42,27 @@ function Extension () { } catch (e) {} try { + if (browser[api]) { + _this[api] = browser[api] + } + } catch (e) {} + try { _this.api = browser.extension[api] } catch (e) {} - }) + + try { + if (browser && browser.runtime) { + this.runtime = browser.runtime + } + } catch (e) {} + + try { + if (browser && browser.browserAction) { + this.browserAction = browser.browserAction + } + } catch (e) {} + } module.exports = Extension diff --git a/app/scripts/lib/id-management.js b/app/scripts/lib/id-management.js index 9b8ceb415..421f2105f 100644 --- a/app/scripts/lib/id-management.js +++ b/app/scripts/lib/id-management.js @@ -1,4 +1,13 @@ +/* ID Management + * + * This module exists to hold the decrypted credentials for the current session. + * It therefore exposes sign methods, because it is able to perform these + * with noa dditional authentication, because its very instantiation + * means the vault is unlocked. + */ + const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN const Transaction = require('ethereumjs-tx') module.exports = IdManagement @@ -16,9 +25,15 @@ function IdManagement (opts) { } this.signTx = function (txParams) { + // calculate gas with custom gas multiplier + var gasMultiplier = this.configManager.getGasMultiplier() || 1 + var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) + gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) + txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) // normalize values + txParams.to = ethUtil.addHexPrefix(txParams.to) - txParams.from = ethUtil.addHexPrefix(txParams.from) + txParams.from = ethUtil.addHexPrefix(txParams.from.toLowerCase()) txParams.value = ethUtil.addHexPrefix(txParams.value) txParams.data = ethUtil.addHexPrefix(txParams.data) txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas) @@ -43,7 +58,7 @@ function IdManagement (opts) { this.signMsg = function (address, message) { // sign message - var privKeyHex = this.exportPrivateKey(address) + var privKeyHex = this.exportPrivateKey(address.toLowerCase()) var privKey = ethUtil.toBuffer(privKeyHex) var msgSig = ethUtil.ecsign(new Buffer(message.replace('0x', ''), 'hex'), privKey) var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s)) diff --git a/app/scripts/lib/idStore-migrator.js b/app/scripts/lib/idStore-migrator.js new file mode 100644 index 000000000..655aed0af --- /dev/null +++ b/app/scripts/lib/idStore-migrator.js @@ -0,0 +1,80 @@ +const IdentityStore = require('./idStore') +const HdKeyring = require('../keyrings/hd') +const sigUtil = require('./sig-util') +const normalize = sigUtil.normalize +const denodeify = require('denodeify') + +module.exports = class IdentityStoreMigrator { + + constructor ({ configManager }) { + this.configManager = configManager + const hasOldVault = this.hasOldVault() + if (!hasOldVault) { + this.idStore = new IdentityStore({ configManager }) + } + } + + migratedVaultForPassword (password) { + const hasOldVault = this.hasOldVault() + const configManager = this.configManager + + if (!this.idStore) { + this.idStore = new IdentityStore({ configManager }) + } + + if (!hasOldVault) { + return Promise.resolve(null) + } + + const idStore = this.idStore + const submitPassword = denodeify(idStore.submitPassword.bind(idStore)) + + return submitPassword(password) + .then(() => { + const serialized = this.serializeVault() + return this.checkForLostAccounts(serialized) + }) + } + + serializeVault () { + const mnemonic = this.idStore._idmgmt.getSeed() + const numberOfAccounts = this.idStore._getAddresses().length + + return { + type: 'HD Key Tree', + data: { mnemonic, numberOfAccounts }, + } + } + + checkForLostAccounts (serialized) { + const hd = new HdKeyring() + return hd.deserialize(serialized.data) + .then((hexAccounts) => { + const newAccounts = hexAccounts.map(normalize) + const oldAccounts = this.idStore._getAddresses().map(normalize) + const lostAccounts = oldAccounts.reduce((result, account) => { + if (newAccounts.includes(account)) { + return result + } else { + result.push(account) + return result + } + }, []) + + return { + serialized, + lostAccounts: lostAccounts.map((address) => { + return { + address, + privateKey: this.idStore.exportAccount(address), + } + }), + } + }) + } + + hasOldVault () { + const wallet = this.configManager.getWallet() + return wallet + } +} diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index 7ac71e409..7a6968c6c 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -1,18 +1,14 @@ const EventEmitter = require('events').EventEmitter const inherits = require('util').inherits -const async = require('async') const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') -const LightwalletKeyStore = require('eth-lightwallet').keystore +const KeyStore = require('eth-lightwallet').keystore const clone = require('clone') const extend = require('xtend') -const createId = require('web3-provider-engine/util/random-id') -const ethBinToOps = require('eth-bin-to-ops') const autoFaucet = require('./auto-faucet') -const messageManager = require('./message-manager') const DEFAULT_RPC = 'https://testrpc.metamask.io/' const IdManagement = require('./id-management') + module.exports = IdentityStore inherits(IdentityStore, EventEmitter) @@ -33,28 +29,32 @@ function IdentityStore (opts = {}) { selectedAddress: null, identities: {}, } - // not part of serilized metamask state - only kept in memory - this._unconfTxCbs = {} - this._unconfMsgCbs = {} } // // public // -IdentityStore.prototype.createNewVault = function (password, entropy, cb) { +IdentityStore.prototype.createNewVault = function (password, cb) { delete this._keyStore + var serializedKeystore = this.configManager.getWallet() + + if (serializedKeystore) { + this.configManager.setData({}) + } - this._createIdmgmt(password, null, entropy, (err) => { + this.purgeCache() + this._createVault(password, null, (err) => { if (err) return cb(err) - this._loadIdentities() - this._didUpdate() this._autoFaucet() this.configManager.setShowSeedWords(true) var seedWords = this._idmgmt.getSeed() + + this._loadIdentities() + cb(null, seedWords) }) } @@ -67,11 +67,12 @@ IdentityStore.prototype.recoverSeed = function (cb) { } IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) { - this._createIdmgmt(password, seed, null, (err) => { + this.purgeCache() + + this._createVault(password, seed, (err) => { if (err) return cb(err) this._loadIdentities() - this._didUpdate() cb(null, this.getState()) }) } @@ -93,17 +94,8 @@ IdentityStore.prototype.getState = function () { isInitialized: !!configManager.getWallet() && !seedWords, isUnlocked: this._isUnlocked(), seedWords: seedWords, - isConfirmed: configManager.getConfirmed(), - isEthConfirmed: configManager.getShouldntShowWarning(), - unconfTxs: configManager.unconfirmedTxs(), - transactions: configManager.getTxList(), - unconfMsgs: messageManager.unconfirmedMsgs(), - messages: messageManager.getMsgList(), selectedAddress: configManager.getSelectedAccount(), - shapeShiftTxList: configManager.getShapeShiftTxList(), - currentFiat: configManager.getCurrentFiat(), - conversionRate: configManager.getConversionRate(), - conversionDate: configManager.getConversionDate(), + gasMultiplier: configManager.getGasMultiplier(), })) } @@ -121,7 +113,7 @@ IdentityStore.prototype.getSelectedAddress = function () { return configManager.getSelectedAccount() } -IdentityStore.prototype.setSelectedAddress = function (address, cb) { +IdentityStore.prototype.setSelectedAddressSync = function (address) { const configManager = this.configManager if (!address) { var addresses = this._getAddresses() @@ -129,7 +121,12 @@ IdentityStore.prototype.setSelectedAddress = function (address, cb) { } configManager.setSelectedAccount(address) - if (cb) return cb(null, address) + return address +} + +IdentityStore.prototype.setSelectedAddress = function (address, cb) { + const resultAddress = this.setSelectedAddressSync(address) + if (cb) return cb(null, resultAddress) } IdentityStore.prototype.revealAccount = function (cb) { @@ -139,6 +136,11 @@ IdentityStore.prototype.revealAccount = function (cb) { keyStore.setDefaultHdDerivationPath(this.hdPathString) keyStore.generateNewAddress(derivedKey, 1) + const addresses = keyStore.getAddresses() + const address = addresses[ addresses.length - 1 ] + + this._ethStore.addAccount(ethUtil.addHexPrefix(address)) + configManager.setWallet(keyStore.serialize()) this._loadIdentities() @@ -183,192 +185,10 @@ IdentityStore.prototype.submitPassword = function (password, cb) { IdentityStore.prototype.exportAccount = function (address, cb) { var privateKey = this._idmgmt.exportPrivateKey(address) - cb(null, privateKey) + if (cb) cb(null, privateKey) + return privateKey } -// -// Transactions -// - -// comes from dapp via zero-client hooked-wallet provider -IdentityStore.prototype.addUnconfirmedTransaction = function (txParams, onTxDoneCb, cb) { - const configManager = this.configManager - var self = this - // create txData obj with parameters and meta data - var time = (new Date()).getTime() - var txId = createId() - txParams.metamaskId = txId - txParams.metamaskNetworkId = self._currentState.network - var txData = { - id: txId, - txParams: txParams, - time: time, - status: 'unconfirmed', - } - - console.log('addUnconfirmedTransaction:', txData) - - // keep the onTxDoneCb around for after approval/denial (requires user interaction) - // This onTxDoneCb fires completion to the Dapp's write operation. - self._unconfTxCbs[txId] = onTxDoneCb - - var provider = self._ethStore._query.currentProvider - var query = new EthQuery(provider) - - // calculate metadata for tx - async.parallel([ - analyzeForDelegateCall, - estimateGas, - ], didComplete) - - // perform static analyis on the target contract code - function analyzeForDelegateCall(cb){ - if (txParams.to) { - query.getCode(txParams.to, function (err, result) { - if (err) return cb(err) - var code = ethUtil.toBuffer(result) - if (code !== '0x') { - var ops = ethBinToOps(code) - var containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL') - txData.containsDelegateCall = containsDelegateCall - cb() - } else { - cb() - } - }) - } else { - cb() - } - } - - function estimateGas(cb){ - query.estimateGas(txParams, function(err, result){ - if (err) return cb(err) - txData.estimatedGas = result - cb() - }) - } - - function didComplete (err) { - if (err) return cb(err) - configManager.addTx(txData) - // signal update - self._didUpdate() - // signal completion of add tx - cb(null, txData) - } -} - -// comes from metamask ui -IdentityStore.prototype.approveTransaction = function (txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // accept tx - cb() - approvalCb(null, true) - // clean up - configManager.confirmTx(txId) - delete this._unconfTxCbs[txId] - this._didUpdate() -} - -// comes from metamask ui -IdentityStore.prototype.cancelTransaction = function (txId) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // reject tx - approvalCb(null, false) - // clean up - configManager.rejectTx(txId) - delete this._unconfTxCbs[txId] - this._didUpdate() -} - -// performs the actual signing, no autofill of params -IdentityStore.prototype.signTransaction = function (txParams, cb) { - try { - console.log('signing tx...', txParams) - var rawTx = this._idmgmt.signTx(txParams) - cb(null, rawTx) - } catch (err) { - cb(err) - } -} - -// -// Messages -// - -// comes from dapp via zero-client hooked-wallet provider -IdentityStore.prototype.addUnconfirmedMessage = function (msgParams, cb) { - // 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: 'unconfirmed', - } - messageManager.addMsg(msgData) - console.log('addUnconfirmedMessage:', msgData) - - // keep the cb around for after approval (requires user interaction) - // This cb fires completion to the Dapp's write operation. - this._unconfMsgCbs[msgId] = cb - - // signal update - this._didUpdate() - - return msgId -} - -// comes from metamask ui -IdentityStore.prototype.approveMessage = function (msgId, cb) { - var approvalCb = this._unconfMsgCbs[msgId] || noop - - // accept msg - cb() - approvalCb(null, true) - // clean up - messageManager.confirmMsg(msgId) - delete this._unconfMsgCbs[msgId] - this._didUpdate() -} - -// comes from metamask ui -IdentityStore.prototype.cancelMessage = function (msgId) { - var approvalCb = this._unconfMsgCbs[msgId] || noop - - // reject tx - approvalCb(null, false) - // clean up - messageManager.rejectMsg(msgId) - delete this._unconfTxCbs[msgId] - this._didUpdate() -} - -// performs the actual signing, no autofill of params -IdentityStore.prototype.signMessage = function (msgParams, cb) { - try { - console.log('signing msg...', msgParams.data) - var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data) - if ('metamaskId' in msgParams) { - var id = msgParams.metamaskId - delete msgParams.metamaskId - - this.approveMessage(id, cb) - } else { - cb(null, rawMsg) - } - } catch (err) { - cb(err) - } -} - -// // private // @@ -389,9 +209,11 @@ IdentityStore.prototype._loadIdentities = function () { var addresses = this._getAddresses() addresses.forEach((address, i) => { // // add to ethStore - this._ethStore.addAccount(address) + if (this._ethStore) { + this._ethStore.addAccount(ethUtil.addHexPrefix(address)) + } // add to identities - const defaultLabel = 'Wallet ' + (i + 1) + const defaultLabel = 'Account ' + (i + 1) const nickname = configManager.nicknameForWallet(address) var identity = { name: nickname || defaultLabel, @@ -408,7 +230,6 @@ IdentityStore.prototype.saveAccountLabel = function (account, label, cb) { configManager.setNicknameForWallet(account, label) this._loadIdentities() cb(null, label) - this._didUpdate() } // mayBeFauceting @@ -432,76 +253,87 @@ IdentityStore.prototype._mayBeFauceting = function (i) { // IdentityStore.prototype.tryPassword = function (password, cb) { - this._createIdmgmt(password, null, null, cb) + var serializedKeystore = this.configManager.getWallet() + var keyStore = KeyStore.deserialize(serializedKeystore) + + keyStore.keyFromPassword(password, (err, pwDerivedKey) => { + if (err) return cb(err) + + const isCorrect = keyStore.isDerivedKeyCorrect(pwDerivedKey) + if (!isCorrect) return cb(new Error('Lightwallet - password incorrect')) + + this._keyStore = keyStore + this._createIdMgmt(pwDerivedKey) + cb() + }) } -IdentityStore.prototype._createIdmgmt = function (password, seed, entropy, cb) { - const configManager = this.configManager - var keyStore = null - LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => { +IdentityStore.prototype._createVault = function (password, seedPhrase, cb) { + const opts = { + password, + hdPathString: this.hdPathString, + } + + if (seedPhrase) { + opts.seedPhrase = seedPhrase + } + + KeyStore.createVault(opts, (err, keyStore) => { if (err) return cb(err) - var serializedKeystore = configManager.getWallet() - - if (seed) { - try { - keyStore = this._restoreFromSeed(password, seed, derivedKey) - } catch (e) { - return cb(e) - } - - // returning user, recovering from storage - } else if (serializedKeystore) { - keyStore = LightwalletKeyStore.deserialize(serializedKeystore) - var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey) - if (!isCorrect) return cb(new Error('Lightwallet - password incorrect')) - - // first time here - } else { - keyStore = this._createFirstWallet(entropy, derivedKey) - } this._keyStore = keyStore - this._idmgmt = new IdManagement({ - keyStore: keyStore, - derivedKey: derivedKey, - hdPathSTring: this.hdPathString, - configManager: this.configManager, - }) - cb() + keyStore.keyFromPassword(password, (err, derivedKey) => { + if (err) return cb(err) + + this.purgeCache() + + keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}) + + this._createFirstWallet(derivedKey) + this._createIdMgmt(derivedKey) + this.setSelectedAddressSync() + + cb() + }) }) } -IdentityStore.prototype._restoreFromSeed = function (password, seed, derivedKey) { - const configManager = this.configManager - var keyStore = new LightwalletKeyStore(seed, derivedKey, this.hdPathString) - keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}) - keyStore.setDefaultHdDerivationPath(this.hdPathString) +IdentityStore.prototype._createIdMgmt = function (derivedKey) { + this._idmgmt = new IdManagement({ + keyStore: this._keyStore, + derivedKey: derivedKey, + configManager: this.configManager, + }) +} - keyStore.generateNewAddress(derivedKey, 3) - configManager.setWallet(keyStore.serialize()) - if (global.METAMASK_DEBUG) { - console.log('restored from seed. saved to keystore') +IdentityStore.prototype.purgeCache = function () { + this._currentState.identities = {} + let accounts + try { + accounts = Object.keys(this._ethStore._currentState.accounts) + } catch (e) { + accounts = [] } - return keyStore + accounts.forEach((address) => { + this._ethStore.removeAccount(address) + }) } -IdentityStore.prototype._createFirstWallet = function (entropy, derivedKey) { - const configManager = this.configManager - var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy) - var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString) - keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'}) +IdentityStore.prototype._createFirstWallet = function (derivedKey) { + const keyStore = this._keyStore keyStore.setDefaultHdDerivationPath(this.hdPathString) - keyStore.generateNewAddress(derivedKey, 1) - configManager.setWallet(keyStore.serialize()) - console.log('saved to keystore') - return keyStore + this.configManager.setWallet(keyStore.serialize()) + var addresses = keyStore.getAddresses() + this._ethStore.addAccount(ethUtil.addHexPrefix(addresses[0])) } // get addresses and normalize address hexString IdentityStore.prototype._getAddresses = function () { - return this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x' + address }) + return this._keyStore.getAddresses(this.hdPathString).map((address) => { + return ethUtil.addHexPrefix(address) + }) } IdentityStore.prototype._autoFaucet = function () { @@ -510,5 +342,3 @@ IdentityStore.prototype._autoFaucet = function () { } // util - -function noop () {} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 65354cd3d..92936de2f 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,7 +1,8 @@ -const Streams = require('mississippi') -const ObjectMultiplex = require('./obj-multiplex') +const pipe = require('pump') const StreamProvider = require('web3-stream-provider') -const RemoteStore = require('./remote-store.js').RemoteStore +const LocalStorageStore = require('obs-store') +const ObjectMultiplex = require('./obj-multiplex') +const createRandomId = require('./random-id') module.exports = MetamaskInpageProvider @@ -9,64 +10,89 @@ function MetamaskInpageProvider (connectionStream) { const self = this // setup connectionStream multiplexing - var multiStream = ObjectMultiplex() - Streams.pipe(connectionStream, multiStream, connectionStream, function (err) { - console.warn('MetamaskInpageProvider - lost connection to MetaMask') - if (err) throw err - }) - self.multiStream = multiStream - - // subscribe to metamask public config - var publicConfigStore = remoteStoreWithLocalStorageCache('MetaMask-Config') - var storeStream = publicConfigStore.createStream() - Streams.pipe(storeStream, multiStream.createStream('publicConfig'), storeStream, function (err) { - console.warn('MetamaskInpageProvider - lost connection to MetaMask publicConfig') - if (err) throw err - }) - self.publicConfigStore = publicConfigStore + var multiStream = self.multiStream = ObjectMultiplex() + pipe( + connectionStream, + multiStream, + connectionStream, + (err) => logStreamDisconnectWarning('MetaMask', err) + ) + + // subscribe to metamask public config (one-way) + self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) + pipe( + multiStream.createStream('publicConfig'), + self.publicConfigStore, + (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) + ) // connect to async provider - var asyncProvider = new StreamProvider() - Streams.pipe(asyncProvider, multiStream.createStream('provider'), asyncProvider, function (err) { - console.warn('MetamaskInpageProvider - lost connection to MetaMask provider') - if (err) throw err - }) - asyncProvider.on('error', console.error.bind(console)) - self.asyncProvider = asyncProvider + const asyncProvider = self.asyncProvider = new StreamProvider() + pipe( + asyncProvider, + multiStream.createStream('provider'), + asyncProvider, + (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) + ) + + self.idMap = {} // handle sendAsync requests via asyncProvider - self.sendAsync = function(payload, cb){ + self.sendAsync = function (payload, cb) { // rewrite request ids - var request = jsonrpcMessageTransform(payload, (message) => { - message.id = createRandomId() + var request = eachJsonMessage(payload, (message) => { + var newId = createRandomId() + self.idMap[newId] = message.id + message.id = newId return message }) // forward to asyncProvider - asyncProvider.sendAsync(request, cb) + asyncProvider.sendAsync(request, function (err, res) { + if (err) return cb(err) + // transform messages to original ids + eachJsonMessage(res, (message) => { + var oldId = self.idMap[message.id] + delete self.idMap[message.id] + message.id = oldId + return message + }) + cb(null, res) + }) } } MetamaskInpageProvider.prototype.send = function (payload) { const self = this - + let selectedAddress let result = null switch (payload.method) { case 'eth_accounts': // read from localStorage - selectedAddress = self.publicConfigStore.get('selectedAddress') + selectedAddress = self.publicConfigStore.getState().selectedAddress result = selectedAddress ? [selectedAddress] : [] break case 'eth_coinbase': // read from localStorage - selectedAddress = self.publicConfigStore.get('selectedAddress') - result = selectedAddress || '0x0000000000000000000000000000000000000000' + selectedAddress = self.publicConfigStore.getState().selectedAddress + result = selectedAddress + break + + case 'eth_uninstallFilter': + self.sendAsync(payload, noop) + result = true + break + + case 'net_version': + let networkVersion = self.publicConfigStore.getState().networkVersion + result = networkVersion break // throw not-supported Error default: - var message = 'The MetaMask Web3 object does not support synchronous methods. See https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#all-async---think-of-metamask-as-a-light-client for details.' + var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client' + var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.` throw new Error(message) } @@ -87,34 +113,22 @@ MetamaskInpageProvider.prototype.isConnected = function () { return true } -// util - -function remoteStoreWithLocalStorageCache (storageKey) { - // read local cache - var initState = JSON.parse(localStorage[storageKey] || '{}') - var store = new RemoteStore(initState) - // cache the latest state locally - store.subscribe(function (state) { - localStorage[storageKey] = JSON.stringify(state) - }) - - return store -} +MetamaskInpageProvider.prototype.isMetaMask = true -function createRandomId(){ - const extraDigits = 3 - // 13 time digits - const datePart = new Date().getTime() * Math.pow(10, extraDigits) - // 3 random digits - const extraPart = Math.floor(Math.random() * Math.pow(10, extraDigits)) - // 16 digits - return datePart + extraPart -} +// util -function jsonrpcMessageTransform(payload, transformFn){ +function eachJsonMessage (payload, transformFn) { if (Array.isArray(payload)) { return payload.map(transformFn) } else { return transformFn(payload) } -}
\ No newline at end of file +} + +function logStreamDisconnectWarning(remoteLabel, err){ + let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` + if (err) warningMsg += '\n' + err.stack + console.warn(warningMsg) +} + +function noop () {} diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js new file mode 100644 index 000000000..693fa8751 --- /dev/null +++ b/app/scripts/lib/is-popup-or-notification.js @@ -0,0 +1,8 @@ +module.exports = function isPopupOrNotification () { + const url = window.location.href + if (url.match(/popup.html$/)) { + return 'popup' + } else { + return 'notification' + } +} diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index b609b820e..ceaf8ee2f 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -1,61 +1,118 @@ -module.exports = new MessageManager() +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const createId = require('./random-id') -function MessageManager (opts) { - this.messages = [] -} -MessageManager.prototype.getMsgList = function () { - return this.messages -} +module.exports = class MessageManager extends EventEmitter{ + constructor (opts) { + super() + this.memStore = new ObservableStore({ + unapprovedMsgs: {}, + unapprovedMsgCount: 0, + }) + this.messages = [] + } -MessageManager.prototype.unconfirmedMsgs = function () { - var messages = this.getMsgList() - return messages.filter(msg => msg.status === 'unconfirmed') - .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) -} + get unapprovedMsgCount () { + return Object.keys(this.getUnapprovedMsgs()).length + } -MessageManager.prototype._saveMsgList = function (msgList) { - this.messages = msgList -} + getUnapprovedMsgs () { + return this.messages.filter(msg => msg.status === 'unapproved') + .reduce((result, msg) => { result[msg.id] = msg; return result }, {}) + } -MessageManager.prototype.addMsg = function (msg) { - var messages = this.getMsgList() - messages.push(msg) - this._saveMsgList(messages) -} + 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', + } + this.addMsg(msgData) -MessageManager.prototype.getMsg = function (msgId) { - var messages = this.getMsgList() - var matching = messages.filter(msg => msg.id === msgId) - return matching.length > 0 ? matching[0] : null -} + // signal update + this.emit('update') + return msgId + } -MessageManager.prototype.confirmMsg = function (msgId) { - this._setMsgStatus(msgId, 'confirmed') -} + addMsg (msg) { + this.messages.push(msg) + this._saveMsgList() + } -MessageManager.prototype.rejectMsg = function (msgId) { - this._setMsgStatus(msgId, 'rejected') -} + getMsg (msgId) { + return this.messages.find(msg => msg.id === msgId) + } -MessageManager.prototype._setMsgStatus = function (msgId, status) { - var msg = this.getMsg(msgId) - if (msg) msg.status = status - this.updateMsg(msg) -} + approveMessage (msgParams) { + this.setMsgStatusApproved(msgParams.metamaskId) + return this.prepMsgForSigning(msgParams) + } -MessageManager.prototype.updateMsg = function (msg) { - var messages = this.getMsgList() - var found, index - messages.forEach((otherMsg, i) => { - if (otherMsg.id === msg.id) { - found = true - index = i + 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('MessageManager - 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 } - }) - if (found) { - messages[index] = msg + this._saveMsgList() } - this._saveMsgList(messages) + + _saveMsgList () { + const unapprovedMsgs = this.getUnapprovedMsgs() + const unapprovedMsgCount = Object.keys(unapprovedMsgs).length + this.memStore.updateState({ unapprovedMsgs, unapprovedMsgCount }) + 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')) + } +}
\ No newline at end of file diff --git a/app/scripts/lib/migrations.js b/app/scripts/lib/migrations.js deleted file mode 100644 index f026cbe53..000000000 --- a/app/scripts/lib/migrations.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = [ - require('../migrations/002'), - require('../migrations/003'), - require('../migrations/004'), -] diff --git a/app/scripts/lib/migrator/index.js b/app/scripts/lib/migrator/index.js new file mode 100644 index 000000000..312345263 --- /dev/null +++ b/app/scripts/lib/migrator/index.js @@ -0,0 +1,51 @@ +const asyncQ = require('async-q') + +class Migrator { + + constructor (opts = {}) { + let migrations = opts.migrations || [] + this.migrations = migrations.sort((a, b) => a.version - b.version) + let lastMigration = this.migrations.slice(-1)[0] + // use specified defaultVersion or highest migration version + this.defaultVersion = opts.defaultVersion || (lastMigration && lastMigration.version) || 0 + } + + // run all pending migrations on meta in place + migrateData (versionedData = this.generateInitialState()) { + let remaining = this.migrations.filter(migrationIsPending) + + return ( + asyncQ.eachSeries(remaining, (migration) => this.runMigration(versionedData, migration)) + .then(() => versionedData) + ) + + // migration is "pending" if hit has a higher + // version number than currentVersion + function migrationIsPending(migration) { + return migration.version > versionedData.meta.version + } + } + + runMigration(versionedData, migration) { + return ( + migration.migrate(versionedData) + .then((versionedData) => { + if (!versionedData.data) return Promise.reject(new Error('Migrator - Migration returned empty data')) + if (migration.version !== undefined && versionedData.meta.version !== migration.version) return Promise.reject(new Error('Migrator - Migration did not update version number correctly')) + return Promise.resolve(versionedData) + }) + ) + } + + generateInitialState (initState) { + return { + meta: { + version: this.defaultVersion, + }, + data: initState, + } + } + +} + +module.exports = Migrator diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js new file mode 100644 index 000000000..51d89a8fb --- /dev/null +++ b/app/scripts/lib/nodeify.js @@ -0,0 +1,24 @@ +module.exports = function (promiseFn) { + return function () { + var args = [] + for (var i = 0; i < arguments.length - 1; i++) { + args.push(arguments[i]) + } + var cb = arguments[arguments.length - 1] + + const nodeified = promiseFn.apply(this, args) + + if (!nodeified) { + const methodName = String(promiseFn).split('(')[0] + throw new Error(`The ${methodName} did not return a Promise, but was nodeified.`) + } + nodeified.then(function (result) { + cb(null, result) + }) + .catch(function (reason) { + cb(reason) + }) + + return nodeified + } +} diff --git a/app/scripts/lib/notifications.js b/app/scripts/lib/notifications.js index 6c1601df1..3db1ac6b5 100644 --- a/app/scripts/lib/notifications.js +++ b/app/scripts/lib/notifications.js @@ -1,159 +1,65 @@ -const createId = require('hat') -const extend = require('xtend') -const unmountComponentAtNode = require('react-dom').unmountComponentAtNode -const findDOMNode = require('react-dom').findDOMNode -const render = require('react-dom').render -const h = require('react-hyperscript') -const PendingTxDetails = require('../../../ui/app/components/pending-tx-details') -const PendingMsgDetails = require('../../../ui/app/components/pending-msg-details') -const MetaMaskUiCss = require('../../../ui/css') const extension = require('./extension') -var notificationHandlers = {} +const height = 520 +const width = 360 const notifications = { - createUnlockRequestNotification: createUnlockRequestNotification, - createTxNotification: createTxNotification, - createMsgNotification: createMsgNotification, + show, + getPopup, + closePopup, } module.exports = notifications window.METAMASK_NOTIFIER = notifications -setupListeners() - -function setupListeners () { - // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 - if (!extension.notifications) return console.error('Chrome notifications API missing...') +function show () { + getPopup((err, popup) => { + if (err) throw err - // notification button press - extension.notifications.onButtonClicked.addListener(function (notificationId, buttonIndex) { - var handlers = notificationHandlers[notificationId] - if (buttonIndex === 0) { - handlers.confirm() + if (popup) { + // bring focus to existing popup + extension.windows.update(popup.id, { focused: true }) } else { - handlers.cancel() + // create new popup + extension.windows.create({ + url: 'notification.html', + type: 'popup', + focused: true, + width, + height, + }) } - extension.notifications.clear(notificationId) - }) - - // notification teardown - extension.notifications.onClosed.addListener(function (notificationId) { - delete notificationHandlers[notificationId] }) } -// creation helper -function createUnlockRequestNotification (opts) { - // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 - if (!extension.notifications) return console.error('Chrome notifications API missing...') - var message = 'An Ethereum app has requested a signature. Please unlock your account.' - - var id = createId() - extension.notifications.create(id, { - type: 'basic', - iconUrl: '/images/icon-128.png', - title: opts.title, - message: message, - }) -} - -function createTxNotification (state) { - // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 - if (!extension.notifications) return console.error('Chrome notifications API missing...') - - renderTxNotificationSVG(state, function (err, notificationSvgSource) { - if (err) throw err +function getWindows (cb) { + // Ignore in test environment + if (!extension.windows) { + return cb() + } - showNotification(extend(state, { - title: 'New Unsigned Transaction', - imageUrl: toSvgUri(notificationSvgSource), - })) + extension.windows.getAll({}, (windows) => { + cb(null, windows) }) } -function createMsgNotification (state) { - // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 - if (!extension.notifications) return console.error('Chrome notifications API missing...') - - renderMsgNotificationSVG(state, function (err, notificationSvgSource) { +function getPopup (cb) { + getWindows((err, windows) => { if (err) throw err - - showNotification(extend(state, { - title: 'New Unsigned Message', - imageUrl: toSvgUri(notificationSvgSource), - })) + cb(null, getPopupIn(windows)) }) } -function showNotification (state) { - // guard for extension bug https://github.com/MetaMask/metamask-plugin/issues/236 - if (!extension.notifications) return console.error('Chrome notifications API missing...') - - var id = createId() - extension.notifications.create(id, { - type: 'image', - requireInteraction: true, - iconUrl: '/images/icon-128.png', - imageUrl: state.imageUrl, - title: state.title, - message: '', - buttons: [{ - title: 'Approve', - }, { - title: 'Reject', - }], - }) - notificationHandlers[id] = { - confirm: state.onConfirm, - cancel: state.onCancel, - } -} - -function renderTxNotificationSVG (state, cb) { - var content = h(PendingTxDetails, state) - renderNotificationSVG(content, cb) -} - -function renderMsgNotificationSVG (state, cb) { - var content = h(PendingMsgDetails, state) - renderNotificationSVG(content, cb) +function getPopupIn (windows) { + return windows ? windows.find((win) => { + return (win && win.type === 'popup' && + win.height === height && + win.width === width) + }) : null } -function renderNotificationSVG (content, cb) { - var container = document.createElement('div') - var confirmView = h('div.app-primary', { - style: { - width: '360px', - height: '240px', - padding: '16px', - // background: '#F7F7F7', - background: 'white', - }, - }, [ - h('style', MetaMaskUiCss()), - content, - ]) - - render(confirmView, container, function ready() { - var rootElement = findDOMNode(this) - var viewSource = rootElement.outerHTML - unmountComponentAtNode(container) - var svgSource = svgWrapper(viewSource) - // insert content into svg wrapper - cb(null, svgSource) +function closePopup () { + getPopup((err, popup) => { + if (err) throw err + if (!popup) return + extension.windows.remove(popup.id, console.error) }) } - -function svgWrapper (content) { - var wrapperSource = ` - <svg xmlns="http://www.w3.org/2000/svg" width="360" height="240"> - <foreignObject x="0" y="0" width="100%" height="100%"> - <body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body> - </foreignObject> - </svg> - ` - return wrapperSource.split('{{content}}').join(content) -} - -function toSvgUri (content) { - return 'data:image/svg+xml;utf8,' + encodeURIComponent(content) -} diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js index f54ff7653..bd114c394 100644 --- a/app/scripts/lib/obj-multiplex.js +++ b/app/scripts/lib/obj-multiplex.js @@ -10,9 +10,9 @@ function ObjectMultiplex (opts) { var data = chunk.data var substream = mx.streams[name] if (!substream) { - console.warn('orphaned data for stream ' + name) + console.warn(`orphaned data for stream "${name}"`) } else { - substream.push(data) + if (substream.push) substream.push(data) } return cb() }) @@ -36,5 +36,9 @@ function ObjectMultiplex (opts) { } return substream } + // ignore streams (dont display orphaned data warning) + mx.ignoreStream = function (name) { + mx.streams[name] = true + } return mx } diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index 1889e3c04..607a9c9ed 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -30,8 +30,7 @@ PortDuplexStream.prototype._onMessage = function (msg) { PortDuplexStream.prototype._onDisconnect = function () { try { - // this.end() - this.emit('close') + this.push(null) } catch (err) { this.emit('error', err) } @@ -52,12 +51,11 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) { // console.log('PortDuplexStream - sent message', msg) this._port.postMessage(msg) } - cb() } catch (err) { - console.error(err) - // this.emit('error', err) - cb(new Error('PortDuplexStream - disconnected')) + // console.error(err) + return cb(new Error('PortDuplexStream - disconnected')) } + cb() } // util diff --git a/app/scripts/lib/random-id.js b/app/scripts/lib/random-id.js new file mode 100644 index 000000000..788f3370f --- /dev/null +++ b/app/scripts/lib/random-id.js @@ -0,0 +1,9 @@ +const MAX = Number.MAX_SAFE_INTEGER + +let idCounter = Math.round(Math.random() * MAX) +function createRandomId () { + idCounter = idCounter % MAX + return idCounter++ +} + +module.exports = createRandomId diff --git a/app/scripts/lib/remote-store.js b/app/scripts/lib/remote-store.js deleted file mode 100644 index fbfab7bad..000000000 --- a/app/scripts/lib/remote-store.js +++ /dev/null @@ -1,97 +0,0 @@ -const Dnode = require('dnode') -const inherits = require('util').inherits - -module.exports = { - HostStore: HostStore, - RemoteStore: RemoteStore, -} - -function BaseStore (initState) { - this._state = initState || {} - this._subs = [] -} - -BaseStore.prototype.set = function (key, value) { - throw Error('Not implemented.') -} - -BaseStore.prototype.get = function (key) { - return this._state[key] -} - -BaseStore.prototype.subscribe = function (fn) { - this._subs.push(fn) - var unsubscribe = this.unsubscribe.bind(this, fn) - return unsubscribe -} - -BaseStore.prototype.unsubscribe = function (fn) { - var index = this._subs.indexOf(fn) - if (index !== -1) this._subs.splice(index, 1) -} - -BaseStore.prototype._emitUpdates = function (state) { - this._subs.forEach(function (handler) { - handler(state) - }) -} - -// -// host -// - -inherits(HostStore, BaseStore) -function HostStore (initState, opts) { - BaseStore.call(this, initState) -} - -HostStore.prototype.set = function (key, value) { - this._state[key] = value - process.nextTick(this._emitUpdates.bind(this, this._state)) -} - -HostStore.prototype.createStream = function () { - var dnode = Dnode({ - // update: this._didUpdate.bind(this), - }) - dnode.on('remote', this._didConnect.bind(this)) - return dnode -} - -HostStore.prototype._didConnect = function (remote) { - this.subscribe(function (state) { - remote.update(state) - }) - remote.update(this._state) -} - -// -// remote -// - -inherits(RemoteStore, BaseStore) -function RemoteStore (initState, opts) { - BaseStore.call(this, initState) - this._remote = null -} - -RemoteStore.prototype.set = function (key, value) { - this._remote.set(key, value) -} - -RemoteStore.prototype.createStream = function () { - var dnode = Dnode({ - update: this._didUpdate.bind(this), - }) - dnode.once('remote', this._didConnect.bind(this)) - return dnode -} - -RemoteStore.prototype._didConnect = function (remote) { - this._remote = remote -} - -RemoteStore.prototype._didUpdate = function (state) { - this._state = state - this._emitUpdates(state) -} diff --git a/app/scripts/lib/sig-util.js b/app/scripts/lib/sig-util.js new file mode 100644 index 000000000..193dda381 --- /dev/null +++ b/app/scripts/lib/sig-util.js @@ -0,0 +1,28 @@ +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/stream-utils.js b/app/scripts/lib/stream-utils.js index 1b7b89d14..ba79990cc 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -1,4 +1,5 @@ const Through = require('through2') +const endOfStream = require('end-of-stream') const ObjectMultiplex = require('./obj-multiplex') module.exports = { @@ -24,11 +25,11 @@ function jsonStringifyStream () { function setupMultiplex (connectionStream) { var mx = ObjectMultiplex() connectionStream.pipe(mx).pipe(connectionStream) - mx.on('error', function (err) { - console.error(err) + endOfStream(mx, function (err) { + if (err) console.error(err) }) - connectionStream.on('error', function (err) { - console.error(err) + endOfStream(connectionStream, function (err) { + if (err) console.error(err) mx.destroy() }) return mx diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js new file mode 100644 index 000000000..5116cb93b --- /dev/null +++ b/app/scripts/lib/tx-utils.js @@ -0,0 +1,132 @@ +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 BN = ethUtil.BN + +/* +tx-utils are utility methods for Transaction manager +its passed a provider and that is passed to ethquery +and used to do things like calculate gas of a tx. +*/ + +module.exports = class txProviderUtils { + constructor (provider) { + this.provider = provider + this.query = new EthQuery(provider) + } + + analyzeGasUsage (txData, cb) { + var self = this + this.query.getBlockByNumber('latest', true, (err, block) => { + if (err) return cb(err) + async.waterfall([ + self.estimateTxGas.bind(self, txData, block.gasLimit), + self.setTxGas.bind(self, txData, block.gasLimit), + ], cb) + }) + } + + estimateTxGas (txData, blockGasLimitHex, cb) { + const txParams = txData.txParams + // check if gasLimit is already specified + txData.gasLimitSpecified = Boolean(txParams.gas) + // if not, fallback to block gasLimit + if (!txData.gasLimitSpecified) { + txParams.gas = blockGasLimitHex + } + // run tx, see if it will OOG + this.query.estimateGas(txParams, cb) + } + + setTxGas (txData, blockGasLimitHex, estimatedGasHex, cb) { + txData.estimatedGas = estimatedGasHex + const txParams = txData.txParams + + // if gasLimit was specified and doesnt OOG, + // use original specified amount + if (txData.gasLimitSpecified) { + txData.estimatedGas = txParams.gas + cb() + return + } + // if gasLimit not originally specified, + // try adding an additional gas buffer to our estimation for safety + const estimatedGasBn = new BN(ethUtil.stripHexPrefix(txData.estimatedGas), 16) + const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(blockGasLimitHex), 16) + const estimationWithBuffer = new BN(this.addGasBuffer(estimatedGasBn), 16) + // added gas buffer is too high + if (estimationWithBuffer.gt(blockGasLimitBn)) { + txParams.gas = txData.estimatedGas + // added gas buffer is safe + } else { + const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) + txParams.gas = gasWithBufferHex + } + cb() + return + } + + addGasBuffer (gas) { + const gasBuffer = new BN('100000', 10) + const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) + const correct = bnGas.add(gasBuffer) + return ethUtil.addHexPrefix(correct.toString(16)) + } + + fillInTxParams (txParams, cb) { + let fromAddress = txParams.from + let reqs = {} + + if (isUndef(txParams.gas)) reqs.gas = (cb) => this.query.estimateGas(txParams, cb) + if (isUndef(txParams.gasPrice)) reqs.gasPrice = (cb) => this.query.gasPrice(cb) + if (isUndef(txParams.nonce)) reqs.nonce = (cb) => this.query.getTransactionCount(fromAddress, 'pending', cb) + + async.parallel(reqs, function(err, result) { + if (err) return cb(err) + // write results to txParams obj + Object.assign(txParams, result) + cb() + }) + } + + // builds ethTx from txParams object + buildEthTxFromParams (txParams, gasMultiplier = 1) { + // apply gas multiplyer + let gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16) + // multiply and divide by 100 so as to add percision to integer mul + gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10)) + txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber()) + // normalize values + txParams.to = normalize(txParams.to) + txParams.from = normalize(txParams.from) + txParams.value = normalize(txParams.value) + txParams.data = normalize(txParams.data) + txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas) + txParams.nonce = normalize(txParams.nonce) + // build ethTx + const ethTx = new Transaction(txParams) + return ethTx + } + + publishTransaction (rawTx, cb) { + this.query.sendRawTransaction(rawTx, cb) + } + + validateTxParams (txParams, cb) { + if (('value' in txParams) && txParams.value.indexOf('-') === 0) { + cb(new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)) + } else { + cb() + } + } + + +} + +// util + +function isUndef(value) { + return value === undefined +} |