1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
|
/* 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.
*/
const EthQuery = require('eth-query')
const ObservableStore = require('obs-store')
const log = require('loglevel')
const pify = require('pify')
const Web3 = require('web3')
const SINGLE_CALL_BALANCES_ABI = require('single-call-balance-checker-abi')
const { bnToHex } = require('./util')
const { MAINNET_CODE, RINKEYBY_CODE, ROPSTEN_CODE, KOVAN_CODE } = require('../controllers/network/enums')
const { SINGLE_CALL_BALANCES_ADDRESS, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN, SINGLE_CALL_BALANCES_ADDRESS_KOVAN } = require('../controllers/network/contract-addresses')
class AccountTracker {
/**
* 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
* @param {Object} opts Initialize various properties of the class.
* @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
*
*/
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.network = opts.network
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().accounts
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().accounts
// 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().accounts
// remove each state object
addresses.forEach(address => {
delete accounts[address]
})
// save accounts state
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().accounts
const addresses = Object.keys(accounts)
const currentNetwork = parseInt(this.network.getNetworkState())
switch (currentNetwork) {
case MAINNET_CODE:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS)
break
case RINKEYBY_CODE:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_RINKEBY)
break
case ROPSTEN_CODE:
await this._updateAccountsViaBalanceChecker(addresses, SINGLE_CALL_BALANCES_ADDRESS_ROPSTEN)
break
case KOVAN_CODE:
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().accounts
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)
return Promise.all(addresses.map(this._updateAccount.bind(this)))
}
addresses.forEach((address, index) => {
const balance = bnToHex(result[index])
accounts[address] = { address, balance }
})
this.store.updateState({ accounts })
})
}
}
module.exports = AccountTracker
|