mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge branch 'master' into RemoveSlackLink
This commit is contained in:
commit
4b0e6a0a77
27
CHANGELOG.md
27
CHANGELOG.md
@ -3,7 +3,30 @@
|
|||||||
## Current Master
|
## Current Master
|
||||||
|
|
||||||
- Remove Slack link from info page, since it is a big phishing target.
|
- Remove Slack link from info page, since it is a big phishing target.
|
||||||
|
|
||||||
|
## 3.10.8 2017-9-28
|
||||||
|
|
||||||
|
- Fixed usage of new currency fetching API.
|
||||||
|
|
||||||
|
## 3.10.7 2017-9-28
|
||||||
|
|
||||||
|
- Fixed bug where sometimes the current account was not correctly set and exposed to web apps.
|
||||||
|
- Added AUD, HKD, SGD, IDR, PHP to currency conversion list
|
||||||
|
|
||||||
|
## 3.10.6 2017-9-27
|
||||||
|
|
||||||
|
- Fix bug where newly created accounts were not selected.
|
||||||
|
- Fix bug where selected account was not persisted between lockings.
|
||||||
|
|
||||||
|
## 3.10.5 2017-9-27
|
||||||
|
|
||||||
|
- Fix block gas limit estimation.
|
||||||
|
|
||||||
|
## 3.10.4 2017-9-27
|
||||||
|
|
||||||
- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9)
|
- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9)
|
||||||
|
- Fix memory leak warning.
|
||||||
|
- Fix bug where new event filters would not include historical events.
|
||||||
|
|
||||||
## 3.10.3 2017-9-21
|
## 3.10.3 2017-9-21
|
||||||
|
|
||||||
@ -23,7 +46,8 @@ rollback to 3.10.0 due to bug
|
|||||||
- Fixed a long standing memory leak associated with filters installed by dapps
|
- Fixed a long standing memory leak associated with filters installed by dapps
|
||||||
- Fix link to support center.
|
- Fix link to support center.
|
||||||
- Fixed tooltip icon locations to avoid overflow.
|
- Fixed tooltip icon locations to avoid overflow.
|
||||||
- Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher)
|
- Warn users when a dapp proposes a high gas limit (90% of blockGasLimit or higher
|
||||||
|
- Sort currencies by currency name (thanks to strelok1: https://github.com/strelok1).
|
||||||
|
|
||||||
## 3.10.0 2017-9-11
|
## 3.10.0 2017-9-11
|
||||||
|
|
||||||
@ -33,6 +57,7 @@ rollback to 3.10.0 due to bug
|
|||||||
- Add validation preventing users from inputting their own addresses as token tracking 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)
|
- Added button to reject all transactions (thanks to davidp94! https://github.com/davidp94)
|
||||||
|
|
||||||
|
|
||||||
## 3.9.13 2017-9-8
|
## 3.9.13 2017-9-8
|
||||||
|
|
||||||
- Changed the way we initialize the inpage provider to fix a bug affecting some developers.
|
- Changed the way we initialize the inpage provider to fix a bug affecting some developers.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "MetaMask",
|
"name": "MetaMask",
|
||||||
"short_name": "Metamask",
|
"short_name": "Metamask",
|
||||||
"version": "3.10.3",
|
"version": "3.10.8",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"author": "https://metamask.io",
|
"author": "https://metamask.io",
|
||||||
"description": "Ethereum Browser Extension",
|
"description": "Ethereum Browser Extension",
|
||||||
@ -57,7 +57,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"http://localhost:8545/"
|
"http://localhost:8545/",
|
||||||
|
"https://*.infura.io/"
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
"scripts/inpage.js"
|
"scripts/inpage.js"
|
||||||
|
@ -114,7 +114,7 @@ function setupController (initState) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
updateBadge()
|
updateBadge()
|
||||||
controller.txController.on('updateBadge', updateBadge)
|
controller.txController.on('update:badge', updateBadge)
|
||||||
controller.messageManager.on('updateBadge', updateBadge)
|
controller.messageManager.on('updateBadge', updateBadge)
|
||||||
controller.personalMessageManager.on('updateBadge', updateBadge)
|
controller.personalMessageManager.on('updateBadge', updateBadge)
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@ const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
|
|||||||
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
|
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
|
||||||
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
|
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
|
||||||
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
|
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
|
||||||
|
const LOCALHOST_RPC_URL = 'http://localhost:8545'
|
||||||
|
|
||||||
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
network: {
|
network: {
|
||||||
|
localhost: LOCALHOST_RPC_URL,
|
||||||
mainnet: MAINET_RPC_URL,
|
mainnet: MAINET_RPC_URL,
|
||||||
ropsten: ROPSTEN_RPC_URL,
|
ropsten: ROPSTEN_RPC_URL,
|
||||||
kovan: KOVAN_RPC_URL,
|
kovan: KOVAN_RPC_URL,
|
||||||
|
@ -42,16 +42,21 @@ function setupStreams () {
|
|||||||
name: 'contentscript',
|
name: 'contentscript',
|
||||||
target: 'inpage',
|
target: 'inpage',
|
||||||
})
|
})
|
||||||
pageStream.on('error', console.error)
|
|
||||||
const pluginPort = extension.runtime.connect({ name: 'contentscript' })
|
const pluginPort = extension.runtime.connect({ name: 'contentscript' })
|
||||||
const pluginStream = new PortStream(pluginPort)
|
const pluginStream = new PortStream(pluginPort)
|
||||||
pluginStream.on('error', console.error)
|
|
||||||
|
|
||||||
// forward communication plugin->inpage
|
// forward communication plugin->inpage
|
||||||
pageStream.pipe(pluginStream).pipe(pageStream)
|
pump(
|
||||||
|
pageStream,
|
||||||
|
pluginStream,
|
||||||
|
pageStream,
|
||||||
|
(err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err)
|
||||||
|
)
|
||||||
|
|
||||||
// setup local multistream channels
|
// setup local multistream channels
|
||||||
const mux = new ObjectMultiplex()
|
const mux = new ObjectMultiplex()
|
||||||
|
mux.setMaxListeners(25)
|
||||||
|
|
||||||
pump(
|
pump(
|
||||||
mux,
|
mux,
|
||||||
pageStream,
|
pageStream,
|
||||||
|
@ -33,9 +33,18 @@ class BalanceController {
|
|||||||
|
|
||||||
_registerUpdates () {
|
_registerUpdates () {
|
||||||
const update = this.updateBalance.bind(this)
|
const update = this.updateBalance.bind(this)
|
||||||
this.txController.on('submitted', update)
|
|
||||||
this.txController.on('confirmed', update)
|
this.txController.on('tx:status-update', (txId, status) => {
|
||||||
this.txController.on('failed', update)
|
switch (status) {
|
||||||
|
case 'submitted':
|
||||||
|
case 'confirmed':
|
||||||
|
case 'failed':
|
||||||
|
update()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
this.accountTracker.store.subscribe(update)
|
this.accountTracker.store.subscribe(update)
|
||||||
this.blockTracker.on('block', update)
|
this.blockTracker.on('block', update)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ class CurrencyController {
|
|||||||
|
|
||||||
constructor (opts = {}) {
|
constructor (opts = {}) {
|
||||||
const initState = extend({
|
const initState = extend({
|
||||||
currentCurrency: 'USD',
|
currentCurrency: 'usd',
|
||||||
conversionRate: 0,
|
conversionRate: 0,
|
||||||
conversionDate: 'N/A',
|
conversionDate: 'N/A',
|
||||||
}, opts.initState)
|
}, opts.initState)
|
||||||
@ -45,10 +45,10 @@ class CurrencyController {
|
|||||||
|
|
||||||
updateConversionRate () {
|
updateConversionRate () {
|
||||||
const currentCurrency = this.getCurrentCurrency()
|
const currentCurrency = this.getCurrentCurrency()
|
||||||
return fetch(`https://api.cryptonator.com/api/ticker/eth-${currentCurrency}`)
|
return fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((parsedResponse) => {
|
.then((parsedResponse) => {
|
||||||
this.setConversionRate(Number(parsedResponse.ticker.price))
|
this.setConversionRate(Number(parsedResponse.bid))
|
||||||
this.setConversionDate(Number(parsedResponse.timestamp))
|
this.setConversionDate(Number(parsedResponse.timestamp))
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -1,71 +1,40 @@
|
|||||||
|
const assert = require('assert')
|
||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const MetaMaskProvider = require('web3-provider-engine/zero.js')
|
const createMetamaskProvider = require('web3-provider-engine/zero.js')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ComposedStore = require('obs-store/lib/composed')
|
const ComposedStore = require('obs-store/lib/composed')
|
||||||
const extend = require('xtend')
|
const extend = require('xtend')
|
||||||
const EthQuery = require('eth-query')
|
const EthQuery = require('eth-query')
|
||||||
|
const createEventEmitterProxy = require('../lib/events-proxy.js')
|
||||||
const RPC_ADDRESS_LIST = require('../config.js').network
|
const RPC_ADDRESS_LIST = require('../config.js').network
|
||||||
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
|
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
|
||||||
|
|
||||||
module.exports = class NetworkController extends EventEmitter {
|
module.exports = class NetworkController extends EventEmitter {
|
||||||
|
|
||||||
constructor (config) {
|
constructor (config) {
|
||||||
super()
|
super()
|
||||||
this.networkStore = new ObservableStore('loading')
|
|
||||||
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
|
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
|
||||||
|
this.networkStore = new ObservableStore('loading')
|
||||||
this.providerStore = new ObservableStore(config.provider)
|
this.providerStore = new ObservableStore(config.provider)
|
||||||
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
|
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
|
||||||
this._providerListeners = {}
|
this._proxy = createEventEmitterProxy()
|
||||||
|
|
||||||
this.on('networkDidChange', this.lookupNetwork)
|
this.on('networkDidChange', this.lookupNetwork)
|
||||||
this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get provider () {
|
initializeProvider (_providerParams) {
|
||||||
|
this._baseProviderParams = _providerParams
|
||||||
|
const rpcUrl = this.getCurrentRpcAddress()
|
||||||
|
this._configureStandardProvider({ rpcUrl })
|
||||||
|
this._proxy.on('block', this._logBlock.bind(this))
|
||||||
|
this._proxy.on('error', this.verifyNetwork.bind(this))
|
||||||
|
this.ethQuery = new EthQuery(this._proxy)
|
||||||
|
this.lookupNetwork()
|
||||||
return this._proxy
|
return this._proxy
|
||||||
}
|
}
|
||||||
|
|
||||||
set provider (provider) {
|
|
||||||
this._provider = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeProvider (opts, providerContructor = MetaMaskProvider) {
|
|
||||||
this.providerInit = opts
|
|
||||||
this._provider = providerContructor(opts)
|
|
||||||
this._proxy = new Proxy(this._provider, {
|
|
||||||
get: (obj, name) => {
|
|
||||||
if (name === 'on') return this._on.bind(this)
|
|
||||||
return this._provider[name]
|
|
||||||
},
|
|
||||||
set: (obj, name, value) => {
|
|
||||||
this._provider[name] = value
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
})
|
|
||||||
this.provider.on('block', this._logBlock.bind(this))
|
|
||||||
this.provider.on('error', this.verifyNetwork.bind(this))
|
|
||||||
this.ethQuery = new EthQuery(this.provider)
|
|
||||||
this.lookupNetwork()
|
|
||||||
return this.provider
|
|
||||||
}
|
|
||||||
|
|
||||||
switchNetwork (providerInit) {
|
|
||||||
this.setNetworkState('loading')
|
|
||||||
const newInit = extend(this.providerInit, providerInit)
|
|
||||||
this.providerInit = newInit
|
|
||||||
|
|
||||||
this._provider.removeAllListeners()
|
|
||||||
this._provider.stop()
|
|
||||||
this.provider = MetaMaskProvider(newInit)
|
|
||||||
// apply the listners created by other controllers
|
|
||||||
Object.keys(this._providerListeners).forEach((key) => {
|
|
||||||
this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler))
|
|
||||||
})
|
|
||||||
this.emit('networkDidChange')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
verifyNetwork () {
|
verifyNetwork () {
|
||||||
// Check network when restoring connectivity:
|
// Check network when restoring connectivity:
|
||||||
if (this.isNetworkLoading()) this.lookupNetwork()
|
if (this.isNetworkLoading()) this.lookupNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,6 +63,7 @@ module.exports = class NetworkController extends EventEmitter {
|
|||||||
type: 'rpc',
|
type: 'rpc',
|
||||||
rpcTarget: rpcUrl,
|
rpcTarget: rpcUrl,
|
||||||
})
|
})
|
||||||
|
this._switchNetwork({ rpcUrl })
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentRpcAddress () {
|
getCurrentRpcAddress () {
|
||||||
@ -102,10 +72,14 @@ module.exports = class NetworkController extends EventEmitter {
|
|||||||
return this.getRpcAddressForType(provider.type)
|
return this.getRpcAddressForType(provider.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
setProviderType (type) {
|
async setProviderType (type) {
|
||||||
|
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
|
||||||
|
// skip if type already matches
|
||||||
if (type === this.getProviderConfig().type) return
|
if (type === this.getProviderConfig().type) return
|
||||||
const rpcTarget = this.getRpcAddressForType(type)
|
const rpcTarget = this.getRpcAddressForType(type)
|
||||||
this.providerStore.updateState({type, rpcTarget})
|
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
|
||||||
|
this.providerStore.updateState({ type, rpcTarget })
|
||||||
|
this._switchNetwork({ rpcUrl: rpcTarget })
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviderConfig () {
|
getProviderConfig () {
|
||||||
@ -117,14 +91,42 @@ module.exports = class NetworkController extends EventEmitter {
|
|||||||
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
|
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Private
|
||||||
|
//
|
||||||
|
|
||||||
|
_switchNetwork (providerParams) {
|
||||||
|
this.setNetworkState('loading')
|
||||||
|
this._configureStandardProvider(providerParams)
|
||||||
|
this.emit('networkDidChange')
|
||||||
|
}
|
||||||
|
|
||||||
|
_configureStandardProvider(_providerParams) {
|
||||||
|
const providerParams = extend(this._baseProviderParams, _providerParams)
|
||||||
|
const provider = createMetamaskProvider(providerParams)
|
||||||
|
this._setProvider(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
_setProvider (provider) {
|
||||||
|
// collect old block tracker events
|
||||||
|
const oldProvider = this._provider
|
||||||
|
let blockTrackerHandlers
|
||||||
|
if (oldProvider) {
|
||||||
|
// capture old block handlers
|
||||||
|
blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers
|
||||||
|
// tear down
|
||||||
|
oldProvider.removeAllListeners()
|
||||||
|
oldProvider.stop()
|
||||||
|
}
|
||||||
|
// override block tracler
|
||||||
|
provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers)
|
||||||
|
// set as new provider
|
||||||
|
this._provider = provider
|
||||||
|
this._proxy.setTarget(provider)
|
||||||
|
}
|
||||||
|
|
||||||
_logBlock (block) {
|
_logBlock (block) {
|
||||||
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
|
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
|
||||||
this.verifyNetwork()
|
this.verifyNetwork()
|
||||||
}
|
}
|
||||||
|
|
||||||
_on (event, handler) {
|
|
||||||
if (!this._providerListeners[event]) this._providerListeners[event] = []
|
|
||||||
this._providerListeners[event].push(handler)
|
|
||||||
this._provider.on(event, handler)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ class PreferencesController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelectedAddress (_address) {
|
getSelectedAddress () {
|
||||||
return this.store.getState().selectedAddress
|
return this.store.getState().selectedAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,86 +1,90 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const extend = require('xtend')
|
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
|
const Transaction = require('ethereumjs-tx')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const TxProviderUtil = require('../lib/tx-utils')
|
const TransactionStateManger = require('../lib/tx-state-manager')
|
||||||
|
const TxGasUtil = require('../lib/tx-gas-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')
|
|
||||||
|
/*
|
||||||
|
Transaction Controller is an aggregate of sub-controllers and trackers
|
||||||
|
composing them in a way to be exposed to the metamask controller
|
||||||
|
- txStateManager
|
||||||
|
responsible for the state of a transaction and
|
||||||
|
storing the transaction
|
||||||
|
- pendingTxTracker
|
||||||
|
watching blocks for transactions to be include
|
||||||
|
and emitting confirmed events
|
||||||
|
- txGasUtil
|
||||||
|
gas calculations and safety buffering
|
||||||
|
- nonceTracker
|
||||||
|
calculating nonces
|
||||||
|
*/
|
||||||
|
|
||||||
module.exports = class TransactionController extends EventEmitter {
|
module.exports = class TransactionController extends EventEmitter {
|
||||||
constructor (opts) {
|
constructor (opts) {
|
||||||
super()
|
super()
|
||||||
this.store = new ObservableStore(extend({
|
|
||||||
transactions: [],
|
|
||||||
}, opts.initState))
|
|
||||||
this.memStore = new ObservableStore({})
|
|
||||||
this.networkStore = opts.networkStore || new ObservableStore({})
|
this.networkStore = opts.networkStore || new ObservableStore({})
|
||||||
this.preferencesStore = opts.preferencesStore || new ObservableStore({})
|
this.preferencesStore = opts.preferencesStore || new ObservableStore({})
|
||||||
this.txHistoryLimit = opts.txHistoryLimit
|
|
||||||
this.provider = opts.provider
|
this.provider = opts.provider
|
||||||
this.blockTracker = opts.blockTracker
|
this.blockTracker = opts.blockTracker
|
||||||
this.signEthTx = opts.signTransaction
|
this.signEthTx = opts.signTransaction
|
||||||
this.accountTracker = opts.accountTracker
|
|
||||||
|
|
||||||
|
this.memStore = new ObservableStore({})
|
||||||
|
this.query = new EthQuery(this.provider)
|
||||||
|
this.txGasUtil = new TxGasUtil(this.provider)
|
||||||
|
|
||||||
|
this.txStateManager = new TransactionStateManger({
|
||||||
|
initState: opts.initState,
|
||||||
|
txHistoryLimit: opts.txHistoryLimit,
|
||||||
|
getNetwork: this.getNetwork.bind(this),
|
||||||
|
})
|
||||||
|
this.store = this.txStateManager.store
|
||||||
|
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
|
||||||
this.nonceTracker = new NonceTracker({
|
this.nonceTracker = new NonceTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
getPendingTransactions: (address) => {
|
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||||
return this.getFilteredTxList({
|
|
||||||
from: address,
|
|
||||||
status: 'submitted',
|
|
||||||
err: undefined,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getConfirmedTransactions: (address) => {
|
getConfirmedTransactions: (address) => {
|
||||||
return this.getFilteredTxList({
|
return this.txStateManager.getFilteredTxList({
|
||||||
from: address,
|
from: address,
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
err: undefined,
|
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.txProviderUtil = new TxProviderUtil(this.provider)
|
|
||||||
|
|
||||||
this.pendingTxTracker = new PendingTransactionTracker({
|
this.pendingTxTracker = new PendingTransactionTracker({
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
nonceTracker: this.nonceTracker,
|
nonceTracker: this.nonceTracker,
|
||||||
getBalance: (address) => {
|
retryLimit: 3500, // Retry 3500 blocks, or about 1 day.
|
||||||
const account = this.accountTracker.getState().accounts[address]
|
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
|
||||||
if (!account) return
|
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
|
||||||
return account.balance
|
|
||||||
},
|
|
||||||
publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil),
|
|
||||||
getPendingTransactions: () => {
|
|
||||||
const network = this.getNetwork()
|
|
||||||
return this.getFilteredTxList({
|
|
||||||
status: 'submitted',
|
|
||||||
metamaskNetworkId: network,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pendingTxTracker.on('txWarning', this.updateTx.bind(this))
|
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
|
||||||
this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this))
|
|
||||||
this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this))
|
|
||||||
|
|
||||||
this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
this.pendingTxTracker.on('tx:warning', (txMeta) => {
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
|
||||||
|
})
|
||||||
|
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
|
||||||
|
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
|
||||||
|
this.pendingTxTracker.on('tx:retry', (txMeta) => {
|
||||||
|
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
||||||
|
txMeta.retryCount++
|
||||||
|
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
|
||||||
|
})
|
||||||
|
|
||||||
|
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
||||||
// this is a little messy but until ethstore has been either
|
// this is a little messy but until ethstore has been either
|
||||||
// removed or redone this is to guard against the race condition
|
// removed or redone this is to guard against the race condition
|
||||||
// where accountTracker hasent been populated by the results yet
|
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
||||||
this.blockTracker.once('latest', () => {
|
|
||||||
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
|
||||||
})
|
|
||||||
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
|
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
|
||||||
// memstore is computed from a few different stores
|
// memstore is computed from a few different stores
|
||||||
this._updateMemstore()
|
this._updateMemstore()
|
||||||
this.store.subscribe(() => this._updateMemstore())
|
this.txStateManager.store.subscribe(() => this._updateMemstore())
|
||||||
this.networkStore.subscribe(() => this._updateMemstore())
|
this.networkStore.subscribe(() => this._updateMemstore())
|
||||||
this.preferencesStore.subscribe(() => this._updateMemstore())
|
this.preferencesStore.subscribe(() => this._updateMemstore())
|
||||||
}
|
}
|
||||||
@ -97,98 +101,31 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
return this.preferencesStore.getState().selectedAddress
|
return this.preferencesStore.getState().selectedAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the number of txs for the current network.
|
|
||||||
getTxCount () {
|
|
||||||
return this.getTxList().length
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the full tx list across all networks
|
|
||||||
getFullTxList () {
|
|
||||||
return this.store.getState().transactions
|
|
||||||
}
|
|
||||||
|
|
||||||
getUnapprovedTxCount () {
|
getUnapprovedTxCount () {
|
||||||
return Object.keys(this.getUnapprovedTxList()).length
|
return Object.keys(this.txStateManager.getUnapprovedTxList()).length
|
||||||
}
|
}
|
||||||
|
|
||||||
getPendingTxCount () {
|
getPendingTxCount (account) {
|
||||||
return this.getTxsByMetaData('status', 'signed').length
|
return this.txStateManager.getPendingTransactions(account).length
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the tx list
|
getFilteredTxList (opts) {
|
||||||
getTxList () {
|
return this.txStateManager.getFilteredTxList(opts)
|
||||||
const network = this.getNetwork()
|
|
||||||
const fullTxList = this.getFullTxList()
|
|
||||||
return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets tx by Id and returns it
|
getChainId () {
|
||||||
getTx (txId) {
|
const networkState = this.networkStore.getState()
|
||||||
const txList = this.getTxList()
|
const getChainId = parseInt(networkState)
|
||||||
const txMeta = txList.find(txData => txData.id === txId)
|
if (Number.isNaN(getChainId)) {
|
||||||
return txMeta
|
return 0
|
||||||
}
|
} else {
|
||||||
getUnapprovedTxList () {
|
return getChainId
|
||||||
const txList = this.getTxList()
|
}
|
||||||
return txList.filter((txMeta) => txMeta.status === 'unapproved')
|
|
||||||
.reduce((result, tx) => {
|
|
||||||
result[tx.id] = tx
|
|
||||||
return result
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTx (txMeta) {
|
|
||||||
// create txMeta snapshot for history
|
|
||||||
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
|
||||||
// recover previous tx state obj
|
|
||||||
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
|
|
||||||
// generate history entry and add to history
|
|
||||||
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState)
|
|
||||||
txMeta.history.push(entry)
|
|
||||||
|
|
||||||
// commit txMeta to state
|
|
||||||
const txId = txMeta.id
|
|
||||||
const txList = this.getFullTxList()
|
|
||||||
const index = txList.findIndex(txData => txData.id === txId)
|
|
||||||
txList[index] = txMeta
|
|
||||||
this._saveTxList(txList)
|
|
||||||
this.emit('update')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a tx to the txlist
|
// Adds a tx to the txlist
|
||||||
addTx (txMeta) {
|
addTx (txMeta) {
|
||||||
// initialize history
|
this.txStateManager.addTx(txMeta)
|
||||||
txMeta.history = []
|
|
||||||
// capture initial snapshot of txMeta for history
|
|
||||||
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
|
||||||
txMeta.history.push(snapshot)
|
|
||||||
|
|
||||||
// checks if the length of the tx history is
|
|
||||||
// longer then desired persistence limit
|
|
||||||
// and then if it is removes only confirmed
|
|
||||||
// or rejected tx's.
|
|
||||||
// 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) {
|
|
||||||
const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId))
|
|
||||||
fullTxList.splice(index, 1)
|
|
||||||
}
|
|
||||||
fullTxList.push(txMeta)
|
|
||||||
this._saveTxList(fullTxList)
|
|
||||||
this.emit('update')
|
|
||||||
|
|
||||||
this.once(`${txMeta.id}:signed`, function (txId) {
|
|
||||||
this.removeAllListeners(`${txMeta.id}:rejected`)
|
|
||||||
})
|
|
||||||
this.once(`${txMeta.id}:rejected`, function (txId) {
|
|
||||||
this.removeAllListeners(`${txMeta.id}:signed`)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.emit('updateBadge')
|
|
||||||
this.emit(`${txMeta.id}:unapproved`, txMeta)
|
this.emit(`${txMeta.id}:unapproved`, txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +135,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
this.emit('newUnaprovedTx', txMeta)
|
this.emit('newUnaprovedTx', txMeta)
|
||||||
// listen for tx completion (success, fail)
|
// listen for tx completion (success, fail)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.once(`${txMeta.id}:finished`, (completedTx) => {
|
this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
|
||||||
switch (completedTx.status) {
|
switch (completedTx.status) {
|
||||||
case 'submitted':
|
case 'submitted':
|
||||||
return resolve(completedTx.hash)
|
return resolve(completedTx.hash)
|
||||||
@ -213,7 +150,7 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
|
|
||||||
async addUnapprovedTransaction (txParams) {
|
async addUnapprovedTransaction (txParams) {
|
||||||
// validate
|
// validate
|
||||||
await this.txProviderUtil.validateTxParams(txParams)
|
await this.txGasUtil.validateTxParams(txParams)
|
||||||
// construct txMeta
|
// construct txMeta
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@ -232,17 +169,15 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
async addTxDefaults (txMeta) {
|
async addTxDefaults (txMeta) {
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
// ensure value
|
// ensure value
|
||||||
|
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
|
||||||
|
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
|
||||||
txParams.value = txParams.value || '0x0'
|
txParams.value = txParams.value || '0x0'
|
||||||
if (!txParams.gasPrice) {
|
|
||||||
const gasPrice = await this.query.gasPrice()
|
|
||||||
txParams.gasPrice = gasPrice
|
|
||||||
}
|
|
||||||
// set gasLimit
|
// set gasLimit
|
||||||
return await this.txProviderUtil.analyzeGasUsage(txMeta)
|
return await this.txGasUtil.analyzeGasUsage(txMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAndApproveTransaction (txMeta) {
|
async updateAndApproveTransaction (txMeta) {
|
||||||
this.updateTx(txMeta)
|
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
|
||||||
await this.approveTransaction(txMeta.id)
|
await this.approveTransaction(txMeta.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,24 +185,24 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
let nonceLock
|
let nonceLock
|
||||||
try {
|
try {
|
||||||
// approve
|
// approve
|
||||||
this.setTxStatusApproved(txId)
|
this.txStateManager.setTxStatusApproved(txId)
|
||||||
// get next nonce
|
// get next nonce
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
const fromAddress = txMeta.txParams.from
|
const fromAddress = txMeta.txParams.from
|
||||||
// wait for a nonce
|
// wait for a nonce
|
||||||
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
|
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
|
||||||
// add nonce to txParams
|
// add nonce to txParams
|
||||||
txMeta.txParams.nonce = nonceLock.nextNonce
|
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
|
||||||
// add nonce debugging information to txMeta
|
// add nonce debugging information to txMeta
|
||||||
txMeta.nonceDetails = nonceLock.nonceDetails
|
txMeta.nonceDetails = nonceLock.nonceDetails
|
||||||
this.updateTx(txMeta)
|
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')
|
||||||
// sign transaction
|
// sign transaction
|
||||||
const rawTx = await this.signTransaction(txId)
|
const rawTx = await this.signTransaction(txId)
|
||||||
await this.publishTransaction(txId, rawTx)
|
await this.publishTransaction(txId, rawTx)
|
||||||
// must set transaction to submitted/failed before releasing lock
|
// must set transaction to submitted/failed before releasing lock
|
||||||
nonceLock.releaseLock()
|
nonceLock.releaseLock()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setTxStatusFailed(txId, err)
|
this.txStateManager.setTxStatusFailed(txId, err)
|
||||||
// must set transaction to submitted/failed before releasing lock
|
// must set transaction to submitted/failed before releasing lock
|
||||||
if (nonceLock) nonceLock.releaseLock()
|
if (nonceLock) nonceLock.releaseLock()
|
||||||
// continue with error chain
|
// continue with error chain
|
||||||
@ -276,181 +211,46 @@ module.exports = class TransactionController extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signTransaction (txId) {
|
async signTransaction (txId) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
const txParams = txMeta.txParams
|
const txParams = txMeta.txParams
|
||||||
const fromAddress = txParams.from
|
const fromAddress = txParams.from
|
||||||
// add network/chain id
|
// add network/chain id
|
||||||
txParams.chainId = this.getChainId()
|
txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16))
|
||||||
const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams)
|
const ethTx = new Transaction(txParams)
|
||||||
await this.signEthTx(ethTx, fromAddress)
|
await this.signEthTx(ethTx, fromAddress)
|
||||||
this.setTxStatusSigned(txMeta.id)
|
this.txStateManager.setTxStatusSigned(txMeta.id)
|
||||||
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
|
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
|
||||||
return rawTx
|
return rawTx
|
||||||
}
|
}
|
||||||
|
|
||||||
async publishTransaction (txId, rawTx) {
|
async publishTransaction (txId, rawTx) {
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
txMeta.rawTx = rawTx
|
txMeta.rawTx = rawTx
|
||||||
this.updateTx(txMeta)
|
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction')
|
||||||
const txHash = await this.txProviderUtil.publishTransaction(rawTx)
|
const txHash = await this.query.sendRawTransaction(rawTx)
|
||||||
this.setTxHash(txId, txHash)
|
this.setTxHash(txId, txHash)
|
||||||
this.setTxStatusSubmitted(txId)
|
this.txStateManager.setTxStatusSubmitted(txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelTransaction (txId) {
|
async cancelTransaction (txId) {
|
||||||
this.setTxStatusRejected(txId)
|
this.txStateManager.setTxStatusRejected(txId)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
getChainId () {
|
|
||||||
const networkState = this.networkStore.getState()
|
|
||||||
const getChainId = parseInt(networkState)
|
|
||||||
if (Number.isNaN(getChainId)) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return getChainId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// receives a txHash records the tx as signed
|
// receives a txHash records the tx as signed
|
||||||
setTxHash (txId, txHash) {
|
setTxHash (txId, txHash) {
|
||||||
// Add the tx hash to the persisted meta-tx object
|
// Add the tx hash to the persisted meta-tx object
|
||||||
const txMeta = this.getTx(txId)
|
const txMeta = this.txStateManager.getTx(txId)
|
||||||
txMeta.hash = txHash
|
txMeta.hash = txHash
|
||||||
this.updateTx(txMeta)
|
this.txStateManager.updateTx(txMeta, 'transactions#setTxHash')
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
//
|
||||||
Takes an object of fields to search for eg:
|
// PRIVATE METHODS
|
||||||
let thingsToLookFor = {
|
//
|
||||||
to: '0x0..',
|
|
||||||
from: '0x0..',
|
|
||||||
status: 'signed',
|
|
||||||
err: undefined,
|
|
||||||
}
|
|
||||||
and returns a list of tx with all
|
|
||||||
options matching
|
|
||||||
|
|
||||||
****************HINT****************
|
|
||||||
| `err: undefined` is like looking |
|
|
||||||
| for a tx with no err |
|
|
||||||
| so you can also search txs that |
|
|
||||||
| dont have something as well by |
|
|
||||||
| setting the value as undefined |
|
|
||||||
************************************
|
|
||||||
|
|
||||||
this is for things like filtering a the tx list
|
|
||||||
for only tx's from 1 account
|
|
||||||
or for filltering for all txs from one account
|
|
||||||
and that have been 'confirmed'
|
|
||||||
*/
|
|
||||||
getFilteredTxList (opts) {
|
|
||||||
let filteredTxList
|
|
||||||
Object.keys(opts).forEach((key) => {
|
|
||||||
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
|
|
||||||
})
|
|
||||||
return filteredTxList
|
|
||||||
}
|
|
||||||
|
|
||||||
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
|
||||||
return txList.filter((txMeta) => {
|
|
||||||
if (txMeta.txParams[key]) {
|
|
||||||
return txMeta.txParams[key] === value
|
|
||||||
} else {
|
|
||||||
return txMeta[key] === value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// STATUS METHODS
|
|
||||||
// get::set status
|
|
||||||
|
|
||||||
// should return the status of the tx.
|
|
||||||
getTxStatus (txId) {
|
|
||||||
const txMeta = this.getTx(txId)
|
|
||||||
return txMeta.status
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update the status of the tx to 'rejected'.
|
|
||||||
setTxStatusRejected (txId) {
|
|
||||||
this._setTxStatus(txId, 'rejected')
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update the status of the tx to 'approved'.
|
|
||||||
setTxStatusApproved (txId) {
|
|
||||||
this._setTxStatus(txId, 'approved')
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update the status of the tx to 'signed'.
|
|
||||||
setTxStatusSigned (txId) {
|
|
||||||
this._setTxStatus(txId, 'signed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update the status of the tx to 'submitted'.
|
|
||||||
setTxStatusSubmitted (txId) {
|
|
||||||
this._setTxStatus(txId, 'submitted')
|
|
||||||
}
|
|
||||||
|
|
||||||
// should update the status of the tx to 'confirmed'.
|
|
||||||
setTxStatusConfirmed (txId) {
|
|
||||||
this._setTxStatus(txId, 'confirmed')
|
|
||||||
}
|
|
||||||
|
|
||||||
setTxStatusFailed (txId, err) {
|
|
||||||
const txMeta = this.getTx(txId)
|
|
||||||
txMeta.err = {
|
|
||||||
message: err.toString(),
|
|
||||||
stack: err.stack,
|
|
||||||
}
|
|
||||||
this.updateTx(txMeta)
|
|
||||||
this._setTxStatus(txId, 'failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
// merges txParams obj onto txData.txParams
|
|
||||||
// use extend to ensure that all fields are filled
|
|
||||||
updateTxParams (txId, txParams) {
|
|
||||||
const txMeta = this.getTx(txId)
|
|
||||||
txMeta.txParams = extend(txMeta.txParams, txParams)
|
|
||||||
this.updateTx(txMeta)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* _____________________________________
|
|
||||||
| |
|
|
||||||
| PRIVATE METHODS |
|
|
||||||
|______________________________________*/
|
|
||||||
|
|
||||||
|
|
||||||
// Should find the tx in the tx list and
|
|
||||||
// update it.
|
|
||||||
// should set the status in txData
|
|
||||||
// - `'unapproved'` the user has not responded
|
|
||||||
// - `'rejected'` the user has responded no!
|
|
||||||
// - `'approved'` the user has approved the tx
|
|
||||||
// - `'signed'` the tx is signed
|
|
||||||
// - `'submitted'` the tx is sent to a server
|
|
||||||
// - `'confirmed'` the tx has been included in a block.
|
|
||||||
// - `'failed'` the tx failed for some reason, included on tx data.
|
|
||||||
_setTxStatus (txId, status) {
|
|
||||||
const txMeta = this.getTx(txId)
|
|
||||||
txMeta.status = status
|
|
||||||
this.emit(`${txMeta.id}:${status}`, txId)
|
|
||||||
this.emit(`${status}`, txId)
|
|
||||||
if (status === 'submitted' || status === 'rejected') {
|
|
||||||
this.emit(`${txMeta.id}:finished`, txMeta)
|
|
||||||
}
|
|
||||||
this.updateTx(txMeta)
|
|
||||||
this.emit('updateBadge')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saves the new/updated txList.
|
|
||||||
// Function is intended only for internal use
|
|
||||||
_saveTxList (transactions) {
|
|
||||||
this.store.updateState({ transactions })
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateMemstore () {
|
_updateMemstore () {
|
||||||
const unapprovedTxs = this.getUnapprovedTxList()
|
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
|
||||||
const selectedAddressTxList = this.getFilteredTxList({
|
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
|
||||||
from: this.getSelectedAddress(),
|
from: this.getSelectedAddress(),
|
||||||
metamaskNetworkId: this.getNetwork(),
|
metamaskNetworkId: this.getNetwork(),
|
||||||
})
|
})
|
||||||
|
@ -1,596 +0,0 @@
|
|||||||
const ethUtil = require('ethereumjs-util')
|
|
||||||
const BN = ethUtil.BN
|
|
||||||
const bip39 = require('bip39')
|
|
||||||
const EventEmitter = require('events').EventEmitter
|
|
||||||
const ObservableStore = require('obs-store')
|
|
||||||
const filter = require('promise-filter')
|
|
||||||
const encryptor = require('browser-passworder')
|
|
||||||
const sigUtil = require('eth-sig-util')
|
|
||||||
const normalizeAddress = sigUtil.normalize
|
|
||||||
// Keyrings:
|
|
||||||
const SimpleKeyring = require('eth-simple-keyring')
|
|
||||||
const HdKeyring = require('eth-hd-keyring')
|
|
||||||
const keyringTypes = [
|
|
||||||
SimpleKeyring,
|
|
||||||
HdKeyring,
|
|
||||||
]
|
|
||||||
|
|
||||||
class KeyringController extends EventEmitter {
|
|
||||||
|
|
||||||
// PUBLIC METHODS
|
|
||||||
//
|
|
||||||
// THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
|
|
||||||
// MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
|
|
||||||
//
|
|
||||||
// THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.
|
|
||||||
|
|
||||||
constructor (opts) {
|
|
||||||
super()
|
|
||||||
const initState = opts.initState || {}
|
|
||||||
this.keyringTypes = keyringTypes
|
|
||||||
this.store = new ObservableStore(initState)
|
|
||||||
this.memStore = new ObservableStore({
|
|
||||||
isUnlocked: false,
|
|
||||||
keyringTypes: this.keyringTypes.map(krt => krt.type),
|
|
||||||
keyrings: [],
|
|
||||||
identities: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.accountTracker = opts.accountTracker
|
|
||||||
this.encryptor = opts.encryptor || encryptor
|
|
||||||
this.keyrings = []
|
|
||||||
this.getNetwork = opts.getNetwork
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full Update
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Emits the `update` event and
|
|
||||||
// returns a Promise that resolves to the current state.
|
|
||||||
//
|
|
||||||
// Frequently used to end asynchronous chains in this class,
|
|
||||||
// indicating consumers can often either listen for updates,
|
|
||||||
// or accept a state-resolving promise to consume their results.
|
|
||||||
//
|
|
||||||
// Not all methods end with this, that might be a nice refactor.
|
|
||||||
fullUpdate () {
|
|
||||||
this.emit('update')
|
|
||||||
return Promise.resolve(this.memStore.getState())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create New Vault And Keychain
|
|
||||||
// @string password - The password to encrypt the vault with
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Destroys any old encrypted storage,
|
|
||||||
// creates a new encrypted store with the given password,
|
|
||||||
// randomly creates a new HD wallet with 1 account,
|
|
||||||
// faucets that account on the testnet.
|
|
||||||
createNewVaultAndKeychain (password) {
|
|
||||||
return this.persistAllKeyrings(password)
|
|
||||||
.then(this.createFirstKeyTree.bind(this))
|
|
||||||
.then(this.fullUpdate.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateNewVaultAndRestore
|
|
||||||
// @string password - The password to encrypt the vault with
|
|
||||||
// @string seed - The BIP44-compliant seed phrase.
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Destroys any old encrypted storage,
|
|
||||||
// creates a new encrypted store with the given password,
|
|
||||||
// creates a new HD wallet from the given seed with 1 account.
|
|
||||||
createNewVaultAndRestore (password, seed) {
|
|
||||||
if (typeof password !== 'string') {
|
|
||||||
return Promise.reject('Password must be text.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bip39.validateMnemonic(seed)) {
|
|
||||||
return Promise.reject(new Error('Seed phrase is invalid.'))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clearKeyrings()
|
|
||||||
|
|
||||||
return this.persistAllKeyrings(password)
|
|
||||||
.then(() => {
|
|
||||||
return this.addNewKeyring('HD Key Tree', {
|
|
||||||
mnemonic: seed,
|
|
||||||
numberOfAccounts: 1,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then((firstKeyring) => {
|
|
||||||
return firstKeyring.getAccounts()
|
|
||||||
})
|
|
||||||
.then((accounts) => {
|
|
||||||
const firstAccount = accounts[0]
|
|
||||||
if (!firstAccount) throw new Error('KeyringController - First Account not found.')
|
|
||||||
const hexAccount = normalizeAddress(firstAccount)
|
|
||||||
this.emit('newAccount', hexAccount)
|
|
||||||
return this.setupAccounts(accounts)
|
|
||||||
})
|
|
||||||
.then(this.persistAllKeyrings.bind(this, password))
|
|
||||||
.then(this.fullUpdate.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Locked
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// This method deallocates all secrets, and effectively locks metamask.
|
|
||||||
setLocked () {
|
|
||||||
// set locked
|
|
||||||
this.password = null
|
|
||||||
this.memStore.updateState({ isUnlocked: false })
|
|
||||||
// remove keyrings
|
|
||||||
this.keyrings = []
|
|
||||||
this._updateMemStoreKeyrings()
|
|
||||||
return this.fullUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit Password
|
|
||||||
// @string password
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Attempts to decrypt the current vault and load its keyrings
|
|
||||||
// into memory.
|
|
||||||
//
|
|
||||||
// Temporarily also migrates any old-style vaults first, as well.
|
|
||||||
// (Pre MetaMask 3.0.0)
|
|
||||||
submitPassword (password) {
|
|
||||||
return this.unlockKeyrings(password)
|
|
||||||
.then((keyrings) => {
|
|
||||||
this.keyrings = keyrings
|
|
||||||
return this.fullUpdate()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add New Keyring
|
|
||||||
// @string type
|
|
||||||
// @object opts
|
|
||||||
//
|
|
||||||
// returns Promise( @Keyring keyring )
|
|
||||||
//
|
|
||||||
// Adds a new Keyring of the given `type` to the vault
|
|
||||||
// and the current decrypted Keyrings array.
|
|
||||||
//
|
|
||||||
// All Keyring classes implement a unique `type` string,
|
|
||||||
// and this is used to retrieve them from the keyringTypes array.
|
|
||||||
addNewKeyring (type, opts) {
|
|
||||||
const Keyring = this.getKeyringClassForType(type)
|
|
||||||
const keyring = new Keyring(opts)
|
|
||||||
return keyring.deserialize(opts)
|
|
||||||
.then(() => {
|
|
||||||
return keyring.getAccounts()
|
|
||||||
})
|
|
||||||
.then((accounts) => {
|
|
||||||
return this.checkForDuplicate(type, accounts)
|
|
||||||
})
|
|
||||||
.then((checkedAccounts) => {
|
|
||||||
this.keyrings.push(keyring)
|
|
||||||
return this.setupAccounts(checkedAccounts)
|
|
||||||
})
|
|
||||||
.then(() => this.persistAllKeyrings())
|
|
||||||
.then(() => this._updateMemStoreKeyrings())
|
|
||||||
.then(() => this.fullUpdate())
|
|
||||||
.then(() => {
|
|
||||||
return keyring
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now just checks for simple key pairs
|
|
||||||
// but in the future
|
|
||||||
// should possibly add HD and other types
|
|
||||||
//
|
|
||||||
checkForDuplicate (type, newAccount) {
|
|
||||||
return this.getAccounts()
|
|
||||||
.then((accounts) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'Simple Key Pair':
|
|
||||||
const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0]))
|
|
||||||
return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate'))
|
|
||||||
default:
|
|
||||||
return Promise.resolve(newAccount)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Add New Account
|
|
||||||
// @number keyRingNum
|
|
||||||
//
|
|
||||||
// returns Promise( @object state )
|
|
||||||
//
|
|
||||||
// Calls the `addAccounts` method on the Keyring
|
|
||||||
// in the kryings array at index `keyringNum`,
|
|
||||||
// and then saves those changes.
|
|
||||||
addNewAccount (selectedKeyring) {
|
|
||||||
return selectedKeyring.addAccounts(1)
|
|
||||||
.then(this.setupAccounts.bind(this))
|
|
||||||
.then(this.persistAllKeyrings.bind(this))
|
|
||||||
.then(this._updateMemStoreKeyrings.bind(this))
|
|
||||||
.then(this.fullUpdate.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save Account Label
|
|
||||||
// @string account
|
|
||||||
// @string label
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Persists a nickname equal to `label` for the specified account.
|
|
||||||
saveAccountLabel (account, label) {
|
|
||||||
try {
|
|
||||||
const hexAddress = normalizeAddress(account)
|
|
||||||
// update state on diskStore
|
|
||||||
const state = this.store.getState()
|
|
||||||
const walletNicknames = state.walletNicknames || {}
|
|
||||||
walletNicknames[hexAddress] = label
|
|
||||||
this.store.updateState({ walletNicknames })
|
|
||||||
// update state on memStore
|
|
||||||
const identities = this.memStore.getState().identities
|
|
||||||
identities[hexAddress].name = label
|
|
||||||
this.memStore.updateState({ identities })
|
|
||||||
return Promise.resolve(label)
|
|
||||||
} catch (err) {
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export Account
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise( @string privateKey )
|
|
||||||
//
|
|
||||||
// Requests the private key from the keyring controlling
|
|
||||||
// the specified address.
|
|
||||||
//
|
|
||||||
// Returns a Promise that may resolve with the private key string.
|
|
||||||
exportAccount (address) {
|
|
||||||
try {
|
|
||||||
return this.getKeyringForAccount(address)
|
|
||||||
.then((keyring) => {
|
|
||||||
return keyring.exportAccount(normalizeAddress(address))
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// SIGNING METHODS
|
|
||||||
//
|
|
||||||
// This method signs tx and returns a promise for
|
|
||||||
// TX Manager to update the state after signing
|
|
||||||
|
|
||||||
signTransaction (ethTx, _fromAddress) {
|
|
||||||
const fromAddress = normalizeAddress(_fromAddress)
|
|
||||||
return this.getKeyringForAccount(fromAddress)
|
|
||||||
.then((keyring) => {
|
|
||||||
return keyring.signTransaction(fromAddress, ethTx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign Message
|
|
||||||
// @object msgParams
|
|
||||||
//
|
|
||||||
// returns Promise(@buffer rawSig)
|
|
||||||
//
|
|
||||||
// Attempts to sign the provided @object msgParams.
|
|
||||||
signMessage (msgParams) {
|
|
||||||
const address = normalizeAddress(msgParams.from)
|
|
||||||
return this.getKeyringForAccount(address)
|
|
||||||
.then((keyring) => {
|
|
||||||
return keyring.signMessage(address, msgParams.data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign Personal Message
|
|
||||||
// @object msgParams
|
|
||||||
//
|
|
||||||
// returns Promise(@buffer rawSig)
|
|
||||||
//
|
|
||||||
// Attempts to sign the provided @object msgParams.
|
|
||||||
// Prefixes the hash before signing as per the new geth behavior.
|
|
||||||
signPersonalMessage (msgParams) {
|
|
||||||
const address = normalizeAddress(msgParams.from)
|
|
||||||
return this.getKeyringForAccount(address)
|
|
||||||
.then((keyring) => {
|
|
||||||
return keyring.signPersonalMessage(address, msgParams.data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// PRIVATE METHODS
|
|
||||||
//
|
|
||||||
// THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
|
|
||||||
// AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.
|
|
||||||
|
|
||||||
// Create First Key Tree
|
|
||||||
// returns @Promise
|
|
||||||
//
|
|
||||||
// Clears the vault,
|
|
||||||
// creates a new one,
|
|
||||||
// creates a random new HD Keyring with 1 account,
|
|
||||||
// makes that account the selected account,
|
|
||||||
// faucets that account on testnet,
|
|
||||||
// puts the current seed words into the state tree.
|
|
||||||
createFirstKeyTree () {
|
|
||||||
this.clearKeyrings()
|
|
||||||
return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
|
|
||||||
.then((keyring) => {
|
|
||||||
return keyring.getAccounts()
|
|
||||||
})
|
|
||||||
.then((accounts) => {
|
|
||||||
const firstAccount = accounts[0]
|
|
||||||
if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
|
|
||||||
const hexAccount = normalizeAddress(firstAccount)
|
|
||||||
this.emit('newAccount', hexAccount)
|
|
||||||
this.emit('newVault', hexAccount)
|
|
||||||
return this.setupAccounts(accounts)
|
|
||||||
})
|
|
||||||
.then(this.persistAllKeyrings.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup Accounts
|
|
||||||
// @array accounts
|
|
||||||
//
|
|
||||||
// returns @Promise(@object account)
|
|
||||||
//
|
|
||||||
// Initializes the provided account array
|
|
||||||
// Gives them numerically incremented nicknames,
|
|
||||||
// and adds them to the accountTracker for regular balance checking.
|
|
||||||
setupAccounts (accounts) {
|
|
||||||
return this.getAccounts()
|
|
||||||
.then((loadedAccounts) => {
|
|
||||||
const arr = accounts || loadedAccounts
|
|
||||||
return Promise.all(arr.map((account) => {
|
|
||||||
return this.getBalanceAndNickname(account)
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Balance And Nickname
|
|
||||||
// @string account
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Takes an account address and an iterator representing
|
|
||||||
// the current number of named accounts.
|
|
||||||
getBalanceAndNickname (account) {
|
|
||||||
if (!account) {
|
|
||||||
throw new Error('Problem loading account.')
|
|
||||||
}
|
|
||||||
const address = normalizeAddress(account)
|
|
||||||
this.accountTracker.addAccount(address)
|
|
||||||
return this.createNickname(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Nickname
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise( @string label )
|
|
||||||
//
|
|
||||||
// Takes an address, and assigns it an incremented nickname, persisting it.
|
|
||||||
createNickname (address) {
|
|
||||||
const hexAddress = normalizeAddress(address)
|
|
||||||
const identities = this.memStore.getState().identities
|
|
||||||
const currentIdentityCount = Object.keys(identities).length + 1
|
|
||||||
const nicknames = this.store.getState().walletNicknames || {}
|
|
||||||
const existingNickname = nicknames[hexAddress]
|
|
||||||
const name = existingNickname || `Account ${currentIdentityCount}`
|
|
||||||
identities[hexAddress] = {
|
|
||||||
address: hexAddress,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
this.memStore.updateState({ identities })
|
|
||||||
return this.saveAccountLabel(hexAddress, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist All Keyrings
|
|
||||||
// @password string
|
|
||||||
//
|
|
||||||
// returns Promise
|
|
||||||
//
|
|
||||||
// Iterates the current `keyrings` array,
|
|
||||||
// serializes each one into a serialized array,
|
|
||||||
// encrypts that array with the provided `password`,
|
|
||||||
// and persists that encrypted string to storage.
|
|
||||||
persistAllKeyrings (password = this.password) {
|
|
||||||
if (typeof password === 'string') {
|
|
||||||
this.password = password
|
|
||||||
this.memStore.updateState({ isUnlocked: true })
|
|
||||||
}
|
|
||||||
return Promise.all(this.keyrings.map((keyring) => {
|
|
||||||
return Promise.all([keyring.type, keyring.serialize()])
|
|
||||||
.then((serializedKeyringArray) => {
|
|
||||||
// Label the output values on each serialized Keyring:
|
|
||||||
return {
|
|
||||||
type: serializedKeyringArray[0],
|
|
||||||
data: serializedKeyringArray[1],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
.then((serializedKeyrings) => {
|
|
||||||
return this.encryptor.encrypt(this.password, serializedKeyrings)
|
|
||||||
})
|
|
||||||
.then((encryptedString) => {
|
|
||||||
this.store.updateState({ vault: encryptedString })
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock Keyrings
|
|
||||||
// @string password
|
|
||||||
//
|
|
||||||
// returns Promise( @array keyrings )
|
|
||||||
//
|
|
||||||
// Attempts to unlock the persisted encrypted storage,
|
|
||||||
// initializing the persisted keyrings to RAM.
|
|
||||||
unlockKeyrings (password) {
|
|
||||||
const encryptedVault = this.store.getState().vault
|
|
||||||
if (!encryptedVault) {
|
|
||||||
throw new Error('Cannot unlock without a previous vault.')
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.encryptor.decrypt(password, encryptedVault)
|
|
||||||
.then((vault) => {
|
|
||||||
this.password = password
|
|
||||||
this.memStore.updateState({ isUnlocked: true })
|
|
||||||
vault.forEach(this.restoreKeyring.bind(this))
|
|
||||||
return this.keyrings
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore Keyring
|
|
||||||
// @object serialized
|
|
||||||
//
|
|
||||||
// returns Promise( @Keyring deserialized )
|
|
||||||
//
|
|
||||||
// Attempts to initialize a new keyring from the provided
|
|
||||||
// serialized payload.
|
|
||||||
//
|
|
||||||
// On success, returns the resulting @Keyring instance.
|
|
||||||
restoreKeyring (serialized) {
|
|
||||||
const { type, data } = serialized
|
|
||||||
|
|
||||||
const Keyring = this.getKeyringClassForType(type)
|
|
||||||
const keyring = new Keyring()
|
|
||||||
return keyring.deserialize(data)
|
|
||||||
.then(() => {
|
|
||||||
return keyring.getAccounts()
|
|
||||||
})
|
|
||||||
.then((accounts) => {
|
|
||||||
return this.setupAccounts(accounts)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.keyrings.push(keyring)
|
|
||||||
this._updateMemStoreKeyrings()
|
|
||||||
return keyring
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Keyring Class For Type
|
|
||||||
// @string type
|
|
||||||
//
|
|
||||||
// Returns @class Keyring
|
|
||||||
//
|
|
||||||
// Searches the current `keyringTypes` array
|
|
||||||
// for a Keyring class whose unique `type` property
|
|
||||||
// matches the provided `type`,
|
|
||||||
// returning it if it exists.
|
|
||||||
getKeyringClassForType (type) {
|
|
||||||
return this.keyringTypes.find(kr => kr.type === type)
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeyringsByType (type) {
|
|
||||||
return this.keyrings.filter((keyring) => keyring.type === type)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Accounts
|
|
||||||
// returns Promise( @Array[ @string accounts ] )
|
|
||||||
//
|
|
||||||
// Returns the public addresses of all current accounts
|
|
||||||
// managed by all currently unlocked keyrings.
|
|
||||||
getAccounts () {
|
|
||||||
const keyrings = this.keyrings || []
|
|
||||||
return Promise.all(keyrings.map(kr => kr.getAccounts()))
|
|
||||||
.then((keyringArrays) => {
|
|
||||||
return keyringArrays.reduce((res, arr) => {
|
|
||||||
return res.concat(arr)
|
|
||||||
}, [])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Keyring For Account
|
|
||||||
// @string address
|
|
||||||
//
|
|
||||||
// returns Promise(@Keyring keyring)
|
|
||||||
//
|
|
||||||
// Returns the currently initialized keyring that manages
|
|
||||||
// the specified `address` if one exists.
|
|
||||||
getKeyringForAccount (address) {
|
|
||||||
const hexed = normalizeAddress(address)
|
|
||||||
log.debug(`KeyringController - getKeyringForAccount: ${hexed}`)
|
|
||||||
|
|
||||||
return Promise.all(this.keyrings.map((keyring) => {
|
|
||||||
return Promise.all([
|
|
||||||
keyring,
|
|
||||||
keyring.getAccounts(),
|
|
||||||
])
|
|
||||||
}))
|
|
||||||
.then(filter((candidate) => {
|
|
||||||
const accounts = candidate[1].map(normalizeAddress)
|
|
||||||
return accounts.includes(hexed)
|
|
||||||
}))
|
|
||||||
.then((winners) => {
|
|
||||||
if (winners && winners.length > 0) {
|
|
||||||
return winners[0][0]
|
|
||||||
} else {
|
|
||||||
throw new Error('No keyring found for the requested account.')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display For Keyring
|
|
||||||
// @Keyring keyring
|
|
||||||
//
|
|
||||||
// returns Promise( @Object { type:String, accounts:Array } )
|
|
||||||
//
|
|
||||||
// Is used for adding the current keyrings to the state object.
|
|
||||||
displayForKeyring (keyring) {
|
|
||||||
return keyring.getAccounts()
|
|
||||||
.then((accounts) => {
|
|
||||||
return {
|
|
||||||
type: keyring.type,
|
|
||||||
accounts: accounts,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Gas Buffer
|
|
||||||
// @string gas (as hexadecimal value)
|
|
||||||
//
|
|
||||||
// returns @string bufferedGas (as hexadecimal value)
|
|
||||||
//
|
|
||||||
// Adds a healthy buffer of gas to an initial gas estimate.
|
|
||||||
addGasBuffer (gas) {
|
|
||||||
const gasBuffer = new BN('100000', 10)
|
|
||||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
|
|
||||||
const correct = bnGas.add(gasBuffer)
|
|
||||||
return ethUtil.addHexPrefix(correct.toString(16))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear Keyrings
|
|
||||||
//
|
|
||||||
// Deallocates all currently managed keyrings and accounts.
|
|
||||||
// Used before initializing a new vault.
|
|
||||||
clearKeyrings () {
|
|
||||||
let accounts
|
|
||||||
try {
|
|
||||||
accounts = Object.keys(this.accountTracker.getState())
|
|
||||||
} catch (e) {
|
|
||||||
accounts = []
|
|
||||||
}
|
|
||||||
accounts.forEach((address) => {
|
|
||||||
this.accountTracker.removeAccount(address)
|
|
||||||
})
|
|
||||||
|
|
||||||
// clear keyrings from memory
|
|
||||||
this.keyrings = []
|
|
||||||
this.memStore.updateState({
|
|
||||||
keyrings: [],
|
|
||||||
identities: {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateMemStoreKeyrings () {
|
|
||||||
Promise.all(this.keyrings.map(this.displayForKeyring))
|
|
||||||
.then((keyrings) => {
|
|
||||||
this.memStore.updateState({ keyrings })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = KeyringController
|
|
@ -57,10 +57,10 @@ class AccountTracker extends EventEmitter {
|
|||||||
//
|
//
|
||||||
|
|
||||||
_updateForBlock (block) {
|
_updateForBlock (block) {
|
||||||
const blockNumber = '0x' + block.number.toString('hex')
|
this._currentBlockNumber = block.number
|
||||||
this._currentBlockNumber = blockNumber
|
const currentBlockGasLimit = block.gasLimit
|
||||||
|
|
||||||
this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` })
|
this.store.updateState({ currentBlockGasLimit })
|
||||||
|
|
||||||
async.parallel([
|
async.parallel([
|
||||||
this._updateAccounts.bind(this),
|
this._updateAccounts.bind(this),
|
||||||
|
31
app/scripts/lib/events-proxy.js
Normal file
31
app/scripts/lib/events-proxy.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module.exports = function createEventEmitterProxy(eventEmitter, listeners) {
|
||||||
|
let target = eventEmitter
|
||||||
|
const eventHandlers = listeners || {}
|
||||||
|
const proxy = new Proxy({}, {
|
||||||
|
get: (obj, name) => {
|
||||||
|
// intercept listeners
|
||||||
|
if (name === 'on') return addListener
|
||||||
|
if (name === 'setTarget') return setTarget
|
||||||
|
if (name === 'proxyEventHandlers') return eventHandlers
|
||||||
|
return target[name]
|
||||||
|
},
|
||||||
|
set: (obj, name, value) => {
|
||||||
|
target[name] = value
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
function setTarget (eventEmitter) {
|
||||||
|
target = eventEmitter
|
||||||
|
// migrate listeners
|
||||||
|
Object.keys(eventHandlers).forEach((name) => {
|
||||||
|
eventHandlers[name].forEach((handler) => target.on(name, handler))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function addListener (name, handler) {
|
||||||
|
if (!eventHandlers[name]) eventHandlers[name] = []
|
||||||
|
eventHandlers[name].push(handler)
|
||||||
|
target.on(name, handler)
|
||||||
|
}
|
||||||
|
if (listeners) proxy.setTarget(eventEmitter)
|
||||||
|
return proxy
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
const EventEmitter = require('events')
|
const EventEmitter = require('events')
|
||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const sufficientBalance = require('./util').sufficientBalance
|
|
||||||
const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
Utility class for tracking the transactions as they
|
Utility class for tracking the transactions as they
|
||||||
@ -13,7 +11,6 @@ const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day.
|
|||||||
requires a: {
|
requires a: {
|
||||||
provider: //,
|
provider: //,
|
||||||
nonceTracker: //see nonce tracker,
|
nonceTracker: //see nonce tracker,
|
||||||
getBalnce: //(address) a function for getting balances,
|
|
||||||
getPendingTransactions: //() a function for getting an array of transactions,
|
getPendingTransactions: //() a function for getting an array of transactions,
|
||||||
publishTransaction: //(rawTx) a async function for publishing raw transactions,
|
publishTransaction: //(rawTx) a async function for publishing raw transactions,
|
||||||
}
|
}
|
||||||
@ -25,11 +22,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
super()
|
super()
|
||||||
this.query = new EthQuery(config.provider)
|
this.query = new EthQuery(config.provider)
|
||||||
this.nonceTracker = config.nonceTracker
|
this.nonceTracker = config.nonceTracker
|
||||||
|
this.retryLimit = config.retryLimit || Infinity
|
||||||
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
|
||||||
@ -44,18 +39,18 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
if (!txHash) {
|
if (!txHash) {
|
||||||
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
||||||
noTxHashErr.name = 'NoTxHashError'
|
noTxHashErr.name = 'NoTxHashError'
|
||||||
this.emit('txFailed', txId, noTxHashErr)
|
this.emit('tx:failed', txId, noTxHashErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
block.transactions.forEach((tx) => {
|
block.transactions.forEach((tx) => {
|
||||||
if (tx.hash === txHash) this.emit('txConfirmed', txId)
|
if (tx.hash === txHash) this.emit('tx:confirmed', txId)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
queryPendingTxs ({oldBlock, newBlock}) {
|
queryPendingTxs ({ oldBlock, newBlock }) {
|
||||||
// check pending transactions on start
|
// check pending transactions on start
|
||||||
if (!oldBlock) {
|
if (!oldBlock) {
|
||||||
this._checkPendingTxs()
|
this._checkPendingTxs()
|
||||||
@ -96,26 +91,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
// ignore resubmit warnings, return early
|
// ignore resubmit warnings, return early
|
||||||
if (isKnownTx) return
|
if (isKnownTx) return
|
||||||
// encountered real error - transition to error state
|
// encountered real error - transition to error state
|
||||||
this.emit('txFailed', txMeta.id, err)
|
this.emit('tx:failed', txMeta.id, err)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async _resubmitTx (txMeta) {
|
async _resubmitTx (txMeta) {
|
||||||
const address = txMeta.txParams.from
|
if (txMeta.retryCount > this.retryLimit) {
|
||||||
const balance = this.getBalance(address)
|
const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
|
||||||
if (balance === undefined) return
|
return this.emit('tx:failed', txMeta.id, err)
|
||||||
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
|
|
||||||
|
|
||||||
if (txMeta.retryCount > RETRY_LIMIT) {
|
|
||||||
return this.giveUpOnTransaction(txMeta.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the value of the transaction is greater then the balance, fail.
|
|
||||||
if (!sufficientBalance(txMeta.txParams, balance)) {
|
|
||||||
const insufficientFundsError = new Error('Insufficient balance during rebroadcast.')
|
|
||||||
this.emit('txFailed', txMeta.id, insufficientFundsError)
|
|
||||||
log.error(insufficientFundsError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only auto-submit already-signed txs:
|
// Only auto-submit already-signed txs:
|
||||||
@ -125,7 +108,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
const txHash = await this.publishTransaction(rawTx)
|
const txHash = await this.publishTransaction(rawTx)
|
||||||
|
|
||||||
// Increment successful tries:
|
// Increment successful tries:
|
||||||
txMeta.retryCount++
|
this.emit('tx:retry', txMeta)
|
||||||
return txHash
|
return txHash
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +120,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
if (!txHash) {
|
if (!txHash) {
|
||||||
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
|
||||||
noTxHashErr.name = 'NoTxHashError'
|
noTxHashErr.name = 'NoTxHashError'
|
||||||
this.emit('txFailed', txId, noTxHashErr)
|
this.emit('tx:failed', txId, noTxHashErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// get latest transaction status
|
// get latest transaction status
|
||||||
@ -146,14 +129,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
|
|||||||
txParams = await this.query.getTransactionByHash(txHash)
|
txParams = await this.query.getTransactionByHash(txHash)
|
||||||
if (!txParams) return
|
if (!txParams) return
|
||||||
if (txParams.blockNumber) {
|
if (txParams.blockNumber) {
|
||||||
this.emit('txConfirmed', txId)
|
this.emit('tx:confirmed', txId)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
txMeta.warning = {
|
txMeta.warning = {
|
||||||
error: err,
|
error: err,
|
||||||
message: 'There was a problem loading this transaction.',
|
message: 'There was a problem loading this transaction.',
|
||||||
}
|
}
|
||||||
this.emit('txWarning', txMeta)
|
this.emit('tx:warning', txMeta)
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
const EthQuery = require('ethjs-query')
|
const EthQuery = require('ethjs-query')
|
||||||
const Transaction = require('ethereumjs-tx')
|
|
||||||
const normalize = require('eth-sig-util').normalize
|
|
||||||
const {
|
const {
|
||||||
hexToBn,
|
hexToBn,
|
||||||
BnMultiplyByFraction,
|
BnMultiplyByFraction,
|
||||||
@ -78,26 +76,6 @@ module.exports = class txProvideUtil {
|
|||||||
return bnToHex(upperGasLimitBn)
|
return bnToHex(upperGasLimitBn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// builds ethTx from txParams object
|
|
||||||
buildEthTxFromParams (txParams) {
|
|
||||||
// normalize values
|
|
||||||
txParams.to = normalize(txParams.to)
|
|
||||||
txParams.from = normalize(txParams.from)
|
|
||||||
txParams.value = normalize(txParams.value)
|
|
||||||
txParams.data = normalize(txParams.data)
|
|
||||||
txParams.gas = normalize(txParams.gas || txParams.gasLimit)
|
|
||||||
txParams.gasPrice = normalize(txParams.gasPrice)
|
|
||||||
txParams.nonce = normalize(txParams.nonce)
|
|
||||||
// build ethTx
|
|
||||||
log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`)
|
|
||||||
const ethTx = new Transaction(txParams)
|
|
||||||
return ethTx
|
|
||||||
}
|
|
||||||
|
|
||||||
async publishTransaction (rawTx) {
|
|
||||||
return await this.query.sendRawTransaction(rawTx)
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateTxParams (txParams) {
|
async validateTxParams (txParams) {
|
||||||
if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
|
if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
|
||||||
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
|
@ -20,11 +20,15 @@ function migrateFromSnapshotsToDiffs(longHistory) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateHistoryEntry(previousState, newState) {
|
function generateHistoryEntry(previousState, newState, note) {
|
||||||
return jsonDiffer.compare(previousState, newState)
|
const entry = jsonDiffer.compare(previousState, newState)
|
||||||
|
// Add a note to the first op, since it breaks if we append it to the entry
|
||||||
|
if (note && entry[0]) entry[0].note = note
|
||||||
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
function replayHistory(shortHistory) {
|
function replayHistory(_shortHistory) {
|
||||||
|
const shortHistory = clone(_shortHistory)
|
||||||
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
|
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
245
app/scripts/lib/tx-state-manager.js
Normal file
245
app/scripts/lib/tx-state-manager.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
const extend = require('xtend')
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const ObservableStore = require('obs-store')
|
||||||
|
const ethUtil = require('ethereumjs-util')
|
||||||
|
const txStateHistoryHelper = require('./tx-state-history-helper')
|
||||||
|
|
||||||
|
module.exports = class TransactionStateManger extends EventEmitter {
|
||||||
|
constructor ({ initState, txHistoryLimit, getNetwork }) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.store = new ObservableStore(
|
||||||
|
extend({
|
||||||
|
transactions: [],
|
||||||
|
}, initState))
|
||||||
|
this.txHistoryLimit = txHistoryLimit
|
||||||
|
this.getNetwork = getNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the number of txs for the current network.
|
||||||
|
getTxCount () {
|
||||||
|
return this.getTxList().length
|
||||||
|
}
|
||||||
|
|
||||||
|
getTxList () {
|
||||||
|
const network = this.getNetwork()
|
||||||
|
const fullTxList = this.getFullTxList()
|
||||||
|
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFullTxList () {
|
||||||
|
return this.store.getState().transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the tx list
|
||||||
|
getUnapprovedTxList () {
|
||||||
|
const txList = this.getTxsByMetaData('status', 'unapproved')
|
||||||
|
return txList.reduce((result, tx) => {
|
||||||
|
result[tx.id] = tx
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
getPendingTransactions (address) {
|
||||||
|
const opts = { status: 'submitted' }
|
||||||
|
if (address) opts.from = address
|
||||||
|
return this.getFilteredTxList(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
addTx (txMeta) {
|
||||||
|
this.once(`${txMeta.id}:signed`, function (txId) {
|
||||||
|
this.removeAllListeners(`${txMeta.id}:rejected`)
|
||||||
|
})
|
||||||
|
this.once(`${txMeta.id}:rejected`, function (txId) {
|
||||||
|
this.removeAllListeners(`${txMeta.id}:signed`)
|
||||||
|
})
|
||||||
|
// initialize history
|
||||||
|
txMeta.history = []
|
||||||
|
// capture initial snapshot of txMeta for history
|
||||||
|
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
|
txMeta.history.push(snapshot)
|
||||||
|
|
||||||
|
const transactions = this.getFullTxList()
|
||||||
|
const txCount = this.getTxCount()
|
||||||
|
const txHistoryLimit = this.txHistoryLimit
|
||||||
|
|
||||||
|
// checks if the length of the tx history is
|
||||||
|
// longer then desired persistence limit
|
||||||
|
// and then if it is removes only confirmed
|
||||||
|
// or rejected tx's.
|
||||||
|
// not tx's that are pending or unapproved
|
||||||
|
if (txCount > txHistoryLimit - 1) {
|
||||||
|
const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
|
||||||
|
transactions.splice(index, 1)
|
||||||
|
}
|
||||||
|
transactions.push(txMeta)
|
||||||
|
this._saveTxList(transactions)
|
||||||
|
return txMeta
|
||||||
|
}
|
||||||
|
// gets tx by Id and returns it
|
||||||
|
getTx (txId) {
|
||||||
|
const txMeta = this.getTxsByMetaData('id', txId)[0]
|
||||||
|
return txMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTx (txMeta, note) {
|
||||||
|
if (txMeta.txParams) {
|
||||||
|
Object.keys(txMeta.txParams).forEach((key) => {
|
||||||
|
let value = txMeta.txParams[key]
|
||||||
|
if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`)
|
||||||
|
if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// create txMeta snapshot for history
|
||||||
|
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
|
||||||
|
// recover previous tx state obj
|
||||||
|
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
|
||||||
|
// generate history entry and add to history
|
||||||
|
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note)
|
||||||
|
txMeta.history.push(entry)
|
||||||
|
|
||||||
|
// commit txMeta to state
|
||||||
|
const txId = txMeta.id
|
||||||
|
const txList = this.getFullTxList()
|
||||||
|
const index = txList.findIndex(txData => txData.id === txId)
|
||||||
|
txList[index] = txMeta
|
||||||
|
this._saveTxList(txList)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// merges txParams obj onto txData.txParams
|
||||||
|
// use extend to ensure that all fields are filled
|
||||||
|
updateTxParams (txId, txParams) {
|
||||||
|
const txMeta = this.getTx(txId)
|
||||||
|
txMeta.txParams = extend(txMeta.txParams, txParams)
|
||||||
|
this.updateTx(txMeta, `txStateManager#updateTxParams`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Takes an object of fields to search for eg:
|
||||||
|
let thingsToLookFor = {
|
||||||
|
to: '0x0..',
|
||||||
|
from: '0x0..',
|
||||||
|
status: 'signed',
|
||||||
|
err: undefined,
|
||||||
|
}
|
||||||
|
and returns a list of tx with all
|
||||||
|
options matching
|
||||||
|
|
||||||
|
****************HINT****************
|
||||||
|
| `err: undefined` is like looking |
|
||||||
|
| for a tx with no err |
|
||||||
|
| so you can also search txs that |
|
||||||
|
| dont have something as well by |
|
||||||
|
| setting the value as undefined |
|
||||||
|
************************************
|
||||||
|
|
||||||
|
this is for things like filtering a the tx list
|
||||||
|
for only tx's from 1 account
|
||||||
|
or for filltering for all txs from one account
|
||||||
|
and that have been 'confirmed'
|
||||||
|
*/
|
||||||
|
getFilteredTxList (opts, initialList) {
|
||||||
|
let filteredTxList = initialList
|
||||||
|
Object.keys(opts).forEach((key) => {
|
||||||
|
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
|
||||||
|
})
|
||||||
|
return filteredTxList
|
||||||
|
}
|
||||||
|
|
||||||
|
getTxsByMetaData (key, value, txList = this.getTxList()) {
|
||||||
|
return txList.filter((txMeta) => {
|
||||||
|
if (txMeta.txParams[key]) {
|
||||||
|
return txMeta.txParams[key] === value
|
||||||
|
} else {
|
||||||
|
return txMeta[key] === value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATUS METHODS
|
||||||
|
// statuses:
|
||||||
|
// - `'unapproved'` the user has not responded
|
||||||
|
// - `'rejected'` the user has responded no!
|
||||||
|
// - `'approved'` the user has approved the tx
|
||||||
|
// - `'signed'` the tx is signed
|
||||||
|
// - `'submitted'` the tx is sent to a server
|
||||||
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
|
// - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
|
||||||
|
// get::set status
|
||||||
|
|
||||||
|
// should return the status of the tx.
|
||||||
|
getTxStatus (txId) {
|
||||||
|
const txMeta = this.getTx(txId)
|
||||||
|
return txMeta.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update the status of the tx to 'rejected'.
|
||||||
|
setTxStatusRejected (txId) {
|
||||||
|
this._setTxStatus(txId, 'rejected')
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update the status of the tx to 'approved'.
|
||||||
|
setTxStatusApproved (txId) {
|
||||||
|
this._setTxStatus(txId, 'approved')
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update the status of the tx to 'signed'.
|
||||||
|
setTxStatusSigned (txId) {
|
||||||
|
this._setTxStatus(txId, 'signed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update the status of the tx to 'submitted'.
|
||||||
|
setTxStatusSubmitted (txId) {
|
||||||
|
this._setTxStatus(txId, 'submitted')
|
||||||
|
}
|
||||||
|
|
||||||
|
// should update the status of the tx to 'confirmed'.
|
||||||
|
setTxStatusConfirmed (txId) {
|
||||||
|
this._setTxStatus(txId, 'confirmed')
|
||||||
|
}
|
||||||
|
|
||||||
|
setTxStatusFailed (txId, err) {
|
||||||
|
const txMeta = this.getTx(txId)
|
||||||
|
txMeta.err = {
|
||||||
|
message: err.toString(),
|
||||||
|
stack: err.stack,
|
||||||
|
}
|
||||||
|
this.updateTx(txMeta)
|
||||||
|
this._setTxStatus(txId, 'failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// PRIVATE METHODS
|
||||||
|
//
|
||||||
|
|
||||||
|
// Should find the tx in the tx list and
|
||||||
|
// update it.
|
||||||
|
// should set the status in txData
|
||||||
|
// - `'unapproved'` the user has not responded
|
||||||
|
// - `'rejected'` the user has responded no!
|
||||||
|
// - `'approved'` the user has approved the tx
|
||||||
|
// - `'signed'` the tx is signed
|
||||||
|
// - `'submitted'` the tx is sent to a server
|
||||||
|
// - `'confirmed'` the tx has been included in a block.
|
||||||
|
// - `'failed'` the tx failed for some reason, included on tx data.
|
||||||
|
_setTxStatus (txId, status) {
|
||||||
|
const txMeta = this.getTx(txId)
|
||||||
|
txMeta.status = status
|
||||||
|
this.emit(`${txMeta.id}:${status}`, txId)
|
||||||
|
this.emit(`tx:status-update`, txId, status)
|
||||||
|
if (status === 'submitted' || status === 'rejected') {
|
||||||
|
this.emit(`${txMeta.id}:finished`, txMeta)
|
||||||
|
}
|
||||||
|
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
|
||||||
|
this.emit('update:badge')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves the new/updated txList.
|
||||||
|
// Function is intended only for internal use
|
||||||
|
_saveTxList (transactions) {
|
||||||
|
this.store.updateState({ transactions })
|
||||||
|
}
|
||||||
|
}
|
@ -14,7 +14,7 @@ const createOriginMiddleware = require('./lib/createOriginMiddleware')
|
|||||||
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
|
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
|
||||||
const createProviderMiddleware = require('./lib/createProviderMiddleware')
|
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('eth-keyring-controller')
|
||||||
const NetworkController = require('./controllers/network')
|
const NetworkController = require('./controllers/network')
|
||||||
const PreferencesController = require('./controllers/preferences')
|
const PreferencesController = require('./controllers/preferences')
|
||||||
const CurrencyController = require('./controllers/currency')
|
const CurrencyController = require('./controllers/currency')
|
||||||
@ -82,7 +82,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
|
|
||||||
// rpc provider
|
// rpc provider
|
||||||
this.provider = this.initializeProvider()
|
this.provider = this.initializeProvider()
|
||||||
this.blockTracker = this.provider
|
this.blockTracker = this.provider._blockTracker
|
||||||
|
|
||||||
// eth data query tools
|
// eth data query tools
|
||||||
this.ethQuery = new EthQuery(this.provider)
|
this.ethQuery = new EthQuery(this.provider)
|
||||||
@ -100,6 +100,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
encryptor: opts.encryptor || undefined,
|
encryptor: opts.encryptor || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If only one account exists, make sure it is selected.
|
||||||
|
this.keyringController.store.subscribe((state) => {
|
||||||
|
const addresses = Object.keys(state.walletNicknames || {})
|
||||||
|
if (addresses.length === 1) {
|
||||||
|
const address = addresses[0]
|
||||||
|
this.preferencesController.setSelectedAddress(address)
|
||||||
|
}
|
||||||
|
})
|
||||||
this.keyringController.on('newAccount', (address) => {
|
this.keyringController.on('newAccount', (address) => {
|
||||||
this.preferencesController.setSelectedAddress(address)
|
this.preferencesController.setSelectedAddress(address)
|
||||||
this.accountTracker.addAccount(address)
|
this.accountTracker.addAccount(address)
|
||||||
@ -124,7 +132,6 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
blockTracker: this.blockTracker,
|
blockTracker: this.blockTracker,
|
||||||
ethQuery: this.ethQuery,
|
ethQuery: this.ethQuery,
|
||||||
accountTracker: this.accountTracker,
|
|
||||||
})
|
})
|
||||||
this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts))
|
this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts))
|
||||||
|
|
||||||
@ -209,19 +216,18 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
//
|
//
|
||||||
|
|
||||||
initializeProvider () {
|
initializeProvider () {
|
||||||
return this.networkController.initializeProvider({
|
const providerOpts = {
|
||||||
static: {
|
static: {
|
||||||
eth_syncing: false,
|
eth_syncing: false,
|
||||||
web3_clientVersion: `MetaMask/v${version}`,
|
web3_clientVersion: `MetaMask/v${version}`,
|
||||||
},
|
},
|
||||||
// rpc data source
|
|
||||||
rpcUrl: this.networkController.getCurrentRpcAddress(),
|
|
||||||
originHttpHeaderKey: 'X-Metamask-Origin',
|
originHttpHeaderKey: 'X-Metamask-Origin',
|
||||||
// account mgmt
|
// account mgmt
|
||||||
getAccounts: (cb) => {
|
getAccounts: (cb) => {
|
||||||
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
|
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
|
||||||
const result = []
|
const result = []
|
||||||
const selectedAddress = this.preferencesController.getSelectedAddress()
|
const selectedAddress = this.preferencesController.getSelectedAddress()
|
||||||
|
|
||||||
// only show address if account is unlocked
|
// only show address if account is unlocked
|
||||||
if (isUnlocked && selectedAddress) {
|
if (isUnlocked && selectedAddress) {
|
||||||
result.push(selectedAddress)
|
result.push(selectedAddress)
|
||||||
@ -232,10 +238,11 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this),
|
processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this),
|
||||||
// old style msg signing
|
// old style msg signing
|
||||||
processMessage: this.newUnsignedMessage.bind(this),
|
processMessage: this.newUnsignedMessage.bind(this),
|
||||||
|
// personal_sign msg signing
|
||||||
// new style msg signing
|
|
||||||
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
|
||||||
})
|
}
|
||||||
|
const providerProxy = this.networkController.initializeProvider(providerOpts)
|
||||||
|
return providerProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
initPublicConfigStore () {
|
initPublicConfigStore () {
|
||||||
@ -304,13 +311,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
const txController = this.txController
|
const txController = this.txController
|
||||||
const noticeController = this.noticeController
|
const noticeController = this.noticeController
|
||||||
const addressBookController = this.addressBookController
|
const addressBookController = this.addressBookController
|
||||||
|
const networkController = this.networkController
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// etc
|
// etc
|
||||||
getState: (cb) => cb(null, this.getState()),
|
getState: (cb) => cb(null, this.getState()),
|
||||||
setProviderType: this.networkController.setProviderType.bind(this.networkController),
|
|
||||||
setCurrentCurrency: this.setCurrentCurrency.bind(this),
|
setCurrentCurrency: this.setCurrentCurrency.bind(this),
|
||||||
markAccountsFound: this.markAccountsFound.bind(this),
|
markAccountsFound: this.markAccountsFound.bind(this),
|
||||||
|
|
||||||
// coinbase
|
// coinbase
|
||||||
buyEth: this.buyEth.bind(this),
|
buyEth: this.buyEth.bind(this),
|
||||||
// shapeshift
|
// shapeshift
|
||||||
@ -325,12 +333,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
// vault management
|
// vault management
|
||||||
submitPassword: this.submitPassword.bind(this),
|
submitPassword: this.submitPassword.bind(this),
|
||||||
|
|
||||||
|
// network management
|
||||||
|
setProviderType: nodeify(networkController.setProviderType, networkController),
|
||||||
|
setCustomRpc: nodeify(this.setCustomRpc, this),
|
||||||
|
|
||||||
// PreferencesController
|
// PreferencesController
|
||||||
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
|
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
|
||||||
addToken: nodeify(preferencesController.addToken, preferencesController),
|
addToken: nodeify(preferencesController.addToken, preferencesController),
|
||||||
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
|
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
|
||||||
setDefaultRpc: nodeify(this.setDefaultRpc, this),
|
|
||||||
setCustomRpc: nodeify(this.setCustomRpc, this),
|
|
||||||
|
|
||||||
// AddressController
|
// AddressController
|
||||||
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
|
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
|
||||||
@ -681,19 +691,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||||||
createShapeShiftTx (depositAddress, depositType) {
|
createShapeShiftTx (depositAddress, depositType) {
|
||||||
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
|
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
|
||||||
}
|
}
|
||||||
// network
|
|
||||||
|
|
||||||
setDefaultRpc () {
|
// network
|
||||||
this.networkController.setRpcTarget('http://localhost:8545')
|
|
||||||
return Promise.resolve('http://localhost:8545')
|
|
||||||
}
|
|
||||||
|
|
||||||
setCustomRpc (rpcTarget, rpcList) {
|
async setCustomRpc (rpcTarget, rpcList) {
|
||||||
this.networkController.setRpcTarget(rpcTarget)
|
this.networkController.setRpcTarget(rpcTarget)
|
||||||
|
await this.preferencesController.updateFrequentRpcList(rpcTarget)
|
||||||
return this.preferencesController.updateFrequentRpcList(rpcTarget)
|
return rpcTarget
|
||||||
.then(() => {
|
|
||||||
return Promise.resolve(rpcTarget)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ which we dont have access to at the time of this writing.
|
|||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
const ConfigManager = require('../../app/scripts/lib/config-manager')
|
const ConfigManager = require('../../app/scripts/lib/config-manager')
|
||||||
const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
|
const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator')
|
||||||
const KeyringController = require('../../app/scripts/lib/keyring-controller')
|
const KeyringController = require('eth-keyring-controller')
|
||||||
|
|
||||||
const password = 'obviously not correct'
|
const password = 'obviously not correct'
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ class ExtensionPlatform {
|
|||||||
//
|
//
|
||||||
// Public
|
// Public
|
||||||
//
|
//
|
||||||
|
|
||||||
reload () {
|
reload () {
|
||||||
extension.runtime.reload()
|
extension.runtime.reload()
|
||||||
}
|
}
|
||||||
|
@ -3,62 +3,58 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MetaMask</title>
|
<title>MetaMask</title>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- app content -->
|
|
||||||
<div id="app-content" style="height: 100%"></div>
|
|
||||||
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
|
<script src="./bundle.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
</body>
|
<style>
|
||||||
|
html, body, #test-container, .super-dev-container {
|
||||||
<style>
|
height: 100%;
|
||||||
html, body, #test-container, .super-dev-container {
|
width: 100%;
|
||||||
height: 100%;
|
position: relative;
|
||||||
width: 100%;
|
background: white;
|
||||||
position: relative;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
#app-content {
|
|
||||||
background: #F7F7F7;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
liveReloadCode(Date.now(), 300)
|
|
||||||
function liveReloadCode(lastUpdate, updateRate) {
|
|
||||||
setTimeout(iter, updateRate)
|
|
||||||
|
|
||||||
function iter() {
|
|
||||||
var xhr = new XMLHttpRequest()
|
|
||||||
|
|
||||||
xhr.open('GET', '/-/live-reload')
|
|
||||||
xhr.onreadystatechange = function() {
|
|
||||||
if(xhr.readyState !== 4) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
#app-content {
|
||||||
|
background: #F7F7F7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
try {
|
<script>
|
||||||
var change = JSON.parse(xhr.responseText).lastUpdate
|
liveReloadCode(Date.now(), 300)
|
||||||
|
function liveReloadCode(lastUpdate, updateRate) {
|
||||||
|
setTimeout(iter, updateRate)
|
||||||
|
|
||||||
if(lastUpdate < change) {
|
function iter() {
|
||||||
return reload()
|
var xhr = new XMLHttpRequest()
|
||||||
|
|
||||||
|
xhr.open('GET', '/-/live-reload')
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if(xhr.readyState !== 4) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var change = JSON.parse(xhr.responseText).lastUpdate
|
||||||
|
|
||||||
|
if(lastUpdate < change) {
|
||||||
|
return reload()
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr =
|
||||||
|
xhr.onreadystatechange = null
|
||||||
|
setTimeout(iter, updateRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send(null)
|
||||||
}
|
}
|
||||||
} catch(err) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xhr =
|
function reload() {
|
||||||
xhr.onreadystatechange = null
|
window.location.reload()
|
||||||
setTimeout(iter, updateRate)
|
}
|
||||||
}
|
</script>
|
||||||
|
|
||||||
xhr.send(null)
|
</body>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload() {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,57 +1,26 @@
|
|||||||
window.addEventListener('load', web3Detect)
|
const EthQuery = require('ethjs-query')
|
||||||
|
|
||||||
|
window.addEventListener('load', loadProvider)
|
||||||
window.addEventListener('message', console.warn)
|
window.addEventListener('message', console.warn)
|
||||||
|
|
||||||
function web3Detect() {
|
async function loadProvider() {
|
||||||
if (global.web3) {
|
const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
|
||||||
logToDom('web3 detected!')
|
const ethQuery = new EthQuery(ethereumProvider)
|
||||||
startApp()
|
const accounts = await ethQuery.accounts()
|
||||||
} else {
|
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
|
||||||
logToDom('no web3 detected!')
|
setupButton(ethQuery)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startApp(){
|
|
||||||
console.log('app started')
|
|
||||||
|
|
||||||
var primaryAccount
|
|
||||||
console.log('getting main account...')
|
|
||||||
web3.eth.getAccounts((err, addresses) => {
|
|
||||||
if (err) console.error(err)
|
|
||||||
console.log('set address', addresses[0])
|
|
||||||
primaryAccount = addresses[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
document.querySelector('.action-button-1').addEventListener('click', function(){
|
|
||||||
console.log('saw click')
|
|
||||||
console.log('sending tx')
|
|
||||||
primaryAccount
|
|
||||||
web3.eth.sendTransaction({
|
|
||||||
from: primaryAccount,
|
|
||||||
to: primaryAccount,
|
|
||||||
value: 0,
|
|
||||||
}, function(err, txHash){
|
|
||||||
if (err) throw err
|
|
||||||
console.log('sendTransaction result:', err || txHash)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
document.querySelector('.action-button-2').addEventListener('click', function(){
|
|
||||||
console.log('saw click')
|
|
||||||
setTimeout(function(){
|
|
||||||
console.log('sending tx')
|
|
||||||
web3.eth.sendTransaction({
|
|
||||||
from: primaryAccount,
|
|
||||||
to: primaryAccount,
|
|
||||||
value: 0,
|
|
||||||
}, function(err, txHash){
|
|
||||||
if (err) throw err
|
|
||||||
console.log('sendTransaction result:', err || txHash)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function logToDom(message){
|
function logToDom(message){
|
||||||
document.body.appendChild(document.createTextNode(message))
|
document.getElementById('account').innerText = message
|
||||||
console.log(message)
|
console.log(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupButton (ethQuery) {
|
||||||
|
const button = document.getElementById('action-button-1')
|
||||||
|
button.addEventListener('click', async () => {
|
||||||
|
const accounts = await ethQuery.accounts()
|
||||||
|
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined')
|
||||||
|
})
|
||||||
|
}
|
@ -3,13 +3,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>MetaMask ZeroClient Example</title>
|
|
||||||
<script src="http://localhost:9001/metamascara.js"></script>
|
<script src="http://localhost:9001/metamascara.js"></script>
|
||||||
|
<title>MetaMask ZeroClient Example</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<button class="action-button-1">SYNC TX</button>
|
<button id="action-button-1">GET ACCOUNT</button>
|
||||||
<button class="action-button-2">ASYNC TX</button>
|
<div id="account"></div>
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -19,8 +19,7 @@ const migrations = require('../../app/scripts/migrations/')
|
|||||||
const firstTimeState = require('../../app/scripts/first-time-state')
|
const firstTimeState = require('../../app/scripts/first-time-state')
|
||||||
|
|
||||||
const STORAGE_KEY = 'metamask-config'
|
const STORAGE_KEY = 'metamask-config'
|
||||||
// const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
|
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
|
||||||
const METAMASK_DEBUG = true
|
|
||||||
let popupIsOpen = false
|
let popupIsOpen = false
|
||||||
let connectedClientCount = 0
|
let connectedClientCount = 0
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
const Iframe = require('iframe')
|
|
||||||
const createIframeStream = require('iframe-stream').IframeStream
|
|
||||||
|
|
||||||
module.exports = setupIframe
|
|
||||||
|
|
||||||
|
|
||||||
function setupIframe(opts) {
|
|
||||||
opts = opts || {}
|
|
||||||
var frame = Iframe({
|
|
||||||
src: opts.zeroClientProvider || 'https://zero.metamask.io/',
|
|
||||||
container: opts.container || document.head,
|
|
||||||
sandboxAttributes: opts.sandboxAttributes || ['allow-scripts', 'allow-popups'],
|
|
||||||
})
|
|
||||||
var iframe = frame.iframe
|
|
||||||
iframe.style.setProperty('display', 'none')
|
|
||||||
var iframeStream = createIframeStream(iframe)
|
|
||||||
|
|
||||||
return iframeStream
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
const setupIframe = require('./setup-iframe.js')
|
|
||||||
const MetamaskInpageProvider = require('../../../app/scripts/lib/inpage-provider.js')
|
|
||||||
|
|
||||||
module.exports = getProvider
|
|
||||||
|
|
||||||
|
|
||||||
function getProvider(opts){
|
|
||||||
if (global.web3) {
|
|
||||||
console.log('MetaMask ZeroClient - using environmental web3 provider')
|
|
||||||
return global.web3.currentProvider
|
|
||||||
}
|
|
||||||
console.log('MetaMask ZeroClient - injecting zero-client iframe!')
|
|
||||||
var iframeStream = setupIframe({
|
|
||||||
zeroClientProvider: opts.mascaraUrl,
|
|
||||||
sandboxAttributes: ['allow-scripts', 'allow-popups', 'allow-same-origin'],
|
|
||||||
container: document.body,
|
|
||||||
})
|
|
||||||
|
|
||||||
var inpageProvider = new MetamaskInpageProvider(iframeStream)
|
|
||||||
return inpageProvider
|
|
||||||
|
|
||||||
}
|
|
@ -1,47 +1 @@
|
|||||||
const Web3 = require('web3')
|
global.metamask = require('metamascara')
|
||||||
const setupProvider = require('./lib/setup-provider.js')
|
|
||||||
const setupDappAutoReload = require('../../app/scripts/lib/auto-reload.js')
|
|
||||||
const MASCARA_ORIGIN = process.env.MASCARA_ORIGIN || 'http://localhost:9001'
|
|
||||||
console.log('MASCARA_ORIGIN:', MASCARA_ORIGIN)
|
|
||||||
|
|
||||||
//
|
|
||||||
// setup web3
|
|
||||||
//
|
|
||||||
|
|
||||||
const provider = setupProvider({
|
|
||||||
mascaraUrl: MASCARA_ORIGIN + '/proxy/',
|
|
||||||
})
|
|
||||||
instrumentForUserInteractionTriggers(provider)
|
|
||||||
|
|
||||||
const web3 = new Web3(provider)
|
|
||||||
setupDappAutoReload(web3, provider.publicConfigStore)
|
|
||||||
//
|
|
||||||
// ui stuff
|
|
||||||
//
|
|
||||||
|
|
||||||
let shouldPop = false
|
|
||||||
window.addEventListener('click', maybeTriggerPopup)
|
|
||||||
|
|
||||||
//
|
|
||||||
// util
|
|
||||||
//
|
|
||||||
|
|
||||||
function maybeTriggerPopup(){
|
|
||||||
if (!shouldPop) return
|
|
||||||
shouldPop = false
|
|
||||||
window.open(MASCARA_ORIGIN, '', 'width=360 height=500')
|
|
||||||
console.log('opening window...')
|
|
||||||
}
|
|
||||||
|
|
||||||
function instrumentForUserInteractionTriggers(provider){
|
|
||||||
const _super = provider.sendAsync.bind(provider)
|
|
||||||
provider.sendAsync = function(payload, cb){
|
|
||||||
if (payload.method === 'eth_sendTransaction') {
|
|
||||||
console.log('saw send')
|
|
||||||
shouldPop = true
|
|
||||||
}
|
|
||||||
_super(payload, cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,6 +62,7 @@ const controller = new MetamaskController({
|
|||||||
showUnconfirmedMessage: noop,
|
showUnconfirmedMessage: noop,
|
||||||
unlockAccountMessage: noop,
|
unlockAccountMessage: noop,
|
||||||
showUnapprovedTx: noop,
|
showUnapprovedTx: noop,
|
||||||
|
platform: {},
|
||||||
// initial state
|
// initial state
|
||||||
initState: firstTimeState,
|
initState: firstTimeState,
|
||||||
})
|
})
|
||||||
|
15
package.json
15
package.json
@ -9,7 +9,7 @@
|
|||||||
"ui": "npm run test:flat:build:states && beefy ui-dev.js:bundle.js --live --open --index=./development/index.html --cwd ./",
|
"ui": "npm run test:flat:build:states && 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 ./",
|
||||||
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
|
"watch": "mocha watch --recursive \"test/unit/**/*.js\"",
|
||||||
"mascara": "node ./mascara/example/server",
|
"mascara": "METAMASK_DEBUG=true node ./mascara/example/server",
|
||||||
"dist": "npm run dist:clear && npm install && gulp dist",
|
"dist": "npm run dist:clear && npm install && gulp dist",
|
||||||
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
|
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
|
||||||
"test": "npm run lint && npm run test:coverage && npm run test:integration",
|
"test": "npm run lint && npm run test:coverage && npm run test:integration",
|
||||||
@ -53,10 +53,8 @@
|
|||||||
"async": "^2.5.0",
|
"async": "^2.5.0",
|
||||||
"await-semaphore": "^0.1.1",
|
"await-semaphore": "^0.1.1",
|
||||||
"babel-runtime": "^6.23.0",
|
"babel-runtime": "^6.23.0",
|
||||||
"bip39": "^2.2.0",
|
|
||||||
"bluebird": "^3.5.0",
|
"bluebird": "^3.5.0",
|
||||||
"bn.js": "^4.11.7",
|
"bn.js": "^4.11.7",
|
||||||
"browser-passworder": "^2.0.3",
|
|
||||||
"browserify-derequire": "^0.9.4",
|
"browserify-derequire": "^0.9.4",
|
||||||
"client-sw-ready-event": "^3.3.0",
|
"client-sw-ready-event": "^3.3.0",
|
||||||
"clone": "^2.1.1",
|
"clone": "^2.1.1",
|
||||||
@ -69,9 +67,11 @@
|
|||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
"ensnare": "^1.0.0",
|
"ensnare": "^1.0.0",
|
||||||
"eth-bin-to-ops": "^1.0.1",
|
"eth-bin-to-ops": "^1.0.1",
|
||||||
|
"eth-block-tracker": "^2.2.0",
|
||||||
"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-json-rpc-filters": "^1.2.1",
|
||||||
|
"eth-keyring-controller": "^2.0.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",
|
||||||
@ -80,9 +80,10 @@
|
|||||||
"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",
|
||||||
|
"ethjs-contract": "^0.1.9",
|
||||||
"ethjs-ens": "^2.0.0",
|
"ethjs-ens": "^2.0.0",
|
||||||
"ethjs-query": "^0.2.9",
|
"ethjs-query": "^0.2.9",
|
||||||
"express": "^4.14.0",
|
"express": "^4.15.5",
|
||||||
"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-json-patch": "^2.0.4",
|
||||||
@ -90,6 +91,7 @@
|
|||||||
"gulp": "github:gulpjs/gulp#4.0",
|
"gulp": "github:gulpjs/gulp#4.0",
|
||||||
"gulp-eslint": "^4.0.0",
|
"gulp-eslint": "^4.0.0",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
|
"human-standard-token-abi": "^1.0.2",
|
||||||
"idb-global": "^2.1.0",
|
"idb-global": "^2.1.0",
|
||||||
"identicon.js": "^2.3.1",
|
"identicon.js": "^2.3.1",
|
||||||
"iframe": "^1.0.0",
|
"iframe": "^1.0.0",
|
||||||
@ -99,6 +101,7 @@
|
|||||||
"json-rpc-engine": "^3.2.0",
|
"json-rpc-engine": "^3.2.0",
|
||||||
"json-rpc-middleware-stream": "^1.0.1",
|
"json-rpc-middleware-stream": "^1.0.1",
|
||||||
"loglevel": "^1.4.1",
|
"loglevel": "^1.4.1",
|
||||||
|
"metamascara": "^1.3.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",
|
||||||
@ -138,7 +141,7 @@
|
|||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"vreme": "^3.0.2",
|
"vreme": "^3.0.2",
|
||||||
"web3": "^0.20.1",
|
"web3": "^0.20.1",
|
||||||
"web3-provider-engine": "^13.2.9",
|
"web3-provider-engine": "^13.3.1",
|
||||||
"web3-stream-provider": "^3.0.1",
|
"web3-stream-provider": "^3.0.1",
|
||||||
"xtend": "^4.0.1"
|
"xtend": "^4.0.1"
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,8 @@ describe('PendingTx', function () {
|
|||||||
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
|
'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb',
|
||||||
'value': '0xde0b6b3a7640000',
|
'value': '0xde0b6b3a7640000',
|
||||||
gasPrice,
|
gasPrice,
|
||||||
'gas': '0x7b0c'},
|
'gas': '0x7b0c',
|
||||||
|
},
|
||||||
'gasLimitSpecified': false,
|
'gasLimitSpecified': false,
|
||||||
'estimatedGas': '0x5208',
|
'estimatedGas': '0x5208',
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,11 @@ describe('currency-controller', function () {
|
|||||||
describe('currency conversions', function () {
|
describe('currency conversions', function () {
|
||||||
describe('#setCurrentCurrency', function () {
|
describe('#setCurrentCurrency', function () {
|
||||||
it('should return USD as default', function () {
|
it('should return USD as default', function () {
|
||||||
assert.equal(currencyController.getCurrentCurrency(), 'USD')
|
assert.equal(currencyController.getCurrentCurrency(), 'usd')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be able to set to other currency', function () {
|
it('should be able to set to other currency', function () {
|
||||||
assert.equal(currencyController.getCurrentCurrency(), 'USD')
|
assert.equal(currencyController.getCurrentCurrency(), 'usd')
|
||||||
currencyController.setCurrentCurrency('JPY')
|
currencyController.setCurrentCurrency('JPY')
|
||||||
var result = currencyController.getCurrentCurrency()
|
var result = currencyController.getCurrentCurrency()
|
||||||
assert.equal(result, 'JPY')
|
assert.equal(result, 'JPY')
|
||||||
@ -36,12 +36,12 @@ describe('currency-controller', function () {
|
|||||||
describe('#updateConversionRate', function () {
|
describe('#updateConversionRate', function () {
|
||||||
it('should retrieve an update for ETH to USD and set it in memory', function (done) {
|
it('should retrieve an update for ETH to USD and set it in memory', function (done) {
|
||||||
this.timeout(15000)
|
this.timeout(15000)
|
||||||
nock('https://api.cryptonator.com')
|
nock('https://api.infura.io')
|
||||||
.get('/api/ticker/eth-USD')
|
.get('/v1/ticker/ethusd')
|
||||||
.reply(200, '{"ticker":{"base":"ETH","target":"USD","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}')
|
.reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}')
|
||||||
|
|
||||||
assert.equal(currencyController.getConversionRate(), 0)
|
assert.equal(currencyController.getConversionRate(), 0)
|
||||||
currencyController.setCurrentCurrency('USD')
|
currencyController.setCurrentCurrency('usd')
|
||||||
currencyController.updateConversionRate()
|
currencyController.updateConversionRate()
|
||||||
.then(function () {
|
.then(function () {
|
||||||
var result = currencyController.getConversionRate()
|
var result = currencyController.getConversionRate()
|
||||||
@ -57,14 +57,14 @@ describe('currency-controller', function () {
|
|||||||
this.timeout(15000)
|
this.timeout(15000)
|
||||||
assert.equal(currencyController.getConversionRate(), 0)
|
assert.equal(currencyController.getConversionRate(), 0)
|
||||||
|
|
||||||
nock('https://api.cryptonator.com')
|
nock('https://api.infura.io')
|
||||||
.get('/api/ticker/eth-JPY')
|
.get('/v1/ticker/ethjpy')
|
||||||
.reply(200, '{"ticker":{"base":"ETH","target":"JPY","price":"11.02456145","volume":"44948.91745289","change":"-0.01472534"},"timestamp":1472072136,"success":true,"error":""}')
|
.reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}')
|
||||||
|
|
||||||
|
|
||||||
var promise = new Promise(
|
var promise = new Promise(
|
||||||
function (resolve, reject) {
|
function (resolve, reject) {
|
||||||
currencyController.setCurrentCurrency('JPY')
|
currencyController.setCurrentCurrency('jpy')
|
||||||
currencyController.updateConversionRate().then(function () {
|
currencyController.updateConversionRate().then(function () {
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
|
@ -1,164 +0,0 @@
|
|||||||
const assert = require('assert')
|
|
||||||
const KeyringController = require('../../app/scripts/keyring-controller')
|
|
||||||
const configManagerGen = require('../lib/mock-config-manager')
|
|
||||||
const ethUtil = require('ethereumjs-util')
|
|
||||||
const BN = ethUtil.BN
|
|
||||||
const mockEncryptor = require('../lib/mock-encryptor')
|
|
||||||
const sinon = require('sinon')
|
|
||||||
|
|
||||||
describe('KeyringController', function () {
|
|
||||||
let keyringController
|
|
||||||
const password = 'password123'
|
|
||||||
const seedWords = 'puzzle seed penalty soldier say clay field arctic metal hen cage runway'
|
|
||||||
const addresses = ['eF35cA8EbB9669A35c31b5F6f249A9941a812AC1'.toLowerCase()]
|
|
||||||
const accounts = []
|
|
||||||
// let originalKeystore
|
|
||||||
|
|
||||||
beforeEach(function (done) {
|
|
||||||
this.sinon = sinon.sandbox.create()
|
|
||||||
window.localStorage = {} // Hacking localStorage support into JSDom
|
|
||||||
|
|
||||||
keyringController = new KeyringController({
|
|
||||||
configManager: configManagerGen(),
|
|
||||||
txManager: {
|
|
||||||
getTxList: () => [],
|
|
||||||
getUnapprovedTxList: () => [],
|
|
||||||
},
|
|
||||||
accountTracker: {
|
|
||||||
addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
|
|
||||||
},
|
|
||||||
encryptor: mockEncryptor,
|
|
||||||
})
|
|
||||||
|
|
||||||
keyringController.createNewVaultAndKeychain(password)
|
|
||||||
.then(function (newState) {
|
|
||||||
newState
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
done(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
// Cleanup mocks
|
|
||||||
this.sinon.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#createNewVaultAndKeychain', function () {
|
|
||||||
this.timeout(10000)
|
|
||||||
|
|
||||||
it('should set a vault on the configManager', function (done) {
|
|
||||||
keyringController.store.updateState({ vault: null })
|
|
||||||
assert(!keyringController.store.getState().vault, 'no previous vault')
|
|
||||||
keyringController.createNewVaultAndKeychain(password)
|
|
||||||
.then(() => {
|
|
||||||
const vault = keyringController.store.getState().vault
|
|
||||||
assert(vault, 'vault created')
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
done(reason)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#restoreKeyring', function () {
|
|
||||||
it(`should pass a keyring's serialized data back to the correct type.`, function (done) {
|
|
||||||
const mockSerialized = {
|
|
||||||
type: 'HD Key Tree',
|
|
||||||
data: {
|
|
||||||
mnemonic: seedWords,
|
|
||||||
numberOfAccounts: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const mock = this.sinon.mock(keyringController)
|
|
||||||
|
|
||||||
mock.expects('getBalanceAndNickname')
|
|
||||||
.exactly(1)
|
|
||||||
|
|
||||||
keyringController.restoreKeyring(mockSerialized)
|
|
||||||
.then((keyring) => {
|
|
||||||
assert.equal(keyring.wallets.length, 1, 'one wallet restored')
|
|
||||||
return keyring.getAccounts()
|
|
||||||
})
|
|
||||||
.then((accounts) => {
|
|
||||||
assert.equal(accounts[0], addresses[0])
|
|
||||||
mock.verify()
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
done(reason)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#createNickname', function () {
|
|
||||||
it('should add the address to the identities hash', function () {
|
|
||||||
const fakeAddress = '0x12345678'
|
|
||||||
keyringController.createNickname(fakeAddress)
|
|
||||||
const identities = keyringController.memStore.getState().identities
|
|
||||||
const identity = identities[fakeAddress]
|
|
||||||
assert.equal(identity.address, fakeAddress)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#saveAccountLabel', function () {
|
|
||||||
it('sets the nickname', function (done) {
|
|
||||||
const account = addresses[0]
|
|
||||||
var nick = 'Test nickname'
|
|
||||||
const identities = keyringController.memStore.getState().identities
|
|
||||||
identities[ethUtil.addHexPrefix(account)] = {}
|
|
||||||
keyringController.memStore.updateState({ identities })
|
|
||||||
keyringController.saveAccountLabel(account, nick)
|
|
||||||
.then((label) => {
|
|
||||||
try {
|
|
||||||
assert.equal(label, nick)
|
|
||||||
const persisted = keyringController.store.getState().walletNicknames[account]
|
|
||||||
assert.equal(persisted, nick)
|
|
||||||
done()
|
|
||||||
} catch (err) {
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
done(reason)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getAccounts', function () {
|
|
||||||
it('returns the result of getAccounts for each keyring', function (done) {
|
|
||||||
keyringController.keyrings = [
|
|
||||||
{ getAccounts () { return Promise.resolve([1, 2, 3]) } },
|
|
||||||
{ getAccounts () { return Promise.resolve([4, 5, 6]) } },
|
|
||||||
]
|
|
||||||
|
|
||||||
keyringController.getAccounts()
|
|
||||||
.then((result) => {
|
|
||||||
assert.deepEqual(result, [1, 2, 3, 4, 5, 6])
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#addGasBuffer', function () {
|
|
||||||
it('adds 100k gas buffer to estimates', function () {
|
|
||||||
const gas = '0x04ee59' // Actual estimated gas example
|
|
||||||
const tooBigOutput = '0x80674f9' // Actual bad output
|
|
||||||
const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
|
|
||||||
const correctBuffer = new BN('100000', 10)
|
|
||||||
const correct = bnGas.add(correctBuffer)
|
|
||||||
|
|
||||||
// const tooBig = new BN(tooBigOutput, 16)
|
|
||||||
const result = keyringController.addGasBuffer(gas)
|
|
||||||
const bnResult = new BN(ethUtil.stripHexPrefix(result), 16)
|
|
||||||
|
|
||||||
assert.equal(result.indexOf('0x'), 0, 'included hex prefix')
|
|
||||||
assert(bnResult.gt(bnGas), 'Estimate increased in value.')
|
|
||||||
assert.equal(bnResult.sub(bnGas).toString(10), '100000', 'added 100k gas')
|
|
||||||
assert.equal(result, '0x' + correct.toString(16), 'Added the right amount')
|
|
||||||
assert.notEqual(result, tooBigOutput, 'not that bad estimate')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
@ -10,6 +10,7 @@ describe('MetaMaskController', function () {
|
|||||||
showUnconfirmedMessage: noop,
|
showUnconfirmedMessage: noop,
|
||||||
unlockAccountMessage: noop,
|
unlockAccountMessage: noop,
|
||||||
showUnapprovedTx: noop,
|
showUnapprovedTx: noop,
|
||||||
|
platform: {},
|
||||||
// initial state
|
// initial state
|
||||||
initState: clone(firstTimeState),
|
initState: clone(firstTimeState),
|
||||||
})
|
})
|
||||||
|
@ -20,9 +20,9 @@ describe('# Network Controller', function () {
|
|||||||
describe('#provider', function () {
|
describe('#provider', function () {
|
||||||
it('provider should be updatable without reassignment', function () {
|
it('provider should be updatable without reassignment', function () {
|
||||||
networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
|
networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
|
||||||
const provider = networkController.provider
|
const proxy = networkController._proxy
|
||||||
networkController._provider = {test: true}
|
proxy.setTarget({ test: true, on: () => {} })
|
||||||
assert.ok(provider.test)
|
assert.ok(proxy.test)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('#getNetworkState', function () {
|
describe('#getNetworkState', function () {
|
||||||
@ -71,6 +71,7 @@ function dummyProviderConstructor() {
|
|||||||
// provider
|
// provider
|
||||||
sendAsync: noop,
|
sendAsync: noop,
|
||||||
// block tracker
|
// block tracker
|
||||||
|
_blockTracker: {},
|
||||||
start: noop,
|
start: noop,
|
||||||
stop: noop,
|
stop: noop,
|
||||||
on: noop,
|
on: noop,
|
||||||
|
@ -40,14 +40,12 @@ describe('PendingTransactionTracker', function () {
|
|||||||
|
|
||||||
pendingTxTracker = new PendingTransactionTracker({
|
pendingTxTracker = new PendingTransactionTracker({
|
||||||
provider,
|
provider,
|
||||||
getBalance: () => {},
|
|
||||||
nonceTracker: {
|
nonceTracker: {
|
||||||
getGlobalLock: async () => {
|
getGlobalLock: async () => {
|
||||||
return { releaseLock: () => {} }
|
return { releaseLock: () => {} }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getPendingTransactions: () => {return []},
|
getPendingTransactions: () => {return []},
|
||||||
sufficientBalance: () => {},
|
|
||||||
publishTransaction: () => {},
|
publishTransaction: () => {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -62,7 +60,7 @@ describe('PendingTransactionTracker', function () {
|
|||||||
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
||||||
const block = Proxy.revocable({}, {}).revoke()
|
const block = Proxy.revocable({}, {}).revoke()
|
||||||
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash]
|
pendingTxTracker.getPendingTransactions = () => [txMetaNoHash]
|
||||||
pendingTxTracker.once('txFailed', (txId, err) => {
|
pendingTxTracker.once('tx:failed', (txId, err) => {
|
||||||
assert(txId, txMetaNoHash.id, 'should pass txId')
|
assert(txId, txMetaNoHash.id, 'should pass txId')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
@ -71,11 +69,11 @@ describe('PendingTransactionTracker', function () {
|
|||||||
it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
|
it('should emit \'txConfirmed\' if the tx is in the block', function (done) {
|
||||||
const block = { transactions: [txMeta]}
|
const block = { transactions: [txMeta]}
|
||||||
pendingTxTracker.getPendingTransactions = () => [txMeta]
|
pendingTxTracker.getPendingTransactions = () => [txMeta]
|
||||||
pendingTxTracker.once('txConfirmed', (txId) => {
|
pendingTxTracker.once('tx:confirmed', (txId) => {
|
||||||
assert(txId, txMeta.id, 'should pass txId')
|
assert(txId, txMeta.id, 'should pass txId')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
|
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
|
||||||
pendingTxTracker.checkForTxInBlock(block)
|
pendingTxTracker.checkForTxInBlock(block)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -84,14 +82,14 @@ describe('PendingTransactionTracker', function () {
|
|||||||
let newBlock, oldBlock
|
let newBlock, oldBlock
|
||||||
newBlock = { number: '0x01' }
|
newBlock = { number: '0x01' }
|
||||||
pendingTxTracker._checkPendingTxs = done
|
pendingTxTracker._checkPendingTxs = done
|
||||||
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
|
||||||
})
|
})
|
||||||
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) {
|
it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) {
|
||||||
let newBlock, oldBlock
|
let newBlock, oldBlock
|
||||||
oldBlock = { number: '0x01' }
|
oldBlock = { number: '0x01' }
|
||||||
newBlock = { number: '0x03' }
|
newBlock = { number: '0x03' }
|
||||||
pendingTxTracker._checkPendingTxs = done
|
pendingTxTracker._checkPendingTxs = done
|
||||||
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
|
||||||
})
|
})
|
||||||
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) {
|
it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) {
|
||||||
let newBlock, oldBlock
|
let newBlock, oldBlock
|
||||||
@ -101,14 +99,14 @@ describe('PendingTransactionTracker', function () {
|
|||||||
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less')
|
const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less')
|
||||||
done(err)
|
done(err)
|
||||||
}
|
}
|
||||||
pendingTxTracker.queryPendingTxs({oldBlock, newBlock})
|
pendingTxTracker.queryPendingTxs({ oldBlock, newBlock })
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#_checkPendingTx', function () {
|
describe('#_checkPendingTx', function () {
|
||||||
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) {
|
||||||
pendingTxTracker.once('txFailed', (txId, err) => {
|
pendingTxTracker.once('tx:failed', (txId, err) => {
|
||||||
assert(txId, txMetaNoHash.id, 'should pass txId')
|
assert(txId, txMetaNoHash.id, 'should pass txId')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
@ -122,11 +120,11 @@ describe('PendingTransactionTracker', function () {
|
|||||||
|
|
||||||
it('should emit \'txConfirmed\'', function (done) {
|
it('should emit \'txConfirmed\'', function (done) {
|
||||||
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
|
providerResultStub.eth_getTransactionByHash = {blockNumber: '0x01'}
|
||||||
pendingTxTracker.once('txConfirmed', (txId) => {
|
pendingTxTracker.once('tx:confirmed', (txId) => {
|
||||||
assert(txId, txMeta.id, 'should pass txId')
|
assert(txId, txMeta.id, 'should pass txId')
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
pendingTxTracker.once('txFailed', (_, err) => { done(err) })
|
pendingTxTracker.once('tx:failed', (_, err) => { done(err) })
|
||||||
pendingTxTracker._checkPendingTx(txMeta)
|
pendingTxTracker._checkPendingTx(txMeta)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -188,7 +186,7 @@ describe('PendingTransactionTracker', function () {
|
|||||||
]
|
]
|
||||||
const enoughForAllErrors = txList.concat(txList)
|
const enoughForAllErrors = txList.concat(txList)
|
||||||
|
|
||||||
pendingTxTracker.on('txFailed', (_, err) => done(err))
|
pendingTxTracker.on('tx:failed', (_, err) => done(err))
|
||||||
|
|
||||||
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors
|
pendingTxTracker.getPendingTransactions = () => enoughForAllErrors
|
||||||
pendingTxTracker._resubmitTx = async (tx) => {
|
pendingTxTracker._resubmitTx = async (tx) => {
|
||||||
@ -202,7 +200,7 @@ describe('PendingTransactionTracker', function () {
|
|||||||
pendingTxTracker.resubmitPendingTxs()
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
})
|
})
|
||||||
it('should emit \'txFailed\' if it encountered a real error', function (done) {
|
it('should emit \'txFailed\' if it encountered a real error', function (done) {
|
||||||
pendingTxTracker.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err))
|
pendingTxTracker.once('tx:failed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err))
|
||||||
|
|
||||||
pendingTxTracker.getPendingTransactions = () => txList
|
pendingTxTracker.getPendingTransactions = () => txList
|
||||||
pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') }
|
pendingTxTracker._resubmitTx = async (tx) => { throw new TypeError('im some real error') }
|
||||||
@ -213,30 +211,7 @@ describe('PendingTransactionTracker', function () {
|
|||||||
pendingTxTracker.resubmitPendingTxs()
|
pendingTxTracker.resubmitPendingTxs()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('#_resubmitTx with a too-low balance', function () {
|
describe('#_resubmitTx', function () {
|
||||||
it('should return before publishing the transaction because to low of balance', function (done) {
|
|
||||||
const lowBalance = '0x0'
|
|
||||||
pendingTxTracker.getBalance = (address) => {
|
|
||||||
assert.equal(address, txMeta.txParams.from, 'Should pass the address')
|
|
||||||
return lowBalance
|
|
||||||
}
|
|
||||||
pendingTxTracker.publishTransaction = async (rawTx) => {
|
|
||||||
done(new Error('tried to publish transaction'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stubbing out current account state:
|
|
||||||
// Adding the fake tx:
|
|
||||||
pendingTxTracker.once('txFailed', (txId, err) => {
|
|
||||||
assert(err, 'Should have a error')
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
pendingTxTracker._resubmitTx(txMeta)
|
|
||||||
.catch((err) => {
|
|
||||||
assert.ifError(err, 'should not throw an error')
|
|
||||||
done(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should publishing the transaction', function (done) {
|
it('should publishing the transaction', function (done) {
|
||||||
const enoughBalance = '0x100000'
|
const enoughBalance = '0x100000'
|
||||||
pendingTxTracker.getBalance = (address) => {
|
pendingTxTracker.getBalance = (address) => {
|
||||||
|
@ -2,21 +2,19 @@ const assert = require('assert')
|
|||||||
const ethUtil = require('ethereumjs-util')
|
const ethUtil = require('ethereumjs-util')
|
||||||
const EthTx = require('ethereumjs-tx')
|
const EthTx = require('ethereumjs-tx')
|
||||||
const ObservableStore = require('obs-store')
|
const ObservableStore = require('obs-store')
|
||||||
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 TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
|
||||||
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
const { createStubedProvider } = require('../stub/provider')
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
describe('Transaction Controller', function () {
|
describe('Transaction Controller', function () {
|
||||||
let txController, engine, provider, providerResultStub
|
let txController, provider, providerResultStub
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
providerResultStub = {}
|
providerResultStub = {}
|
||||||
@ -27,34 +25,96 @@ describe('Transaction Controller', function () {
|
|||||||
networkStore: new ObservableStore(currentNetworkId),
|
networkStore: new ObservableStore(currentNetworkId),
|
||||||
txHistoryLimit: 10,
|
txHistoryLimit: 10,
|
||||||
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
||||||
accountTracker: { getState: noop },
|
|
||||||
signTransaction: (ethTx) => new Promise((resolve) => {
|
signTransaction: (ethTx) => new Promise((resolve) => {
|
||||||
ethTx.sign(privKey)
|
ethTx.sign(privKey)
|
||||||
resolve()
|
resolve()
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
|
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
|
||||||
txController.txProviderUtils = new TxProvideUtils(txController.provider)
|
txController.txProviderUtils = new TxGasUtils(txController.provider)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#getState', function () {
|
||||||
|
it('should return a state object with the right keys and datat types', function () {
|
||||||
|
const exposedState = txController.getState()
|
||||||
|
assert('unapprovedTxs' in exposedState, 'state should have the key unapprovedTxs')
|
||||||
|
assert('selectedAddressTxList' in exposedState, 'state should have the key selectedAddressTxList')
|
||||||
|
assert(typeof exposedState.unapprovedTxs === 'object', 'should be an object')
|
||||||
|
assert(Array.isArray(exposedState.selectedAddressTxList), 'should be an array')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getUnapprovedTxCount', function () {
|
||||||
|
it('should return the number of unapproved txs', function () {
|
||||||
|
txController.txStateManager._saveTxList([
|
||||||
|
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 2, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
])
|
||||||
|
const unapprovedTxCount = txController.getUnapprovedTxCount()
|
||||||
|
assert.equal(unapprovedTxCount, 3, 'should be 3')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getPendingTxCount', function () {
|
||||||
|
it('should return the number of pending txs', function () {
|
||||||
|
txController.txStateManager._saveTxList([
|
||||||
|
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
])
|
||||||
|
const pendingTxCount = txController.getPendingTxCount()
|
||||||
|
assert.equal(pendingTxCount, 3, 'should be 3')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getConfirmedTransactions', function () {
|
||||||
|
let address
|
||||||
|
beforeEach(function () {
|
||||||
|
address = '0xc684832530fcbddae4b4230a47e991ddcec2831d'
|
||||||
|
const txParams = {
|
||||||
|
'from': address,
|
||||||
|
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
|
}
|
||||||
|
txController.txStateManager._saveTxList([
|
||||||
|
{id: 0, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 3, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 4, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 5, status: 'approved', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 6, status: 'signed', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
{id: 8, status: 'failed', metamaskNetworkId: currentNetworkId, txParams},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the number of confirmed txs', function () {
|
||||||
|
assert.equal(txController.nonceTracker.getConfirmedTransactions(address).length, 3)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
describe('#newUnapprovedTransaction', function () {
|
describe('#newUnapprovedTransaction', function () {
|
||||||
let stub, txMeta, txParams
|
let stub, txMeta, txParams
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
txParams = {
|
txParams = {
|
||||||
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
},
|
}
|
||||||
txMeta = {
|
txMeta = {
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
id: 1,
|
id: 1,
|
||||||
metamaskNetworkId: currentNetworkId,
|
metamaskNetworkId: currentNetworkId,
|
||||||
txParams,
|
txParams,
|
||||||
|
history: [],
|
||||||
}
|
}
|
||||||
txController.addTx(txMeta)
|
txController.txStateManager._saveTxList([txMeta])
|
||||||
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txMeta))
|
stub = sinon.stub(txController, 'addUnapprovedTransaction').returns(Promise.resolve(txController.txStateManager.addTx(txMeta)))
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(function () {
|
afterEach(function () {
|
||||||
|
txController.txStateManager._saveTxList([])
|
||||||
stub.restore()
|
stub.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -72,7 +132,7 @@ describe('Transaction Controller', function () {
|
|||||||
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
txController.setTxHash(txMetaFromEmit.id, '0x0')
|
txController.setTxHash(txMetaFromEmit.id, '0x0')
|
||||||
txController.setTxStatusSubmitted(txMetaFromEmit.id)
|
txController.txStateManager.setTxStatusSubmitted(txMetaFromEmit.id)
|
||||||
}, 10)
|
}, 10)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -87,7 +147,7 @@ describe('Transaction Controller', function () {
|
|||||||
it('should reject when finished and status is rejected', function (done) {
|
it('should reject when finished and status is rejected', function (done) {
|
||||||
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
txController.once('newUnaprovedTx', (txMetaFromEmit) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
txController.setTxStatusRejected(txMetaFromEmit.id)
|
txController.txStateManager.setTxStatusRejected(txMetaFromEmit.id)
|
||||||
}, 10)
|
}, 10)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -110,7 +170,7 @@ describe('Transaction Controller', function () {
|
|||||||
assert(('txParams' in txMeta), 'should have a txParams')
|
assert(('txParams' in txMeta), 'should have a txParams')
|
||||||
assert(('history' in txMeta), 'should have a history')
|
assert(('history' in txMeta), 'should have a history')
|
||||||
|
|
||||||
const memTxMeta = txController.getTx(txMeta.id)
|
const memTxMeta = txController.txStateManager.getTx(txMeta.id)
|
||||||
assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
|
assert.deepEqual(txMeta, memTxMeta, `txMeta should be stored in txController after adding it\n expected: ${txMeta} \n got: ${memTxMeta}`)
|
||||||
addTxDefaultsStub.restore()
|
addTxDefaultsStub.restore()
|
||||||
done()
|
done()
|
||||||
@ -120,10 +180,10 @@ describe('Transaction Controller', function () {
|
|||||||
|
|
||||||
describe('#addTxDefaults', function () {
|
describe('#addTxDefaults', function () {
|
||||||
it('should add the tx defaults if their are none', function (done) {
|
it('should add the tx defaults if their are none', function (done) {
|
||||||
let txMeta = {
|
const txMeta = {
|
||||||
'txParams': {
|
'txParams': {
|
||||||
'from':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
'from': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
'to':'0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
'to': '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
providerResultStub.eth_gasPrice = '4a817c800'
|
providerResultStub.eth_gasPrice = '4a817c800'
|
||||||
@ -131,7 +191,7 @@ describe('Transaction Controller', function () {
|
|||||||
providerResultStub.eth_estimateGas = '5209'
|
providerResultStub.eth_estimateGas = '5209'
|
||||||
txController.addTxDefaults(txMeta)
|
txController.addTxDefaults(txMeta)
|
||||||
.then((txMetaWithDefaults) => {
|
.then((txMetaWithDefaults) => {
|
||||||
assert(txMetaWithDefaults.txParams.value, '0x0','should have added 0x0 as the value')
|
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
|
||||||
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
|
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
|
||||||
assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field')
|
assert(txMetaWithDefaults.txParams.gas, 'should have added the gas field')
|
||||||
done()
|
done()
|
||||||
@ -163,214 +223,31 @@ describe('Transaction Controller', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('#getTxList', function () {
|
|
||||||
it('when new should return empty array', function () {
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.ok(Array.isArray(result))
|
|
||||||
assert.equal(result.length, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#addTx', function () {
|
describe('#addTx', function () {
|
||||||
it('adds a tx returned in getTxList', function () {
|
it('should emit updates', function (done) {
|
||||||
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.ok(Array.isArray(result))
|
|
||||||
assert.equal(result.length, 1)
|
|
||||||
assert.equal(result[0].id, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not override txs from other networks', function () {
|
|
||||||
var tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
var tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
txController.addTx(tx2, noop)
|
|
||||||
var result = txController.getFullTxList()
|
|
||||||
var result2 = txController.getTxList()
|
|
||||||
assert.equal(result.length, 2, 'txs were deleted')
|
|
||||||
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cuts off early txs beyond a limit', function () {
|
|
||||||
const limit = txController.txHistoryLimit
|
|
||||||
for (let i = 0; i < limit + 1; i++) {
|
|
||||||
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
}
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
|
||||||
assert.equal(result[0].id, 1, 'early txs truncted')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
|
|
||||||
const limit = txController.txHistoryLimit
|
|
||||||
for (let i = 0; i < limit + 1; i++) {
|
|
||||||
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
}
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
|
||||||
assert.equal(result[0].id, 1, 'early txs truncted')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
|
|
||||||
var unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(unconfirmedTx, noop)
|
|
||||||
const limit = txController.txHistoryLimit
|
|
||||||
for (let i = 1; i < limit + 1; i++) {
|
|
||||||
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
}
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
|
||||||
assert.equal(result[0].id, 0, 'first tx should still be there')
|
|
||||||
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
|
|
||||||
assert.equal(result[1].id, 2, 'early txs truncted')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#setTxStatusSigned', function () {
|
|
||||||
it('sets the tx status to signed', function () {
|
|
||||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx, noop)
|
|
||||||
txController.setTxStatusSigned(1)
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.ok(Array.isArray(result))
|
|
||||||
assert.equal(result.length, 1)
|
|
||||||
assert.equal(result[0].status, 'signed')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should emit a signed event to signal the exciton of callback', (done) => {
|
|
||||||
this.timeout(10000)
|
|
||||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
const noop = function () {
|
|
||||||
assert(true, 'event listener has been triggered and noop executed')
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
txController.addTx(tx)
|
|
||||||
txController.on('1:signed', noop)
|
|
||||||
txController.setTxStatusSigned(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#setTxStatusRejected', function () {
|
|
||||||
it('sets the tx status to rejected', function () {
|
|
||||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx)
|
|
||||||
txController.setTxStatusRejected(1)
|
|
||||||
var result = txController.getTxList()
|
|
||||||
assert.ok(Array.isArray(result))
|
|
||||||
assert.equal(result.length, 1)
|
|
||||||
assert.equal(result[0].status, 'rejected')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should emit a rejected event to signal the exciton of callback', (done) => {
|
|
||||||
this.timeout(10000)
|
|
||||||
var tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
|
||||||
txController.addTx(tx)
|
|
||||||
const noop = function (err, txId) {
|
|
||||||
assert(true, 'event listener has been triggered and noop executed')
|
|
||||||
done()
|
|
||||||
}
|
|
||||||
txController.on('1:rejected', noop)
|
|
||||||
txController.setTxStatusRejected(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#updateTx', function () {
|
|
||||||
it('replaces the tx with the same id', function () {
|
|
||||||
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
const tx1 = txController.getTx('1')
|
|
||||||
tx1.status = 'blah'
|
|
||||||
tx1.hash = 'foo'
|
|
||||||
txController.updateTx(tx1)
|
|
||||||
const savedResult = txController.getTx('1')
|
|
||||||
assert.equal(savedResult.hash, 'foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updates gas price and adds history items', function () {
|
|
||||||
const originalGasPrice = '0x01'
|
|
||||||
const desiredGasPrice = '0x02'
|
|
||||||
|
|
||||||
const txMeta = {
|
const txMeta = {
|
||||||
id: '1',
|
id: '1',
|
||||||
status: 'unapproved',
|
status: 'unapproved',
|
||||||
metamaskNetworkId: currentNetworkId,
|
metamaskNetworkId: currentNetworkId,
|
||||||
txParams: {
|
txParams: {},
|
||||||
gasPrice: originalGasPrice,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventNames = ['update:badge', '1:unapproved']
|
||||||
|
const listeners = []
|
||||||
|
eventNames.forEach((eventName) => {
|
||||||
|
listeners.push(new Promise((resolve) => {
|
||||||
|
txController.once(eventName, (arg) => {
|
||||||
|
resolve(arg)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
Promise.all(listeners)
|
||||||
|
.then((returnValues) => {
|
||||||
|
assert.deepEqual(returnValues.pop(), txMeta, 'last event 1:unapproved should return txMeta')
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
.catch(done)
|
||||||
txController.addTx(txMeta)
|
txController.addTx(txMeta)
|
||||||
const updatedTx = txController.getTx('1')
|
|
||||||
// verify tx was initialized correctly
|
|
||||||
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')
|
|
||||||
// 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)')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getUnapprovedTxList', function () {
|
|
||||||
it('returns unapproved txs in a hash', function () {
|
|
||||||
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
const result = txController.getUnapprovedTxList()
|
|
||||||
assert.equal(typeof result, 'object')
|
|
||||||
assert.equal(result['1'].status, 'unapproved')
|
|
||||||
assert.equal(result['2'], undefined)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getTx', function () {
|
|
||||||
it('returns a tx with the requested id', function () {
|
|
||||||
txController.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
txController.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
|
||||||
assert.equal(txController.getTx('1').status, 'unapproved')
|
|
||||||
assert.equal(txController.getTx('2').status, 'confirmed')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#getFilteredTxList', function () {
|
|
||||||
it('returns a tx with the requested data', function () {
|
|
||||||
const txMetas = [
|
|
||||||
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
|
||||||
]
|
|
||||||
txMetas.forEach((txMeta) => txController.addTx(txMeta, noop))
|
|
||||||
let filterParams
|
|
||||||
|
|
||||||
filterParams = { status: 'unapproved', from: '0xaa' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
filterParams = { status: 'unapproved', to: '0xaa' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
filterParams = { status: 'confirmed', from: '0xbb' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
filterParams = { status: 'confirmed' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
filterParams = { from: '0xaa' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
filterParams = { to: '0xaa' }
|
|
||||||
assert.equal(txController.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -404,11 +281,11 @@ describe('Transaction Controller', function () {
|
|||||||
|
|
||||||
const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
|
const pubStub = sinon.stub(txController, 'publishTransaction').callsFake(() => {
|
||||||
txController.setTxHash('1', originalValue)
|
txController.setTxHash('1', originalValue)
|
||||||
txController.setTxStatusSubmitted('1')
|
txController.txStateManager.setTxStatusSubmitted('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
txController.approveTransaction(txMeta.id).then(() => {
|
txController.approveTransaction(txMeta.id).then(() => {
|
||||||
const result = txController.getTx(txMeta.id)
|
const result = txController.txStateManager.getTx(txMeta.id)
|
||||||
const params = result.txParams
|
const params = result.txParams
|
||||||
|
|
||||||
assert.equal(params.gas, originalValue, 'gas unmodified')
|
assert.equal(params.gas, originalValue, 'gas unmodified')
|
||||||
@ -431,4 +308,96 @@ describe('Transaction Controller', function () {
|
|||||||
}).catch(done)
|
}).catch(done)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('#updateAndApproveTransaction', function () {
|
||||||
|
let txMeta
|
||||||
|
beforeEach(function () {
|
||||||
|
txMeta = {
|
||||||
|
id: 1,
|
||||||
|
status: 'unapproved',
|
||||||
|
txParams: {
|
||||||
|
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
|
||||||
|
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
|
||||||
|
gasPrice: '0x77359400',
|
||||||
|
gas: '0x7b0d',
|
||||||
|
nonce: '0x4b',
|
||||||
|
},
|
||||||
|
metamaskNetworkId: currentNetworkId,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
it('should update and approve transactions', function () {
|
||||||
|
txController.txStateManager.addTx(txMeta)
|
||||||
|
txController.updateAndApproveTransaction(txMeta)
|
||||||
|
const tx = txController.txStateManager.getTx(1)
|
||||||
|
assert.equal(tx.status, 'approved')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getChainId', function () {
|
||||||
|
it('returns 0 when the chainId is NaN', function () {
|
||||||
|
txController.networkStore = new ObservableStore(NaN)
|
||||||
|
assert.equal(txController.getChainId(), 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#cancelTransaction', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
txController.txStateManager._saveTxList([
|
||||||
|
{ id: 0, status: 'unapproved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 1, status: 'rejected', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 2, status: 'approved', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 3, status: 'signed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 4, status: 'submitted', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 5, status: 'confirmed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
{ id: 6, status: 'failed', txParams: {}, metamaskNetworkId: currentNetworkId, history: [{}] },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should set the transaction to rejected from unapproved', async function () {
|
||||||
|
await txController.cancelTransaction(0)
|
||||||
|
assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#publishTransaction', function () {
|
||||||
|
let hash, txMeta
|
||||||
|
beforeEach(function () {
|
||||||
|
hash = '0x2a5523c6fa98b47b7d9b6c8320179785150b42a16bcff36b398c5062b65657e8'
|
||||||
|
txMeta = {
|
||||||
|
id: 1,
|
||||||
|
status: 'unapproved',
|
||||||
|
txParams: {},
|
||||||
|
metamaskNetworkId: currentNetworkId,
|
||||||
|
}
|
||||||
|
providerResultStub.eth_sendRawTransaction = hash
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should publish a tx, updates the rawTx when provided a one', async function () {
|
||||||
|
txController.txStateManager.addTx(txMeta)
|
||||||
|
await txController.publishTransaction(txMeta.id)
|
||||||
|
const publishedTx = txController.txStateManager.getTx(1)
|
||||||
|
assert.equal(publishedTx.hash, hash)
|
||||||
|
assert.equal(publishedTx.status, 'submitted')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe('#getPendingTransactions', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
txController.txStateManager._saveTxList([
|
||||||
|
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 2, status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 6, status: 'confimed', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
{ id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
it('should show only submitted transactions as pending transasction', function () {
|
||||||
|
assert(txController.pendingTxTracker.getPendingTransactions().length, 1)
|
||||||
|
assert(txController.pendingTxTracker.getPendingTransactions()[0].status, 'submitted')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -20,4 +20,27 @@ describe('tx-state-history-helper', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('replaying history does not mutate the original obj', function () {
|
||||||
|
const initialState = { test: true, message: 'hello', value: 1 }
|
||||||
|
const diff1 = [{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/message",
|
||||||
|
"value": "haay",
|
||||||
|
}]
|
||||||
|
const diff2 = [{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/value",
|
||||||
|
"value": 2,
|
||||||
|
}]
|
||||||
|
const history = [initialState, diff1, diff2]
|
||||||
|
|
||||||
|
const beforeStateSnapshot = JSON.stringify(initialState)
|
||||||
|
const latestState = txStateHistoryHelper.replayHistory(history)
|
||||||
|
const afterStateSnapshot = JSON.stringify(initialState)
|
||||||
|
|
||||||
|
assert.notEqual(initialState, latestState, 'initial state is not the same obj as the latest state')
|
||||||
|
assert.equal(beforeStateSnapshot, afterStateSnapshot, 'initial state is not modified during run')
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
241
test/unit/tx-state-manager-test.js
Normal file
241
test/unit/tx-state-manager-test.js
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
const assert = require('assert')
|
||||||
|
const clone = require('clone')
|
||||||
|
const ObservableStore = require('obs-store')
|
||||||
|
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
|
||||||
|
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
|
||||||
|
const noop = () => true
|
||||||
|
|
||||||
|
describe('TransactionStateManger', function () {
|
||||||
|
let txStateManager
|
||||||
|
const currentNetworkId = 42
|
||||||
|
const otherNetworkId = 2
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
txStateManager = new TxStateManager({
|
||||||
|
initState: {
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
txHistoryLimit: 10,
|
||||||
|
getNetwork: () => currentNetworkId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#setTxStatusSigned', function () {
|
||||||
|
it('sets the tx status to signed', function () {
|
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
txStateManager.setTxStatusSigned(1)
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.ok(Array.isArray(result))
|
||||||
|
assert.equal(result.length, 1)
|
||||||
|
assert.equal(result[0].status, 'signed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit a signed event to signal the exciton of callback', (done) => {
|
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
const noop = function () {
|
||||||
|
assert(true, 'event listener has been triggered and noop executed')
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
txStateManager.addTx(tx)
|
||||||
|
txStateManager.on('1:signed', noop)
|
||||||
|
txStateManager.setTxStatusSigned(1)
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#setTxStatusRejected', function () {
|
||||||
|
it('sets the tx status to rejected', function () {
|
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx)
|
||||||
|
txStateManager.setTxStatusRejected(1)
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.ok(Array.isArray(result))
|
||||||
|
assert.equal(result.length, 1)
|
||||||
|
assert.equal(result[0].status, 'rejected')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should emit a rejected event to signal the exciton of callback', (done) => {
|
||||||
|
let tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx)
|
||||||
|
const noop = function (err, txId) {
|
||||||
|
assert(true, 'event listener has been triggered and noop executed')
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
txStateManager.on('1:rejected', noop)
|
||||||
|
txStateManager.setTxStatusRejected(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getFullTxList', function () {
|
||||||
|
it('when new should return empty array', function () {
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.ok(Array.isArray(result))
|
||||||
|
assert.equal(result.length, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTxList', function () {
|
||||||
|
it('when new should return empty array', function () {
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.ok(Array.isArray(result))
|
||||||
|
assert.equal(result.length, 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#addTx', function () {
|
||||||
|
it('adds a tx returned in getTxList', function () {
|
||||||
|
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.ok(Array.isArray(result))
|
||||||
|
assert.equal(result.length, 1)
|
||||||
|
assert.equal(result[0].id, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not override txs from other networks', function () {
|
||||||
|
let tx = { id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
let tx2 = { id: 2, status: 'confirmed', metamaskNetworkId: otherNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
txStateManager.addTx(tx2, noop)
|
||||||
|
let result = txStateManager.getFullTxList()
|
||||||
|
let result2 = txStateManager.getTxList()
|
||||||
|
assert.equal(result.length, 2, 'txs were deleted')
|
||||||
|
assert.equal(result2.length, 1, 'incorrect number of txs on network.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit', function () {
|
||||||
|
const limit = txStateManager.txHistoryLimit
|
||||||
|
for (let i = 0; i < limit + 1; i++) {
|
||||||
|
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
}
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
||||||
|
assert.equal(result[0].id, 1, 'early txs truncted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () {
|
||||||
|
const limit = txStateManager.txHistoryLimit
|
||||||
|
for (let i = 0; i < limit + 1; i++) {
|
||||||
|
const tx = { id: i, time: new Date(), status: 'rejected', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
}
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
||||||
|
assert.equal(result[0].id, 1, 'early txs truncted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () {
|
||||||
|
let unconfirmedTx = { id: 0, time: new Date(), status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(unconfirmedTx, noop)
|
||||||
|
const limit = txStateManager.txHistoryLimit
|
||||||
|
for (let i = 1; i < limit + 1; i++) {
|
||||||
|
const tx = { id: i, time: new Date(), status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||||
|
txStateManager.addTx(tx, noop)
|
||||||
|
}
|
||||||
|
let result = txStateManager.getTxList()
|
||||||
|
assert.equal(result.length, limit, `limit of ${limit} txs enforced`)
|
||||||
|
assert.equal(result[0].id, 0, 'first tx should still be there')
|
||||||
|
assert.equal(result[0].status, 'unapproved', 'first tx should be unapproved')
|
||||||
|
assert.equal(result[1].id, 2, 'early txs truncted')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#updateTx', function () {
|
||||||
|
it('replaces the tx with the same id', function () {
|
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
const txMeta = txStateManager.getTx('1')
|
||||||
|
txMeta.hash = 'foo'
|
||||||
|
txStateManager.updateTx(txMeta)
|
||||||
|
let result = txStateManager.getTx('1')
|
||||||
|
assert.equal(result.hash, 'foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates gas price and adds history items', function () {
|
||||||
|
const originalGasPrice = '0x01'
|
||||||
|
const desiredGasPrice = '0x02'
|
||||||
|
|
||||||
|
const txMeta = {
|
||||||
|
id: '1',
|
||||||
|
status: 'unapproved',
|
||||||
|
metamaskNetworkId: currentNetworkId,
|
||||||
|
txParams: {
|
||||||
|
gasPrice: originalGasPrice,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMeta = clone(txMeta)
|
||||||
|
|
||||||
|
txStateManager.addTx(txMeta)
|
||||||
|
const updatedTx = txStateManager.getTx('1')
|
||||||
|
// verify tx was initialized correctly
|
||||||
|
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
|
||||||
|
txStateManager.updateTx(updatedTx)
|
||||||
|
// check updated value
|
||||||
|
const result = txStateManager.getTx('1')
|
||||||
|
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)')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getUnapprovedTxList', function () {
|
||||||
|
it('returns unapproved txs in a hash', function () {
|
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
const result = txStateManager.getUnapprovedTxList()
|
||||||
|
assert.equal(typeof result, 'object')
|
||||||
|
assert.equal(result['1'].status, 'unapproved')
|
||||||
|
assert.equal(result['2'], undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTx', function () {
|
||||||
|
it('returns a tx with the requested id', function () {
|
||||||
|
txStateManager.addTx({ id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
txStateManager.addTx({ id: '2', status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} }, noop)
|
||||||
|
assert.equal(txStateManager.getTx('1').status, 'unapproved')
|
||||||
|
assert.equal(txStateManager.getTx('2').status, 'confirmed')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getFilteredTxList', function () {
|
||||||
|
it('returns a tx with the requested data', function () {
|
||||||
|
const txMetas = [
|
||||||
|
{ id: 0, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 1, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 2, status: 'unapproved', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 3, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 4, status: 'unapproved', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 5, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 6, status: 'confirmed', txParams: { from: '0xaa', to: '0xbb' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 7, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 8, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
{ id: 9, status: 'confirmed', txParams: { from: '0xbb', to: '0xaa' }, metamaskNetworkId: currentNetworkId },
|
||||||
|
]
|
||||||
|
txMetas.forEach((txMeta) => txStateManager.addTx(txMeta, noop))
|
||||||
|
let filterParams
|
||||||
|
|
||||||
|
filterParams = { status: 'unapproved', from: '0xaa' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
filterParams = { status: 'unapproved', to: '0xaa' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 2, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
filterParams = { status: 'confirmed', from: '0xbb' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 3, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
filterParams = { status: 'confirmed' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
filterParams = { from: '0xaa' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
filterParams = { to: '0xaa' }
|
||||||
|
assert.equal(txStateManager.getFilteredTxList(filterParams).length, 5, `getFilteredTxList - ${JSON.stringify(filterParams)}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -1,8 +1,10 @@
|
|||||||
const assert = require('assert')
|
const assert = require('assert')
|
||||||
|
const Transaction = require('ethereumjs-tx')
|
||||||
const BN = require('bn.js')
|
const BN = require('bn.js')
|
||||||
|
|
||||||
|
|
||||||
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
|
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
|
||||||
const TxUtils = require('../../app/scripts/lib/tx-utils')
|
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
|
||||||
|
|
||||||
|
|
||||||
describe('txUtils', function () {
|
describe('txUtils', function () {
|
||||||
@ -28,7 +30,7 @@ describe('txUtils', function () {
|
|||||||
nonce: '0x3',
|
nonce: '0x3',
|
||||||
chainId: 42,
|
chainId: 42,
|
||||||
}
|
}
|
||||||
const ethTx = txUtils.buildEthTxFromParams(txParams)
|
const ethTx = new Transaction(txParams)
|
||||||
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
|
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
45
ui-dev.js
45
ui-dev.js
@ -61,30 +61,37 @@ const actions = {
|
|||||||
var css = MetaMaskUiCss()
|
var css = MetaMaskUiCss()
|
||||||
injectCss(css)
|
injectCss(css)
|
||||||
|
|
||||||
const container = document.querySelector('#test-container')
|
|
||||||
|
|
||||||
// parse opts
|
// parse opts
|
||||||
var store = configureStore(states[selectedView])
|
var store = configureStore(states[selectedView])
|
||||||
|
|
||||||
// start app
|
// start app
|
||||||
render(
|
startApp()
|
||||||
h('.super-dev-container', [
|
|
||||||
|
|
||||||
h(Selector, { actions, selectedKey: selectedView, states, store }),
|
function startApp(){
|
||||||
|
const body = document.body
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.id = 'test-container'
|
||||||
|
body.appendChild(container)
|
||||||
|
|
||||||
h('#app-content', {
|
render(
|
||||||
style: {
|
h('.super-dev-container', [
|
||||||
height: '500px',
|
|
||||||
width: '360px',
|
|
||||||
boxShadow: 'grey 0px 2px 9px',
|
|
||||||
margin: '20px',
|
|
||||||
},
|
|
||||||
}, [
|
|
||||||
h(Root, {
|
|
||||||
store: store,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
|
|
||||||
]
|
h(Selector, { actions, selectedKey: selectedView, states, store }),
|
||||||
), container)
|
|
||||||
|
h('#app-content', {
|
||||||
|
style: {
|
||||||
|
height: '500px',
|
||||||
|
width: '360px',
|
||||||
|
boxShadow: 'grey 0px 2px 9px',
|
||||||
|
margin: '20px',
|
||||||
|
},
|
||||||
|
}, [
|
||||||
|
h(Root, {
|
||||||
|
store: store,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
|
||||||
|
]
|
||||||
|
), container)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -119,14 +119,11 @@ var actions = {
|
|||||||
SET_RPC_TARGET: 'SET_RPC_TARGET',
|
SET_RPC_TARGET: 'SET_RPC_TARGET',
|
||||||
SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET',
|
SET_DEFAULT_RPC_TARGET: 'SET_DEFAULT_RPC_TARGET',
|
||||||
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
|
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
|
||||||
USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER',
|
|
||||||
useEtherscanProvider: useEtherscanProvider,
|
|
||||||
showConfigPage,
|
showConfigPage,
|
||||||
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
|
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
|
||||||
showAddTokenPage,
|
showAddTokenPage,
|
||||||
addToken,
|
addToken,
|
||||||
setRpcTarget: setRpcTarget,
|
setRpcTarget: setRpcTarget,
|
||||||
setDefaultRpcTarget: setDefaultRpcTarget,
|
|
||||||
setProviderType: setProviderType,
|
setProviderType: setProviderType,
|
||||||
// loading overlay
|
// loading overlay
|
||||||
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
|
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
|
||||||
@ -706,16 +703,19 @@ function markAccountsFound () {
|
|||||||
// config
|
// config
|
||||||
//
|
//
|
||||||
|
|
||||||
// default rpc target refers to localhost:8545 in this instance.
|
function setProviderType (type) {
|
||||||
function setDefaultRpcTarget () {
|
|
||||||
log.debug(`background.setDefaultRpcTarget`)
|
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
background.setDefaultRpc((err, result) => {
|
log.debug(`background.setProviderType`)
|
||||||
|
background.setProviderType(type, (err, result) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error(err)
|
log.error(err)
|
||||||
return dispatch(self.displayWarning('Had a problem changing networks.'))
|
return dispatch(self.displayWarning('Had a problem changing networks!'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return {
|
||||||
|
type: actions.SET_PROVIDER_TYPE,
|
||||||
|
value: type,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -744,23 +744,6 @@ function addToAddressBook (recipient, nickname) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProviderType (type) {
|
|
||||||
log.debug(`background.setProviderType`)
|
|
||||||
background.setProviderType(type)
|
|
||||||
return {
|
|
||||||
type: actions.SET_PROVIDER_TYPE,
|
|
||||||
value: type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function useEtherscanProvider () {
|
|
||||||
log.debug(`background.useEtherscanProvider`)
|
|
||||||
background.useEtherscanProvider()
|
|
||||||
return {
|
|
||||||
type: actions.USE_ETHERSCAN_PROVIDER,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoadingIndication (message) {
|
function showLoadingIndication (message) {
|
||||||
return {
|
return {
|
||||||
type: actions.SHOW_LOADING,
|
type: actions.SHOW_LOADING,
|
||||||
|
@ -319,7 +319,7 @@ App.prototype.renderNetworkDropdown = function () {
|
|||||||
{
|
{
|
||||||
key: 'default',
|
key: 'default',
|
||||||
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
closeMenu: () => this.setState({ isNetworkMenuOpen: !isOpen }),
|
||||||
onClick: () => props.dispatch(actions.setDefaultRpcTarget()),
|
onClick: () => props.dispatch(actions.setProviderType('localhost')),
|
||||||
style: {
|
style: {
|
||||||
fontSize: '18px',
|
fontSize: '18px',
|
||||||
},
|
},
|
||||||
|
@ -13,6 +13,7 @@ function FiatValue () {
|
|||||||
FiatValue.prototype.render = function () {
|
FiatValue.prototype.render = function () {
|
||||||
const props = this.props
|
const props = this.props
|
||||||
const { conversionRate, currentCurrency } = props
|
const { conversionRate, currentCurrency } = props
|
||||||
|
const renderedCurrency = currentCurrency || ''
|
||||||
|
|
||||||
const value = formatBalance(props.value, 6)
|
const value = formatBalance(props.value, 6)
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ FiatValue.prototype.render = function () {
|
|||||||
fiatTooltipNumber = 'Unknown'
|
fiatTooltipNumber = 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
return fiatDisplay(fiatDisplayNumber, currentCurrency)
|
return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
|
function fiatDisplay (fiatDisplayNumber, fiatSuffix) {
|
||||||
|
@ -3,7 +3,9 @@ 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 currencies = require('./conversion.json').rows
|
const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => {
|
||||||
|
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
|
||||||
|
})
|
||||||
const validUrl = require('valid-url')
|
const validUrl = require('valid-url')
|
||||||
const exportAsFile = require('./util').exportAsFile
|
const exportAsFile = require('./util').exportAsFile
|
||||||
|
|
||||||
@ -167,8 +169,8 @@ function currentConversionInformation (metamaskState, state) {
|
|||||||
state.dispatch(actions.setCurrentCurrency(newCurrency))
|
state.dispatch(actions.setCurrentCurrency(newCurrency))
|
||||||
},
|
},
|
||||||
defaultValue: currentCurrency,
|
defaultValue: currentCurrency,
|
||||||
}, currencies.map((currency) => {
|
}, infuraCurrencies.map((currency) => {
|
||||||
return h('option', {key: currency.code, value: currency.code}, `${currency.code} - ${currency.name}`)
|
return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
@ -1,207 +0,0 @@
|
|||||||
{
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"code": "REP",
|
|
||||||
"name": "Augur",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "BCN",
|
|
||||||
"name": "Bytecoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "BTC",
|
|
||||||
"name": "Bitcoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "BTS",
|
|
||||||
"name": "BitShares",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "BLK",
|
|
||||||
"name": "Blackcoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "GBP",
|
|
||||||
"name": "British Pound Sterling",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "CAD",
|
|
||||||
"name": "Canadian Dollar",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "CNY",
|
|
||||||
"name": "Chinese Yuan",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "DSH",
|
|
||||||
"name": "Dashcoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "DOGE",
|
|
||||||
"name": "Dogecoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ETC",
|
|
||||||
"name": "Ethereum Classic",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "EUR",
|
|
||||||
"name": "Euro",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "GNO",
|
|
||||||
"name": "GNO",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "GNT",
|
|
||||||
"name": "GNT",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "JPY",
|
|
||||||
"name": "Japanese Yen",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "LTC",
|
|
||||||
"name": "Litecoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "MAID",
|
|
||||||
"name": "MaidSafeCoin",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "XEM",
|
|
||||||
"name": "NEM",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "XLM",
|
|
||||||
"name": "Stellar",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "XMR",
|
|
||||||
"name": "Monero",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "XRP",
|
|
||||||
"name": "Ripple",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "RUR",
|
|
||||||
"name": "Ruble",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "STEEM",
|
|
||||||
"name": "Steem",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "STRAT",
|
|
||||||
"name": "STRAT",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "UAH",
|
|
||||||
"name": "Ukrainian Hryvnia",
|
|
||||||
"statuses": [
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "USD",
|
|
||||||
"name": "US Dollar",
|
|
||||||
"statuses": [
|
|
||||||
"primary",
|
|
||||||
"secondary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "WAVES",
|
|
||||||
"name": "WAVES",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": "ZEC",
|
|
||||||
"name": "Zcash",
|
|
||||||
"statuses": [
|
|
||||||
"primary"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
653
ui/app/infura-conversion.json
Normal file
653
ui/app/infura-conversion.json
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"symbol": "ethaud",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "aud",
|
||||||
|
"name": "Australian Dollar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethhkd",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "hkd",
|
||||||
|
"name": "Hong Kong Dollar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethsgd",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "sgd",
|
||||||
|
"name": "Singapore Dollar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethidr",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "idr",
|
||||||
|
"name": "Indonesian Rupiah"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethphp",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "php",
|
||||||
|
"name": "Philippine Peso"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "eth1st",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "1st",
|
||||||
|
"name": "FirstBlood"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethadt",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "adt",
|
||||||
|
"name": "adToken"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethadx",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "adx",
|
||||||
|
"name": "AdEx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethant",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "ant",
|
||||||
|
"name": "Aragon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethbat",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "bat",
|
||||||
|
"name": "Basic Attention Token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethbnt",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "bnt",
|
||||||
|
"name": "Bancor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethbtc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "btc",
|
||||||
|
"name": "Bitcoin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethcad",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "cad",
|
||||||
|
"name": "Canadian Dollar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethcfi",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "cfi",
|
||||||
|
"name": "Cofound.it"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethcrb",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "crb",
|
||||||
|
"name": "CreditBit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethcvc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "cvc",
|
||||||
|
"name": "Civic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethdash",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "dash",
|
||||||
|
"name": "Dash"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethdgd",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "dgd",
|
||||||
|
"name": "DigixDAO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethetc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "etc",
|
||||||
|
"name": "Ethereum Classic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "etheur",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "eur",
|
||||||
|
"name": "Euro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethfun",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "fun",
|
||||||
|
"name": "FunFair"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethgbp",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "gbp",
|
||||||
|
"name": "Pound Sterling"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethgno",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "gno",
|
||||||
|
"name": "Gnosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethgnt",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "gnt",
|
||||||
|
"name": "Golem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethgup",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "gup",
|
||||||
|
"name": "Matchpool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethhmq",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "hmq",
|
||||||
|
"name": "Humaniq"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethjpy",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "jpy",
|
||||||
|
"name": "Japanese Yen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethlgd",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "lgd",
|
||||||
|
"name": "Legends Room"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethlsk",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "lsk",
|
||||||
|
"name": "Lisk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethltc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "ltc",
|
||||||
|
"name": "Litecoin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethlun",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "lun",
|
||||||
|
"name": "Lunyr"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethmco",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "mco",
|
||||||
|
"name": "Monaco"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethmtl",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "mtl",
|
||||||
|
"name": "Metal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethmyst",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "myst",
|
||||||
|
"name": "Mysterium"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethnmr",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "nmr",
|
||||||
|
"name": "Numeraire"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethomg",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "omg",
|
||||||
|
"name": "OmiseGO"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethpay",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "pay",
|
||||||
|
"name": "TenX"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethptoy",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "ptoy",
|
||||||
|
"name": "Patientory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethqrl",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "qrl",
|
||||||
|
"name": "Quantum-Resistant Ledger"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethqtum",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "qtum",
|
||||||
|
"name": "Qtum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethrep",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "rep",
|
||||||
|
"name": "Augur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethrlc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "rlc",
|
||||||
|
"name": "iEx.ec"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethrub",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "rub",
|
||||||
|
"name": "Russian Ruble"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethsc",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "sc",
|
||||||
|
"name": "Siacoin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethsngls",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "sngls",
|
||||||
|
"name": "SingularDTV"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethsnt",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "snt",
|
||||||
|
"name": "Status"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethsteem",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "steem",
|
||||||
|
"name": "Steem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethstorj",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "storj",
|
||||||
|
"name": "Storj"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethtime",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "time",
|
||||||
|
"name": "ChronoBank"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethtkn",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "tkn",
|
||||||
|
"name": "TokenCard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethtrst",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "trst",
|
||||||
|
"name": "WeTrust"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethuah",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "uah",
|
||||||
|
"name": "Ukrainian Hryvnia"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethusd",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "usd",
|
||||||
|
"name": "United States Dollar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethwings",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "wings",
|
||||||
|
"name": "Wings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethxem",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "xem",
|
||||||
|
"name": "NEM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethxlm",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "xlm",
|
||||||
|
"name": "Stellar Lumen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethxmr",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "xmr",
|
||||||
|
"name": "Monero"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethxrp",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "xrp",
|
||||||
|
"name": "Ripple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"symbol": "ethzec",
|
||||||
|
"base": {
|
||||||
|
"code": "eth",
|
||||||
|
"name": "Ethereum"
|
||||||
|
},
|
||||||
|
"quote": {
|
||||||
|
"code": "zec",
|
||||||
|
"name": "Zcash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user