mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
003f1306d3
The account tracker had one doc comment above the constructor that partially served to document the constructor, but mostly contained a type definition for the class itself. It has been split into two blocks; one for the class, one for the constructor. The constructor doc comment has also been expanded to document all constructor options.
276 lines
8.9 KiB
JavaScript
276 lines
8.9 KiB
JavaScript
/* Account Tracker
|
|
*
|
|
* This module is responsible for tracking any number of accounts
|
|
* and caching their current balances & transaction counts.
|
|
*
|
|
* It also tracks transaction hashes, and checks their inclusion status
|
|
* on each new block.
|
|
*/
|
|
|
|
import EthQuery from 'eth-query'
|
|
|
|
import ObservableStore from 'obs-store'
|
|
import log from 'loglevel'
|
|
import pify from 'pify'
|
|
import Web3 from 'web3'
|
|
import SINGLE_CALL_BALANCES_ABI from 'single-call-balance-checker-abi'
|
|
import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, ROPSTEN_CHAIN_ID, KOVAN_CHAIN_ID } from '../controllers/network/enums'
|
|
|
|
import {
|
|
SINGLE_CALL_BALANCES_ADDRESS,
|
|
SINGLE_CALL_BALANCES_ADDRESS_RINKEBY,
|
|
SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN,
|
|
SINGLE_CALL_BALANCES_ADDRESS_KOVAN,
|
|
} from '../controllers/network/contract-addresses'
|
|
import { bnToHex } from './util'
|
|
|
|
/**
|
|
* This module is responsible for tracking any number of accounts and caching their current balances & transaction
|
|
* counts.
|
|
*
|
|
* It also tracks transaction hashes, and checks their inclusion status on each new block.
|
|
*
|
|
* @typedef {Object} AccountTracker
|
|
* @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit.
|
|
* @property {Object} store.accounts The accounts currently stored in this AccountTracker
|
|
* @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block
|
|
* @property {Object} _provider A provider needed to create the EthQuery instance used within this AccountTracker.
|
|
* @property {EthQuery} _query An EthQuery instance used to access account information from the blockchain
|
|
* @property {BlockTracker} _blockTracker A BlockTracker instance. Needed to ensure that accounts and their info updates
|
|
* when a new block is created.
|
|
* @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block
|
|
*
|
|
*/
|
|
export default class AccountTracker {
|
|
|
|
/**
|
|
* @param {Object} opts - Options for initializing the controller
|
|
* @param {Object} opts.provider - An EIP-1193 provider instance that uses the current global network
|
|
* @param {Object} opts.blockTracker - A block tracker, which emits events for each new block
|
|
* @param {Function} opts.getCurrentChainId - A function that returns the `chainId` for the current global network
|
|
*/
|
|
constructor (opts = {}) {
|
|
const initState = {
|
|
accounts: {},
|
|
currentBlockGasLimit: '',
|
|
}
|
|
this.store = new ObservableStore(initState)
|
|
|
|
this._provider = opts.provider
|
|
this._query = pify(new EthQuery(this._provider))
|
|
this._blockTracker = opts.blockTracker
|
|
// blockTracker.currentBlock may be null
|
|
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
|
|
this._blockTracker.once('latest', (blockNumber) => {
|
|
this._currentBlockNumber = blockNumber
|
|
})
|
|
// bind function for easier listener syntax
|
|
this._updateForBlock = this._updateForBlock.bind(this)
|
|
this.getCurrentChainId = opts.getCurrentChainId
|
|
|
|
this.web3 = new Web3(this._provider)
|
|
}
|
|
|
|
start () {
|
|
// remove first to avoid double add
|
|
this._blockTracker.removeListener('latest', this._updateForBlock)
|
|
// add listener
|
|
this._blockTracker.addListener('latest', this._updateForBlock)
|
|
// fetch account balances
|
|
this._updateAccounts()
|
|
}
|
|
|
|
stop () {
|
|
// remove listener
|
|
this._blockTracker.removeListener('latest', this._updateForBlock)
|
|
}
|
|
|
|
/**
|
|
* Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this
|
|
* AccountTracker.
|
|
*
|
|
* Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each
|
|
* of these accounts are given an updated balance via EthQuery.
|
|
*
|
|
* @param {array} address - The array of hex addresses for accounts with which this AccountTracker's accounts should be
|
|
* in sync
|
|
*
|
|
*/
|
|
syncWithAddresses (addresses) {
|
|
const { accounts } = this.store.getState()
|
|
const locals = Object.keys(accounts)
|
|
|
|
const accountsToAdd = []
|
|
addresses.forEach((upstream) => {
|
|
if (!locals.includes(upstream)) {
|
|
accountsToAdd.push(upstream)
|
|
}
|
|
})
|
|
|
|
const accountsToRemove = []
|
|
locals.forEach((local) => {
|
|
if (!addresses.includes(local)) {
|
|
accountsToRemove.push(local)
|
|
}
|
|
})
|
|
|
|
this.addAccounts(accountsToAdd)
|
|
this.removeAccount(accountsToRemove)
|
|
}
|
|
|
|
/**
|
|
* Adds new addresses to track the balances of
|
|
* given a balance as long this._currentBlockNumber is defined.
|
|
*
|
|
* @param {array} addresses - An array of hex addresses of new accounts to track
|
|
*
|
|
*/
|
|
addAccounts (addresses) {
|
|
const { accounts } = this.store.getState()
|
|
// add initial state for addresses
|
|
addresses.forEach((address) => {
|
|
accounts[address] = {}
|
|
})
|
|
// save accounts state
|
|
this.store.updateState({ accounts })
|
|
// fetch balances for the accounts if there is block number ready
|
|
if (!this._currentBlockNumber) {
|
|
return
|
|
}
|
|
this._updateAccounts()
|
|
}
|
|
|
|
/**
|
|
* Removes accounts from being tracked
|
|
*
|
|
* @param {array} an - array of hex addresses to stop tracking
|
|
*
|
|
*/
|
|
removeAccount (addresses) {
|
|
const { accounts } = this.store.getState()
|
|
// remove each state object
|
|
addresses.forEach((address) => {
|
|
delete accounts[address]
|
|
})
|
|
// save accounts state
|
|
this.store.updateState({ accounts })
|
|
}
|
|
|
|
/**
|
|
* Removes all addresses and associated balances
|
|
*/
|
|
|
|
clearAccounts () {
|
|
this.store.updateState({ accounts: {} })
|
|
}
|
|
|
|
/**
|
|
* Given a block, updates this AccountTracker's currentBlockGasLimit, and then updates each local account's balance
|
|
* via EthQuery
|
|
*
|
|
* @private
|
|
* @param {number} blockNumber - the block number to update to.
|
|
* @fires 'block' The updated state, if all account updates are successful
|
|
*
|
|
*/
|
|
async _updateForBlock (blockNumber) {
|
|
this._currentBlockNumber = blockNumber
|
|
|
|
// block gasLimit polling shouldn't be in account-tracker shouldn't be here...
|
|
const currentBlock = await this._query.getBlockByNumber(blockNumber, false)
|
|
if (!currentBlock) {
|
|
return
|
|
}
|
|
const currentBlockGasLimit = currentBlock.gasLimit
|
|
this.store.updateState({ currentBlockGasLimit })
|
|
|
|
try {
|
|
await this._updateAccounts()
|
|
} catch (err) {
|
|
log.error(err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* balanceChecker is deployed on main eth (test)nets and requires a single call
|
|
* for all other networks, calls this._updateAccount for each account in this.store
|
|
*
|
|
* @returns {Promise} - after all account balances updated
|
|
*
|
|
*/
|
|
async _updateAccounts () {
|
|
const { accounts } = this.store.getState()
|
|
const addresses = Object.keys(accounts)
|
|
const chainId = this.getCurrentChainId()
|
|
|
|
switch (chainId) {
|
|
case MAINNET_CHAIN_ID:
|
|
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS)
|
|
break
|
|
|
|
case RINKEBY_CHAIN_ID:
|
|
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY)
|
|
break
|
|
|
|
case ROPSTEN_CHAIN_ID:
|
|
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN)
|
|
break
|
|
|
|
case KOVAN_CHAIN_ID:
|
|
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_KOVAN)
|
|
break
|
|
|
|
default:
|
|
await Promise.all(addresses.map(this._updateAccount.bind(this)))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the current balance of an account.
|
|
*
|
|
* @private
|
|
* @param {string} address - A hex address of a the account to be updated
|
|
* @returns {Promise} - after the account balance is updated
|
|
*
|
|
*/
|
|
async _updateAccount (address) {
|
|
// query balance
|
|
const balance = await this._query.getBalance(address)
|
|
const result = { address, balance }
|
|
// update accounts state
|
|
const { accounts } = this.store.getState()
|
|
// only populate if the entry is still present
|
|
if (!accounts[address]) {
|
|
return
|
|
}
|
|
accounts[address] = result
|
|
this.store.updateState({ accounts })
|
|
}
|
|
|
|
/**
|
|
* Updates current address balances from balanceChecker deployed contract instance
|
|
* @param {*} addresses
|
|
* @param {*} deployedContractAddress
|
|
*/
|
|
async _updateAccountsViaBalanceChecker (addresses, deployedContractAddress) {
|
|
const { accounts } = this.store.getState()
|
|
this.web3.setProvider(this._provider)
|
|
const ethContract = this.web3.eth.contract(SINGLE_CALL_BALANCES_ABI).at(deployedContractAddress)
|
|
const ethBalance = ['0x0']
|
|
|
|
ethContract.balances(addresses, ethBalance, (error, result) => {
|
|
if (error) {
|
|
log.warn(`MetaMask - Account Tracker single call balance fetch failed`, error)
|
|
Promise.all(addresses.map(this._updateAccount.bind(this)))
|
|
return
|
|
}
|
|
addresses.forEach((address, index) => {
|
|
const balance = bnToHex(result[index])
|
|
accounts[address] = { address, balance }
|
|
})
|
|
this.store.updateState({ accounts })
|
|
})
|
|
}
|
|
|
|
}
|