@exodus/balances 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +232 -0
- package/package.json +34 -0
package/index.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { isNumberUnit } from '@exodus/currency'
|
|
2
|
+
import ExodusModule from '@exodus/module'
|
|
3
|
+
import { cloneDeepWith, difference, isEmpty, merge, set, zipObject } from 'lodash'
|
|
4
|
+
import makeConcurrent from 'make-concurrent'
|
|
5
|
+
|
|
6
|
+
const cloneBalances = (balances) =>
|
|
7
|
+
cloneDeepWith(balances, (val) => {
|
|
8
|
+
if (isNumberUnit(val)) return val
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const zipNonNull = (keys, values) =>
|
|
12
|
+
zipObject(
|
|
13
|
+
keys.filter((key, i) => values[i] != null),
|
|
14
|
+
values.filter((value) => value != null)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class Balances extends ExodusModule {
|
|
18
|
+
#assetsModule
|
|
19
|
+
#blockchainMetadata
|
|
20
|
+
#balanceFields
|
|
21
|
+
#walletAccounts = null // string[]
|
|
22
|
+
#ready = false
|
|
23
|
+
|
|
24
|
+
#oldBalances = {}
|
|
25
|
+
#balances = {}
|
|
26
|
+
|
|
27
|
+
constructor({ assetsModule, blockchainMetadata, walletAccountsAtom, balanceFields, logger }) {
|
|
28
|
+
super({ name: 'Balances', logger })
|
|
29
|
+
|
|
30
|
+
this.#assetsModule = assetsModule
|
|
31
|
+
this.#blockchainMetadata = blockchainMetadata
|
|
32
|
+
this.#balanceFields = balanceFields
|
|
33
|
+
|
|
34
|
+
const shareQueue =
|
|
35
|
+
(fn) =>
|
|
36
|
+
(...args) => {
|
|
37
|
+
if (!this.#ready) return
|
|
38
|
+
|
|
39
|
+
this.#pushIntoSharedQueue(() => fn.apply(this, args))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// these share a queue
|
|
43
|
+
this.#updateAssetSource = shareQueue(this.#updateAssetSource)
|
|
44
|
+
this.#recompute = shareQueue(this.#recompute)
|
|
45
|
+
|
|
46
|
+
blockchainMetadata.on('account-state', async (opts) => {
|
|
47
|
+
return this.#updateAssetSource(opts)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
blockchainMetadata.on('tx-log', async (opts) => {
|
|
51
|
+
if (this.#isHistoryBasedBalanceAsset(opts.assetName)) return this.#updateAssetSource(opts)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
walletAccountsAtom.observe((payload) =>
|
|
55
|
+
this.#setWalletAccounts(
|
|
56
|
+
Object.keys(payload).filter((walletAccount) => payload[walletAccount].enabled)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
assetsModule.on('assets-add', this.#onAssetsChanged)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#getAsset = (assetName) => this.#assetsModule.getAsset(assetName)
|
|
64
|
+
|
|
65
|
+
#pushIntoSharedQueue = makeConcurrent((fn) => fn(), { concurrency: 1 })
|
|
66
|
+
|
|
67
|
+
#reset = ({ walletAccounts = this.#walletAccounts } = {}) => {
|
|
68
|
+
walletAccounts.forEach((walletAccount) => {
|
|
69
|
+
this.#balances[walletAccount] = {}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#getBalancesForAssetSource = async ({ walletAccount, assetName }) => {
|
|
74
|
+
const metadata = await this.#loadBlockchainMetadataForBalance({ walletAccount, assetName })
|
|
75
|
+
return this.#getBalancesFromBlockchainMetadata({ assetName, ...metadata })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#getBalancesFromBlockchainMetadata = ({ assetName, accountState, txLog }) => {
|
|
79
|
+
// in the future accountState may have all the balances and we won't need to rely on txLog
|
|
80
|
+
// for now, accountState-based balance has both the baseAsset and token balances while txLog is per asset/token
|
|
81
|
+
const getAssetBalances = (assetName) => {
|
|
82
|
+
const asset = this.#getAsset(assetName)
|
|
83
|
+
return asset.api.getBalances({ asset, accountState, txLog })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.#isHistoryBasedBalanceAsset(assetName)) {
|
|
87
|
+
const balances = getAssetBalances(assetName)
|
|
88
|
+
return balances
|
|
89
|
+
? {
|
|
90
|
+
[assetName]: balances,
|
|
91
|
+
}
|
|
92
|
+
: {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const assetNames = [assetName, ...this.#assetsModule.getTokenNames(assetName)].filter(
|
|
96
|
+
this.#isSupportedAsset
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const balances = assetNames.map(getAssetBalances)
|
|
100
|
+
return zipNonNull(assetNames, balances)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#getBalancesForWalletAccount = async ({ walletAccount }) => {
|
|
104
|
+
const assetNames = this.#assetsModule.getBaseAssetNames()
|
|
105
|
+
const balances = await Promise.all(
|
|
106
|
+
assetNames.map((assetName) => this.#getBalancesForAssetSource({ assetName, walletAccount }))
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return merge({}, ...balances)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#load = async ({ walletAccount }) => {
|
|
113
|
+
this._logger.log(`loading walletAccount ${walletAccount}`)
|
|
114
|
+
this.#balances[walletAccount] = await this.#getBalancesForWalletAccount({ walletAccount })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#setWalletAccounts = (walletAccounts) => {
|
|
118
|
+
const added = difference(walletAccounts, this.#walletAccounts)
|
|
119
|
+
const removed = difference(this.#walletAccounts, walletAccounts)
|
|
120
|
+
if (added.length === 0 && removed.length === 0) return
|
|
121
|
+
|
|
122
|
+
this.#walletAccounts = walletAccounts
|
|
123
|
+
this.#ready = true
|
|
124
|
+
if (removed.length > 0) {
|
|
125
|
+
this._logger.log('recomputing after removing walletAccounts', removed)
|
|
126
|
+
removed.forEach((walletAccount) => {
|
|
127
|
+
delete this.#balances[walletAccount]
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (added.length > 0) {
|
|
132
|
+
this._logger.log('recomputing after adding walletAccounts', added)
|
|
133
|
+
this.#recompute({ walletAccounts: added })
|
|
134
|
+
} else {
|
|
135
|
+
this.sync()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#onAssetsChanged = async () => {
|
|
140
|
+
// TODO: make this more efficient
|
|
141
|
+
this._logger.log('recomputing based on new assets list')
|
|
142
|
+
this.#recompute()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#isSupportedAsset = (assetName) => !this.#getAsset(assetName).isCombined
|
|
146
|
+
|
|
147
|
+
#isHistoryBasedBalanceAsset = (assetName) => {
|
|
148
|
+
const asset = this.#getAsset(assetName)
|
|
149
|
+
return asset.api?.hasFeature('historyBasedBalance')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#loadBlockchainMetadataForBalance = async ({ assetName, walletAccount, accountState, txLog }) => {
|
|
153
|
+
return {
|
|
154
|
+
accountState:
|
|
155
|
+
accountState ||
|
|
156
|
+
(await this.#blockchainMetadata.getAccountState({
|
|
157
|
+
assetName: this.#getAsset(assetName).baseAsset.name,
|
|
158
|
+
walletAccount,
|
|
159
|
+
})),
|
|
160
|
+
txLog: txLog || (await this.#blockchainMetadata.getTxLog({ assetName, walletAccount })),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#updateAssetSource = async ({
|
|
165
|
+
assetName,
|
|
166
|
+
walletAccount,
|
|
167
|
+
accountState: providedAccountState,
|
|
168
|
+
txLog: providedTxLog,
|
|
169
|
+
}) => {
|
|
170
|
+
if (!this.#isSupportedAsset(assetName)) return
|
|
171
|
+
|
|
172
|
+
if (!this.#walletAccounts.includes(walletAccount)) {
|
|
173
|
+
this._logger.warn(`unexpected update for disabled walletAccount ${walletAccount}`)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { accountState, txLog } = await this.#loadBlockchainMetadataForBalance({
|
|
178
|
+
assetName,
|
|
179
|
+
walletAccount,
|
|
180
|
+
accountState: providedAccountState,
|
|
181
|
+
txLog: providedTxLog,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const accountBalances = this.#balances[walletAccount]
|
|
185
|
+
const newBalances = this.#getBalancesFromBlockchainMetadata({ assetName, accountState, txLog })
|
|
186
|
+
const changes = {}
|
|
187
|
+
for (let subAssetName in newBalances) {
|
|
188
|
+
const { currency } = this.#getAsset(subAssetName)
|
|
189
|
+
const newAssetBalances = newBalances?.[subAssetName]
|
|
190
|
+
// e.g. totalBalance or spendableBalance
|
|
191
|
+
for (const field of this.#balanceFields) {
|
|
192
|
+
const oldBalance = accountBalances?.[subAssetName]?.[field]
|
|
193
|
+
const newBalance = newAssetBalances?.[field]
|
|
194
|
+
if (!newBalance) continue
|
|
195
|
+
if ((oldBalance || currency.ZERO).equals(newBalance)) continue
|
|
196
|
+
|
|
197
|
+
this._logger.log(`updating assetSource ${walletAccount}:${assetName}`)
|
|
198
|
+
set(changes, [walletAccount, subAssetName, field], {
|
|
199
|
+
from: oldBalance ?? currency.ZERO,
|
|
200
|
+
to: newBalance,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
set(accountBalances, [subAssetName, field], newBalance)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isEmpty(changes)) this.sync({ changes })
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#recompute = async ({ walletAccounts = this.#walletAccounts } = {}) => {
|
|
211
|
+
this.#reset({ walletAccounts })
|
|
212
|
+
await Promise.all(walletAccounts.map((walletAccount) => this.#load({ walletAccount })))
|
|
213
|
+
this.sync()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
sync = ({ changes } = {}) => {
|
|
217
|
+
this._logger.log('balances updated')
|
|
218
|
+
|
|
219
|
+
const oldBalances = this.#oldBalances
|
|
220
|
+
this.emit('balances', {
|
|
221
|
+
balances: this.#balances,
|
|
222
|
+
oldBalances,
|
|
223
|
+
changes,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
this.#oldBalances = cloneBalances(this.#balances)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const createBalances = (opts) => new Balances(opts)
|
|
231
|
+
|
|
232
|
+
export default createBalances
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@exodus/balances",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "balances",
|
|
5
|
+
"author": "Exodus Movement Inc",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "yarn lint --fix"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ExodusMovement/exodus-hydra.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/ExodusMovement/exodus-hydra/tree/master/modules/balances",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/ExodusMovement/exodus-hydra/issues?q=is%3Aissue+is%3Aopen+label%3Abalances"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@exodus/currency": "^2.1.2",
|
|
25
|
+
"@exodus/module": "^1.0.0",
|
|
26
|
+
"lodash": "^4.17.21",
|
|
27
|
+
"make-concurrent": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@exodus/atoms": "^1.0.0",
|
|
31
|
+
"@exodus/basic-utils": "^0.7.0",
|
|
32
|
+
"@exodus/models": "^8.9.0"
|
|
33
|
+
}
|
|
34
|
+
}
|