aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/lib
diff options
context:
space:
mode:
authorChi Kei Chan <chikeichan@gmail.com>2017-09-19 02:28:10 +0800
committerChi Kei Chan <chikeichan@gmail.com>2017-09-19 02:28:10 +0800
commit6c5865d564167c1097d6010e47d1e82a75087fd1 (patch)
tree9ced5adba4b6e5b77724f19a086a10981ba7e375 /app/scripts/lib
parent54bbf8d8590014b92e7857f30bdc2d8f3779431a (diff)
parent693655e2da7cacf5a5326b50bddc37bcece9422e (diff)
downloadtangerine-wallet-browser-6c5865d564167c1097d6010e47d1e82a75087fd1.tar.gz
tangerine-wallet-browser-6c5865d564167c1097d6010e47d1e82a75087fd1.tar.zst
tangerine-wallet-browser-6c5865d564167c1097d6010e47d1e82a75087fd1.zip
Merge branch 'master' into nm
Diffstat (limited to 'app/scripts/lib')
-rw-r--r--app/scripts/lib/auto-reload.js62
-rw-r--r--app/scripts/lib/createLoggerMiddleware.js15
-rw-r--r--app/scripts/lib/createOriginMiddleware.js9
-rw-r--r--app/scripts/lib/createProviderMiddleware.js13
-rw-r--r--app/scripts/lib/inpage-provider.js86
-rw-r--r--app/scripts/lib/nonce-tracker.js107
-rw-r--r--app/scripts/lib/obj-multiplex.js48
-rw-r--r--app/scripts/lib/pending-tx-tracker.js8
-rw-r--r--app/scripts/lib/port-stream.js16
-rw-r--r--app/scripts/lib/stream-utils.js24
-rw-r--r--app/scripts/lib/tx-state-history-helper.js37
11 files changed, 250 insertions, 175 deletions
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index 6abce73ea..cce31c3d2 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -2,33 +2,55 @@ module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
+ let hasBeenWarned = false
+ let reloadInProgress = false
+ let lastTimeUsed
+ let lastSeenNetwork
+
global.web3 = new Proxy(web3, {
- get: (_web3, name) => {
- // get the time of use
- if (name !== '_used') {
- console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/ethereum/mist/releases/tag/v0.9.0')
- _web3._used = Date.now()
+ get: (_web3, key) => {
+ // show warning once on web3 access
+ if (!hasBeenWarned && key !== 'currentProvider') {
+ console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
+ hasBeenWarned = true
}
- return _web3[name]
+ // get the time of use
+ lastTimeUsed = Date.now()
+ // return value normally
+ return _web3[key]
},
- set: (_web3, name, value) => {
- _web3[name] = value
+ set: (_web3, key, value) => {
+ // set value normally
+ _web3[key] = value
},
})
- var networkVersion
observable.subscribe(function (state) {
- // get the initial network
- const curentNetVersion = state.networkVersion
- if (!networkVersion) networkVersion = curentNetVersion
-
- if (curentNetVersion !== networkVersion && web3._used) {
- const timeSinceUse = Date.now() - web3._used
- // if web3 was recently used then delay the reloading of the page
- timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500)
- // prevent reentry into if statement if state updates again before
- // reload
- networkVersion = curentNetVersion
+ // if reload in progress, no need to check reload logic
+ if (reloadInProgress) return
+
+ const currentNetwork = state.networkVersion
+
+ // set the initial network
+ if (!lastSeenNetwork) {
+ lastSeenNetwork = currentNetwork
+ return
+ }
+
+ // skip reload logic if web3 not used
+ if (!lastTimeUsed) return
+
+ // if network did not change, exit
+ if (currentNetwork === lastSeenNetwork) return
+
+ // initiate page reload
+ reloadInProgress = true
+ const timeSinceUse = Date.now() - lastTimeUsed
+ // if web3 was recently used then delay the reloading of the page
+ if (timeSinceUse > 500) {
+ triggerReset()
+ } else {
+ setTimeout(triggerReset, 500)
}
})
}
diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js
new file mode 100644
index 000000000..b92a965de
--- /dev/null
+++ b/app/scripts/lib/createLoggerMiddleware.js
@@ -0,0 +1,15 @@
+// log rpc activity
+module.exports = createLoggerMiddleware
+
+function createLoggerMiddleware({ origin }) {
+ return function loggerMiddleware (req, res, next, end) {
+ next((cb) => {
+ if (res.error) {
+ log.error('Error in RPC response:\n', res)
+ }
+ if (req.isMetamaskInternal) return
+ log.info(`RPC (${origin}):`, req, '->', res)
+ cb()
+ })
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js
new file mode 100644
index 000000000..e1e097cc4
--- /dev/null
+++ b/app/scripts/lib/createOriginMiddleware.js
@@ -0,0 +1,9 @@
+// append dapp origin domain to request
+module.exports = createOriginMiddleware
+
+function createOriginMiddleware({ origin }) {
+ return function originMiddleware (req, res, next, end) {
+ req.origin = origin
+ next()
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js
new file mode 100644
index 000000000..6dd192411
--- /dev/null
+++ b/app/scripts/lib/createProviderMiddleware.js
@@ -0,0 +1,13 @@
+
+module.exports = createProviderMiddleware
+
+// forward requests to provider
+function createProviderMiddleware({ provider }) {
+ return (req, res, next, end) => {
+ provider.sendAsync(req, (err, _res) => {
+ if (err) return end(err)
+ res.result = _res.result
+ end()
+ })
+ }
+} \ No newline at end of file
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index fd032a673..da75c4be2 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -1,8 +1,9 @@
-const pipe = require('pump')
-const StreamProvider = require('web3-stream-provider')
+const pump = require('pump')
+const RpcEngine = require('json-rpc-engine')
+const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
+const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store')
-const ObjectMultiplex = require('./obj-multiplex')
-const createRandomId = require('./random-id')
+const ObjectMultiplex = require('obj-multiplex')
module.exports = MetamaskInpageProvider
@@ -10,60 +11,49 @@ function MetamaskInpageProvider (connectionStream) {
const self = this
// setup connectionStream multiplexing
- var multiStream = self.multiStream = ObjectMultiplex()
- pipe(
+ const mux = self.mux = new ObjectMultiplex()
+ pump(
connectionStream,
- multiStream,
+ mux,
connectionStream,
(err) => logStreamDisconnectWarning('MetaMask', err)
)
// subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
- pipe(
- multiStream.createStream('publicConfig'),
+ pump(
+ mux.createStream('publicConfig'),
self.publicConfigStore,
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
)
// ignore phishing warning message (handled elsewhere)
- multiStream.ignoreStream('phishing')
+ mux.ignoreStream('phishing')
// connect to async provider
- const asyncProvider = self.asyncProvider = new StreamProvider()
- pipe(
- asyncProvider,
- multiStream.createStream('provider'),
- asyncProvider,
+ const streamMiddleware = createStreamMiddleware()
+ pump(
+ streamMiddleware.stream,
+ mux.createStream('provider'),
+ streamMiddleware.stream,
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
)
- // start and stop polling to unblock first block lock
-
- self.idMap = {}
- // handle sendAsync requests via asyncProvider
- self.sendAsync = function (payload, cb) {
- // rewrite request ids
- var request = eachJsonMessage(payload, (message) => {
- var newId = createRandomId()
- self.idMap[newId] = message.id
- message.id = newId
- return message
- })
- // forward to asyncProvider
- 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)
- })
- }
+
+ // handle sendAsync requests via dapp-side rpc engine
+ const rpcEngine = new RpcEngine()
+ rpcEngine.push(createIdRemapMiddleware())
+ rpcEngine.push(streamMiddleware)
+ self.rpcEngine = rpcEngine
+}
+
+// handle sendAsync requests via asyncProvider
+// also remap ids inbound and outbound
+MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
+ const self = this
+ self.rpcEngine.handle(payload, cb)
}
+
MetamaskInpageProvider.prototype.send = function (payload) {
const self = this
@@ -80,7 +70,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'eth_coinbase':
// read from localStorage
selectedAddress = self.publicConfigStore.getState().selectedAddress
- result = selectedAddress
+ result = selectedAddress || null
break
case 'eth_uninstallFilter':
@@ -90,7 +80,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
case 'net_version':
const networkVersion = self.publicConfigStore.getState().networkVersion
- result = networkVersion
+ result = networkVersion || null
break
// throw not-supported Error
@@ -109,10 +99,6 @@ MetamaskInpageProvider.prototype.send = function (payload) {
}
}
-MetamaskInpageProvider.prototype.sendAsync = function () {
- throw new Error('MetamaskInpageProvider - sendAsync not overwritten')
-}
-
MetamaskInpageProvider.prototype.isConnected = function () {
return true
}
@@ -121,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true
// util
-function eachJsonMessage (payload, transformFn) {
- if (Array.isArray(payload)) {
- return payload.map(transformFn)
- } else {
- return transformFn(payload)
- }
-}
-
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js
index 8328e81ec..0029ac953 100644
--- a/app/scripts/lib/nonce-tracker.js
+++ b/app/scripts/lib/nonce-tracker.js
@@ -1,13 +1,14 @@
-const EthQuery = require('eth-query')
+const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
class NonceTracker {
- constructor ({ provider, getPendingTransactions }) {
+ constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
+ this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {}
}
@@ -25,21 +26,28 @@ class NonceTracker {
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
- // calculate next nonce
- // we need to make sure our base count
- // and pending count are from the same block
- const currentBlock = await this._getCurrentBlock()
- const pendingTransactions = this.getPendingTransactions(address)
- const pendingCount = pendingTransactions.length
- assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`)
- const baseCountHex = await this._getTxCount(address, currentBlock)
- const baseCount = parseInt(baseCountHex, 16)
- assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
- const nextNonce = baseCount + pendingCount
+ // evaluate multiple nextNonce strategies
+ const nonceDetails = {}
+ const networkNonceResult = await this._getNetworkNextNonce(address)
+ const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
+ const nextNetworkNonce = networkNonceResult.nonce
+ const highestLocalNonce = highestLocallyConfirmed
+ const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
+
+ const pendingTxs = this.getPendingTransactions(address)
+ const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
+
+ nonceDetails.params = {
+ highestLocalNonce,
+ highestSuggested,
+ nextNetworkNonce,
+ }
+ nonceDetails.local = localNonceResult
+ nonceDetails.network = networkNonceResult
+
+ const nextNonce = Math.max(networkNonceResult.nonce, localNonceResult.nonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
- // collect the numbers used to calculate the nonce for debugging
- const blockNumber = currentBlock.number
- const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount }
+
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
}
@@ -53,15 +61,6 @@ class NonceTracker {
})
}
- async _getTxCount (address, currentBlock) {
- const blockNumber = currentBlock.number
- return new Promise((resolve, reject) => {
- this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => {
- err ? reject(err) : resolve(result)
- })
- })
- }
-
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire()
@@ -83,12 +82,68 @@ class NonceTracker {
return mutex
}
+ async _getNetworkNextNonce (address) {
+ // calculate next nonce
+ // we need to make sure our base count
+ // and pending count are from the same block
+ const currentBlock = await this._getCurrentBlock()
+ const blockNumber = currentBlock.blockNumber
+ const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
+ const baseCount = baseCountBN.toNumber()
+ assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
+ const nonceDetails = { blockNumber, baseCount }
+ return { name: 'network', nonce: baseCount, details: nonceDetails }
+ }
+
+ _getHighestLocallyConfirmed (address) {
+ const confirmedTransactions = this.getConfirmedTransactions(address)
+ const highest = this._getHighestNonce(confirmedTransactions)
+ return Number.isInteger(highest) ? highest + 1 : 0
+ }
+
+ _reduceTxListToUniqueNonces (txList) {
+ const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
+ if (!index) return [txMeta]
+ const nonceMatches = txList.filter((txData) => {
+ return txMeta.txParams.nonce === txData.txParams.nonce
+ })
+ if (nonceMatches.length > 1) return reducedList
+ reducedList.push(txMeta)
+ return reducedList
+ }, [])
+ return reducedTxList
+ }
+
+ _getHighestNonce (txList) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+ const highestNonce = Math.max.apply(null, nonces)
+ return highestNonce
+ }
+
+ _getHighestContinuousFrom (txList, startPoint) {
+ const nonces = txList.map((txMeta) => {
+ const nonce = txMeta.txParams.nonce
+ assert(typeof nonce, 'string', 'nonces should be hex strings')
+ return parseInt(nonce, 16)
+ })
+
+ let highest = startPoint
+ while (nonces.includes(highest)) {
+ highest++
+ }
+
+ return { name: 'local', nonce: highest, details: { startPoint, highest } }
+ }
+
// this is a hotfix for the fact that the blockTracker will
// change when the network changes
_getBlockTracker () {
return this.provider._blockTracker
}
-
}
module.exports = NonceTracker
diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js
deleted file mode 100644
index 0034febe0..000000000
--- a/app/scripts/lib/obj-multiplex.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const through = require('through2')
-
-module.exports = ObjectMultiplex
-
-function ObjectMultiplex (opts) {
- opts = opts || {}
- // create multiplexer
- const mx = through.obj(function (chunk, enc, cb) {
- const name = chunk.name
- const data = chunk.data
- if (!name) {
- console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`)
- return cb()
- }
- const substream = mx.streams[name]
- if (!substream) {
- console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`)
- } else {
- if (substream.push) substream.push(data)
- }
- return cb()
- })
- mx.streams = {}
- // create substreams
- mx.createStream = function (name) {
- const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) {
- mx.push({
- name: name,
- data: chunk,
- })
- return cb()
- })
- mx.on('end', function () {
- return substream.emit('end')
- })
- if (opts.error) {
- mx.on('error', function () {
- return substream.emit('error')
- })
- }
- 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/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 19720db3f..b90851b58 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -1,6 +1,7 @@
const EventEmitter = require('events')
const EthQuery = require('ethjs-query')
const sufficientBalance = require('./util').sufficientBalance
+const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
/*
Utility class for tracking the transactions as they
@@ -28,6 +29,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.getBalance = config.getBalance
this.getPendingTransactions = config.getPendingTransactions
this.publishTransaction = config.publishTransaction
+ this.giveUpOnTransaction = config.giveUpOnTransaction
}
// checks if a signed tx is in a block and
@@ -100,6 +102,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (balance === undefined) return
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
+ if (txMeta.retryCount > RETRY_LIMIT) {
+ return this.giveUpOnTransaction(txMeta.id)
+ }
+
// if the value of the transaction is greater then the balance, fail.
if (!sufficientBalance(txMeta.txParams, balance)) {
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
@@ -160,4 +166,4 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
nonceGlobalLock.releaseLock()
}
-} \ No newline at end of file
+}
diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js
index 607a9c9ed..648d88087 100644
--- a/app/scripts/lib/port-stream.js
+++ b/app/scripts/lib/port-stream.js
@@ -1,5 +1,6 @@
const Duplex = require('readable-stream').Duplex
const inherits = require('util').inherits
+const noop = function(){}
module.exports = PortDuplexStream
@@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) {
if (Buffer.isBuffer(msg)) {
delete msg._isBuffer
var data = new Buffer(msg)
- // console.log('PortDuplexStream - saw message as buffer', data)
this.push(data)
} else {
- // console.log('PortDuplexStream - saw message', msg)
this.push(msg)
}
}
PortDuplexStream.prototype._onDisconnect = function () {
- try {
- this.push(null)
- } catch (err) {
- this.emit('error', err)
- }
+ this.destroy()
}
// stream plumbing
@@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
if (Buffer.isBuffer(msg)) {
var data = msg.toJSON()
data._isBuffer = true
- // console.log('PortDuplexStream - sent message as buffer', data)
this._port.postMessage(data)
} else {
- // console.log('PortDuplexStream - sent message', msg)
this._port.postMessage(msg)
}
} catch (err) {
- // console.error(err)
return cb(new Error('PortDuplexStream - disconnected'))
}
cb()
}
-
-// util
-
-function noop () {}
diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js
index ba79990cc..8bb0b4f3c 100644
--- a/app/scripts/lib/stream-utils.js
+++ b/app/scripts/lib/stream-utils.js
@@ -1,6 +1,6 @@
const Through = require('through2')
-const endOfStream = require('end-of-stream')
-const ObjectMultiplex = require('./obj-multiplex')
+const ObjectMultiplex = require('obj-multiplex')
+const pump = require('pump')
module.exports = {
jsonParseStream: jsonParseStream,
@@ -23,14 +23,14 @@ function jsonStringifyStream () {
}
function setupMultiplex (connectionStream) {
- var mx = ObjectMultiplex()
- connectionStream.pipe(mx).pipe(connectionStream)
- endOfStream(mx, function (err) {
- if (err) console.error(err)
- })
- endOfStream(connectionStream, function (err) {
- if (err) console.error(err)
- mx.destroy()
- })
- return mx
+ const mux = new ObjectMultiplex()
+ pump(
+ connectionStream,
+ mux,
+ connectionStream,
+ (err) => {
+ if (err) console.error(err)
+ }
+ )
+ return mux
}
diff --git a/app/scripts/lib/tx-state-history-helper.js b/app/scripts/lib/tx-state-history-helper.js
new file mode 100644
index 000000000..304069d57
--- /dev/null
+++ b/app/scripts/lib/tx-state-history-helper.js
@@ -0,0 +1,37 @@
+const jsonDiffer = require('fast-json-patch')
+const clone = require('clone')
+
+module.exports = {
+ generateHistoryEntry,
+ replayHistory,
+ snapshotFromTxMeta,
+ migrateFromSnapshotsToDiffs,
+}
+
+
+function migrateFromSnapshotsToDiffs(longHistory) {
+ return (
+ longHistory
+ // convert non-initial history entries into diffs
+ .map((entry, index) => {
+ if (index === 0) return entry
+ return generateHistoryEntry(longHistory[index - 1], entry)
+ })
+ )
+}
+
+function generateHistoryEntry(previousState, newState) {
+ return jsonDiffer.compare(previousState, newState)
+}
+
+function replayHistory(shortHistory) {
+ return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
+}
+
+function snapshotFromTxMeta(txMeta) {
+ // create txMeta snapshot for history
+ const snapshot = clone(txMeta)
+ // dont include previous history in this snapshot
+ delete snapshot.history
+ return snapshot
+} \ No newline at end of file