mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
Merge branch 'master' into filter-fixes-moar
This commit is contained in:
commit
e72083f6e8
@ -2,6 +2,10 @@
|
||||
|
||||
## Current Master
|
||||
|
||||
- Fix bug that could mis-render token balances when very small. (Not actually included in 3.9.9)
|
||||
|
||||
## 3.10.3 2017-9-21
|
||||
|
||||
- Fix bug where metamask-dapp connections are lost on rpc error
|
||||
- Fix bug that would sometimes display transactions as failed that could be successfully mined.
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "MetaMask",
|
||||
"short_name": "Metamask",
|
||||
"version": "3.10.2",
|
||||
"version": "3.10.3",
|
||||
"manifest_version": 2,
|
||||
"author": "https://metamask.io",
|
||||
"description": "Ethereum Browser Extension",
|
||||
|
61
app/scripts/controllers/balance.js
Normal file
61
app/scripts/controllers/balance.js
Normal file
@ -0,0 +1,61 @@
|
||||
const ObservableStore = require('obs-store')
|
||||
const PendingBalanceCalculator = require('../lib/pending-balance-calculator')
|
||||
const BN = require('ethereumjs-util').BN
|
||||
|
||||
class BalanceController {
|
||||
|
||||
constructor (opts = {}) {
|
||||
const { address, accountTracker, txController, blockTracker } = opts
|
||||
this.address = address
|
||||
this.accountTracker = accountTracker
|
||||
this.txController = txController
|
||||
this.blockTracker = blockTracker
|
||||
|
||||
const initState = {
|
||||
ethBalance: undefined,
|
||||
}
|
||||
this.store = new ObservableStore(initState)
|
||||
|
||||
this.balanceCalc = new PendingBalanceCalculator({
|
||||
getBalance: () => this._getBalance(),
|
||||
getPendingTransactions: this._getPendingTransactions.bind(this),
|
||||
})
|
||||
|
||||
this._registerUpdates()
|
||||
}
|
||||
|
||||
async updateBalance () {
|
||||
const balance = await this.balanceCalc.getBalance()
|
||||
this.store.updateState({
|
||||
ethBalance: balance,
|
||||
})
|
||||
}
|
||||
|
||||
_registerUpdates () {
|
||||
const update = this.updateBalance.bind(this)
|
||||
this.txController.on('submitted', update)
|
||||
this.txController.on('confirmed', update)
|
||||
this.txController.on('failed', update)
|
||||
this.accountTracker.store.subscribe(update)
|
||||
this.blockTracker.on('block', update)
|
||||
}
|
||||
|
||||
async _getBalance () {
|
||||
const { accounts } = this.accountTracker.store.getState()
|
||||
const entry = accounts[this.address]
|
||||
const balance = entry.balance
|
||||
return balance ? new BN(balance.substring(2), 16) : undefined
|
||||
}
|
||||
|
||||
async _getPendingTransactions () {
|
||||
const pending = this.txController.getFilteredTxList({
|
||||
from: this.address,
|
||||
status: 'submitted',
|
||||
err: undefined,
|
||||
})
|
||||
return pending
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = BalanceController
|
66
app/scripts/controllers/computed-balances.js
Normal file
66
app/scripts/controllers/computed-balances.js
Normal file
@ -0,0 +1,66 @@
|
||||
const ObservableStore = require('obs-store')
|
||||
const extend = require('xtend')
|
||||
const BalanceController = require('./balance')
|
||||
|
||||
class ComputedbalancesController {
|
||||
|
||||
constructor (opts = {}) {
|
||||
const { accountTracker, txController, blockTracker } = opts
|
||||
this.accountTracker = accountTracker
|
||||
this.txController = txController
|
||||
this.blockTracker = blockTracker
|
||||
|
||||
const initState = extend({
|
||||
computedBalances: {},
|
||||
}, opts.initState)
|
||||
this.store = new ObservableStore(initState)
|
||||
this.balances = {}
|
||||
|
||||
this._initBalanceUpdating()
|
||||
}
|
||||
|
||||
updateAllBalances () {
|
||||
for (let address in this.accountTracker.store.getState().accounts) {
|
||||
this.balances[address].updateBalance()
|
||||
}
|
||||
}
|
||||
|
||||
_initBalanceUpdating () {
|
||||
const store = this.accountTracker.store.getState()
|
||||
this.addAnyAccountsFromStore(store)
|
||||
this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this))
|
||||
}
|
||||
|
||||
addAnyAccountsFromStore(store) {
|
||||
const balances = store.accounts
|
||||
|
||||
for (let address in balances) {
|
||||
this.trackAddressIfNotAlready(address)
|
||||
}
|
||||
}
|
||||
|
||||
trackAddressIfNotAlready (address) {
|
||||
const state = this.store.getState()
|
||||
if (!(address in state.computedBalances)) {
|
||||
this.trackAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
trackAddress (address) {
|
||||
let updater = new BalanceController({
|
||||
address,
|
||||
accountTracker: this.accountTracker,
|
||||
txController: this.txController,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
updater.store.subscribe((accountBalance) => {
|
||||
let newState = this.store.getState()
|
||||
newState.computedBalances[address] = accountBalance
|
||||
this.store.updateState(newState)
|
||||
})
|
||||
this.balances[address] = updater
|
||||
updater.updateBalance()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ComputedbalancesController
|
@ -22,7 +22,7 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
this.provider = opts.provider
|
||||
this.blockTracker = opts.blockTracker
|
||||
this.signEthTx = opts.signTransaction
|
||||
this.ethStore = opts.ethStore
|
||||
this.accountTracker = opts.accountTracker
|
||||
|
||||
this.nonceTracker = new NonceTracker({
|
||||
provider: this.provider,
|
||||
@ -52,7 +52,7 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
provider: this.provider,
|
||||
nonceTracker: this.nonceTracker,
|
||||
getBalance: (address) => {
|
||||
const account = this.ethStore.getState().accounts[address]
|
||||
const account = this.accountTracker.getState().accounts[address]
|
||||
if (!account) return
|
||||
return account.balance
|
||||
},
|
||||
@ -73,7 +73,7 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
|
||||
// this is a little messy but until ethstore has been either
|
||||
// removed or redone this is to guard against the race condition
|
||||
// where ethStore hasent been populated by the results yet
|
||||
// where accountTracker hasent been populated by the results yet
|
||||
this.blockTracker.once('latest', () => {
|
||||
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
|
||||
})
|
||||
@ -434,6 +434,7 @@ module.exports = class TransactionController extends EventEmitter {
|
||||
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)
|
||||
}
|
||||
|
@ -35,8 +35,9 @@ class KeyringController extends EventEmitter {
|
||||
keyrings: [],
|
||||
identities: {},
|
||||
})
|
||||
this.ethStore = opts.ethStore
|
||||
this.encryptor = encryptor
|
||||
|
||||
this.accountTracker = opts.accountTracker
|
||||
this.encryptor = opts.encryptor || encryptor
|
||||
this.keyrings = []
|
||||
this.getNetwork = opts.getNetwork
|
||||
}
|
||||
@ -338,7 +339,7 @@ class KeyringController extends EventEmitter {
|
||||
//
|
||||
// Initializes the provided account array
|
||||
// Gives them numerically incremented nicknames,
|
||||
// and adds them to the ethStore for regular balance checking.
|
||||
// and adds them to the accountTracker for regular balance checking.
|
||||
setupAccounts (accounts) {
|
||||
return this.getAccounts()
|
||||
.then((loadedAccounts) => {
|
||||
@ -361,7 +362,7 @@ class KeyringController extends EventEmitter {
|
||||
throw new Error('Problem loading account.')
|
||||
}
|
||||
const address = normalizeAddress(account)
|
||||
this.ethStore.addAccount(address)
|
||||
this.accountTracker.addAccount(address)
|
||||
return this.createNickname(address)
|
||||
}
|
||||
|
||||
@ -567,12 +568,12 @@ class KeyringController extends EventEmitter {
|
||||
clearKeyrings () {
|
||||
let accounts
|
||||
try {
|
||||
accounts = Object.keys(this.ethStore.getState())
|
||||
accounts = Object.keys(this.accountTracker.getState())
|
||||
} catch (e) {
|
||||
accounts = []
|
||||
}
|
||||
accounts.forEach((address) => {
|
||||
this.ethStore.removeAccount(address)
|
||||
this.accountTracker.removeAccount(address)
|
||||
})
|
||||
|
||||
// clear keyrings from memory
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Ethereum Store
|
||||
/* Account Tracker
|
||||
*
|
||||
* This module is responsible for tracking any number of accounts
|
||||
* and caching their current balances & transaction counts.
|
||||
@ -10,19 +10,21 @@
|
||||
const async = require('async')
|
||||
const EthQuery = require('eth-query')
|
||||
const ObservableStore = require('obs-store')
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
function noop () {}
|
||||
|
||||
|
||||
class EthereumStore extends ObservableStore {
|
||||
class AccountTracker extends EventEmitter {
|
||||
|
||||
constructor (opts = {}) {
|
||||
super({
|
||||
super()
|
||||
|
||||
const initState = {
|
||||
accounts: {},
|
||||
transactions: {},
|
||||
currentBlockNumber: '0',
|
||||
currentBlockHash: '',
|
||||
currentBlockGasLimit: '',
|
||||
})
|
||||
}
|
||||
this.store = new ObservableStore(initState)
|
||||
|
||||
this._provider = opts.provider
|
||||
this._query = new EthQuery(this._provider)
|
||||
this._blockTracker = opts.blockTracker
|
||||
@ -37,34 +39,19 @@ class EthereumStore extends ObservableStore {
|
||||
//
|
||||
|
||||
addAccount (address) {
|
||||
const accounts = this.getState().accounts
|
||||
const accounts = this.store.getState().accounts
|
||||
accounts[address] = {}
|
||||
this.updateState({ accounts })
|
||||
this.store.updateState({ accounts })
|
||||
if (!this._currentBlockNumber) return
|
||||
this._updateAccount(address)
|
||||
}
|
||||
|
||||
removeAccount (address) {
|
||||
const accounts = this.getState().accounts
|
||||
const accounts = this.store.getState().accounts
|
||||
delete accounts[address]
|
||||
this.updateState({ accounts })
|
||||
this.store.updateState({ accounts })
|
||||
}
|
||||
|
||||
addTransaction (txHash) {
|
||||
const transactions = this.getState().transactions
|
||||
transactions[txHash] = {}
|
||||
this.updateState({ transactions })
|
||||
if (!this._currentBlockNumber) return
|
||||
this._updateTransaction(this._currentBlockNumber, txHash, noop)
|
||||
}
|
||||
|
||||
removeTransaction (txHash) {
|
||||
const transactions = this.getState().transactions
|
||||
delete transactions[txHash]
|
||||
this.updateState({ transactions })
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// private
|
||||
//
|
||||
@ -72,53 +59,32 @@ class EthereumStore extends ObservableStore {
|
||||
_updateForBlock (block) {
|
||||
const blockNumber = '0x' + block.number.toString('hex')
|
||||
this._currentBlockNumber = blockNumber
|
||||
this.updateState({ currentBlockNumber: parseInt(blockNumber) })
|
||||
this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`})
|
||||
this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` })
|
||||
|
||||
this.store.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` })
|
||||
|
||||
async.parallel([
|
||||
this._updateAccounts.bind(this),
|
||||
this._updateTransactions.bind(this, blockNumber),
|
||||
], (err) => {
|
||||
if (err) return console.error(err)
|
||||
this.emit('block', this.getState())
|
||||
this.emit('block', this.store.getState())
|
||||
})
|
||||
}
|
||||
|
||||
_updateAccounts (cb = noop) {
|
||||
const accounts = this.getState().accounts
|
||||
const accounts = this.store.getState().accounts
|
||||
const addresses = Object.keys(accounts)
|
||||
async.each(addresses, this._updateAccount.bind(this), cb)
|
||||
}
|
||||
|
||||
_updateAccount (address, cb = noop) {
|
||||
const accounts = this.getState().accounts
|
||||
this._getAccount(address, (err, result) => {
|
||||
if (err) return cb(err)
|
||||
result.address = address
|
||||
const accounts = this.store.getState().accounts
|
||||
// only populate if the entry is still present
|
||||
if (accounts[address]) {
|
||||
accounts[address] = result
|
||||
this.updateState({ accounts })
|
||||
}
|
||||
cb(null, result)
|
||||
})
|
||||
}
|
||||
|
||||
_updateTransactions (block, cb = noop) {
|
||||
const transactions = this.getState().transactions
|
||||
const txHashes = Object.keys(transactions)
|
||||
async.each(txHashes, this._updateTransaction.bind(this, block), cb)
|
||||
}
|
||||
|
||||
_updateTransaction (block, txHash, cb = noop) {
|
||||
// would use the block here to determine how many confirmations the tx has
|
||||
const transactions = this.getState().transactions
|
||||
this._query.getTransaction(txHash, (err, result) => {
|
||||
if (err) return cb(err)
|
||||
// only populate if the entry is still present
|
||||
if (transactions[txHash]) {
|
||||
transactions[txHash] = result
|
||||
this.updateState({ transactions })
|
||||
this.store.updateState({ accounts })
|
||||
}
|
||||
cb(null, result)
|
||||
})
|
||||
@ -135,4 +101,4 @@ class EthereumStore extends ObservableStore {
|
||||
|
||||
}
|
||||
|
||||
module.exports = EthereumStore
|
||||
module.exports = AccountTracker
|
51
app/scripts/lib/pending-balance-calculator.js
Normal file
51
app/scripts/lib/pending-balance-calculator.js
Normal file
@ -0,0 +1,51 @@
|
||||
const BN = require('ethereumjs-util').BN
|
||||
const normalize = require('eth-sig-util').normalize
|
||||
|
||||
class PendingBalanceCalculator {
|
||||
|
||||
// Must be initialized with two functions:
|
||||
// getBalance => Returns a promise of a BN of the current balance in Wei
|
||||
// getPendingTransactions => Returns an array of TxMeta Objects,
|
||||
// which have txParams properties, which include value, gasPrice, and gas,
|
||||
// all in a base=16 hex format.
|
||||
constructor ({ getBalance, getPendingTransactions }) {
|
||||
this.getPendingTransactions = getPendingTransactions
|
||||
this.getNetworkBalance = getBalance
|
||||
}
|
||||
|
||||
async getBalance() {
|
||||
const results = await Promise.all([
|
||||
this.getNetworkBalance(),
|
||||
this.getPendingTransactions(),
|
||||
])
|
||||
|
||||
const [ balance, pending ] = results
|
||||
if (!balance) return undefined
|
||||
|
||||
const pendingValue = pending.reduce((total, tx) => {
|
||||
return total.add(this.calculateMaxCost(tx))
|
||||
}, new BN(0))
|
||||
|
||||
return `0x${balance.sub(pendingValue).toString(16)}`
|
||||
}
|
||||
|
||||
calculateMaxCost (tx) {
|
||||
const txValue = tx.txParams.value
|
||||
const value = this.hexToBn(txValue)
|
||||
const gasPrice = this.hexToBn(tx.txParams.gasPrice)
|
||||
|
||||
const gas = tx.txParams.gas
|
||||
const gasLimit = tx.txParams.gasLimit
|
||||
const gasLimitBn = this.hexToBn(gas || gasLimit)
|
||||
|
||||
const gasCost = gasPrice.mul(gasLimitBn)
|
||||
return value.add(gasCost)
|
||||
}
|
||||
|
||||
hexToBn (hex) {
|
||||
return new BN(normalize(hex).substring(2), 16)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = PendingBalanceCalculator
|
@ -4,7 +4,7 @@ const promiseToCallback = require('promise-to-callback')
|
||||
const pump = require('pump')
|
||||
const Dnode = require('dnode')
|
||||
const ObservableStore = require('obs-store')
|
||||
const EthStore = require('./lib/eth-store')
|
||||
const AccountTracker = require('./lib/account-tracker')
|
||||
const EthQuery = require('eth-query')
|
||||
const RpcEngine = require('json-rpc-engine')
|
||||
const debounce = require('debounce')
|
||||
@ -14,7 +14,7 @@ const createOriginMiddleware = require('./lib/createOriginMiddleware')
|
||||
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
|
||||
const createProviderMiddleware = require('./lib/createProviderMiddleware')
|
||||
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 PreferencesController = require('./controllers/preferences')
|
||||
const CurrencyController = require('./controllers/currency')
|
||||
@ -26,6 +26,7 @@ const BlacklistController = require('./controllers/blacklist')
|
||||
const MessageManager = require('./lib/message-manager')
|
||||
const PersonalMessageManager = require('./lib/personal-message-manager')
|
||||
const TransactionController = require('./controllers/transactions')
|
||||
const BalancesController = require('./controllers/computed-balances')
|
||||
const ConfigManager = require('./lib/config-manager')
|
||||
const nodeify = require('./lib/nodeify')
|
||||
const accountImporter = require('./account-import-strategies')
|
||||
@ -85,7 +86,8 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
// eth data query tools
|
||||
this.ethQuery = new EthQuery(this.provider)
|
||||
this.ethStore = new EthStore({
|
||||
// account tracker watches balances, nonces, and any code at their address.
|
||||
this.accountTracker = new AccountTracker({
|
||||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
@ -93,11 +95,17 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
// key mgmt
|
||||
this.keyringController = new KeyringController({
|
||||
initState: initState.KeyringController,
|
||||
ethStore: this.ethStore,
|
||||
accountTracker: this.accountTracker,
|
||||
getNetwork: this.networkController.getNetworkState.bind(this.networkController),
|
||||
encryptor: opts.encryptor || undefined,
|
||||
})
|
||||
|
||||
this.keyringController.on('newAccount', (address) => {
|
||||
this.preferencesController.setSelectedAddress(address)
|
||||
this.accountTracker.addAccount(address)
|
||||
})
|
||||
this.keyringController.on('removedAccount', (address) => {
|
||||
this.accountTracker.removeAccount(address)
|
||||
})
|
||||
|
||||
// address book controller
|
||||
@ -116,10 +124,21 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
ethQuery: this.ethQuery,
|
||||
ethStore: this.ethStore,
|
||||
accountTracker: this.accountTracker,
|
||||
})
|
||||
this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts))
|
||||
|
||||
// computed balances (accounting for pending transactions)
|
||||
this.balancesController = new BalancesController({
|
||||
accountTracker: this.accountTracker,
|
||||
txController: this.txController,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
this.networkController.on('networkDidChange', () => {
|
||||
this.balancesController.updateAllBalances()
|
||||
})
|
||||
this.balancesController.updateAllBalances()
|
||||
|
||||
// notices
|
||||
this.noticeController = new NoticeController({
|
||||
initState: initState.NoticeController,
|
||||
@ -171,8 +190,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
|
||||
// manual mem state subscriptions
|
||||
this.networkController.store.subscribe(this.sendUpdate.bind(this))
|
||||
this.ethStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.accountTracker.store.subscribe(this.sendUpdate.bind(this))
|
||||
this.txController.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.balancesController.store.subscribe(this.sendUpdate.bind(this))
|
||||
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
@ -247,16 +267,18 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
const wallet = this.configManager.getWallet()
|
||||
const vault = this.keyringController.store.getState().vault
|
||||
const isInitialized = (!!wallet || !!vault)
|
||||
|
||||
return extend(
|
||||
{
|
||||
isInitialized,
|
||||
},
|
||||
this.networkController.store.getState(),
|
||||
this.ethStore.getState(),
|
||||
this.accountTracker.store.getState(),
|
||||
this.txController.memStore.getState(),
|
||||
this.messageManager.memStore.getState(),
|
||||
this.personalMessageManager.memStore.getState(),
|
||||
this.keyringController.memStore.getState(),
|
||||
this.balancesController.store.getState(),
|
||||
this.preferencesController.store.getState(),
|
||||
this.addressBookController.store.getState(),
|
||||
this.currencyController.store.getState(),
|
||||
@ -674,4 +696,4 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
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 ConfigManager = require('../../app/scripts/lib/config-manager')
|
||||
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'
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
"isUnlocked": false,
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {},
|
||||
"computedBalances": {},
|
||||
"frequentRpcList": [],
|
||||
"unapprovedTxs": {},
|
||||
"currentCurrency": "USD",
|
||||
@ -48,5 +49,6 @@
|
||||
"isLoading": false,
|
||||
"warning": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"computedBalances": {}
|
||||
}
|
||||
|
@ -6,10 +6,37 @@ MetaMask has been under continuous development for nearly two years now, and we
|
||||
|
||||
The core functionality of MetaMask all lives in what we call [The MetaMask Controller](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js). Our goal for this file is for it to eventually be its own javascript module that can be imported into any JS-compatible context, allowing it to fully manage an app's relationship to Ethereum.
|
||||
|
||||
The MM Controller exposes most of its functionality via two methods:
|
||||
#### Constructor
|
||||
|
||||
- [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241) - This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated)
|
||||
- [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335) - Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works!
|
||||
When calling `new MetaMask(opts)`, many platform-specific options are configured. The keys on `opts` are as follows:
|
||||
|
||||
- initState: The last emitted state, used for restoring persistent state between sessions.
|
||||
- platform: The `platform` object defines a variety of platform-specific functions, including opening the confirmation view, and opening web sites.
|
||||
- encryptor - An object that provides access to the desired encryption methods.
|
||||
|
||||
##### Encryptor
|
||||
|
||||
An object that provides two simple methods, which can encrypt in any format you prefer. This parameter is optional, and will default to the browser-native WebCrypto API.
|
||||
|
||||
- encrypt(password, object) - returns a Promise of a string that is ready for storage.
|
||||
- decrypt(password, encryptedString) - Accepts the encrypted output of `encrypt` and returns a Promise of a restored `object` as it was encrypted.
|
||||
|
||||
|
||||
##### Platform Options
|
||||
|
||||
The `platform` object has a variety of options:
|
||||
|
||||
- reload (function) - Will be called when MetaMask would like to reload its own context.
|
||||
- openWindow ({ url }) - Will be called when MetaMask would like to open a web page. It will be passed a single `options` object with a `url` key, with a string value.
|
||||
- getVersion() - Should return the current MetaMask version, as described in the current `CHANGELOG.md` or `app/manifest.json`.
|
||||
|
||||
#### [metamask.getState()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L241)
|
||||
|
||||
This method returns a javascript object representing the current MetaMask state. This includes things like known accounts, sent transactions, current exchange rates, and more! The controller is also an event emitter, so you can subscribe to state updates via `metamask.on('update', handleStateUpdate)`. State examples available [here](https://github.com/MetaMask/metamask-extension/tree/master/development/states) under the `metamask` key. (Warning: some are outdated)
|
||||
|
||||
#### [metamask.getApi()](https://github.com/MetaMask/metamask-extension/blob/master/app/scripts/metamask-controller.js#L274-L335)
|
||||
|
||||
Returns a JavaScript object filled with callback functions representing every operation our user interface ever performs. Everything from creating new accounts, changing the current network, to sending a transaction, is provided via these API methods. We export this external API on an object because it allows us to easily expose this API over a port using [dnode](https://www.npmjs.com/package/dnode), which is how our WebExtension's UI works!
|
||||
|
||||
### The UI
|
||||
|
||||
@ -62,4 +89,4 @@ If streams seem new and confusing to you, that's ok, they can seem strange at fi
|
||||
|
||||
## Conclusion
|
||||
|
||||
I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)!
|
||||
I hope this has been helpful to you! If you have any other questionsm, or points you think need clarification in this guide, please [open an issue on our GitHub](https://github.com/MetaMask/metamask-plugin/issues/new)!
|
||||
|
@ -53,10 +53,8 @@
|
||||
"async": "^2.5.0",
|
||||
"await-semaphore": "^0.1.1",
|
||||
"babel-runtime": "^6.23.0",
|
||||
"bip39": "^2.2.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"bn.js": "^4.11.7",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"browserify-derequire": "^0.9.4",
|
||||
"client-sw-ready-event": "^3.3.0",
|
||||
"clone": "^2.1.1",
|
||||
@ -73,11 +71,12 @@
|
||||
"eth-contract-metadata": "^1.1.4",
|
||||
"eth-hd-keyring": "^1.1.1",
|
||||
"eth-json-rpc-filters": "^1.2.1",
|
||||
"eth-keyring-controller": "^1.0.1",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-sig-util": "^1.2.2",
|
||||
"eth-simple-keyring": "^1.1.1",
|
||||
"eth-token-tracker": "^1.1.3",
|
||||
"eth-token-tracker": "^1.1.4",
|
||||
"ethereumjs-tx": "^1.3.0",
|
||||
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
|
||||
"ethereumjs-wallet": "^0.6.0",
|
||||
@ -132,7 +131,7 @@
|
||||
"redux": "^3.0.5",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"request-promise": "^4.1.1",
|
||||
"request-promise": "^4.2.1",
|
||||
"sandwich-expando": "^1.0.5",
|
||||
"semaphore": "^1.0.5",
|
||||
"sw-stream": "^2.0.0",
|
||||
@ -199,7 +198,7 @@
|
||||
"react-addons-test-utils": "^15.5.1",
|
||||
"react-test-renderer": "^15.5.4",
|
||||
"react-testutils-additions": "^15.2.0",
|
||||
"sinon": "^3.2.0",
|
||||
"sinon": "^4.0.0",
|
||||
"tape": "^4.5.1",
|
||||
"testem": "^1.10.3",
|
||||
"uglifyify": "^4.0.2",
|
||||
|
@ -29,4 +29,8 @@ module.exports = {
|
||||
return 'WHADDASALT!'
|
||||
},
|
||||
|
||||
getRandomValues () {
|
||||
return 'SOO RANDO!!!1'
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -34,10 +34,15 @@ describe('PendingTx', function () {
|
||||
const renderer = ReactTestUtils.createRenderer()
|
||||
const newGasPrice = '0x77359400'
|
||||
|
||||
const computedBalances = {}
|
||||
computedBalances[Object.keys(identities)[0]] = {
|
||||
ethBalance: '0x00000000000000056bc75e2d63100000',
|
||||
}
|
||||
const props = {
|
||||
identities,
|
||||
accounts: identities,
|
||||
txData,
|
||||
computedBalances,
|
||||
sendTransaction: (txMeta, event) => {
|
||||
// Assert changes:
|
||||
const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice)
|
||||
|
@ -1,167 +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: () => [],
|
||||
},
|
||||
ethStore: {
|
||||
addAccount (acct) { accounts.push(ethUtil.addHexPrefix(acct)) },
|
||||
},
|
||||
})
|
||||
|
||||
// Stub out the browser crypto for a mock encryptor.
|
||||
// Browser crypto is tested in the integration test suite.
|
||||
keyringController.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')
|
||||
})
|
||||
})
|
||||
})
|
93
test/unit/pending-balance-test.js
Normal file
93
test/unit/pending-balance-test.js
Normal file
@ -0,0 +1,93 @@
|
||||
const assert = require('assert')
|
||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator')
|
||||
const MockTxGen = require('../lib/mock-tx-gen')
|
||||
const BN = require('ethereumjs-util').BN
|
||||
let providerResultStub = {}
|
||||
|
||||
const zeroBn = new BN(0)
|
||||
const etherBn = new BN(String(1e18))
|
||||
const ether = '0x' + etherBn.toString(16)
|
||||
|
||||
describe('PendingBalanceCalculator', function () {
|
||||
let balanceCalculator
|
||||
|
||||
describe('#calculateMaxCost(tx)', function () {
|
||||
it('returns a BN for a given tx value', function () {
|
||||
const txGen = new MockTxGen()
|
||||
pendingTxs = txGen.generate({
|
||||
status: 'submitted',
|
||||
txParams: {
|
||||
value: ether,
|
||||
gasPrice: '0x0',
|
||||
gas: '0x0',
|
||||
}
|
||||
}, { count: 1 })
|
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn)
|
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0])
|
||||
assert.equal(result.toString(), etherBn.toString(), 'computes one ether')
|
||||
})
|
||||
|
||||
it('calculates gas costs as well', function () {
|
||||
const txGen = new MockTxGen()
|
||||
pendingTxs = txGen.generate({
|
||||
status: 'submitted',
|
||||
txParams: {
|
||||
value: '0x0',
|
||||
gasPrice: '0x2',
|
||||
gas: '0x3',
|
||||
}
|
||||
}, { count: 1 })
|
||||
|
||||
const balanceCalculator = generateBalanceCalcWith([], zeroBn)
|
||||
const result = balanceCalculator.calculateMaxCost(pendingTxs[0])
|
||||
assert.equal(result.toString(), '6', 'computes 6 wei of gas')
|
||||
})
|
||||
})
|
||||
|
||||
describe('if you have no pending txs and one ether', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
balanceCalculator = generateBalanceCalcWith([], etherBn)
|
||||
})
|
||||
|
||||
it('returns the network balance', async function () {
|
||||
const result = await balanceCalculator.getBalance()
|
||||
assert.equal(result, ether, `gave ${result} needed ${ether}`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('if you have a one ether pending tx and one ether', function () {
|
||||
beforeEach(function () {
|
||||
const txGen = new MockTxGen()
|
||||
pendingTxs = txGen.generate({
|
||||
status: 'submitted',
|
||||
txParams: {
|
||||
value: ether,
|
||||
gasPrice: '0x0',
|
||||
gas: '0x0',
|
||||
}
|
||||
}, { count: 1 })
|
||||
|
||||
balanceCalculator = generateBalanceCalcWith(pendingTxs, etherBn)
|
||||
})
|
||||
|
||||
it('returns the subtracted result', async function () {
|
||||
const result = await balanceCalculator.getBalance()
|
||||
assert.equal(result, '0x0', `gave ${result} needed '0x0'`)
|
||||
return true
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
function generateBalanceCalcWith (transactions, providerStub = zeroBn) {
|
||||
const getPendingTransactions = async () => transactions
|
||||
const getBalance = async () => providerStub
|
||||
|
||||
return new PendingBalanceCalculator({
|
||||
getBalance,
|
||||
getPendingTransactions,
|
||||
})
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ describe('Transaction Controller', function () {
|
||||
networkStore: new ObservableStore(currentNetworkId),
|
||||
txHistoryLimit: 10,
|
||||
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
|
||||
ethStore: { getState: noop },
|
||||
accountTracker: { getState: noop },
|
||||
signTransaction: (ethTx) => new Promise((resolve) => {
|
||||
ethTx.sign(privKey)
|
||||
resolve()
|
||||
@ -431,4 +431,4 @@ describe('Transaction Controller', function () {
|
||||
}).catch(done)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -32,6 +32,7 @@ function mapStateToProps (state) {
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
currentAccountTab: state.metamask.currentAccountTab,
|
||||
tokens: state.metamask.tokens,
|
||||
computedBalances: state.metamask.computedBalances,
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +46,7 @@ AccountDetailScreen.prototype.render = function () {
|
||||
var selected = props.address || Object.keys(props.accounts)[0]
|
||||
var checksumAddress = selected && ethUtil.toChecksumAddress(selected)
|
||||
var identity = props.identities[selected]
|
||||
var account = props.accounts[selected]
|
||||
var account = props.computedBalances[selected]
|
||||
const { network, conversionRate, currentCurrency } = props
|
||||
|
||||
return (
|
||||
@ -180,7 +181,7 @@ AccountDetailScreen.prototype.render = function () {
|
||||
}, [
|
||||
|
||||
h(EthBalance, {
|
||||
value: account && account.balance,
|
||||
value: account && account.ethBalance,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
style: {
|
||||
|
@ -33,7 +33,7 @@ function PendingTx () {
|
||||
|
||||
PendingTx.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { currentCurrency, blockGasLimit } = props
|
||||
const { currentCurrency, blockGasLimit, computedBalances } = props
|
||||
|
||||
const conversionRate = props.conversionRate
|
||||
const txMeta = this.gatherTxMeta()
|
||||
@ -42,8 +42,8 @@ PendingTx.prototype.render = function () {
|
||||
// Account Details
|
||||
const address = txParams.from || props.selectedAddress
|
||||
const identity = props.identities[address] || { address: address }
|
||||
const account = props.accounts[address]
|
||||
const balance = account ? account.balance : '0x0'
|
||||
const account = computedBalances[address]
|
||||
const balance = account ? account.ethBalance : '0x0'
|
||||
|
||||
// recipient check
|
||||
const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)
|
||||
|
@ -29,6 +29,7 @@ function mapStateToProps (state) {
|
||||
conversionRate: state.metamask.conversionRate,
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
blockGasLimit: state.metamask.currentBlockGasLimit,
|
||||
computedBalances: state.metamask.computedBalances,
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +40,7 @@ function ConfirmTxScreen () {
|
||||
|
||||
ConfirmTxScreen.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { network, provider, unapprovedTxs, currentCurrency,
|
||||
const { network, provider, unapprovedTxs, currentCurrency, computedBalances,
|
||||
unapprovedMsgs, unapprovedPersonalMsgs, conversionRate, blockGasLimit } = props
|
||||
|
||||
var unconfTxList = txHelper(unapprovedTxs, unapprovedMsgs, unapprovedPersonalMsgs, network)
|
||||
@ -48,7 +49,6 @@ ConfirmTxScreen.prototype.render = function () {
|
||||
var txParams = txData.params || {}
|
||||
var isNotification = isPopupOrNotification() === 'notification'
|
||||
|
||||
|
||||
log.info(`rendering a combined ${unconfTxList.length} unconf msg & txs`)
|
||||
if (unconfTxList.length === 0) return h(Loading, { isLoading: true })
|
||||
|
||||
@ -104,6 +104,7 @@ ConfirmTxScreen.prototype.render = function () {
|
||||
currentCurrency,
|
||||
blockGasLimit,
|
||||
unconfTxListLength,
|
||||
computedBalances,
|
||||
// Actions
|
||||
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
|
||||
sendTransaction: this.sendTransaction.bind(this),
|
||||
|
Loading…
Reference in New Issue
Block a user