aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/scripts/lib')
-rw-r--r--app/scripts/lib/auto-faucet.js5
-rw-r--r--app/scripts/lib/auto-reload.js21
-rw-r--r--app/scripts/lib/config-manager.js275
-rw-r--r--app/scripts/lib/controllers/currency.js70
-rw-r--r--app/scripts/lib/controllers/preferences.js33
-rw-r--r--app/scripts/lib/controllers/shapeshift.js104
-rw-r--r--app/scripts/lib/eth-store.js132
-rw-r--r--app/scripts/lib/extension-instance.js19
-rw-r--r--app/scripts/lib/id-management.js19
-rw-r--r--app/scripts/lib/idStore-migrator.js80
-rw-r--r--app/scripts/lib/idStore.js364
-rw-r--r--app/scripts/lib/inpage-provider.js130
-rw-r--r--app/scripts/lib/is-popup-or-notification.js8
-rw-r--r--app/scripts/lib/message-manager.js151
-rw-r--r--app/scripts/lib/migrations.js5
-rw-r--r--app/scripts/lib/migrator/index.js51
-rw-r--r--app/scripts/lib/nodeify.js24
-rw-r--r--app/scripts/lib/notifications.js174
-rw-r--r--app/scripts/lib/obj-multiplex.js8
-rw-r--r--app/scripts/lib/port-stream.js10
-rw-r--r--app/scripts/lib/random-id.js9
-rw-r--r--app/scripts/lib/remote-store.js97
-rw-r--r--app/scripts/lib/sig-util.js28
-rw-r--r--app/scripts/lib/stream-utils.js9
-rw-r--r--app/scripts/lib/tx-utils.js132
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
+}