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
|
/* 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')
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
// subscribe to latest block
this._blockTracker.on('latest', this._updateForBlock.bind(this))
// blockTracker.currentBlock may be null
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
}
/**
* 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
addresses.forEach(address => this._updateAccount(address))
}
/**
* 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)
}
}
/**
* 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)
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 })
}
}
module.exports = AccountTracker
|