diff options
Diffstat (limited to 'app/scripts/lib')
-rw-r--r-- | app/scripts/lib/auto-faucet.js | 5 | ||||
-rw-r--r-- | app/scripts/lib/auto-reload.js | 5 | ||||
-rw-r--r-- | app/scripts/lib/config-manager.js | 150 | ||||
-rw-r--r-- | app/scripts/lib/eth-store.js | 146 | ||||
-rw-r--r-- | app/scripts/lib/idStore-migrator.js | 80 | ||||
-rw-r--r-- | app/scripts/lib/idStore.js | 273 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 18 | ||||
-rw-r--r-- | app/scripts/lib/is-popup-or-notification.js | 2 | ||||
-rw-r--r-- | app/scripts/lib/nodeify.js | 24 | ||||
-rw-r--r-- | app/scripts/lib/notifications.js | 12 | ||||
-rw-r--r-- | app/scripts/lib/port-stream.js | 4 | ||||
-rw-r--r-- | app/scripts/lib/random-id.js | 6 | ||||
-rw-r--r-- | app/scripts/lib/sig-util.js | 28 | ||||
-rw-r--r-- | app/scripts/lib/tx-utils.js | 132 |
14 files changed, 517 insertions, 368 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 3c90905db..1302df35f 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -18,17 +18,16 @@ function setupDappAutoReload (web3) { return handleResetRequest - function handleResetRequest() { + function handleResetRequest () { resetWasRequested = true // ignore if web3 was not used if (!pageIsUsingWeb3) return // reload after short timeout setTimeout(triggerReset, 500) } - } // reload the page function triggerReset () { global.location.reload() -}
\ No newline at end of file +} diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index b8ffb6991..3a1f12ac0 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,12 +1,12 @@ 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 MORDEN_RPC = MetamaskConfig.network.morden -const txLimit = 40 /* The config-manager is a convenience object * wrapping a pojo-migrator. @@ -17,8 +17,6 @@ const txLimit = 40 */ module.exports = ConfigManager function ConfigManager (opts) { - this.txLimit = txLimit - // ConfigManager is observable and will emit updates this._subs = [] @@ -111,6 +109,27 @@ ConfigManager.prototype.setWallet = function (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.migrator.getData().keychains || [] +} + +ConfigManager.prototype.setKeychains = function (keychains) { + var data = this.migrator.getData() + data.keychains = keychains + this.setData(data) +} + ConfigManager.prototype.getSelectedAccount = function () { var config = this.getConfig() return config.selectedAccount @@ -118,7 +137,7 @@ ConfigManager.prototype.getSelectedAccount = function () { ConfigManager.prototype.setSelectedAccount = function (address) { var config = this.getConfig() - config.selectedAccount = address + config.selectedAccount = ethUtil.addHexPrefix(address) this.setConfig(config) } @@ -133,11 +152,23 @@ ConfigManager.prototype.setShowSeedWords = function (should) { this.setData(data) } + ConfigManager.prototype.getShouldShowSeedWords = function () { var data = this.migrator.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 ('seedWords' in data) && data.seedWords +} + ConfigManager.prototype.getCurrentRpcAddress = function () { var provider = this.getProvider() if (!provider) return null @@ -174,61 +205,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) { +ConfigManager.prototype.setTxList = function (txList) { var data = this.migrator.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 @@ -239,13 +221,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) @@ -253,6 +237,17 @@ ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { // observable +ConfigManager.prototype.getSalt = function () { + var data = this.getData() + return ('salt' in data) && 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) @@ -270,15 +265,15 @@ ConfigManager.prototype._emitUpdates = function (state) { }) } -ConfigManager.prototype.setConfirmed = function (confirmed) { +ConfigManager.prototype.setConfirmedDisclaimer = function (confirmed) { var data = this.getData() - data.isConfirmed = confirmed + data.isDisclaimerConfirmed = confirmed this.setData(data) } -ConfigManager.prototype.getConfirmed = function () { +ConfigManager.prototype.getConfirmedDisclaimer = function () { var data = this.getData() - return ('isConfirmed' in data) && data.isConfirmed + return ('isDisclaimerConfirmed' in data) && data.isDisclaimerConfirmed } ConfigManager.prototype.setTOSHash = function (hash) { @@ -305,9 +300,9 @@ ConfigManager.prototype.getCurrentFiat = function () { 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) + return fetch(`https://www.cryptonator.com/api/ticker/eth-${data.fiatCurrency}`) + .then(response => response.json()) + .then((parsedResponse) => { this.setConversionPrice(parsedResponse.ticker.price) this.setConversionDate(parsedResponse.timestamp) }).catch((err) => { @@ -315,7 +310,6 @@ ConfigManager.prototype.updateConversionRate = function () { this.setConversionPrice(0) this.setConversionDate('N/A') }) - } ConfigManager.prototype.setConversionPrice = function (price) { @@ -340,21 +334,6 @@ ConfigManager.prototype.getConversionDate = function () { 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 : [] @@ -400,3 +379,14 @@ ConfigManager.prototype.setGasMultiplier = function (gasMultiplier) { data.gasMultiplier = gasMultiplier this.setData(data) } + +ConfigManager.prototype.setLostAccounts = function (lostAccounts) { + var data = this.getData() + data.lostAccounts = lostAccounts + this.setData(data) +} + +ConfigManager.prototype.getLostAccounts = function () { + var data = this.getData() + return data.lostAccounts || [] +} diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js new file mode 100644 index 000000000..7e2caf884 --- /dev/null +++ b/app/scripts/lib/eth-store.js @@ -0,0 +1,146 @@ +/* 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 EventEmitter = require('events').EventEmitter +const inherits = require('util').inherits +const async = require('async') +const clone = require('clone') +const EthQuery = require('eth-query') + +module.exports = EthereumStore + + +inherits(EthereumStore, EventEmitter) +function EthereumStore(engine) { + const self = this + EventEmitter.call(self) + self._currentState = { + accounts: {}, + transactions: {}, + } + self._query = new EthQuery(engine) + + engine.on('block', self._updateForBlock.bind(self)) +} + +// +// public +// + +EthereumStore.prototype.getState = function () { + const self = this + return clone(self._currentState) +} + +EthereumStore.prototype.addAccount = function (address) { + const self = this + self._currentState.accounts[address] = {} + self._didUpdate() + if (!self.currentBlockNumber) return + self._updateAccount(address, () => { + self._didUpdate() + }) +} + +EthereumStore.prototype.removeAccount = function (address) { + const self = this + delete self._currentState.accounts[address] + self._didUpdate() +} + +EthereumStore.prototype.addTransaction = function (txHash) { + const self = this + self._currentState.transactions[txHash] = {} + self._didUpdate() + if (!self.currentBlockNumber) return + self._updateTransaction(self.currentBlockNumber, txHash, noop) +} + +EthereumStore.prototype.removeTransaction = function (address) { + const self = this + delete self._currentState.transactions[address] + self._didUpdate() +} + + +// +// private +// + +EthereumStore.prototype._didUpdate = function () { + const self = this + var state = self.getState() + self.emit('update', state) +} + +EthereumStore.prototype._updateForBlock = function (block) { + const self = this + var blockNumber = '0x' + block.number.toString('hex') + self.currentBlockNumber = blockNumber + async.parallel([ + self._updateAccounts.bind(self), + self._updateTransactions.bind(self, blockNumber), + ], function (err) { + if (err) return console.error(err) + self.emit('block', self.getState()) + self._didUpdate() + }) +} + +EthereumStore.prototype._updateAccounts = function (cb) { + var accountsState = this._currentState.accounts + var addresses = Object.keys(accountsState) + async.each(addresses, this._updateAccount.bind(this), cb) +} + +EthereumStore.prototype._updateAccount = function (address, cb) { + var accountsState = this._currentState.accounts + this.getAccount(address, function (err, result) { + if (err) return cb(err) + result.address = address + // only populate if the entry is still present + if (accountsState[address]) { + accountsState[address] = result + } + cb(null, result) + }) +} + +EthereumStore.prototype.getAccount = function (address, cb) { + 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) +} + +EthereumStore.prototype._updateTransactions = function (block, cb) { + const self = this + var transactionsState = self._currentState.transactions + var txHashes = Object.keys(transactionsState) + async.each(txHashes, self._updateTransaction.bind(self, block), cb) +} + +EthereumStore.prototype._updateTransaction = function (block, txHash, cb) { + const self = this + // would use the block here to determine how many confirmations the tx has + var transactionsState = self._currentState.transactions + self._query.getTransaction(txHash, function (err, result) { + if (err) return cb(err) + // only populate if the entry is still present + if (transactionsState[txHash]) { + transactionsState[txHash] = result + self._didUpdate() + } + cb(null, result) + }) +} + +function noop() {} 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 1077d263d..e4cbca456 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -1,19 +1,14 @@ const EventEmitter = require('events').EventEmitter const inherits = require('util').inherits -const async = require('async') const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const EthQuery = require('eth-query') const KeyStore = require('eth-lightwallet').keystore const clone = require('clone') const extend = require('xtend') -const createId = require('./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) @@ -34,17 +29,14 @@ 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() @@ -53,7 +45,7 @@ IdentityStore.prototype.createNewVault = function (password, entropy, cb) { } this.purgeCache() - this._createVault(password, null, entropy, (err) => { + this._createVault(password, null, (err) => { if (err) return cb(err) this._autoFaucet() @@ -77,7 +69,7 @@ IdentityStore.prototype.recoverSeed = function (cb) { IdentityStore.prototype.recoverFromSeed = function (password, seed, cb) { this.purgeCache() - this._createVault(password, seed, null, (err) => { + this._createVault(password, seed, (err) => { if (err) return cb(err) this._loadIdentities() @@ -102,19 +94,13 @@ 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(), + isDisclaimerConfirmed: configManager.getConfirmedDisclaimer(), selectedAddress: configManager.getSelectedAccount(), shapeShiftTxList: configManager.getShapeShiftTxList(), currentFiat: configManager.getCurrentFiat(), conversionRate: configManager.getConversionRate(), conversionDate: configManager.getConversionDate(), gasMultiplier: configManager.getGasMultiplier(), - })) } @@ -204,248 +190,10 @@ IdentityStore.prototype.submitPassword = function (password, cb) { IdentityStore.prototype.exportAccount = function (address, cb) { var privateKey = this._idmgmt.exportPrivateKey(address) - cb(null, 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', - gasMultiplier: configManager.getGasMultiplier() || 1, - } - - 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, (err, result) => { - if (err) return cb(err.message || err) - var containsDelegateCall = self.checkForDelegateCall(result) - txData.containsDelegateCall = containsDelegateCall - cb() - }) - } else { - cb() - } - } - - function estimateGas(cb){ - var estimationParams = extend(txParams) - query.getBlockByNumber('latest', true, function(err, block){ - if (err) return cb(err) - // check if gasLimit is already specified - const gasLimitSpecified = Boolean(txParams.gas) - // if not, fallback to block gasLimit - if (!gasLimitSpecified) { - estimationParams.gas = block.gasLimit - } - // run tx, see if it will OOG - query.estimateGas(estimationParams, function(err, estimatedGasHex){ - if (err) return cb(err.message || err) - // all gas used - must be an error - if (estimatedGasHex === estimationParams.gas) { - txData.simulationFails = true - txData.estimatedGas = estimatedGasHex - txData.txParams.gas = estimatedGasHex - cb() - return - } - // otherwise, did not use all gas, must be ok - - // if specified gasLimit and no error, we're done - if (gasLimitSpecified) { - txData.estimatedGas = txParams.gas - cb() - return - } - - // try adding an additional gas buffer to our estimation for safety - const estimatedGasBn = new BN(ethUtil.stripHexPrefix(estimatedGasHex), 16) - const blockGasLimitBn = new BN(ethUtil.stripHexPrefix(block.gasLimit), 16) - const estimationWithBuffer = self.addGasBuffer(estimatedGasBn) - // added gas buffer is too high - if (estimationWithBuffer.gt(blockGasLimitBn)) { - txData.estimatedGas = estimatedGasHex - txData.txParams.gas = estimatedGasHex - // added gas buffer is safe - } else { - const gasWithBufferHex = ethUtil.intToHex(estimationWithBuffer) - txData.estimatedGas = gasWithBufferHex - txData.txParams.gas = gasWithBufferHex - } - cb() - return - }) - }) - } - - function didComplete (err) { - if (err) return cb(err.message || err) - configManager.addTx(txData) - // signal update - self._didUpdate() - // signal completion of add tx - cb(null, txData) - } -} - -IdentityStore.prototype.checkForDelegateCall = function (codeHex) { - const code = ethUtil.toBuffer(codeHex) - if (code !== '0x') { - const ops = ethBinToOps(code) - const containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL') - return containsDelegateCall - } else { - return false - } -} - -IdentityStore.prototype.addGasBuffer = function (gasBn) { - // add 20% to specified gas - const gasBuffer = gasBn.div(new BN('5', 10)) - const gasWithBuffer = gasBn.add(gasBuffer) - return gasWithBuffer + if (cb) cb(null, privateKey) + return privateKey } -// 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 // @@ -466,7 +214,9 @@ IdentityStore.prototype._loadIdentities = function () { var addresses = this._getAddresses() addresses.forEach((address, i) => { // // add to ethStore - this._ethStore.addAccount(ethUtil.addHexPrefix(address)) + if (this._ethStore) { + this._ethStore.addAccount(ethUtil.addHexPrefix(address)) + } // add to identities const defaultLabel = 'Account ' + (i + 1) const nickname = configManager.nicknameForWallet(address) @@ -523,7 +273,7 @@ IdentityStore.prototype.tryPassword = function (password, cb) { }) } -IdentityStore.prototype._createVault = function (password, seedPhrase, entropy, cb) { +IdentityStore.prototype._createVault = function (password, seedPhrase, cb) { const opts = { password, hdPathString: this.hdPathString, @@ -598,4 +348,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 71ac69ad1..11bd5cc3a 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -40,7 +40,7 @@ function MetamaskInpageProvider (connectionStream) { self.idMap = {} // handle sendAsync requests via asyncProvider - self.sendAsync = function(payload, cb){ + self.sendAsync = function (payload, cb) { // rewrite request ids var request = eachJsonMessage(payload, (message) => { var newId = createRandomId() @@ -49,7 +49,7 @@ function MetamaskInpageProvider (connectionStream) { return message }) // forward to asyncProvider - asyncProvider.sendAsync(request, function(err, res){ + asyncProvider.sendAsync(request, function (err, res) { if (err) return cb(err) // transform messages to original ids eachJsonMessage(res, (message) => { @@ -66,20 +66,20 @@ function MetamaskInpageProvider (connectionStream) { MetamaskInpageProvider.prototype.send = function (payload) { const self = this - let selectedAddress + let selectedAccount let result = null switch (payload.method) { case 'eth_accounts': // read from localStorage - selectedAddress = self.publicConfigStore.get('selectedAddress') - result = selectedAddress ? [selectedAddress] : [] + selectedAccount = self.publicConfigStore.get('selectedAccount') + result = selectedAccount ? [selectedAccount] : [] break case 'eth_coinbase': // read from localStorage - selectedAddress = self.publicConfigStore.get('selectedAddress') - result = selectedAddress || '0x0000000000000000000000000000000000000000' + selectedAccount = self.publicConfigStore.get('selectedAccount') + result = selectedAccount || '0x0000000000000000000000000000000000000000' break case 'eth_uninstallFilter': @@ -111,6 +111,8 @@ MetamaskInpageProvider.prototype.isConnected = function () { return true } +MetamaskInpageProvider.prototype.isMetaMask = true + // util function remoteStoreWithLocalStorageCache (storageKey) { @@ -125,7 +127,7 @@ function remoteStoreWithLocalStorageCache (storageKey) { return store } -function eachJsonMessage(payload, transformFn){ +function eachJsonMessage (payload, transformFn) { if (Array.isArray(payload)) { return payload.map(transformFn) } else { diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js index 5c38ac823..693fa8751 100644 --- a/app/scripts/lib/is-popup-or-notification.js +++ b/app/scripts/lib/is-popup-or-notification.js @@ -1,4 +1,4 @@ -module.exports = function isPopupOrNotification() { +module.exports = function isPopupOrNotification () { const url = window.location.href if (url.match(/popup.html$/)) { return 'popup' 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 cd7535232..3db1ac6b5 100644 --- a/app/scripts/lib/notifications.js +++ b/app/scripts/lib/notifications.js @@ -15,12 +15,9 @@ function show () { if (err) throw err if (popup) { - // bring focus to existing popup extension.windows.update(popup.id, { focused: true }) - } else { - // create new popup extension.windows.create({ url: 'notification.html', @@ -29,12 +26,11 @@ function show () { width, height, }) - } }) } -function getWindows(cb) { +function getWindows (cb) { // Ignore in test environment if (!extension.windows) { return cb() @@ -45,14 +41,14 @@ function getWindows(cb) { }) } -function getPopup(cb) { +function getPopup (cb) { getWindows((err, windows) => { if (err) throw err cb(null, getPopupIn(windows)) }) } -function getPopupIn(windows) { +function getPopupIn (windows) { return windows ? windows.find((win) => { return (win && win.type === 'popup' && win.height === height && @@ -60,7 +56,7 @@ function getPopupIn(windows) { }) : null } -function closePopup() { +function closePopup () { getPopup((err, popup) => { if (err) throw err if (!popup) return diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js index 6f4ccc6ab..607a9c9ed 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -51,11 +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) - cb(new Error('PortDuplexStream - disconnected')) + return cb(new Error('PortDuplexStream - disconnected')) } + cb() } // util diff --git a/app/scripts/lib/random-id.js b/app/scripts/lib/random-id.js index 3c5ae5600..788f3370f 100644 --- a/app/scripts/lib/random-id.js +++ b/app/scripts/lib/random-id.js @@ -1,7 +1,7 @@ -const MAX = 1000000000 +const MAX = Number.MAX_SAFE_INTEGER -let idCounter = Math.round( Math.random() * MAX ) -function createRandomId() { +let idCounter = Math.round(Math.random() * MAX) +function createRandomId () { idCounter = idCounter % MAX return idCounter++ } 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/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 +} |