mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'master' into nm
This commit is contained in:
commit
6c5865d564
55
CHANGELOG.md
55
CHANGELOG.md
@ -2,6 +2,61 @@
|
|||||||
|
|
||||||
## Current Master
|
## Current Master
|
||||||
|
|
||||||
|
- Add ability to export private keys as a file.
|
||||||
|
- Add ability to export seed words as a file.
|
||||||
|
- Changed state logs to a file download than a clipboard copy.
|
||||||
|
- Fixed a long standing memory leak associated with filters installed by dapps
|
||||||
|
- Fix link to support center.
|
||||||
|
|
||||||
|
## 3.10.0 2017-9-11
|
||||||
|
|
||||||
|
- Readded loose keyring label back into the account list.
|
||||||
|
- Remove cryptonator from chrome permissions.
|
||||||
|
- Add info on token contract addresses.
|
||||||
|
- Add validation preventing users from inputting their own addresses as token tracking addresses.
|
||||||
|
- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94)
|
||||||
|
|
||||||
|
## 3.9.13 2017-9-8
|
||||||
|
|
||||||
|
- Changed the way we initialize the inpage provider to fix a bug affecting some developers.
|
||||||
|
|
||||||
|
## 3.9.12 2017-9-6
|
||||||
|
|
||||||
|
- Fix bug that prevented Web3 1.0 compatibility
|
||||||
|
- Make eth_sign deprecation warning less noisy
|
||||||
|
- Add useful link to eth_sign deprecation warning.
|
||||||
|
- Fix bug with network version serialization over synchronous RPC
|
||||||
|
- Add MetaMask version to state logs.
|
||||||
|
- Add the total amount of tokens when multiple tokens are added under the token list
|
||||||
|
- Use HTTPS links for Etherscan.
|
||||||
|
- Update Support center link to new one with HTTPS.
|
||||||
|
- Make web3 deprecation notice more useful by linking to a descriptive article.
|
||||||
|
|
||||||
|
## 3.9.11 2017-8-24
|
||||||
|
|
||||||
|
- Fix nonce calculation bug that would sometimes generate very wrong nonces.
|
||||||
|
- Give up resubmitting a transaction after 3500 blocks.
|
||||||
|
|
||||||
|
## 3.9.10 2017-8-23
|
||||||
|
|
||||||
|
- Improve nonce calculation, to prevent bug where people are unable to send transactions reliably.
|
||||||
|
- Remove link to eth-tx-viz from identicons in tx history.
|
||||||
|
|
||||||
|
## 3.9.9 2017-8-18
|
||||||
|
|
||||||
|
- Fix bug where some transaction submission errors would show an empty screen.
|
||||||
|
- Fix bug that could mis-render token balances when very small.
|
||||||
|
- Fix formatting of eth_sign "Sign Message" view.
|
||||||
|
- Add deprecation warning to eth_sign "Sign Message" view.
|
||||||
|
|
||||||
|
## 3.9.8 2017-8-16
|
||||||
|
|
||||||
|
- Reenable token list.
|
||||||
|
- Remove default tokens.
|
||||||
|
|
||||||
|
## 3.9.7 2017-8-15
|
||||||
|
|
||||||
|
- hotfix - disable token list
|
||||||
- Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0
|
- Added a deprecation warning for web3 https://github.com/ethereum/mist/releases/tag/v0.9.0
|
||||||
|
|
||||||
## 3.9.6 2017-8-09
|
## 3.9.6 2017-8-09
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "MetaMask",
|
"name": "MetaMask",
|
||||||
"short_name": "Metamask",
|
"short_name": "Metamask",
|
||||||
"version": "3.9.6",
|
"version": "3.10.0",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "https://metamask.io",
|
"author": "https://metamask.io",
|
||||||
"description": "Ethereum Browser Extension",
|
"description": "Ethereum Browser Extension",
|
||||||
@ -57,8 +57,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"http://localhost:8545/",
|
"http://localhost:8545/"
|
||||||
"https://api.cryptonator.com/"
|
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"scripts/inpage.js"
|
"scripts/inpage.js"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
const urlUtil = require('url')
|
const urlUtil = require('url')
|
||||||
const endOfStream = require('end-of-stream')
|
const endOfStream = require('end-of-stream')
|
||||||
const pipe = require('pump')
|
const pipe = require('pump')
|
||||||
|
const log = require('loglevel')
|
||||||
|
const extension = require('extensionizer')
|
||||||
const LocalStorageStore = require('obs-store/lib/localStorage')
|
const LocalStorageStore = require('obs-store/lib/localStorage')
|
||||||
const storeTransform = require('obs-store/lib/transform')
|
const storeTransform = require('obs-store/lib/transform')
|
||||||
const ExtensionPlatform = require('./platforms/extension')
|
const ExtensionPlatform = require('./platforms/extension')
|
||||||
@ -9,13 +11,11 @@ const migrations = require('./migrations/')
|
|||||||
const PortStream = require('./lib/port-stream.js')
|
const PortStream = require('./lib/port-stream.js')
|
||||||
const NotificationManager = require('./lib/notification-manager.js')
|
const NotificationManager = require('./lib/notification-manager.js')
|
||||||
const MetamaskController = require('./metamask-controller')
|
const MetamaskController = require('./metamask-controller')
|
||||||
const extension = require('extensionizer')
|
|
||||||
const firstTimeState = require('./first-time-state')
|
const firstTimeState = require('./first-time-state')
|
||||||
|
|
||||||
const STORAGE_KEY = 'metamask-config'
|
const STORAGE_KEY = 'metamask-config'
|
||||||
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||||
|
|
||||||
const log = require('loglevel')
|
|
||||||
window.log = log
|
window.log = log
|
||||||
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
|
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
|
||||||
|
|
||||||
@ -29,12 +29,12 @@ let popupIsOpen = false
|
|||||||
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
|
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
|
||||||
|
|
||||||
// initialization flow
|
// initialization flow
|
||||||
initialize().catch(console.error)
|
initialize().catch(log.error)
|
||||||
|
|
||||||
async function initialize () {
|
async function initialize () {
|
||||||
const initState = await loadStateFromPersistence()
|
const initState = await loadStateFromPersistence()
|
||||||
await setupController(initState)
|
await setupController(initState)
|
||||||
console.log('MetaMask initialization complete.')
|
log.debug('MetaMask initialization complete.')
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
const LocalMessageDuplexStream = require('post-message-stream')
|
|
||||||
const PongStream = require('ping-pong-stream/pong')
|
|
||||||
const PortStream = require('./lib/port-stream.js')
|
|
||||||
const ObjectMultiplex = require('./lib/obj-multiplex')
|
|
||||||
const extension = require('extensionizer')
|
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const pump = require('pump')
|
||||||
|
const LocalMessageDuplexStream = require('post-message-stream')
|
||||||
|
const PongStream = require('ping-pong-stream/pong')
|
||||||
|
const ObjectMultiplex = require('obj-multiplex')
|
||||||
|
const extension = require('extensionizer')
|
||||||
|
const PortStream = require('./lib/port-stream.js')
|
||||||
|
|
||||||
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
|
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
|
||||||
|
|
||||||
// Eventually this streaming injection could be replaced with:
|
// Eventually this streaming injection could be replaced with:
|
||||||
@ -50,22 +51,42 @@ function setupStreams () {
|
|||||||
pageStream.pipe(pluginStream).pipe(pageStream)
|
pageStream.pipe(pluginStream).pipe(pageStream)
|
||||||
|
|
||||||
// setup local multistream channels
|
// setup local multistream channels
|
||||||
const mx = ObjectMultiplex()
|
const mux = new ObjectMultiplex()
|
||||||
mx.on('error', console.error)
|
pump(
|
||||||
mx.pipe(pageStream).pipe(mx)
|
mux,
|
||||||
mx.pipe(pluginStream).pipe(mx)
|
pageStream,
|
||||||
|
mux,
|
||||||
|
(err) => logStreamDisconnectWarning('MetaMask Inpage', err)
|
||||||
|
)
|
||||||
|
pump(
|
||||||
|
mux,
|
||||||
|
pluginStream,
|
||||||
|
mux,
|
||||||
|
(err) => logStreamDisconnectWarning('MetaMask Background', err)
|
||||||
|
)
|
||||||
|
|
||||||
// connect ping stream
|
// connect ping stream
|
||||||
const pongStream = new PongStream({ objectMode: true })
|
const pongStream = new PongStream({ objectMode: true })
|
||||||
pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream)
|
pump(
|
||||||
|
mux,
|
||||||
|
pongStream,
|
||||||
|
mux,
|
||||||
|
(err) => logStreamDisconnectWarning('MetaMask PingPongStream', err)
|
||||||
|
)
|
||||||
|
|
||||||
// connect phishing warning stream
|
// connect phishing warning stream
|
||||||
const phishingStream = mx.createStream('phishing')
|
const phishingStream = mux.createStream('phishing')
|
||||||
phishingStream.once('data', redirectToPhishingWarning)
|
phishingStream.once('data', redirectToPhishingWarning)
|
||||||
|
|
||||||
// ignore unused channels (handled by background, inpage)
|
// ignore unused channels (handled by background, inpage)
|
||||||
mx.ignoreStream('provider')
|
mux.ignoreStream('provider')
|
||||||
mx.ignoreStream('publicConfig')
|
mux.ignoreStream('publicConfig')
|
||||||
|
}
|
||||||
|
|
||||||
|
function logStreamDisconnectWarning (remoteLabel, err) {
|
||||||
|
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
|
||||||
|
if (err) warningMsg += '\n' + err.stack
|
||||||
|
console.warn(warningMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldInjectWeb3 () {
|
function shouldInjectWeb3 () {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const clone = require('clone')
|
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
@ -8,6 +7,7 @@ const TxProviderUtil = require('../lib/tx-utils')
|
|||||||
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
|
||||||
const createId = require('../lib/random-id')
|
const createId = require('../lib/random-id')
|
||||||
const NonceTracker = require('../lib/nonce-tracker')
|
const NonceTracker = require('../lib/nonce-tracker')
|
||||||
|
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
|
||||||
|
|
||||||
module.exports = class TransactionController extends EventEmitter {
|
module.exports = class TransactionController extends EventEmitter {
|
||||||
constructor (opts) {
|
constructor (opts) {
|
||||||
@ -33,6 +33,17 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
err: undefined,
|
err: undefined,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getConfirmedTransactions: (address) => {
|
||||||
|
return this.getFilteredTxList({
|
||||||
|
from: address,
|
||||||
|
status: 'confirmed',
|
||||||
|
err: undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
giveUpOnTransaction: (txId) => {
|
||||||
|
const msg = `Gave up submitting after 3500 blocks un-mined.`
|
||||||
|
this.setTxStatusFailed(txId, msg)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
this.query = new EthQuery(this.provider)
|
this.query = new EthQuery(this.provider)
|
||||||
this.txProviderUtil = new TxProviderUtil(this.provider)
|
this.txProviderUtil = new TxProviderUtil(this.provider)
|
||||||
@ -128,19 +139,17 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
updateTx (txMeta) {
|
updateTx (txMeta) {
|
||||||
// create txMeta snapshot for history
|
// create txMeta snapshot for history
|
||||||
const txMetaForHistory = clone(txMeta)
|
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
// dont include previous history in this snapshot
|
// recover previous tx state obj
|
||||||
delete txMetaForHistory.history
|
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
|
||||||
// add snapshot to tx history
|
// generate history entry and add to history
|
||||||
if (!txMeta.history) txMeta.history = []
|
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState)
|
||||||
txMeta.history.push(txMetaForHistory)
|
txMeta.history.push(entry)
|
||||||
|
|
||||||
|
// commit txMeta to state
|
||||||
const txId = txMeta.id
|
const txId = txMeta.id
|
||||||
const txList = this.getFullTxList()
|
const txList = this.getFullTxList()
|
||||||
const index = txList.findIndex(txData => txData.id === txId)
|
const index = txList.findIndex(txData => txData.id === txId)
|
||||||
if (!txMeta.history) txMeta.history = []
|
|
||||||
txMeta.history.push(txMetaForHistory)
|
|
||||||
|
|
||||||
txList[index] = txMeta
|
txList[index] = txMeta
|
||||||
this._saveTxList(txList)
|
this._saveTxList(txList)
|
||||||
this.emit('update')
|
this.emit('update')
|
||||||
@ -148,16 +157,22 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
// Adds a tx to the txlist
|
// Adds a tx to the txlist
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
const txCount = this.getTxCount()
|
// initialize history
|
||||||
const network = this.getNetwork()
|
txMeta.history = []
|
||||||
const fullTxList = this.getFullTxList()
|
// capture initial snapshot of txMeta for history
|
||||||
const txHistoryLimit = this.txHistoryLimit
|
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
|
txMeta.history.push(snapshot)
|
||||||
|
|
||||||
// checks if the length of the tx history is
|
// checks if the length of the tx history is
|
||||||
// longer then desired persistence limit
|
// longer then desired persistence limit
|
||||||
// and then if it is removes only confirmed
|
// and then if it is removes only confirmed
|
||||||
// or rejected tx's.
|
// or rejected tx's.
|
||||||
// not tx's that are pending or unapproved
|
// not tx's that are pending or unapproved
|
||||||
|
const txCount = this.getTxCount()
|
||||||
|
const network = this.getNetwork()
|
||||||
|
const fullTxList = this.getFullTxList()
|
||||||
|
const txHistoryLimit = this.txHistoryLimit
|
||||||
|
|
||||||
if (txCount > txHistoryLimit - 1) {
|
if (txCount > txHistoryLimit - 1) {
|
||||||
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
||||||
fullTxList.splice(index, 1)
|
fullTxList.splice(index, 1)
|
||||||
@ -206,7 +221,6 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
metamaskNetworkId: this.getNetwork(),
|
metamaskNetworkId: this.getNetwork(),
|
||||||
txParams: txParams,
|
txParams: txParams,
|
||||||
history: [],
|
|
||||||
}
|
}
|
||||||
// add default tx params
|
// add default tx params
|
||||||
await this.addTxDefaults(txMeta)
|
await this.addTxDefaults(txMeta)
|
||||||
|
@ -171,9 +171,9 @@ class KeyringController extends EventEmitter {
|
|||||||
return this.setupAccounts(checkedAccounts)
|
return this.setupAccounts(checkedAccounts)
|
||||||
})
|
})
|
||||||
.then(() => this.persistAllKeyrings())
|
.then(() => this.persistAllKeyrings())
|
||||||
|
.then(() => this._updateMemStoreKeyrings())
|
||||||
.then(() => this.fullUpdate())
|
.then(() => this.fullUpdate())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._updateMemStoreKeyrings()
|
|
||||||
return keyring
|
return keyring
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -208,6 +208,7 @@ class KeyringController extends EventEmitter {
|
|||||||
return selectedKeyring.addAccounts(1)
|
return selectedKeyring.addAccounts(1)
|
||||||
.then(this.setupAccounts.bind(this))
|
.then(this.setupAccounts.bind(this))
|
||||||
.then(this.persistAllKeyrings.bind(this))
|
.then(this.persistAllKeyrings.bind(this))
|
||||||
|
.then(this._updateMemStoreKeyrings.bind(this))
|
||||||
.then(this.fullUpdate.bind(this))
|
.then(this.fullUpdate.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,33 +2,55 @@ module.exports = setupDappAutoReload
|
|||||||
|
|
||||||
function setupDappAutoReload (web3, observable) {
|
function setupDappAutoReload (web3, observable) {
|
||||||
// export web3 as a global, checking for usage
|
// export web3 as a global, checking for usage
|
||||||
|
let hasBeenWarned = false
|
||||||
|
let reloadInProgress = false
|
||||||
|
let lastTimeUsed
|
||||||
|
let lastSeenNetwork
|
||||||
|
|
||||||
global.web3 = new Proxy(web3, {
|
global.web3 = new Proxy(web3, {
|
||||||
get: (_web3, name) => {
|
get: (_web3, key) => {
|
||||||
// get the time of use
|
// show warning once on web3 access
|
||||||
if (name !== '_used') {
|
if (!hasBeenWarned && key !== 'currentProvider') {
|
||||||
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')
|
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')
|
||||||
_web3._used = Date.now()
|
hasBeenWarned = true
|
||||||
}
|
}
|
||||||
return _web3[name]
|
// get the time of use
|
||||||
|
lastTimeUsed = Date.now()
|
||||||
|
// return value normally
|
||||||
|
return _web3[key]
|
||||||
},
|
},
|
||||||
set: (_web3, name, value) => {
|
set: (_web3, key, value) => {
|
||||||
_web3[name] = value
|
// set value normally
|
||||||
|
_web3[key] = value
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
var networkVersion
|
|
||||||
|
|
||||||
observable.subscribe(function (state) {
|
observable.subscribe(function (state) {
|
||||||
// get the initial network
|
// if reload in progress, no need to check reload logic
|
||||||
const curentNetVersion = state.networkVersion
|
if (reloadInProgress) return
|
||||||
if (!networkVersion) networkVersion = curentNetVersion
|
|
||||||
|
|
||||||
if (curentNetVersion !== networkVersion && web3._used) {
|
const currentNetwork = state.networkVersion
|
||||||
const timeSinceUse = Date.now() - web3._used
|
|
||||||
|
// 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 web3 was recently used then delay the reloading of the page
|
||||||
timeSinceUse > 500 ? triggerReset() : setTimeout(triggerReset, 500)
|
if (timeSinceUse > 500) {
|
||||||
// prevent reentry into if statement if state updates again before
|
triggerReset()
|
||||||
// reload
|
} else {
|
||||||
networkVersion = curentNetVersion
|
setTimeout(triggerReset, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
15
app/scripts/lib/createLoggerMiddleware.js
Normal file
15
app/scripts/lib/createLoggerMiddleware.js
Normal file
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
9
app/scripts/lib/createOriginMiddleware.js
Normal file
9
app/scripts/lib/createOriginMiddleware.js
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
13
app/scripts/lib/createProviderMiddleware.js
Normal file
13
app/scripts/lib/createProviderMiddleware.js
Normal file
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
const pipe = require('pump')
|
const pump = require('pump')
|
||||||
const StreamProvider = require('web3-stream-provider')
|
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 LocalStorageStore = require('obs-store')
|
||||||
const ObjectMultiplex = require('./obj-multiplex')
|
const ObjectMultiplex = require('obj-multiplex')
|
||||||
const createRandomId = require('./random-id')
|
|
||||||
|
|
||||||
module.exports = MetamaskInpageProvider
|
module.exports = MetamaskInpageProvider
|
||||||
|
|
||||||
@ -10,60 +11,49 @@ function MetamaskInpageProvider (connectionStream) {
|
|||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
// setup connectionStream multiplexing
|
// setup connectionStream multiplexing
|
||||||
var multiStream = self.multiStream = ObjectMultiplex()
|
const mux = self.mux = new ObjectMultiplex()
|
||||||
pipe(
|
pump(
|
||||||
connectionStream,
|
connectionStream,
|
||||||
multiStream,
|
mux,
|
||||||
connectionStream,
|
connectionStream,
|
||||||
(err) => logStreamDisconnectWarning('MetaMask', err)
|
(err) => logStreamDisconnectWarning('MetaMask', err)
|
||||||
)
|
)
|
||||||
|
|
||||||
// subscribe to metamask public config (one-way)
|
// subscribe to metamask public config (one-way)
|
||||||
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
|
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
|
||||||
pipe(
|
pump(
|
||||||
multiStream.createStream('publicConfig'),
|
mux.createStream('publicConfig'),
|
||||||
self.publicConfigStore,
|
self.publicConfigStore,
|
||||||
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
|
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ignore phishing warning message (handled elsewhere)
|
// ignore phishing warning message (handled elsewhere)
|
||||||
multiStream.ignoreStream('phishing')
|
mux.ignoreStream('phishing')
|
||||||
|
|
||||||
// connect to async provider
|
// connect to async provider
|
||||||
const asyncProvider = self.asyncProvider = new StreamProvider()
|
const streamMiddleware = createStreamMiddleware()
|
||||||
pipe(
|
pump(
|
||||||
asyncProvider,
|
streamMiddleware.stream,
|
||||||
multiStream.createStream('provider'),
|
mux.createStream('provider'),
|
||||||
asyncProvider,
|
streamMiddleware.stream,
|
||||||
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
|
(err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
|
||||||
)
|
)
|
||||||
// start and stop polling to unblock first block lock
|
|
||||||
|
|
||||||
self.idMap = {}
|
// 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
|
// handle sendAsync requests via asyncProvider
|
||||||
self.sendAsync = function (payload, cb) {
|
// also remap ids inbound and outbound
|
||||||
// rewrite request ids
|
MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) {
|
||||||
var request = eachJsonMessage(payload, (message) => {
|
const self = this
|
||||||
var newId = createRandomId()
|
self.rpcEngine.handle(payload, cb)
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
MetamaskInpageProvider.prototype.send = function (payload) {
|
MetamaskInpageProvider.prototype.send = function (payload) {
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
@ -80,7 +70,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
|
|||||||
case 'eth_coinbase':
|
case 'eth_coinbase':
|
||||||
// read from localStorage
|
// read from localStorage
|
||||||
selectedAddress = self.publicConfigStore.getState().selectedAddress
|
selectedAddress = self.publicConfigStore.getState().selectedAddress
|
||||||
result = selectedAddress
|
result = selectedAddress || null
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'eth_uninstallFilter':
|
case 'eth_uninstallFilter':
|
||||||
@ -90,7 +80,7 @@ MetamaskInpageProvider.prototype.send = function (payload) {
|
|||||||
|
|
||||||
case 'net_version':
|
case 'net_version':
|
||||||
const networkVersion = self.publicConfigStore.getState().networkVersion
|
const networkVersion = self.publicConfigStore.getState().networkVersion
|
||||||
result = networkVersion
|
result = networkVersion || null
|
||||||
break
|
break
|
||||||
|
|
||||||
// throw not-supported Error
|
// 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 () {
|
MetamaskInpageProvider.prototype.isConnected = function () {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -121,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true
|
|||||||
|
|
||||||
// util
|
// util
|
||||||
|
|
||||||
function eachJsonMessage (payload, transformFn) {
|
|
||||||
if (Array.isArray(payload)) {
|
|
||||||
return payload.map(transformFn)
|
|
||||||
} else {
|
|
||||||
return transformFn(payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function logStreamDisconnectWarning (remoteLabel, err) {
|
function logStreamDisconnectWarning (remoteLabel, err) {
|
||||||
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
|
let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
|
||||||
if (err) warningMsg += '\n' + err.stack
|
if (err) warningMsg += '\n' + err.stack
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
const EthQuery = require('eth-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const Mutex = require('await-semaphore').Mutex
|
const Mutex = require('await-semaphore').Mutex
|
||||||
|
|
||||||
class NonceTracker {
|
class NonceTracker {
|
||||||
|
|
||||||
constructor ({ provider, getPendingTransactions }) {
|
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
|
||||||
this.provider = provider
|
this.provider = provider
|
||||||
this.ethQuery = new EthQuery(provider)
|
this.ethQuery = new EthQuery(provider)
|
||||||
this.getPendingTransactions = getPendingTransactions
|
this.getPendingTransactions = getPendingTransactions
|
||||||
|
this.getConfirmedTransactions = getConfirmedTransactions
|
||||||
this.lockMap = {}
|
this.lockMap = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,21 +26,28 @@ class NonceTracker {
|
|||||||
await this._globalMutexFree()
|
await this._globalMutexFree()
|
||||||
// await lock free, then take lock
|
// await lock free, then take lock
|
||||||
const releaseLock = await this._takeMutex(address)
|
const releaseLock = await this._takeMutex(address)
|
||||||
// calculate next nonce
|
// evaluate multiple nextNonce strategies
|
||||||
// we need to make sure our base count
|
const nonceDetails = {}
|
||||||
// and pending count are from the same block
|
const networkNonceResult = await this._getNetworkNextNonce(address)
|
||||||
const currentBlock = await this._getCurrentBlock()
|
const highestLocallyConfirmed = this._getHighestLocallyConfirmed(address)
|
||||||
const pendingTransactions = this.getPendingTransactions(address)
|
const nextNetworkNonce = networkNonceResult.nonce
|
||||||
const pendingCount = pendingTransactions.length
|
const highestLocalNonce = highestLocallyConfirmed
|
||||||
assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`)
|
const highestSuggested = Math.max(nextNetworkNonce, highestLocalNonce)
|
||||||
const baseCountHex = await this._getTxCount(address, currentBlock)
|
|
||||||
const baseCount = parseInt(baseCountHex, 16)
|
const pendingTxs = this.getPendingTransactions(address)
|
||||||
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
|
const localNonceResult = this._getHighestContinuousFrom(pendingTxs, highestSuggested) || 0
|
||||||
const nextNonce = baseCount + pendingCount
|
|
||||||
|
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}"`)
|
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 nonce and release cb
|
||||||
return { nextNonce, nonceDetails, releaseLock }
|
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 () {
|
async _globalMutexFree () {
|
||||||
const globalMutex = this._lookupMutex('global')
|
const globalMutex = this._lookupMutex('global')
|
||||||
const release = await globalMutex.acquire()
|
const release = await globalMutex.acquire()
|
||||||
@ -83,12 +82,68 @@ class NonceTracker {
|
|||||||
return mutex
|
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
|
// this is a hotfix for the fact that the blockTracker will
|
||||||
// change when the network changes
|
// change when the network changes
|
||||||
_getBlockTracker () {
|
_getBlockTracker () {
|
||||||
return this.provider._blockTracker
|
return this.provider._blockTracker
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = NonceTracker
|
module.exports = NonceTracker
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const sufficientBalance = require('./util').sufficientBalance
|
const sufficientBalance = require('./util').sufficientBalance
|
||||||
|
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Utility class for tracking the transactions as they
|
Utility class for tracking the transactions as they
|
||||||
@ -28,6 +29,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
this.getBalance = config.getBalance
|
this.getBalance = config.getBalance
|
||||||
this.getPendingTransactions = config.getPendingTransactions
|
this.getPendingTransactions = config.getPendingTransactions
|
||||||
this.publishTransaction = config.publishTransaction
|
this.publishTransaction = config.publishTransaction
|
||||||
|
this.giveUpOnTransaction = config.giveUpOnTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if a signed tx is in a block and
|
// 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 (balance === undefined) return
|
||||||
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
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 the value of the transaction is greater then the balance, fail.
|
||||||
if (!sufficientBalance(txMeta.txParams, balance)) {
|
if (!sufficientBalance(txMeta.txParams, balance)) {
|
||||||
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
|
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const Duplex = require('readable-stream').Duplex
|
const Duplex = require('readable-stream').Duplex
|
||||||
const inherits = require('util').inherits
|
const inherits = require('util').inherits
|
||||||
|
const noop = function(){}
|
||||||
|
|
||||||
module.exports = PortDuplexStream
|
module.exports = PortDuplexStream
|
||||||
|
|
||||||
@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) {
|
|||||||
if (Buffer.isBuffer(msg)) {
|
if (Buffer.isBuffer(msg)) {
|
||||||
delete msg._isBuffer
|
delete msg._isBuffer
|
||||||
var data = new Buffer(msg)
|
var data = new Buffer(msg)
|
||||||
// console.log('PortDuplexStream - saw message as buffer', data)
|
|
||||||
this.push(data)
|
this.push(data)
|
||||||
} else {
|
} else {
|
||||||
// console.log('PortDuplexStream - saw message', msg)
|
|
||||||
this.push(msg)
|
this.push(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PortDuplexStream.prototype._onDisconnect = function () {
|
PortDuplexStream.prototype._onDisconnect = function () {
|
||||||
try {
|
this.destroy()
|
||||||
this.push(null)
|
|
||||||
} catch (err) {
|
|
||||||
this.emit('error', err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stream plumbing
|
// stream plumbing
|
||||||
@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) {
|
|||||||
if (Buffer.isBuffer(msg)) {
|
if (Buffer.isBuffer(msg)) {
|
||||||
var data = msg.toJSON()
|
var data = msg.toJSON()
|
||||||
data._isBuffer = true
|
data._isBuffer = true
|
||||||
// console.log('PortDuplexStream - sent message as buffer', data)
|
|
||||||
this._port.postMessage(data)
|
this._port.postMessage(data)
|
||||||
} else {
|
} else {
|
||||||
// console.log('PortDuplexStream - sent message', msg)
|
|
||||||
this._port.postMessage(msg)
|
this._port.postMessage(msg)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error(err)
|
|
||||||
return cb(new Error('PortDuplexStream - disconnected'))
|
return cb(new Error('PortDuplexStream - disconnected'))
|
||||||
}
|
}
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
// util
|
|
||||||
|
|
||||||
function noop () {}
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const Through = require('through2')
|
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 = {
|
module.exports = {
|
||||||
jsonParseStream: jsonParseStream,
|
jsonParseStream: jsonParseStream,
|
||||||
@ -23,14 +23,14 @@ function jsonStringifyStream () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setupMultiplex (connectionStream) {
|
function setupMultiplex (connectionStream) {
|
||||||
var mx = ObjectMultiplex()
|
const mux = new ObjectMultiplex()
|
||||||
connectionStream.pipe(mx).pipe(connectionStream)
|
pump(
|
||||||
endOfStream(mx, function (err) {
|
connectionStream,
|
||||||
|
mux,
|
||||||
|
connectionStream,
|
||||||
|
(err) => {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
})
|
}
|
||||||
endOfStream(connectionStream, function (err) {
|
)
|
||||||
if (err) console.error(err)
|
return mux
|
||||||
mx.destroy()
|
|
||||||
})
|
|
||||||
return mx
|
|
||||||
}
|
}
|
||||||
|
37
app/scripts/lib/tx-state-history-helper.js
Normal file
37
app/scripts/lib/tx-state-history-helper.js
Normal file
@ -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
|
||||||
|
}
|
@ -1,12 +1,18 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const promiseToCallback = require('promise-to-callback')
|
const promiseToCallback = require('promise-to-callback')
|
||||||
const pipe = require('pump')
|
const pump = require('pump')
|
||||||
const Dnode = require('dnode')
|
const Dnode = require('dnode')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const EthStore = require('./lib/eth-store')
|
const EthStore = require('./lib/eth-store')
|
||||||
const EthQuery = require('eth-query')
|
const EthQuery = require('eth-query')
|
||||||
const streamIntoProvider = require('web3-stream-provider/handler')
|
const RpcEngine = require('json-rpc-engine')
|
||||||
|
const debounce = require('debounce')
|
||||||
|
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
|
||||||
|
const createFilterMiddleware = require('eth-json-rpc-filters')
|
||||||
|
const createOriginMiddleware = require('./lib/createOriginMiddleware')
|
||||||
|
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
|
||||||
|
const createProviderMiddleware = require('./lib/createProviderMiddleware')
|
||||||
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
|
||||||
const KeyringController = require('./keyring-controller')
|
const KeyringController = require('./keyring-controller')
|
||||||
const NetworkController = require('./controllers/network')
|
const NetworkController = require('./controllers/network')
|
||||||
@ -24,8 +30,6 @@ const ConfigManager = require('./lib/config-manager')
|
|||||||
const nodeify = require('./lib/nodeify')
|
const nodeify = require('./lib/nodeify')
|
||||||
const accountImporter = require('./account-import-strategies')
|
const accountImporter = require('./account-import-strategies')
|
||||||
const getBuyEthUrl = require('./lib/buy-eth-url')
|
const getBuyEthUrl = require('./lib/buy-eth-url')
|
||||||
const debounce = require('debounce')
|
|
||||||
|
|
||||||
const version = require('../manifest.json').version
|
const version = require('../manifest.json').version
|
||||||
|
|
||||||
module.exports = class MetamaskController extends EventEmitter {
|
module.exports = class MetamaskController extends EventEmitter {
|
||||||
@ -77,12 +81,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
|
|
||||||
// rpc provider
|
// rpc provider
|
||||||
this.provider = this.initializeProvider()
|
this.provider = this.initializeProvider()
|
||||||
|
this.blockTracker = this.provider
|
||||||
|
|
||||||
// eth data query tools
|
// eth data query tools
|
||||||
this.ethQuery = new EthQuery(this.provider)
|
this.ethQuery = new EthQuery(this.provider)
|
||||||
this.ethStore = new EthStore({
|
this.ethStore = new EthStore({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
blockTracker: this.provider,
|
blockTracker: this.blockTracker,
|
||||||
})
|
})
|
||||||
|
|
||||||
// key mgmt
|
// key mgmt
|
||||||
@ -109,7 +114,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
getNetwork: this.networkController.getNetworkState.bind(this),
|
getNetwork: this.networkController.getNetworkState.bind(this),
|
||||||
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
blockTracker: this.provider,
|
blockTracker: this.blockTracker,
|
||||||
ethQuery: this.ethQuery,
|
ethQuery: this.ethQuery,
|
||||||
ethStore: this.ethStore,
|
ethStore: this.ethStore,
|
||||||
})
|
})
|
||||||
@ -337,36 +342,43 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
setupUntrustedCommunication (connectionStream, originDomain) {
|
setupUntrustedCommunication (connectionStream, originDomain) {
|
||||||
// Check if new connection is blacklisted
|
// Check if new connection is blacklisted
|
||||||
if (this.blacklistController.checkForPhishing(originDomain)) {
|
if (this.blacklistController.checkForPhishing(originDomain)) {
|
||||||
console.log('MetaMask - sending phishing warning for', originDomain)
|
log.debug('MetaMask - sending phishing warning for', originDomain)
|
||||||
this.sendPhishingWarning(connectionStream, originDomain)
|
this.sendPhishingWarning(connectionStream, originDomain)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup multiplexing
|
// setup multiplexing
|
||||||
const mx = setupMultiplex(connectionStream)
|
const mux = setupMultiplex(connectionStream)
|
||||||
// connect features
|
// connect features
|
||||||
this.setupProviderConnection(mx.createStream('provider'), originDomain)
|
this.setupProviderConnection(mux.createStream('provider'), originDomain)
|
||||||
this.setupPublicConfig(mx.createStream('publicConfig'))
|
this.setupPublicConfig(mux.createStream('publicConfig'))
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTrustedCommunication (connectionStream, originDomain) {
|
setupTrustedCommunication (connectionStream, originDomain) {
|
||||||
// setup multiplexing
|
// setup multiplexing
|
||||||
const mx = setupMultiplex(connectionStream)
|
const mux = setupMultiplex(connectionStream)
|
||||||
// connect features
|
// connect features
|
||||||
this.setupControllerConnection(mx.createStream('controller'))
|
this.setupControllerConnection(mux.createStream('controller'))
|
||||||
this.setupProviderConnection(mx.createStream('provider'), originDomain)
|
this.setupProviderConnection(mux.createStream('provider'), originDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPhishingWarning (connectionStream, hostname) {
|
sendPhishingWarning (connectionStream, hostname) {
|
||||||
const mx = setupMultiplex(connectionStream)
|
const mux = setupMultiplex(connectionStream)
|
||||||
const phishingStream = mx.createStream('phishing')
|
const phishingStream = mux.createStream('phishing')
|
||||||
phishingStream.write({ hostname })
|
phishingStream.write({ hostname })
|
||||||
}
|
}
|
||||||
|
|
||||||
setupControllerConnection (outStream) {
|
setupControllerConnection (outStream) {
|
||||||
const api = this.getApi()
|
const api = this.getApi()
|
||||||
const dnode = Dnode(api)
|
const dnode = Dnode(api)
|
||||||
outStream.pipe(dnode).pipe(outStream)
|
pump(
|
||||||
|
outStream,
|
||||||
|
dnode,
|
||||||
|
outStream,
|
||||||
|
(err) => {
|
||||||
|
if (err) log.error(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
dnode.on('remote', (remote) => {
|
dnode.on('remote', (remote) => {
|
||||||
// push updates to popup
|
// push updates to popup
|
||||||
const sendUpdate = remote.sendUpdate.bind(remote)
|
const sendUpdate = remote.sendUpdate.bind(remote)
|
||||||
@ -374,27 +386,42 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setupProviderConnection (outStream, originDomain) {
|
setupProviderConnection (outStream, origin) {
|
||||||
streamIntoProvider(outStream, this.provider, onRequest, onResponse)
|
// setup json rpc engine stack
|
||||||
// append dapp origin domain to request
|
const engine = new RpcEngine()
|
||||||
function onRequest (request) {
|
|
||||||
request.origin = originDomain
|
// create filter polyfill middleware
|
||||||
}
|
const filterMiddleware = createFilterMiddleware({
|
||||||
// log rpc activity
|
provider: this.provider,
|
||||||
function onResponse (err, request, response) {
|
blockTracker: this.blockTracker,
|
||||||
if (err) return console.error(err)
|
})
|
||||||
if (response.error) {
|
|
||||||
console.error('Error in RPC response:\n', response)
|
engine.push(createOriginMiddleware({ origin }))
|
||||||
}
|
engine.push(createLoggerMiddleware({ origin }))
|
||||||
if (request.isMetamaskInternal) return
|
engine.push(filterMiddleware)
|
||||||
log.info(`RPC (${originDomain}):`, request, '->', response)
|
engine.push(createProviderMiddleware({ provider: this.provider }))
|
||||||
|
|
||||||
|
// setup connection
|
||||||
|
const providerStream = createEngineStream({ engine })
|
||||||
|
pump(
|
||||||
|
outStream,
|
||||||
|
providerStream,
|
||||||
|
outStream,
|
||||||
|
(err) => {
|
||||||
|
// cleanup filter polyfill middleware
|
||||||
|
filterMiddleware.destroy()
|
||||||
|
if (err) log.error(err)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPublicConfig (outStream) {
|
setupPublicConfig (outStream) {
|
||||||
pipe(
|
pump(
|
||||||
this.publicConfigStore,
|
this.publicConfigStore,
|
||||||
outStream
|
outStream,
|
||||||
|
(err) => {
|
||||||
|
if (err) log.error(err)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
52
app/scripts/migrations/018.js
Normal file
52
app/scripts/migrations/018.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const version = 18
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This migration updates "transaction state history" to diffs style
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const clone = require('clone')
|
||||||
|
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version,
|
||||||
|
|
||||||
|
migrate: function (originalVersionedData) {
|
||||||
|
const versionedData = clone(originalVersionedData)
|
||||||
|
versionedData.meta.version = version
|
||||||
|
try {
|
||||||
|
const state = versionedData.data
|
||||||
|
const newState = transformState(state)
|
||||||
|
versionedData.data = newState
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`MetaMask Migration #${version}` + err.stack)
|
||||||
|
}
|
||||||
|
return Promise.resolve(versionedData)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformState (state) {
|
||||||
|
const newState = state
|
||||||
|
const transactions = newState.TransactionController.transactions
|
||||||
|
newState.TransactionController.transactions = transactions.map((txMeta) => {
|
||||||
|
// no history: initialize
|
||||||
|
if (!txMeta.history || txMeta.history.length === 0) {
|
||||||
|
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
|
txMeta.history = [snapshot]
|
||||||
|
return txMeta
|
||||||
|
}
|
||||||
|
// has history: migrate
|
||||||
|
const newHistory = (
|
||||||
|
txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
|
||||||
|
// remove empty diffs
|
||||||
|
.filter((entry) => {
|
||||||
|
return !Array.isArray(entry) || entry.length > 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
txMeta.history = newHistory
|
||||||
|
return txMeta
|
||||||
|
})
|
||||||
|
return newState
|
||||||
|
}
|
83
app/scripts/migrations/019.js
Normal file
83
app/scripts/migrations/019.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
const version = 19
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This migration sets transactions as failed
|
||||||
|
whos nonce is too high
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const clone = require('clone')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version,
|
||||||
|
|
||||||
|
migrate: function (originalVersionedData) {
|
||||||
|
const versionedData = clone(originalVersionedData)
|
||||||
|
versionedData.meta.version = version
|
||||||
|
try {
|
||||||
|
const state = versionedData.data
|
||||||
|
const newState = transformState(state)
|
||||||
|
versionedData.data = newState
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`MetaMask Migration #${version}` + err.stack)
|
||||||
|
}
|
||||||
|
return Promise.resolve(versionedData)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformState (state) {
|
||||||
|
const newState = state
|
||||||
|
const transactions = newState.TransactionController.transactions
|
||||||
|
|
||||||
|
newState.TransactionController.transactions = transactions.map((txMeta, _, txList) => {
|
||||||
|
if (txMeta.status !== 'submitted') return txMeta
|
||||||
|
|
||||||
|
const confirmedTxs = txList.filter((tx) => tx.status === 'confirmed')
|
||||||
|
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
|
||||||
|
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
|
||||||
|
const highestConfirmedNonce = getHighestNonce(confirmedTxs)
|
||||||
|
|
||||||
|
const pendingTxs = txList.filter((tx) => tx.status === 'submitted')
|
||||||
|
.filter((tx) => tx.txParams.from === txMeta.txParams.from)
|
||||||
|
.filter((tx) => tx.metamaskNetworkId.from === txMeta.metamaskNetworkId.from)
|
||||||
|
const highestContinuousNonce = getHighestContinuousFrom(pendingTxs, highestConfirmedNonce)
|
||||||
|
|
||||||
|
const maxNonce = Math.max(highestContinuousNonce, highestConfirmedNonce)
|
||||||
|
|
||||||
|
if (parseInt(txMeta.txParams.nonce, 16) > maxNonce + 1) {
|
||||||
|
txMeta.status = 'failed'
|
||||||
|
txMeta.err = {
|
||||||
|
message: 'nonce too high',
|
||||||
|
note: 'migration 019 custom error',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return txMeta
|
||||||
|
})
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighestContinuousFrom (txList, startPoint) {
|
||||||
|
const nonces = txList.map((txMeta) => {
|
||||||
|
const nonce = txMeta.txParams.nonce
|
||||||
|
return parseInt(nonce, 16)
|
||||||
|
})
|
||||||
|
|
||||||
|
let highest = startPoint
|
||||||
|
while (nonces.includes(highest)) {
|
||||||
|
highest++
|
||||||
|
}
|
||||||
|
|
||||||
|
return highest
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHighestNonce (txList) {
|
||||||
|
const nonces = txList.map((txMeta) => {
|
||||||
|
const nonce = txMeta.txParams.nonce
|
||||||
|
return parseInt(nonce || '0x0', 16)
|
||||||
|
})
|
||||||
|
const highestNonce = Math.max.apply(null, nonces)
|
||||||
|
return highestNonce
|
||||||
|
}
|
||||||
|
|
@ -28,4 +28,6 @@ module.exports = [
|
|||||||
require('./015'),
|
require('./015'),
|
||||||
require('./016'),
|
require('./016'),
|
||||||
require('./017'),
|
require('./017'),
|
||||||
|
require('./018'),
|
||||||
|
require('./019'),
|
||||||
]
|
]
|
||||||
|
15
circle.yml
15
circle.yml
@ -1,10 +1,17 @@
|
|||||||
machine:
|
machine:
|
||||||
node:
|
node:
|
||||||
version: 8.1.4
|
version: 8.1.4
|
||||||
dependencies:
|
|
||||||
pre:
|
|
||||||
- "npm i -g testem"
|
|
||||||
- "npm i -g mocha"
|
|
||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- "npm run ci"
|
- "npm run ci"
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- sudo apt-get update
|
||||||
|
# get latest stable firefox
|
||||||
|
- sudo apt-get install firefox
|
||||||
|
- firefox_cmd=`which firefox`; sudo rm -f $firefox_cmd; sudo ln -s `which firefox.ubuntu` $firefox_cmd
|
||||||
|
# get latest stable chrome
|
||||||
|
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||||
|
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||||
|
- sudo apt-get update
|
||||||
|
- sudo apt-get install google-chrome-stable
|
@ -1,7 +1,7 @@
|
|||||||
const createStore = require('redux').createStore
|
const createStore = require('redux').createStore
|
||||||
const applyMiddleware = require('redux').applyMiddleware
|
const applyMiddleware = require('redux').applyMiddleware
|
||||||
const thunkMiddleware = require('redux-thunk')
|
const thunkMiddleware = require('redux-thunk').default
|
||||||
const createLogger = require('redux-logger')
|
const createLogger = require('redux-logger').createLogger
|
||||||
const rootReducer = require('../ui/app/reducers')
|
const rootReducer = require('../ui/app/reducers')
|
||||||
|
|
||||||
module.exports = configureStore
|
module.exports = configureStore
|
||||||
|
61
karma.conf.js
Normal file
61
karma.conf.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Karma configuration
|
||||||
|
// Generated on Mon Sep 11 2017 18:45:48 GMT-0700 (PDT)
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
|
config.set({
|
||||||
|
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||||
|
basePath: process.cwd(),
|
||||||
|
|
||||||
|
browserConsoleLogOptions: {
|
||||||
|
terminal: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// frameworks to use
|
||||||
|
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||||
|
frameworks: ['qunit'],
|
||||||
|
|
||||||
|
// list of files / patterns to load in the browser
|
||||||
|
files: [
|
||||||
|
'development/bundle.js',
|
||||||
|
'test/integration/jquery-3.1.0.min.js',
|
||||||
|
'test/integration/bundle.js',
|
||||||
|
{ pattern: 'dist/chrome/images/**/*.*', watched: false, included: false, served: true },
|
||||||
|
{ pattern: 'dist/chrome/fonts/**/*.*', watched: false, included: false, served: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
proxies: {
|
||||||
|
'/images/': '/base/dist/chrome/images/',
|
||||||
|
'/fonts/': '/base/dist/chrome/fonts/',
|
||||||
|
},
|
||||||
|
|
||||||
|
// test results reporter to use
|
||||||
|
// possible values: 'dots', 'progress'
|
||||||
|
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||||
|
reporters: ['progress'],
|
||||||
|
|
||||||
|
// web server port
|
||||||
|
port: 9876,
|
||||||
|
|
||||||
|
// enable / disable colors in the output (reporters and logs)
|
||||||
|
colors: true,
|
||||||
|
|
||||||
|
// level of logging
|
||||||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
|
||||||
|
// enable / disable watching file and executing tests whenever any file changes
|
||||||
|
autoWatch: false,
|
||||||
|
|
||||||
|
// start these browsers
|
||||||
|
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||||
|
browsers: ['Chrome', 'Firefox'],
|
||||||
|
|
||||||
|
// Continuous Integration mode
|
||||||
|
// if true, Karma captures browsers, runs the tests and exits
|
||||||
|
singleRun: true,
|
||||||
|
|
||||||
|
// Concurrency level
|
||||||
|
// how many browser should be started simultaneous
|
||||||
|
concurrency: Infinity
|
||||||
|
})
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
const Iframe = require('iframe')
|
const Iframe = require('iframe')
|
||||||
const IframeStream = require('iframe-stream').IframeStream
|
const createIframeStream = require('iframe-stream').IframeStream
|
||||||
|
|
||||||
module.exports = setupIframe
|
module.exports = setupIframe
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ function setupIframe(opts) {
|
|||||||
})
|
})
|
||||||
var iframe = frame.iframe
|
var iframe = frame.iframe
|
||||||
iframe.style.setProperty('display', 'none')
|
iframe.style.setProperty('display', 'none')
|
||||||
var iframeStream = new IframeStream(iframe)
|
var iframeStream = createIframeStream(iframe)
|
||||||
|
|
||||||
return iframeStream
|
return iframeStream
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const ParentStream = require('iframe-stream').ParentStream
|
const createParentStream = require('iframe-stream').ParentStream
|
||||||
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
|
const SWcontroller = require('client-sw-ready-event/lib/sw-client.js')
|
||||||
const SwStream = require('sw-stream/lib/sw-stream.js')
|
const SwStream = require('sw-stream/lib/sw-stream.js')
|
||||||
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
|
const SetupUntrustedComunication = ('./lib/setup-untrusted-connection.js')
|
||||||
@ -11,7 +11,7 @@ const background = new SWcontroller({
|
|||||||
intervalDelay,
|
intervalDelay,
|
||||||
})
|
})
|
||||||
|
|
||||||
const pageStream = new ParentStream()
|
const pageStream = createParentStream()
|
||||||
background.on('ready', (_) => {
|
background.on('ready', (_) => {
|
||||||
let swStream = SwStream({
|
let swStream = SwStream({
|
||||||
serviceWorker: background.controller,
|
serviceWorker: background.controller,
|
||||||
|
13
mock-dev.js
13
mock-dev.js
@ -85,12 +85,19 @@ actions.update = function(stateName) {
|
|||||||
var css = MetaMaskUiCss()
|
var css = MetaMaskUiCss()
|
||||||
injectCss(css)
|
injectCss(css)
|
||||||
|
|
||||||
const container = document.querySelector('#app-content')
|
|
||||||
|
|
||||||
// parse opts
|
// parse opts
|
||||||
var store = configureStore(firstState)
|
var store = configureStore(firstState)
|
||||||
|
|
||||||
// start app
|
// start app
|
||||||
|
startApp()
|
||||||
|
|
||||||
|
function startApp(){
|
||||||
|
const body = document.body
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.id = 'app-content'
|
||||||
|
body.appendChild(container)
|
||||||
|
console.log('container', container)
|
||||||
|
|
||||||
render(
|
render(
|
||||||
h('.super-dev-container', [
|
h('.super-dev-container', [
|
||||||
|
|
||||||
@ -121,4 +128,4 @@ render(
|
|||||||
|
|
||||||
]
|
]
|
||||||
), container)
|
), container)
|
||||||
|
}
|
||||||
|
23
package.json
23
package.json
@ -12,8 +12,8 @@
|
|||||||
"test": "npm run lint && npm run test-unit && npm run test-integration",
|
"test": "npm run lint && npm run test-unit && npm run test-integration",
|
||||||
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
|
"test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"",
|
||||||
"single-test": "METAMASK_ENV=test mocha --require test/helper.js",
|
"single-test": "METAMASK_ENV=test mocha --require test/helper.js",
|
||||||
"test-integration": "npm run buildMock && npm run buildCiUnits && testem ci -P 2",
|
"test-integration": "npm run buildMock && npm run buildCiUnits && karma start",
|
||||||
"test-coverage": "nyc npm run test-unit && nyc report --reporter=text-lcov | coveralls",
|
"test-coverage": "nyc npm run test-unit && if [ $COVERALLS_REPO_TOKEN ]; then nyc report --reporter=text-lcov | coveralls; fi",
|
||||||
"ci": "npm run lint && npm run test-coverage && npm run test-integration",
|
"ci": "npm run lint && npm run test-coverage && npm run test-integration",
|
||||||
"lint": "gulp lint",
|
"lint": "gulp lint",
|
||||||
"buildCiUnits": "node test/integration/index.js",
|
"buildCiUnits": "node test/integration/index.js",
|
||||||
@ -22,7 +22,6 @@
|
|||||||
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
"ui": "npm run genStates && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
||||||
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
"mock": "beefy mock-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
||||||
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
|
"buildMock": "npm run genStates && browserify ./mock-dev.js -o ./development/bundle.js",
|
||||||
"testem": "npm run buildMock && testem",
|
|
||||||
"announce": "node development/announcer.js",
|
"announce": "node development/announcer.js",
|
||||||
"generateNotice": "node notices/notice-generator.js",
|
"generateNotice": "node notices/notice-generator.js",
|
||||||
"deleteNotice": "node notices/notice-delete.js",
|
"deleteNotice": "node notices/notice-delete.js",
|
||||||
@ -74,11 +73,12 @@
|
|||||||
"eth-bin-to-ops": "^1.0.1",
|
"eth-bin-to-ops": "^1.0.1",
|
||||||
"eth-contract-metadata": "^1.1.4",
|
"eth-contract-metadata": "^1.1.4",
|
||||||
"eth-hd-keyring": "^1.1.1",
|
"eth-hd-keyring": "^1.1.1",
|
||||||
|
"eth-json-rpc-filters": "^1.1.0",
|
||||||
"eth-phishing-detect": "^1.1.4",
|
"eth-phishing-detect": "^1.1.4",
|
||||||
"eth-query": "^2.1.2",
|
"eth-query": "^2.1.2",
|
||||||
"eth-sig-util": "^1.2.2",
|
"eth-sig-util": "^1.2.2",
|
||||||
"eth-simple-keyring": "^1.1.1",
|
"eth-simple-keyring": "^1.1.1",
|
||||||
"eth-token-tracker": "^1.1.2",
|
"eth-token-tracker": "^1.1.3",
|
||||||
"ethereumjs-tx": "^1.3.0",
|
"ethereumjs-tx": "^1.3.0",
|
||||||
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
|
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
|
||||||
"ethereumjs-wallet": "^0.6.0",
|
"ethereumjs-wallet": "^0.6.0",
|
||||||
@ -88,6 +88,7 @@
|
|||||||
"express": "^4.14.0",
|
"express": "^4.14.0",
|
||||||
"extension-link-enabler": "^1.0.0",
|
"extension-link-enabler": "^1.0.0",
|
||||||
"extensionizer": "^1.0.0",
|
"extensionizer": "^1.0.0",
|
||||||
|
"fast-json-patch": "^2.0.4",
|
||||||
"fast-levenshtein": "^2.0.6",
|
"fast-levenshtein": "^2.0.6",
|
||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-autoprefixer": "^4.0.0",
|
"gulp-autoprefixer": "^4.0.0",
|
||||||
@ -101,12 +102,15 @@
|
|||||||
"iframe-stream": "^3.0.0",
|
"iframe-stream": "^3.0.0",
|
||||||
"inject-css": "^0.1.1",
|
"inject-css": "^0.1.1",
|
||||||
"jazzicon": "^1.2.0",
|
"jazzicon": "^1.2.0",
|
||||||
|
"json-rpc-engine": "^3.1.0",
|
||||||
|
"json-rpc-middleware-stream": "^1.0.0",
|
||||||
"loglevel": "^1.4.1",
|
"loglevel": "^1.4.1",
|
||||||
"metamask-logo": "^2.1.2",
|
"metamask-logo": "^2.1.2",
|
||||||
"mississippi": "^1.2.0",
|
"mississippi": "^1.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"multiplex": "^6.7.0",
|
"multiplex": "^6.7.0",
|
||||||
"number-to-bn": "^1.7.0",
|
"number-to-bn": "^1.7.0",
|
||||||
|
"obj-multiplex": "^1.0.0",
|
||||||
"obs-store": "^2.3.1",
|
"obs-store": "^2.3.1",
|
||||||
"once": "^1.3.3",
|
"once": "^1.3.3",
|
||||||
"ping-pong-stream": "^1.0.0",
|
"ping-pong-stream": "^1.0.0",
|
||||||
@ -130,7 +134,7 @@
|
|||||||
"react-tooltip-component": "^0.3.0",
|
"react-tooltip-component": "^0.3.0",
|
||||||
"react-transition-group": "^2.2.0",
|
"react-transition-group": "^2.2.0",
|
||||||
"reactify": "^1.1.1",
|
"reactify": "^1.1.1",
|
||||||
"readable-stream": "^2.1.2",
|
"readable-stream": "^2.3.3",
|
||||||
"redux": "^3.0.5",
|
"redux": "^3.0.5",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"redux-thunk": "^2.2.0",
|
"redux-thunk": "^2.2.0",
|
||||||
@ -150,7 +154,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.24.1",
|
"babel-core": "^6.24.1",
|
||||||
"babel-eslint": "^7.2.3",
|
"babel-eslint": "^8.0.0",
|
||||||
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-polyfill": "^6.23.0",
|
"babel-polyfill": "^6.23.0",
|
||||||
@ -186,6 +190,11 @@
|
|||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"jshint-stylish": "~2.2.1",
|
"jshint-stylish": "~2.2.1",
|
||||||
"json-rpc-engine": "^3.0.1",
|
"json-rpc-engine": "^3.0.1",
|
||||||
|
"karma": "^1.7.1",
|
||||||
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-cli": "^1.0.1",
|
||||||
|
"karma-firefox-launcher": "^1.0.1",
|
||||||
|
"karma-qunit": "^1.2.1",
|
||||||
"lodash.assign": "^4.0.6",
|
"lodash.assign": "^4.0.6",
|
||||||
"mocha": "^3.4.2",
|
"mocha": "^3.4.2",
|
||||||
"mocha-eslint": "^4.0.0",
|
"mocha-eslint": "^4.0.0",
|
||||||
@ -200,8 +209,8 @@
|
|||||||
"react-addons-test-utils": "^15.5.1",
|
"react-addons-test-utils": "^15.5.1",
|
||||||
"react-test-renderer": "^15.5.4",
|
"react-test-renderer": "^15.5.4",
|
||||||
"react-testutils-additions": "^15.2.0",
|
"react-testutils-additions": "^15.2.0",
|
||||||
"sinon": "^2.3.8",
|
|
||||||
"stylelint-config-standard": "^17.0.0",
|
"stylelint-config-standard": "^17.0.0",
|
||||||
|
"sinon": "^3.2.0",
|
||||||
"tape": "^4.5.1",
|
"tape": "^4.5.1",
|
||||||
"testem": "^1.10.3",
|
"testem": "^1.10.3",
|
||||||
"uglifyify": "^4.0.2",
|
"uglifyify": "^4.0.2",
|
||||||
|
3053
test/data/v17-long-history.json
Normal file
3053
test/data/v17-long-history.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
|||||||
function wait(time) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
setTimeout(function () {
|
|
||||||
resolve()
|
|
||||||
}, time * 3 || 1500)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const pump = require('pump')
|
||||||
const browserify = require('browserify')
|
const browserify = require('browserify')
|
||||||
const tests = fs.readdirSync(path.join(__dirname, 'lib'))
|
const tests = fs.readdirSync(path.join(__dirname, 'lib'))
|
||||||
const bundlePath = path.join(__dirname, 'bundle.js')
|
const bundlePath = path.join(__dirname, 'bundle.js')
|
||||||
@ -9,11 +10,17 @@ const b = browserify()
|
|||||||
const writeStream = fs.createWriteStream(bundlePath)
|
const writeStream = fs.createWriteStream(bundlePath)
|
||||||
|
|
||||||
tests.forEach(function (fileName) {
|
tests.forEach(function (fileName) {
|
||||||
b.add(path.join(__dirname, 'lib', fileName))
|
const filePath = path.join(__dirname, 'lib', fileName)
|
||||||
|
console.log(`bundling test "${filePath}"`)
|
||||||
|
b.add(filePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
b.bundle()
|
pump(
|
||||||
.pipe(writeStream)
|
b.bundle(),
|
||||||
.on('error', (err) => {
|
writeStream,
|
||||||
throw err
|
(err) => {
|
||||||
})
|
if (err) throw err
|
||||||
|
console.log(`Integration test build completed: "${bundlePath}"`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
)
|
@ -2,125 +2,137 @@ const PASSWORD = 'password123'
|
|||||||
|
|
||||||
QUnit.module('first time usage')
|
QUnit.module('first time usage')
|
||||||
|
|
||||||
QUnit.test('render init screen', function (assert) {
|
QUnit.test('render init screen', (assert) => {
|
||||||
var done = assert.async()
|
const done = assert.async()
|
||||||
let app
|
runFirstTimeUsageTest(assert).then(done).catch((err) => {
|
||||||
|
assert.notOk(err, `Error was thrown: ${err.stack}`)
|
||||||
wait().then(function() {
|
|
||||||
app = $('iframe').contents().find('#app-content .mock-app-root')
|
|
||||||
|
|
||||||
const recurseNotices = function () {
|
|
||||||
let button = app.find('button')
|
|
||||||
if (button.html() === 'Accept') {
|
|
||||||
let termsPage = app.find('.markdown')[0]
|
|
||||||
termsPage.scrollTop = termsPage.scrollHeight
|
|
||||||
return wait().then(() => {
|
|
||||||
button.click()
|
|
||||||
return wait()
|
|
||||||
}).then(() => {
|
|
||||||
return recurseNotices()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return wait()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return recurseNotices()
|
|
||||||
}).then(function() {
|
|
||||||
// Scroll through terms
|
|
||||||
var title = app.find('h1').text()
|
|
||||||
assert.equal(title, 'MetaMask', 'title screen')
|
|
||||||
|
|
||||||
// enter password
|
|
||||||
var pwBox = app.find('#password-box')[0]
|
|
||||||
var confBox = app.find('#password-box-confirm')[0]
|
|
||||||
pwBox.value = PASSWORD
|
|
||||||
confBox.value = PASSWORD
|
|
||||||
|
|
||||||
return wait()
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
// create vault
|
|
||||||
var createButton = app.find('button.primary')[0]
|
|
||||||
createButton.click()
|
|
||||||
|
|
||||||
return wait(1500)
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
var created = app.find('h3')[0]
|
|
||||||
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
|
|
||||||
|
|
||||||
// Agree button
|
|
||||||
var button = app.find('button')[0]
|
|
||||||
assert.ok(button, 'button present')
|
|
||||||
button.click()
|
|
||||||
|
|
||||||
return wait(1000)
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
var detail = app.find('.account-detail-section')[0]
|
|
||||||
assert.ok(detail, 'Account detail section loaded.')
|
|
||||||
|
|
||||||
var sandwich = app.find('.sandwich-expando')[0]
|
|
||||||
sandwich.click()
|
|
||||||
|
|
||||||
return wait()
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
var sandwich = app.find('.menu-droppo')[0]
|
|
||||||
var children = sandwich.children
|
|
||||||
var lock = children[children.length - 2]
|
|
||||||
assert.ok(lock, 'Lock menu item found')
|
|
||||||
lock.click()
|
|
||||||
|
|
||||||
return wait(1000)
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
var pwBox = app.find('#password-box')[0]
|
|
||||||
pwBox.value = PASSWORD
|
|
||||||
|
|
||||||
var createButton = app.find('button.primary')[0]
|
|
||||||
createButton.click()
|
|
||||||
|
|
||||||
return wait(1000)
|
|
||||||
}).then(function() {
|
|
||||||
|
|
||||||
var detail = app.find('.account-detail-section')[0]
|
|
||||||
assert.ok(detail, 'Account detail section loaded again.')
|
|
||||||
|
|
||||||
return wait()
|
|
||||||
}).then(function (){
|
|
||||||
|
|
||||||
var qrButton = app.find('.fa.fa-ellipsis-h')[0] // open account settings dropdown
|
|
||||||
qrButton.click()
|
|
||||||
|
|
||||||
return wait(1000)
|
|
||||||
}).then(function (){
|
|
||||||
|
|
||||||
var qrButton = app.find('.dropdown-menu-item')[1] // qr code item
|
|
||||||
qrButton.click()
|
|
||||||
|
|
||||||
return wait(1000)
|
|
||||||
}).then(function (){
|
|
||||||
|
|
||||||
var qrHeader = app.find('.qr-header')[0]
|
|
||||||
var qrContainer = app.find('#qr-container')[0]
|
|
||||||
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
|
|
||||||
assert.ok(qrContainer, 'QR Container found')
|
|
||||||
|
|
||||||
return wait()
|
|
||||||
}).then(function (){
|
|
||||||
|
|
||||||
var networkMenu = app.find('.network-indicator')[0]
|
|
||||||
networkMenu.click()
|
|
||||||
|
|
||||||
return wait()
|
|
||||||
}).then(function (){
|
|
||||||
|
|
||||||
var networkMenu = app.find('.network-indicator')[0]
|
|
||||||
var children = networkMenu.children
|
|
||||||
children.length[3]
|
|
||||||
assert.ok(children, 'All network options present')
|
|
||||||
|
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// QUnit.testDone(({ module, name, total, passed, failed, skipped, todo, runtime }) => {
|
||||||
|
// if (failed > 0) {
|
||||||
|
// const app = $('iframe').contents()[0].documentElement
|
||||||
|
// console.warn('Test failures - dumping DOM:')
|
||||||
|
// console.log(app.innerHTML)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
async function runFirstTimeUsageTest(assert, done) {
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
const app = $('#app-content .mock-app-root')
|
||||||
|
|
||||||
|
// recurse notices
|
||||||
|
while (true) {
|
||||||
|
const button = app.find('button')
|
||||||
|
if (button.html() === 'Accept') {
|
||||||
|
// still notices to accept
|
||||||
|
const termsPage = app.find('.markdown')[0]
|
||||||
|
termsPage.scrollTop = termsPage.scrollHeight
|
||||||
|
await timeout()
|
||||||
|
button.click()
|
||||||
|
await timeout()
|
||||||
|
} else {
|
||||||
|
// exit loop
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
// Scroll through terms
|
||||||
|
const title = app.find('h1').text()
|
||||||
|
assert.equal(title, 'MetaMask', 'title screen')
|
||||||
|
|
||||||
|
// enter password
|
||||||
|
const pwBox = app.find('#password-box')[0]
|
||||||
|
const confBox = app.find('#password-box-confirm')[0]
|
||||||
|
pwBox.value = PASSWORD
|
||||||
|
confBox.value = PASSWORD
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
// create vault
|
||||||
|
const createButton = app.find('button.primary')[0]
|
||||||
|
createButton.click()
|
||||||
|
|
||||||
|
await timeout(1500)
|
||||||
|
|
||||||
|
const created = app.find('h3')[0]
|
||||||
|
assert.equal(created.textContent, 'Vault Created', 'Vault created screen')
|
||||||
|
|
||||||
|
// Agree button
|
||||||
|
const button = app.find('button')[0]
|
||||||
|
assert.ok(button, 'button present')
|
||||||
|
button.click()
|
||||||
|
|
||||||
|
await timeout(1000)
|
||||||
|
|
||||||
|
const detail = app.find('.account-detail-section')[0]
|
||||||
|
assert.ok(detail, 'Account detail section loaded.')
|
||||||
|
|
||||||
|
const sandwich = app.find('.sandwich-expando')[0]
|
||||||
|
sandwich.click()
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
const menu = app.find('.menu-droppo')[0]
|
||||||
|
const children = menu.children
|
||||||
|
const lock = children[children.length - 2]
|
||||||
|
assert.ok(lock, 'Lock menu item found')
|
||||||
|
lock.click()
|
||||||
|
|
||||||
|
await timeout(1000)
|
||||||
|
|
||||||
|
const pwBox2 = app.find('#password-box')[0]
|
||||||
|
pwBox2.value = PASSWORD
|
||||||
|
|
||||||
|
const createButton2 = app.find('button.primary')[0]
|
||||||
|
createButton2.click()
|
||||||
|
|
||||||
|
await timeout(1000)
|
||||||
|
|
||||||
|
const detail2 = app.find('.account-detail-section')[0]
|
||||||
|
assert.ok(detail2, 'Account detail section loaded again.')
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
// open account settings dropdown
|
||||||
|
const qrButton = app.find('.fa.fa-ellipsis-h')[0]
|
||||||
|
qrButton.click()
|
||||||
|
|
||||||
|
await timeout(1000)
|
||||||
|
|
||||||
|
// qr code item
|
||||||
|
const qrButton2 = app.find('.dropdown-menu-item')[1]
|
||||||
|
qrButton2.click()
|
||||||
|
|
||||||
|
await timeout(1000)
|
||||||
|
|
||||||
|
const qrHeader = app.find('.qr-header')[0]
|
||||||
|
const qrContainer = app.find('#qr-container')[0]
|
||||||
|
assert.equal(qrHeader.textContent, 'Account 1', 'Should show account label.')
|
||||||
|
assert.ok(qrContainer, 'QR Container found')
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
const networkMenu = app.find('.network-indicator')[0]
|
||||||
|
networkMenu.click()
|
||||||
|
|
||||||
|
await timeout()
|
||||||
|
|
||||||
|
const networkMenu2 = app.find('.network-indicator')[0]
|
||||||
|
const children2 = networkMenu2.children
|
||||||
|
children2.length[3]
|
||||||
|
assert.ok(children2, 'All network options present')
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeout(time) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
setTimeout(function () {
|
||||||
|
resolve()
|
||||||
|
}, time * 3 || 1500)
|
||||||
|
})
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
const createStore = require('redux').createStore
|
const createStore = require('redux').createStore
|
||||||
const applyMiddleware = require('redux').applyMiddleware
|
const applyMiddleware = require('redux').applyMiddleware
|
||||||
const thunkMiddleware = require('redux-thunk')
|
const thunkMiddleware = require('redux-thunk').default
|
||||||
const createLogger = require('redux-logger')
|
const createLogger = require('redux-logger').createLogger
|
||||||
const rootReducer = function () {}
|
const rootReducer = function () {}
|
||||||
|
|
||||||
module.exports = configureStore
|
module.exports = configureStore
|
||||||
|
40
test/lib/mock-tx-gen.js
Normal file
40
test/lib/mock-tx-gen.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const extend = require('xtend')
|
||||||
|
const BN = require('ethereumjs-util').BN
|
||||||
|
const template = {
|
||||||
|
'status': 'submitted',
|
||||||
|
'txParams': {
|
||||||
|
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
|
||||||
|
'gas': '0x30d40',
|
||||||
|
'value': '0x0',
|
||||||
|
'nonce': '0x3',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class TxGenerator {
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.txs = []
|
||||||
|
}
|
||||||
|
|
||||||
|
generate (tx = {}, opts = {}) {
|
||||||
|
let { count, fromNonce } = opts
|
||||||
|
let nonce = fromNonce || this.txs.length
|
||||||
|
let txs = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
txs.push(extend(template, {
|
||||||
|
txParams: {
|
||||||
|
nonce: hexify(nonce++),
|
||||||
|
}
|
||||||
|
}, tx))
|
||||||
|
}
|
||||||
|
this.txs = this.txs.concat(txs)
|
||||||
|
return txs
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexify (number) {
|
||||||
|
return '0x' + (new BN(number)).toString(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TxGenerator
|
@ -65,91 +65,6 @@ describe('tx confirmation screen', function () {
|
|||||||
assert.equal(count, 0)
|
assert.equal(count, 0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('sendTx', function () {
|
|
||||||
var result
|
|
||||||
|
|
||||||
describe('when there is an error', function () {
|
|
||||||
before(function (done) {
|
|
||||||
actions._setBackgroundConnection({
|
|
||||||
approveTransaction (txId, cb) { cb({message: 'An error!'}) },
|
|
||||||
})
|
|
||||||
|
|
||||||
actions.sendTx({id: firstTxId})(function (action) {
|
|
||||||
result = reducers(initialState, action)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should stay on the page', function () {
|
|
||||||
assert.equal(result.appState.currentView.name, 'confTx')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should set errorMessage on the currentView', function () {
|
|
||||||
assert(result.appState.currentView.errorMessage)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when there is success', function () {
|
|
||||||
it('should complete tx and go home', function () {
|
|
||||||
actions._setBackgroundConnection({
|
|
||||||
approveTransaction (txId, cb) { cb() },
|
|
||||||
})
|
|
||||||
|
|
||||||
var dispatchExpect = sinon.mock()
|
|
||||||
dispatchExpect.twice()
|
|
||||||
|
|
||||||
actions.sendTx({id: firstTxId})(dispatchExpect)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('when there are two pending txs', function () {
|
|
||||||
var firstTxId = 1457634084250832
|
|
||||||
var result, initialState
|
|
||||||
before(function (done) {
|
|
||||||
initialState = {
|
|
||||||
appState: {
|
|
||||||
currentView: {
|
|
||||||
name: 'confTx',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metamask: {
|
|
||||||
unapprovedTxs: {
|
|
||||||
'1457634084250832': {
|
|
||||||
id: firstTxId,
|
|
||||||
status: 'unconfirmed',
|
|
||||||
time: 1457634084250,
|
|
||||||
},
|
|
||||||
'1457634084250833': {
|
|
||||||
id: 1457634084250833,
|
|
||||||
status: 'unconfirmed',
|
|
||||||
time: 1457634084255,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
freeze(initialState)
|
|
||||||
|
|
||||||
// Mocking a background connection:
|
|
||||||
actions._setBackgroundConnection({
|
|
||||||
approveTransaction (firstTxId, cb) { cb() },
|
|
||||||
})
|
|
||||||
|
|
||||||
actions.sendTx({id: firstTxId})(function (action) {
|
|
||||||
result = reducers(initialState, action)
|
|
||||||
})
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should stay on the confTx view', function () {
|
|
||||||
assert.equal(result.appState.currentView.name, 'confTx')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should transition to the first tx', function () {
|
|
||||||
assert.equal(result.appState.currentView.context, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,41 +1,203 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
|
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
|
||||||
|
const MockTxGen = require('../lib/mock-tx-gen')
|
||||||
|
let providerResultStub = {}
|
||||||
|
|
||||||
describe('Nonce Tracker', function () {
|
describe('Nonce Tracker', function () {
|
||||||
let nonceTracker, provider, getPendingTransactions, pendingTxs
|
let nonceTracker, provider
|
||||||
|
let getPendingTransactions, pendingTxs
|
||||||
|
let getConfirmedTransactions, confirmedTxs
|
||||||
beforeEach(function () {
|
|
||||||
pendingTxs = [{
|
|
||||||
'status': 'submitted',
|
|
||||||
'txParams': {
|
|
||||||
'from': '0x7d3517b0d011698406d6e0aed8453f0be2697926',
|
|
||||||
'gas': '0x30d40',
|
|
||||||
'value': '0x0',
|
|
||||||
'nonce': '0x0',
|
|
||||||
},
|
|
||||||
}]
|
|
||||||
|
|
||||||
|
|
||||||
getPendingTransactions = () => pendingTxs
|
|
||||||
provider = {
|
|
||||||
sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) },
|
|
||||||
_blockTracker: {
|
|
||||||
getCurrentBlock: () => '0x11b568',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
nonceTracker = new NonceTracker({
|
|
||||||
provider,
|
|
||||||
getPendingTransactions,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getNonceLock', function () {
|
describe('#getNonceLock', function () {
|
||||||
it('should work', async function () {
|
|
||||||
|
describe('with 3 confirmed and 1 pending', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 1 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 4', async function () {
|
||||||
this.timeout(15000)
|
this.timeout(15000)
|
||||||
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
assert.equal(nonceLock.nextNonce, '1', 'nonce should be 1')
|
assert.equal(nonceLock.nextNonce, '4', `nonce should be 4 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use localNonce if network returns a nonce lower then a confirmed tx in state', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '4', 'nonce should be 4')
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with no previous txs', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
nonceTracker = generateNonceTrackerWith([], [])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 0', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 returned ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with multiple previous txs with same nonce', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 1 })
|
||||||
|
pendingTxs = txGen.generate({
|
||||||
|
status: 'submitted',
|
||||||
|
txParams: { nonce: '0x01' },
|
||||||
|
}, { count: 5 })
|
||||||
|
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when local confirmed count is higher than network nonce', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 3 })
|
||||||
|
nonceTracker = generateNonceTrackerWith([], confirmedTxs, '0x1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '3', `nonce should be 3 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when local pending count is higher than other metrics', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '2', `nonce should be 2 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when provider nonce is higher than other metrics', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 2 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x05')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are some pending nonces below the remote one and some over.', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x03')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after those', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '5', `nonce should be 5 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are pending nonces non sequentially over the network nonce.', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
txGen.generate({ status: 'submitted' }, { count: 5 })
|
||||||
|
// 5 over that number
|
||||||
|
pendingTxs = txGen.generate({ status: 'submitted' }, { count: 5 })
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x00')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after network nonce', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '0', `nonce should be 0 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When all three return different values', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 10 })
|
||||||
|
const pendingTxs = txGen.generate({
|
||||||
|
status: 'submitted',
|
||||||
|
nonce: 100,
|
||||||
|
}, { count: 1 })
|
||||||
|
// 0x32 is 50 in hex:
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, confirmedTxs, '0x32')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after network nonce', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '50', `nonce should be 50 got ${nonceLock.nextNonce}`)
|
||||||
|
await nonceLock.releaseLock()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Faq issue 67', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
const txGen = new MockTxGen()
|
||||||
|
const confirmedTxs = txGen.generate({ status: 'confirmed' }, { count: 64 })
|
||||||
|
const pendingTxs = txGen.generate({
|
||||||
|
status: 'submitted',
|
||||||
|
}, { count: 10 })
|
||||||
|
// 0x40 is 64 in hex:
|
||||||
|
nonceTracker = generateNonceTrackerWith(pendingTxs, [], '0x40')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return nonce after network nonce', async function () {
|
||||||
|
this.timeout(15000)
|
||||||
|
const nonceLock = await nonceTracker.getNonceLock('0x7d3517b0d011698406d6e0aed8453f0be2697926')
|
||||||
|
assert.equal(nonceLock.nextNonce, '74', `nonce should be 74 got ${nonceLock.nextNonce}`)
|
||||||
await nonceLock.releaseLock()
|
await nonceLock.releaseLock()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') {
|
||||||
|
const getPendingTransactions = () => pending
|
||||||
|
const getConfirmedTransactions = () => confirmed
|
||||||
|
providerResultStub.result = providerStub
|
||||||
|
const provider = {
|
||||||
|
sendAsync: (_, cb) => { cb(undefined, providerResultStub) },
|
||||||
|
_blockTracker: {
|
||||||
|
getCurrentBlock: () => '0x11b568',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return new NonceTracker({
|
||||||
|
provider,
|
||||||
|
getPendingTransactions,
|
||||||
|
getConfirmedTransactions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -6,12 +6,15 @@ const clone = require('clone')
|
|||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const TransactionController = require('../../app/scripts/controllers/transactions')
|
const TransactionController = require('../../app/scripts/controllers/transactions')
|
||||||
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
|
const TxProvideUtils = require('../../app/scripts/lib/tx-utils')
|
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
||||||
|
|
||||||
const noop = () => true
|
const noop = () => true
|
||||||
const currentNetworkId = 42
|
const currentNetworkId = 42
|
||||||
const otherNetworkId = 36
|
const otherNetworkId = 36
|
||||||
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
|
||||||
const { createStubedProvider } = require('../stub/provider')
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
|
|
||||||
|
|
||||||
describe('Transaction Controller', function () {
|
describe('Transaction Controller', function () {
|
||||||
let txController, engine, provider, providerResultStub
|
let txController, engine, provider, providerResultStub
|
||||||
|
|
||||||
@ -47,7 +50,7 @@ describe('Transaction Controller', function () {
|
|||||||
metamaskNetworkId: currentNetworkId,
|
metamaskNetworkId: currentNetworkId,
|
||||||
txParams,
|
txParams,
|
||||||
}
|
}
|
||||||
txController._saveTxList([txMeta])
|
txController.addTx(txMeta)
|
||||||
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
|
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -279,12 +282,15 @@ describe('Transaction Controller', function () {
|
|||||||
it('replaces the tx with the same id', function () {
|
it('replaces the tx with the same id', function () {
|
||||||
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
txController.updateTx({ id: '1', status: 'blah', hash: 'foo', metamaskNetworkId: currentNetworkId, txParams: {} })
|
const tx1 = txController.getTx('1')
|
||||||
var result = txController.getTx('1')
|
tx1.status = 'blah'
|
||||||
assert.equal(result.hash, 'foo')
|
tx1.hash = 'foo'
|
||||||
|
txController.updateTx(tx1)
|
||||||
|
const savedResult = txController.getTx('1')
|
||||||
|
assert.equal(savedResult.hash, 'foo')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates gas price', function () {
|
it('updates gas price and adds history items', function () {
|
||||||
const originalGasPrice = '0x01'
|
const originalGasPrice = '0x01'
|
||||||
const desiredGasPrice = '0x02'
|
const desiredGasPrice = '0x02'
|
||||||
|
|
||||||
@ -297,13 +303,22 @@ describe('Transaction Controller', function () {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMeta = clone(txMeta)
|
|
||||||
|
|
||||||
txController.addTx(txMeta)
|
txController.addTx(txMeta)
|
||||||
updatedMeta.txParams.gasPrice = desiredGasPrice
|
const updatedTx = txController.getTx('1')
|
||||||
txController.updateTx(updatedMeta)
|
// verify tx was initialized correctly
|
||||||
var result = txController.getTx('1')
|
assert.equal(updatedTx.history.length, 1, 'one history item (initial)')
|
||||||
|
assert.equal(Array.isArray(updatedTx.history[0]), false, 'first history item is initial state')
|
||||||
|
assert.deepEqual(updatedTx.history[0], txStateHistoryHelper.snapshotFromTxMeta(updatedTx), 'first history item is initial state')
|
||||||
|
// modify value and updateTx
|
||||||
|
updatedTx.txParams.gasPrice = desiredGasPrice
|
||||||
|
txController.updateTx(updatedTx)
|
||||||
|
// check updated value
|
||||||
|
const result = txController.getTx('1')
|
||||||
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
|
assert.equal(result.txParams.gasPrice, desiredGasPrice, 'gas price updated')
|
||||||
|
// validate history was updated
|
||||||
|
assert.equal(result.history.length, 2, 'two history items (initial + diff)')
|
||||||
|
const expectedEntry = { op: 'replace', path: '/txParams/gasPrice', value: desiredGasPrice }
|
||||||
|
assert.deepEqual(result.history[1], [expectedEntry], 'two history items (initial + diff)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
26
test/unit/tx-state-history-helper-test.js
Normal file
26
test/unit/tx-state-history-helper-test.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const clone = require('clone')
|
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
||||||
|
|
||||||
|
describe('deepCloneFromTxMeta', function () {
|
||||||
|
it('should clone deep', function () {
|
||||||
|
const input = {
|
||||||
|
foo: {
|
||||||
|
bar: {
|
||||||
|
bam: 'baz'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
|
||||||
|
assert('foo' in output, 'has a foo key')
|
||||||
|
assert('bar' in output.foo, 'has a bar key')
|
||||||
|
assert('bam' in output.foo.bar, 'has a bar key')
|
||||||
|
assert.equal(output.foo.bar.bam, 'baz', 'has a baz value')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove the history key', function () {
|
||||||
|
const input = { foo: 'bar', history: 'remembered' }
|
||||||
|
const output = txStateHistoryHelper.snapshotFromTxMeta(input)
|
||||||
|
assert(typeof output.history, 'undefined', 'should remove history')
|
||||||
|
})
|
||||||
|
})
|
23
test/unit/tx-state-history-helper.js
Normal file
23
test/unit/tx-state-history-helper.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
||||||
|
const testVault = require('../data/v17-long-history.json')
|
||||||
|
|
||||||
|
|
||||||
|
describe('tx-state-history-helper', function () {
|
||||||
|
it('migrates history to diffs and can recover original values', function () {
|
||||||
|
testVault.data.TransactionController.transactions.forEach((tx, index) => {
|
||||||
|
const newHistory = txStateHistoryHelper.migrateFromSnapshotsToDiffs(tx.history)
|
||||||
|
newHistory.forEach((newEntry, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
assert.equal(Array.isArray(newEntry), false, 'initial history item IS NOT a json patch obj')
|
||||||
|
} else {
|
||||||
|
assert.equal(Array.isArray(newEntry), true, 'non-initial history entry IS a json patch obj')
|
||||||
|
}
|
||||||
|
const oldEntry = tx.history[index]
|
||||||
|
const historySubset = newHistory.slice(0, index + 1)
|
||||||
|
const reconstructedValue = txStateHistoryHelper.replayHistory(historySubset)
|
||||||
|
assert.deepEqual(oldEntry, reconstructedValue, 'was able to reconstruct old entry from diffs')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
10
testem.yml
10
testem.yml
@ -1,10 +0,0 @@
|
|||||||
launch_in_dev:
|
|
||||||
- Chrome
|
|
||||||
- Firefox
|
|
||||||
launch_in_ci:
|
|
||||||
- Chrome
|
|
||||||
- Firefox
|
|
||||||
framework:
|
|
||||||
- qunit
|
|
||||||
before_tests: "npm run buildCiUnits"
|
|
||||||
test_page: "test/integration/index.html"
|
|
@ -126,6 +126,7 @@ var actions = {
|
|||||||
txError: txError,
|
txError: txError,
|
||||||
nextTx: nextTx,
|
nextTx: nextTx,
|
||||||
previousTx: previousTx,
|
previousTx: previousTx,
|
||||||
|
cancelAllTx: cancelAllTx,
|
||||||
viewPendingTx: viewPendingTx,
|
viewPendingTx: viewPendingTx,
|
||||||
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
|
VIEW_PENDING_TX: 'VIEW_PENDING_TX',
|
||||||
// app messages
|
// app messages
|
||||||
@ -420,6 +421,7 @@ function signPersonalMsg (msgData) {
|
|||||||
|
|
||||||
function signTx (txData) {
|
function signTx (txData) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
|
dispatch(actions.showLoadingIndication())
|
||||||
global.ethQuery.sendTransaction(txData, (err, data) => {
|
global.ethQuery.sendTransaction(txData, (err, data) => {
|
||||||
dispatch(actions.hideLoadingIndication())
|
dispatch(actions.hideLoadingIndication())
|
||||||
if (err) return dispatch(actions.displayWarning(err.message))
|
if (err) return dispatch(actions.displayWarning(err.message))
|
||||||
@ -464,6 +466,7 @@ function updateAndApproveTx (txData) {
|
|||||||
dispatch(actions.hideLoadingIndication())
|
dispatch(actions.hideLoadingIndication())
|
||||||
if (err) {
|
if (err) {
|
||||||
dispatch(actions.txError(err))
|
dispatch(actions.txError(err))
|
||||||
|
dispatch(actions.goHome())
|
||||||
return log.error(err.message)
|
return log.error(err.message)
|
||||||
}
|
}
|
||||||
dispatch(actions.completedTx(txData.id))
|
dispatch(actions.completedTx(txData.id))
|
||||||
@ -506,6 +509,16 @@ function cancelTx (txData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelAllTx (txsData) {
|
||||||
|
return (dispatch) => {
|
||||||
|
txsData.forEach((txData, i) => {
|
||||||
|
background.cancelTransaction(txData.id, () => {
|
||||||
|
dispatch(actions.completedTx(txData.id))
|
||||||
|
i === txsData.length - 1 ? dispatch(actions.goHome()) : null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
//
|
//
|
||||||
// initialize screen
|
// initialize screen
|
||||||
//
|
//
|
||||||
|
@ -3,6 +3,8 @@ const Component = require('react').Component
|
|||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const connect = require('react-redux').connect
|
const connect = require('react-redux').connect
|
||||||
const actions = require('./actions')
|
const actions = require('./actions')
|
||||||
|
const Tooltip = require('./components/tooltip.js')
|
||||||
|
|
||||||
|
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const abi = require('human-standard-token-abi')
|
const abi = require('human-standard-token-abi')
|
||||||
@ -15,6 +17,7 @@ module.exports = connect(mapStateToProps)(AddTokenScreen)
|
|||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
return {
|
return {
|
||||||
|
identities: state.metamask.identities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,15 +67,25 @@ AddTokenScreen.prototype.render = function () {
|
|||||||
}, [
|
}, [
|
||||||
|
|
||||||
h('div', [
|
h('div', [
|
||||||
h('span', {
|
h(Tooltip, {
|
||||||
|
position: 'top',
|
||||||
|
title: 'The contract of the actual token contract. Click for more info.',
|
||||||
|
}, [
|
||||||
|
h('a', {
|
||||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||||
}, 'Token Address'),
|
href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address',
|
||||||
|
target: '_blank',
|
||||||
|
}, [
|
||||||
|
h('span', 'Token Contract Address '),
|
||||||
|
h('i.fa.fa-question-circle'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h('section.flex-row.flex-center', [
|
h('section.flex-row.flex-center', [
|
||||||
h('input#token-address', {
|
h('input#token-address', {
|
||||||
name: 'address',
|
name: 'address',
|
||||||
placeholder: 'Token Address',
|
placeholder: 'Token Contract Address',
|
||||||
onChange: this.tokenAddressDidChange.bind(this),
|
onChange: this.tokenAddressDidChange.bind(this),
|
||||||
style: {
|
style: {
|
||||||
width: 'inherit',
|
width: 'inherit',
|
||||||
@ -171,7 +184,9 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (event) {
|
|||||||
AddTokenScreen.prototype.validateInputs = function () {
|
AddTokenScreen.prototype.validateInputs = function () {
|
||||||
let msg = ''
|
let msg = ''
|
||||||
const state = this.state
|
const state = this.state
|
||||||
|
const identitiesList = Object.keys(this.props.identities)
|
||||||
const { address, symbol, decimals } = state
|
const { address, symbol, decimals } = state
|
||||||
|
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
|
||||||
|
|
||||||
const validAddress = ethUtil.isValidAddress(address)
|
const validAddress = ethUtil.isValidAddress(address)
|
||||||
if (!validAddress) {
|
if (!validAddress) {
|
||||||
@ -189,7 +204,12 @@ AddTokenScreen.prototype.validateInputs = function () {
|
|||||||
msg += 'Symbol must be between 0 and 10 characters.'
|
msg += 'Symbol must be between 0 and 10 characters.'
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = validAddress && validDecimals
|
const ownAddress = identitiesList.includes(standardAddress)
|
||||||
|
if (ownAddress) {
|
||||||
|
msg = 'Personal address detected. Input the token contract address.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = validAddress && validDecimals && !ownAddress
|
||||||
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -215,4 +235,3 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
|
|||||||
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
|
this.setState({ symbol: symbol[0], decimals: decimals[0].toString() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ function mapStateToProps (state) {
|
|||||||
identities,
|
identities,
|
||||||
accounts,
|
accounts,
|
||||||
address,
|
address,
|
||||||
|
keyrings,
|
||||||
} = state.metamask
|
} = state.metamask
|
||||||
const selected = address || Object.keys(accounts)[0]
|
const selected = address || Object.keys(accounts)[0]
|
||||||
|
|
||||||
@ -75,6 +76,7 @@ function mapStateToProps (state) {
|
|||||||
// state needed to get account dropdown temporarily rendering from app bar
|
// state needed to get account dropdown temporarily rendering from app bar
|
||||||
identities,
|
identities,
|
||||||
selected,
|
selected,
|
||||||
|
keyrings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Component = require('react').Component
|
const Component = require('react').Component
|
||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const inherits = require('util').inherits
|
const inherits = require('util').inherits
|
||||||
|
const exportAsFile = require('../util').exportAsFile
|
||||||
const copyToClipboard = require('copy-to-clipboard')
|
const copyToClipboard = require('copy-to-clipboard')
|
||||||
const actions = require('../actions')
|
const actions = require('../actions')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
@ -20,20 +21,21 @@ function mapStateToProps (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ExportAccountView.prototype.render = function () {
|
ExportAccountView.prototype.render = function () {
|
||||||
var state = this.props
|
const state = this.props
|
||||||
var accountDetail = state.accountDetail
|
const accountDetail = state.accountDetail
|
||||||
|
const nickname = state.identities[state.address].name
|
||||||
|
|
||||||
if (!accountDetail) return h('div')
|
if (!accountDetail) return h('div')
|
||||||
var accountExport = accountDetail.accountExport
|
const accountExport = accountDetail.accountExport
|
||||||
|
|
||||||
var notExporting = accountExport === 'none'
|
const notExporting = accountExport === 'none'
|
||||||
var exportRequested = accountExport === 'requested'
|
const exportRequested = accountExport === 'requested'
|
||||||
var accountExported = accountExport === 'completed'
|
const accountExported = accountExport === 'completed'
|
||||||
|
|
||||||
if (notExporting) return h('div')
|
if (notExporting) return h('div')
|
||||||
|
|
||||||
if (exportRequested) {
|
if (exportRequested) {
|
||||||
var warning = `Export private keys at your own risk.`
|
const warning = `Export private keys at your own risk.`
|
||||||
return (
|
return (
|
||||||
h('div', {
|
h('div', {
|
||||||
style: {
|
style: {
|
||||||
@ -89,6 +91,8 @@ ExportAccountView.prototype.render = function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (accountExported) {
|
if (accountExported) {
|
||||||
|
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
|
||||||
|
|
||||||
return h('div.privateKey', {
|
return h('div.privateKey', {
|
||||||
style: {
|
style: {
|
||||||
margin: '0 20px',
|
margin: '0 20px',
|
||||||
@ -105,10 +109,16 @@ ExportAccountView.prototype.render = function () {
|
|||||||
onClick: function (event) {
|
onClick: function (event) {
|
||||||
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
|
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
|
||||||
},
|
},
|
||||||
}, ethUtil.stripHexPrefix(accountDetail.privateKey)),
|
}, plainKey),
|
||||||
h('button', {
|
h('button', {
|
||||||
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
|
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
|
||||||
}, 'Done'),
|
}, 'Done'),
|
||||||
|
h('button', {
|
||||||
|
style: {
|
||||||
|
marginLeft: '10px',
|
||||||
|
},
|
||||||
|
onClick: () => exportAsFile(`MetaMask ${nickname} Private Key`, plainKey),
|
||||||
|
}, 'Save as File'),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,6 +127,6 @@ ExportAccountView.prototype.onExportKeyPress = function (event) {
|
|||||||
if (event.key !== 'Enter') return
|
if (event.key !== 'Enter') return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
var input = document.getElementById('exportAccount').value
|
const input = document.getElementById('exportAccount').value
|
||||||
this.props.dispatch(actions.exportAccount(input, this.props.address))
|
this.props.dispatch(actions.exportAccount(input, this.props.address))
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ class AccountDropdowns extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderAccounts () {
|
renderAccounts () {
|
||||||
const { identities, accounts, selected, menuItemStyles, actions } = this.props
|
const { identities, accounts, selected, menuItemStyles, actions, keyrings } = this.props
|
||||||
|
|
||||||
return Object.keys(identities).map((key, index) => {
|
return Object.keys(identities).map((key, index) => {
|
||||||
const identity = identities[key]
|
const identity = identities[key]
|
||||||
@ -33,6 +33,12 @@ class AccountDropdowns extends Component {
|
|||||||
|
|
||||||
const balanceValue = accounts[key].balance
|
const balanceValue = accounts[key].balance
|
||||||
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
|
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6) : '...'
|
||||||
|
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||||
|
|
||||||
|
const keyring = keyrings.find((kr) => {
|
||||||
|
return kr.accounts.includes(simpleAddress) ||
|
||||||
|
kr.accounts.includes(identity.address)
|
||||||
|
})
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@ -88,6 +94,7 @@ class AccountDropdowns extends Component {
|
|||||||
marginLeft: '10px',
|
marginLeft: '10px',
|
||||||
},
|
},
|
||||||
}, [
|
}, [
|
||||||
|
this.indicateIfLoose(keyring),
|
||||||
h('span.account-dropdown-name', {
|
h('span.account-dropdown-name', {
|
||||||
style: {
|
style: {
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
@ -97,6 +104,7 @@ class AccountDropdowns extends Component {
|
|||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
},
|
},
|
||||||
}, identity.name || ''),
|
}, identity.name || ''),
|
||||||
|
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
|
||||||
|
|
||||||
h('span.account-dropdown-balance', {
|
h('span.account-dropdown-balance', {
|
||||||
style: {
|
style: {
|
||||||
@ -125,11 +133,35 @@ class AccountDropdowns extends Component {
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
]),
|
]),
|
||||||
|
// =======
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
// this.indicateIfLoose(keyring),
|
||||||
|
// h('span', {
|
||||||
|
// style: {
|
||||||
|
// marginLeft: '20px',
|
||||||
|
// fontSize: '24px',
|
||||||
|
// maxWidth: '145px',
|
||||||
|
// whiteSpace: 'nowrap',
|
||||||
|
// overflow: 'hidden',
|
||||||
|
// textOverflow: 'ellipsis',
|
||||||
|
// },
|
||||||
|
// }, identity.name || ''),
|
||||||
|
// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
|
||||||
|
// >>>>>>> master:ui/app/components/account-dropdowns.js
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indicateIfLoose (keyring) {
|
||||||
|
try { // Sometimes keyrings aren't loaded yet:
|
||||||
|
const type = keyring.type
|
||||||
|
const isLoose = type !== 'HD Key Tree'
|
||||||
|
return isLoose ? h('.keyring-label', 'LOOSE') : null
|
||||||
|
} catch (e) { return }
|
||||||
|
}
|
||||||
|
|
||||||
renderAccountSelector () {
|
renderAccountSelector () {
|
||||||
const { actions, useCssTransition, innerStyle } = this.props
|
const { actions, useCssTransition, innerStyle } = this.props
|
||||||
const { accountSelectorActive, menuItemStyles } = this.state
|
const { accountSelectorActive, menuItemStyles } = this.state
|
||||||
@ -389,7 +421,8 @@ AccountDropdowns.defaultProps = {
|
|||||||
|
|
||||||
AccountDropdowns.propTypes = {
|
AccountDropdowns.propTypes = {
|
||||||
identities: PropTypes.objectOf(PropTypes.object),
|
identities: PropTypes.objectOf(PropTypes.object),
|
||||||
selected: PropTypes.string, // TODO: refactor to be more explicit: selectedAddress
|
selected: PropTypes.string,
|
||||||
|
keyrings: PropTypes.array,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => {
|
const mapDispatchToProps = (dispatch) => {
|
||||||
|
@ -23,7 +23,7 @@ Network.prototype.render = function () {
|
|||||||
let iconName, hoverText
|
let iconName, hoverText
|
||||||
|
|
||||||
if (networkNumber === 'loading') {
|
if (networkNumber === 'loading') {
|
||||||
return h('span', {
|
return h('span.pointer', {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -38,7 +38,7 @@ Network.prototype.render = function () {
|
|||||||
},
|
},
|
||||||
src: 'images/loading.svg',
|
src: 'images/loading.svg',
|
||||||
}),
|
}),
|
||||||
h('i.fa.fa-sort-desc'),
|
h('i.fa.fa-caret-down'),
|
||||||
])
|
])
|
||||||
} else if (providerName === 'mainnet') {
|
} else if (providerName === 'mainnet') {
|
||||||
hoverText = 'Main Ethereum Network'
|
hoverText = 'Main Ethereum Network'
|
||||||
@ -77,7 +77,8 @@ Network.prototype.render = function () {
|
|||||||
style: {
|
style: {
|
||||||
color: '#039396',
|
color: '#039396',
|
||||||
}},
|
}},
|
||||||
'Ethereum Main Net'),
|
'Main Network'),
|
||||||
|
h('i.fa.fa-caret-down.fa-lg'),
|
||||||
])
|
])
|
||||||
case 'ropsten-test-network':
|
case 'ropsten-test-network':
|
||||||
return h('.network-indicator', [
|
return h('.network-indicator', [
|
||||||
@ -90,6 +91,7 @@ Network.prototype.render = function () {
|
|||||||
color: '#ff6666',
|
color: '#ff6666',
|
||||||
}},
|
}},
|
||||||
'Ropsten Test Net'),
|
'Ropsten Test Net'),
|
||||||
|
h('i.fa.fa-caret-down.fa-lg'),
|
||||||
])
|
])
|
||||||
case 'kovan-test-network':
|
case 'kovan-test-network':
|
||||||
return h('.network-indicator', [
|
return h('.network-indicator', [
|
||||||
@ -102,6 +104,7 @@ Network.prototype.render = function () {
|
|||||||
color: '#690496',
|
color: '#690496',
|
||||||
}},
|
}},
|
||||||
'Kovan Test Net'),
|
'Kovan Test Net'),
|
||||||
|
h('i.fa.fa-caret-down.fa-lg'),
|
||||||
])
|
])
|
||||||
case 'rinkeby-test-network':
|
case 'rinkeby-test-network':
|
||||||
return h('.network-indicator', [
|
return h('.network-indicator', [
|
||||||
@ -114,6 +117,7 @@ Network.prototype.render = function () {
|
|||||||
color: '#e7a218',
|
color: '#e7a218',
|
||||||
}},
|
}},
|
||||||
'Rinkeby Test Net'),
|
'Rinkeby Test Net'),
|
||||||
|
h('i.fa.fa-caret-down.fa-lg'),
|
||||||
])
|
])
|
||||||
default:
|
default:
|
||||||
return h('.network-indicator', [
|
return h('.network-indicator', [
|
||||||
@ -129,6 +133,7 @@ Network.prototype.render = function () {
|
|||||||
color: '#AEAEAE',
|
color: '#AEAEAE',
|
||||||
}},
|
}},
|
||||||
'Private Network'),
|
'Private Network'),
|
||||||
|
h('i.fa.fa-caret-down.fa-lg'),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
|
@ -38,7 +38,7 @@ PendingMsgDetails.prototype.render = function () {
|
|||||||
|
|
||||||
// message data
|
// message data
|
||||||
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
|
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||||
h('.flex-row.flex-space-between', [
|
h('.flex-column.flex-space-between', [
|
||||||
h('label.font-small', 'MESSAGE'),
|
h('label.font-small', 'MESSAGE'),
|
||||||
h('span.font-small', msgParams.data),
|
h('span.font-small', msgParams.data),
|
||||||
]),
|
]),
|
||||||
|
@ -18,6 +18,9 @@ PendingMsg.prototype.render = function () {
|
|||||||
|
|
||||||
h('div', {
|
h('div', {
|
||||||
key: msgData.id,
|
key: msgData.id,
|
||||||
|
style: {
|
||||||
|
maxWidth: '350px',
|
||||||
|
},
|
||||||
}, [
|
}, [
|
||||||
|
|
||||||
// header
|
// header
|
||||||
@ -32,10 +35,21 @@ PendingMsg.prototype.render = function () {
|
|||||||
style: {
|
style: {
|
||||||
margin: '10px',
|
margin: '10px',
|
||||||
},
|
},
|
||||||
}, `Signing this message can have
|
}, [
|
||||||
|
`Signing this message can have
|
||||||
dangerous side effects. Only sign messages from
|
dangerous side effects. Only sign messages from
|
||||||
sites you fully trust with your entire account.
|
sites you fully trust with your entire account.
|
||||||
This will be fixed in a future version.`),
|
This dangerous method will be removed in a future version. `,
|
||||||
|
h('a', {
|
||||||
|
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
|
||||||
|
style: { color: 'rgb(247, 134, 28)' },
|
||||||
|
onClick: (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
|
||||||
|
global.platform.openWindow({ url })
|
||||||
|
},
|
||||||
|
}, 'Read more here.'),
|
||||||
|
]),
|
||||||
|
|
||||||
// message details
|
// message details
|
||||||
h(PendingTxDetails, state),
|
h(PendingTxDetails, state),
|
||||||
|
@ -240,6 +240,15 @@ PendingTx.prototype.render = function () {
|
|||||||
totalInETH,
|
totalInETH,
|
||||||
} = this.getData()
|
} = this.getData()
|
||||||
|
|
||||||
|
// This is from the latest master
|
||||||
|
// It handles some of the errors that we are not currently handling
|
||||||
|
// Leaving as comments fo reference
|
||||||
|
|
||||||
|
// const balanceBn = hexToBn(balance)
|
||||||
|
// const insufficientBalance = balanceBn.lt(maxCost)
|
||||||
|
// const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
|
||||||
|
// const showRejectAll = props.unconfTxListLength > 1
|
||||||
|
|
||||||
this.inputs = []
|
this.inputs = []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -335,6 +344,85 @@ PendingTx.prototype.render = function () {
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// These are latest errors handling from master
|
||||||
|
// Leaving as comments as reference when we start implementing error handling
|
||||||
|
// h('style', `
|
||||||
|
// .conf-buttons button {
|
||||||
|
// margin-left: 10px;
|
||||||
|
// text-transform: uppercase;
|
||||||
|
// }
|
||||||
|
// `),
|
||||||
|
|
||||||
|
// txMeta.simulationFails ?
|
||||||
|
// h('.error', {
|
||||||
|
// style: {
|
||||||
|
// marginLeft: 50,
|
||||||
|
// fontSize: '0.9em',
|
||||||
|
// },
|
||||||
|
// }, 'Transaction Error. Exception thrown in contract code.')
|
||||||
|
// : null,
|
||||||
|
|
||||||
|
// !isValidAddress ?
|
||||||
|
// h('.error', {
|
||||||
|
// style: {
|
||||||
|
// marginLeft: 50,
|
||||||
|
// fontSize: '0.9em',
|
||||||
|
// },
|
||||||
|
// }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
|
||||||
|
// : null,
|
||||||
|
|
||||||
|
// insufficientBalance ?
|
||||||
|
// h('span.error', {
|
||||||
|
// style: {
|
||||||
|
// marginLeft: 50,
|
||||||
|
// fontSize: '0.9em',
|
||||||
|
// },
|
||||||
|
// }, 'Insufficient balance for transaction')
|
||||||
|
// : null,
|
||||||
|
|
||||||
|
// // send + cancel
|
||||||
|
// h('.flex-row.flex-space-around.conf-buttons', {
|
||||||
|
// style: {
|
||||||
|
// display: 'flex',
|
||||||
|
// justifyContent: 'flex-end',
|
||||||
|
// margin: '14px 25px',
|
||||||
|
// },
|
||||||
|
// }, [
|
||||||
|
// h('button', {
|
||||||
|
// onClick: (event) => {
|
||||||
|
// this.resetGasFields()
|
||||||
|
// event.preventDefault()
|
||||||
|
// },
|
||||||
|
// }, 'Reset'),
|
||||||
|
|
||||||
|
// // Accept Button or Buy Button
|
||||||
|
// insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
|
||||||
|
// h('input.confirm.btn-green', {
|
||||||
|
// type: 'submit',
|
||||||
|
// value: 'SUBMIT',
|
||||||
|
// style: { marginLeft: '10px' },
|
||||||
|
// disabled: buyDisabled,
|
||||||
|
// }),
|
||||||
|
|
||||||
|
// h('button.cancel.btn-red', {
|
||||||
|
// onClick: props.cancelTransaction,
|
||||||
|
// }, 'Reject'),
|
||||||
|
// ]),
|
||||||
|
// showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
|
||||||
|
// style: {
|
||||||
|
// display: 'flex',
|
||||||
|
// justifyContent: 'flex-end',
|
||||||
|
// margin: '14px 25px',
|
||||||
|
// },
|
||||||
|
// }, [
|
||||||
|
// h('button.cancel.btn-red', {
|
||||||
|
// onClick: props.cancelAllTransactions,
|
||||||
|
// }, 'Reject All'),
|
||||||
|
// ]) : null,
|
||||||
|
// ]),
|
||||||
|
// ])
|
||||||
|
// )
|
||||||
|
// }
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h('form#pending-tx-form.flex-column.flex-center', {
|
h('form#pending-tx-form.flex-column.flex-center', {
|
||||||
|
@ -48,10 +48,28 @@ TokenList.prototype.render = function () {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return this.message('There was a problem loading your token balances.')
|
return h('.hotFix', {
|
||||||
|
style: {
|
||||||
|
padding: '80px',
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
'We had trouble loading your token balances. You can view them ',
|
||||||
|
h('span.hotFix', {
|
||||||
|
style: {
|
||||||
|
color: 'rgba(247, 134, 28, 1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
onClick: () => {
|
||||||
|
global.platform.openWindow({
|
||||||
|
url: `https://ethplorer.io/address/${userAddress}`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}, 'here'),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
|
return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TokenList.prototype.message = function (body) {
|
TokenList.prototype.message = function (body) {
|
||||||
@ -84,7 +102,7 @@ TokenList.prototype.createFreshTokenTracker = function () {
|
|||||||
this.tracker = new TokenTracker({
|
this.tracker = new TokenTracker({
|
||||||
userAddress,
|
userAddress,
|
||||||
provider: global.ethereumProvider,
|
provider: global.ethereumProvider,
|
||||||
tokens: uniqueMergeTokens(defaultTokens, this.props.tokens),
|
tokens: this.props.tokens,
|
||||||
pollingInterval: 8000,
|
pollingInterval: 8000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -149,4 +167,3 @@ function uniqueMergeTokens (tokensA, tokensB = []) {
|
|||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,17 +60,8 @@ TransactionListItem.prototype.render = function () {
|
|||||||
}, [
|
}, [
|
||||||
|
|
||||||
h('.identicon-wrapper.flex-column.flex-center.select-none', [
|
h('.identicon-wrapper.flex-column.flex-center.select-none', [
|
||||||
h('.pop-hover', {
|
|
||||||
onClick: (event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
if (!isTx || isPending) return
|
|
||||||
var url = `https://metamask.github.io/eth-tx-viz/?tx=${transaction.hash}`
|
|
||||||
global.platform.openWindow({ url })
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
|
h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
|
||||||
]),
|
]),
|
||||||
]),
|
|
||||||
|
|
||||||
h(Tooltip, {
|
h(Tooltip, {
|
||||||
title: 'Transaction Number',
|
title: 'Transaction Number',
|
||||||
|
@ -76,6 +76,7 @@ ConfirmTxScreen.prototype.render = function () {
|
|||||||
cancelMessage: this.cancelMessage.bind(this, txData),
|
cancelMessage: this.cancelMessage.bind(this, txData),
|
||||||
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
|
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentTxView (opts) {
|
function currentTxView (opts) {
|
||||||
@ -116,6 +117,12 @@ ConfirmTxScreen.prototype.cancelTransaction = function (txData, event) {
|
|||||||
this.props.dispatch(actions.cancelTx(txData))
|
this.props.dispatch(actions.cancelTx(txData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTxScreen.prototype.cancelAllTransactions = function (unconfTxList, event) {
|
||||||
|
this.stopPropagation(event)
|
||||||
|
event.preventDefault()
|
||||||
|
this.props.dispatch(actions.cancelAllTx(unconfTxList))
|
||||||
|
}
|
||||||
|
|
||||||
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
|
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
|
||||||
log.info('conf-tx.js: signing message')
|
log.info('conf-tx.js: signing message')
|
||||||
var params = msgData.msgParams
|
var params = msgData.msgParams
|
||||||
|
@ -5,7 +5,8 @@ const connect = require('react-redux').connect
|
|||||||
const actions = require('./actions')
|
const actions = require('./actions')
|
||||||
const currencies = require('./conversion.json').rows
|
const currencies = require('./conversion.json').rows
|
||||||
const validUrl = require('valid-url')
|
const validUrl = require('valid-url')
|
||||||
const copyToClipboard = require('copy-to-clipboard')
|
const exportAsFile = require('./util').exportAsFile
|
||||||
|
|
||||||
|
|
||||||
module.exports = connect(mapStateToProps)(ConfigScreen)
|
module.exports = connect(mapStateToProps)(ConfigScreen)
|
||||||
|
|
||||||
@ -110,9 +111,9 @@ ConfigScreen.prototype.render = function () {
|
|||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
onClick (event) {
|
onClick (event) {
|
||||||
copyToClipboard(window.logState())
|
exportAsFile('MetaMask State Logs', window.logState())
|
||||||
},
|
},
|
||||||
}, 'Copy State Logs'),
|
}, 'Download State Logs'),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
h('hr.horizontal-line'),
|
h('hr.horizontal-line'),
|
||||||
|
@ -238,7 +238,7 @@ hr.horizontal-line {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
position: relative;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -103,7 +103,7 @@ InfoScreen.prototype.render = function () {
|
|||||||
[
|
[
|
||||||
h('div.fa.fa-support', [
|
h('div.fa.fa-support', [
|
||||||
h('a.info', {
|
h('a.info', {
|
||||||
href: 'http://metamask.consensyssupport.happyfox.com',
|
href: 'https://support.metamask.io',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
}, 'Visit our Support Center'),
|
}, 'Visit our Support Center'),
|
||||||
]),
|
]),
|
||||||
|
@ -3,6 +3,7 @@ const Component = require('react').Component
|
|||||||
const connect = require('react-redux').connect
|
const connect = require('react-redux').connect
|
||||||
const h = require('react-hyperscript')
|
const h = require('react-hyperscript')
|
||||||
const actions = require('../../actions')
|
const actions = require('../../actions')
|
||||||
|
const exportAsFile = require('../../util').exportAsFile
|
||||||
|
|
||||||
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
|
module.exports = connect(mapStateToProps)(CreateVaultCompleteScreen)
|
||||||
|
|
||||||
@ -65,8 +66,17 @@ CreateVaultCompleteScreen.prototype.render = function () {
|
|||||||
style: {
|
style: {
|
||||||
margin: '24px',
|
margin: '24px',
|
||||||
fontSize: '0.9em',
|
fontSize: '0.9em',
|
||||||
|
marginBottom: '10px',
|
||||||
},
|
},
|
||||||
}, 'I\'ve copied it somewhere safe'),
|
}, 'I\'ve copied it somewhere safe'),
|
||||||
|
|
||||||
|
h('button.primary', {
|
||||||
|
onClick: () => exportAsFile(`MetaMask Seed Words`, seed),
|
||||||
|
style: {
|
||||||
|
margin: '10px',
|
||||||
|
fontSize: '0.9em',
|
||||||
|
},
|
||||||
|
}, 'Save Seed Words As File'),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,10 @@ function rootReducer (state, action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.logState = function () {
|
window.logState = function () {
|
||||||
var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2)
|
let state = window.METAMASK_CACHED_LOG_STATE
|
||||||
|
const version = global.platform.getVersion()
|
||||||
|
state.version = version
|
||||||
|
let stateString = JSON.stringify(state, removeSeedWords, 2)
|
||||||
return stateString
|
return stateString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ UnlockScreen.prototype.render = function () {
|
|||||||
color: 'rgb(247, 134, 28)',
|
color: 'rgb(247, 134, 28)',
|
||||||
textDecoration: 'underline',
|
textDecoration: 'underline',
|
||||||
},
|
},
|
||||||
}, 'I forgot my password.'),
|
}, 'Restore from seed phrase'),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
@ -53,6 +53,7 @@ module.exports = {
|
|||||||
getTxFeeBn,
|
getTxFeeBn,
|
||||||
shortenBalance,
|
shortenBalance,
|
||||||
getContractAtAddress,
|
getContractAtAddress,
|
||||||
|
exportAsFile: exportAsFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
function valuesFor (obj) {
|
function valuesFor (obj) {
|
||||||
@ -250,3 +251,18 @@ function getTxFeeBn (gas, gasPrice = MIN_GAS_PRICE_BN.toString(16), blockGasLimi
|
|||||||
function getContractAtAddress (tokenAddress) {
|
function getContractAtAddress (tokenAddress) {
|
||||||
return global.eth.contract(abi).at(tokenAddress)
|
return global.eth.contract(abi).at(tokenAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportAsFile (filename, data) {
|
||||||
|
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
|
||||||
|
const blob = new Blob([data], {type: 'text/csv'})
|
||||||
|
if (window.navigator.msSaveOrOpenBlob) {
|
||||||
|
window.navigator.msSaveBlob(blob, filename)
|
||||||
|
} else {
|
||||||
|
const elem = window.document.createElement('a')
|
||||||
|
elem.href = window.URL.createObjectURL(blob)
|
||||||
|
elem.download = filename
|
||||||
|
document.body.appendChild(elem)
|
||||||
|
elem.click()
|
||||||
|
document.body.removeChild(elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,19 +3,19 @@ module.exports = function (address, network) {
|
|||||||
let link
|
let link
|
||||||
switch (net) {
|
switch (net) {
|
||||||
case 1: // main net
|
case 1: // main net
|
||||||
link = `http://etherscan.io/address/${address}`
|
link = `https://etherscan.io/address/${address}`
|
||||||
break
|
break
|
||||||
case 2: // morden test net
|
case 2: // morden test net
|
||||||
link = `http://morden.etherscan.io/address/${address}`
|
link = `https://morden.etherscan.io/address/${address}`
|
||||||
break
|
break
|
||||||
case 3: // ropsten test net
|
case 3: // ropsten test net
|
||||||
link = `http://ropsten.etherscan.io/address/${address}`
|
link = `https://ropsten.etherscan.io/address/${address}`
|
||||||
break
|
break
|
||||||
case 4: // rinkeby test net
|
case 4: // rinkeby test net
|
||||||
link = `http://rinkeby.etherscan.io/address/${address}`
|
link = `https://rinkeby.etherscan.io/address/${address}`
|
||||||
break
|
break
|
||||||
case 42: // kovan test net
|
case 42: // kovan test net
|
||||||
link = `http://kovan.etherscan.io/address/${address}`
|
link = `https://kovan.etherscan.io/address/${address}`
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
link = ''
|
link = ''
|
||||||
|
Loading…
x
Reference in New Issue
Block a user