@exodus/market-history 1.0.0 → 2.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/CHANGELOG.md +28 -0
- package/README.md +9 -4
- package/module/index.js +296 -0
- package/package.json +17 -10
- package/utils.js +11 -2
- package/index.js +0 -171
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## 1.1.0 (2023-01-24)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- add #/config per-key events ([#303](https://github.com/ExodusMovement/exodus-hydra/issues/303)) ([5391435](https://github.com/ExodusMovement/exodus-hydra/commit/539143598da10966ec18a93d9200542565682ecf))
|
|
11
|
+
- add `restricted-imports` eslint rule ([#719](https://github.com/ExodusMovement/exodus-hydra/issues/719)) ([175de9c](https://github.com/ExodusMovement/exodus-hydra/commit/175de9c19ec00e5a12441022c313837d58f38882))
|
|
12
|
+
- add market history monitor ([#206](https://github.com/ExodusMovement/exodus-hydra/issues/206)) ([121f026](https://github.com/ExodusMovement/exodus-hydra/commit/121f0268f2a3c5cb0a6a4b2788947714740a8c21))
|
|
13
|
+
- immediately show portfolio chart ([#1877](https://github.com/ExodusMovement/exodus-hydra/issues/1877)) ([ed6d9f6](https://github.com/ExodusMovement/exodus-hydra/commit/ed6d9f6e3daa3195fa6de1b91487315ac3282489))
|
|
14
|
+
- prevent monitors from running multiple times ([#497](https://github.com/ExodusMovement/exodus-hydra/issues/497)) ([81238ca](https://github.com/ExodusMovement/exodus-hydra/commit/81238cacaf893cb5019b5e02c481bcace1193f43))
|
|
15
|
+
- use real fusion sync in config ([#258](https://github.com/ExodusMovement/exodus-hydra/issues/258)) ([56f351c](https://github.com/ExodusMovement/exodus-hydra/commit/56f351cc18942499b47b5b3afecafaf181c2989b)), closes [#262](https://github.com/ExodusMovement/exodus-hydra/issues/262)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
- do not load rates on initial currency load ([#1548](https://github.com/ExodusMovement/exodus-hydra/issues/1548)) ([2eb5713](https://github.com/ExodusMovement/exodus-hydra/commit/2eb5713fe53ce3816a0857e917c1de6df91a15b2))
|
|
20
|
+
- do sync if started on start ([#766](https://github.com/ExodusMovement/exodus-hydra/issues/766)) ([1f19664](https://github.com/ExodusMovement/exodus-hydra/commit/1f19664bb8839328e6bb6ec9c9e965f78d8ae86b))
|
|
21
|
+
- fetch market-history prices for all assets, not only solana ([#829](https://github.com/ExodusMovement/exodus-hydra/issues/829)) ([36aa1d5](https://github.com/ExodusMovement/exodus-hydra/commit/36aa1d5dffb3773722bd930202be7c521fb71579))
|
|
22
|
+
- missing await ([#748](https://github.com/ExodusMovement/exodus-hydra/issues/748)) ([3f6b527](https://github.com/ExodusMovement/exodus-hydra/commit/3f6b527e04b7224324e9a2462c361a5c79816146))
|
|
23
|
+
- start monitors on popup re-open ([#249](https://github.com/ExodusMovement/exodus-hydra/issues/249)) ([2b6d257](https://github.com/ExodusMovement/exodus-hydra/commit/2b6d257eb5fd625801a695f6579b114509869750))
|
|
24
|
+
- use new sync event for local-config change ([#302](https://github.com/ExodusMovement/exodus-hydra/issues/302)) ([2042801](https://github.com/ExodusMovement/exodus-hydra/commit/2042801293b30f3347949f5bd687f665c7fa4ee9))
|
|
25
|
+
|
|
26
|
+
### Performance Improvements
|
|
27
|
+
|
|
28
|
+
- send less data from market prices module ([#1451](https://github.com/ExodusMovement/exodus-hydra/issues/1451)) ([eef3673](https://github.com/ExodusMovement/exodus-hydra/commit/eef3673c2dab332f4b1f071a001fccb0977b02c7))
|
package/README.md
CHANGED
|
@@ -6,19 +6,24 @@ returns prices on interval `close`.
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
yarn add @exodus
|
|
9
|
+
yarn add @exodus/market-history
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
14
|
```js
|
|
15
|
-
import createMarketHistoryMonitor from '@exodus/market-history'
|
|
15
|
+
import createMarketHistoryMonitor from '@exodus/market-history/module'
|
|
16
16
|
|
|
17
|
-
const marketHistoryMonitor = createMarketHistoryMonitor({
|
|
17
|
+
const marketHistoryMonitor = createMarketHistoryMonitor.factory({
|
|
18
18
|
assetsModule,
|
|
19
19
|
currencyAtom,
|
|
20
20
|
storage: marketHistoryStorage,
|
|
21
|
-
|
|
21
|
+
pricingClient,
|
|
22
|
+
getCacheKey: ({ currency, assetName, granularity }) =>
|
|
23
|
+
`prices-${currency}-${assetName}-${granularity}`, // optional
|
|
24
|
+
remoteConfigRefreshIntervalAtom, // optional
|
|
25
|
+
clearCacheAtom, // optional. set value if you want to clear cache.
|
|
26
|
+
remoteConfigClearCacheAtom, // optional. If you want to clear cache set any value in this atom.
|
|
22
27
|
})
|
|
23
28
|
|
|
24
29
|
marketHistoryMonitor.on('market-history', ({ currency, granularity, prices }) => {
|
package/module/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { mapValues, SynchronizedTime } from '@exodus/basic-utils'
|
|
2
|
+
import ExodusModule from '@exodus/module'
|
|
3
|
+
import { fetchHistoricalPrices, fetchPricesInterval } from '@exodus/price-api'
|
|
4
|
+
import ms from 'ms'
|
|
5
|
+
|
|
6
|
+
import { getAssetFromTicker, getAssetTickers, parseGranularity } from '../utils'
|
|
7
|
+
import { createInMemoryAtom, difference } from '@exodus/atoms'
|
|
8
|
+
|
|
9
|
+
const CLEAR_MARKET_HISTORY_CACHE_KEY = 'clear-market-history-cache'
|
|
10
|
+
const CLEAR_MARKET_HISTORY_CACHE_FROM_REMOTE_CONFIG_KEY =
|
|
11
|
+
'clear-market-history-cache-from-remote-config'
|
|
12
|
+
const MARKET_HISTORY_REFRESH_KEY = 'market-history-cache-refresh'
|
|
13
|
+
|
|
14
|
+
const transformPriceEntries = (entries = []) => {
|
|
15
|
+
const closePrices = entries.map((entry) => [entry[0], entry[1].close])
|
|
16
|
+
return Object.fromEntries(closePrices)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const transformPricesByAssetName = (pricesByAssetName) =>
|
|
20
|
+
mapValues(pricesByAssetName, (entries) => transformPriceEntries(entries))
|
|
21
|
+
|
|
22
|
+
// when provided cache version from local or remote config is different from stored on device we clear the cache and fetch new prices
|
|
23
|
+
const _invalidateStorageCache = async ({
|
|
24
|
+
storage,
|
|
25
|
+
clearRuntimeCache,
|
|
26
|
+
clearCacheVersion,
|
|
27
|
+
remoteConfigClearCacheVersion,
|
|
28
|
+
}) => {
|
|
29
|
+
const clearCacheVersionInStorage = await storage.get(CLEAR_MARKET_HISTORY_CACHE_KEY)
|
|
30
|
+
const clearCacheFromRemoteConfigVersionInStorage = await storage.get(
|
|
31
|
+
CLEAR_MARKET_HISTORY_CACHE_FROM_REMOTE_CONFIG_KEY
|
|
32
|
+
)
|
|
33
|
+
const localAreEqual = !clearCacheVersion || clearCacheVersionInStorage === clearCacheVersion
|
|
34
|
+
const remoteAreEqual =
|
|
35
|
+
!remoteConfigClearCacheVersion ||
|
|
36
|
+
clearCacheFromRemoteConfigVersionInStorage === remoteConfigClearCacheVersion
|
|
37
|
+
|
|
38
|
+
if (localAreEqual && remoteAreEqual) return // we already cleared cache
|
|
39
|
+
|
|
40
|
+
await storage.clear()
|
|
41
|
+
clearRuntimeCache()
|
|
42
|
+
|
|
43
|
+
return Promise.all([
|
|
44
|
+
storage
|
|
45
|
+
.set(CLEAR_MARKET_HISTORY_CACHE_KEY, clearCacheVersion || '')
|
|
46
|
+
.catch((e) => console.warn(e)),
|
|
47
|
+
storage
|
|
48
|
+
.set(CLEAR_MARKET_HISTORY_CACHE_FROM_REMOTE_CONFIG_KEY, remoteConfigClearCacheVersion || '')
|
|
49
|
+
.catch((e) => console.warn(e)),
|
|
50
|
+
])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getCacheKeyDefault = ({ currency, assetName, granularity }) =>
|
|
54
|
+
`prices-${currency}-${assetName}-${granularity}`
|
|
55
|
+
|
|
56
|
+
const MODULE_ID = 'marketHistory'
|
|
57
|
+
|
|
58
|
+
class MarketHistoryMonitor extends ExodusModule {
|
|
59
|
+
#pricingClient
|
|
60
|
+
#currencyAtom
|
|
61
|
+
#currency = null
|
|
62
|
+
#getCacheKey = getCacheKeyDefault
|
|
63
|
+
#remoteConfigRefreshIntervalAtom = null
|
|
64
|
+
#clearCacheAtom = null
|
|
65
|
+
#remoteConfigClearCacheAtom = null
|
|
66
|
+
#runtimeCache = new Map()
|
|
67
|
+
|
|
68
|
+
constructor({
|
|
69
|
+
assetsModule,
|
|
70
|
+
storage,
|
|
71
|
+
currencyAtom,
|
|
72
|
+
pricingClient,
|
|
73
|
+
getCacheKey = getCacheKeyDefault,
|
|
74
|
+
remoteConfigRefreshIntervalAtom = null,
|
|
75
|
+
clearCacheAtom = createInMemoryAtom({ defaultValue: null }),
|
|
76
|
+
remoteConfigClearCacheAtom = createInMemoryAtom({ defaultValue: null }),
|
|
77
|
+
}) {
|
|
78
|
+
super({ name: 'MarketHistoryMonitor' })
|
|
79
|
+
|
|
80
|
+
this.assetsModule = assetsModule
|
|
81
|
+
this.storage = storage
|
|
82
|
+
this.#pricingClient = pricingClient
|
|
83
|
+
this.#getCacheKey = getCacheKey
|
|
84
|
+
this.#remoteConfigRefreshIntervalAtom = remoteConfigRefreshIntervalAtom
|
|
85
|
+
this.#clearCacheAtom = clearCacheAtom
|
|
86
|
+
this.#remoteConfigClearCacheAtom = remoteConfigClearCacheAtom
|
|
87
|
+
this.#currencyAtom = currencyAtom
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#started = false
|
|
91
|
+
|
|
92
|
+
#setCache = async ({ currency, granularity, pricesByAssetName }) => {
|
|
93
|
+
const changes = Object.keys(pricesByAssetName).reduce((acc, assetName) => {
|
|
94
|
+
const key = this.#getCacheKey({ currency, assetName, granularity })
|
|
95
|
+
const values = pricesByAssetName[assetName]
|
|
96
|
+
if (values.length > 0) {
|
|
97
|
+
acc[key] = values
|
|
98
|
+
this.#runtimeCache.set(key, values)
|
|
99
|
+
}
|
|
100
|
+
return acc
|
|
101
|
+
}, {})
|
|
102
|
+
|
|
103
|
+
await this.storage.batchSet(changes)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#getCache = async ({ currency, granularity, assetName }) => {
|
|
107
|
+
const key = this.#getCacheKey({ currency, assetName, granularity })
|
|
108
|
+
const cachedValue = this.#runtimeCache.get(key)
|
|
109
|
+
if (cachedValue) {
|
|
110
|
+
return cachedValue
|
|
111
|
+
}
|
|
112
|
+
const cache = (await this.storage.get(key)) || []
|
|
113
|
+
this.#runtimeCache.set(key, cache)
|
|
114
|
+
|
|
115
|
+
return cache
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#getRuntimeCacheKey = ({ fiatTicker, granularity, assetTicker }) =>
|
|
119
|
+
this.#getCacheKey({
|
|
120
|
+
currency: fiatTicker,
|
|
121
|
+
granularity,
|
|
122
|
+
assetName: getAssetFromTicker(this.assetsModule.getAssets(), assetTicker).name,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
#fetch = async ({ currency, granularity }) => {
|
|
126
|
+
const assets = this.assetsModule.getAssets()
|
|
127
|
+
const assetTickers = getAssetTickers(assets)
|
|
128
|
+
|
|
129
|
+
const cacheRefreshData = (await this.storage.get(MARKET_HISTORY_REFRESH_KEY)) || {}
|
|
130
|
+
|
|
131
|
+
const cacheRefreshKey = `${granularity}-${currency}`
|
|
132
|
+
const latestRefreshTimestamp = cacheRefreshData[cacheRefreshKey] || 0
|
|
133
|
+
const refreshIntervalMs = this.#remoteConfigRefreshIntervalAtom
|
|
134
|
+
? await this.#remoteConfigRefreshIntervalAtom.get()
|
|
135
|
+
: null
|
|
136
|
+
const ignoreCache =
|
|
137
|
+
!!refreshIntervalMs &&
|
|
138
|
+
!Number.isNaN(refreshIntervalMs) &&
|
|
139
|
+
SynchronizedTime.now() - latestRefreshTimestamp > refreshIntervalMs
|
|
140
|
+
if (ignoreCache) {
|
|
141
|
+
cacheRefreshData[cacheRefreshKey] = SynchronizedTime.now()
|
|
142
|
+
await this.storage.set(MARKET_HISTORY_REFRESH_KEY, cacheRefreshData)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { fetchedPricesMap } = await fetchHistoricalPrices({
|
|
146
|
+
api: (...args) => this.#pricingClient.historicalPrice(...args),
|
|
147
|
+
assetTickers,
|
|
148
|
+
fiatTicker: currency,
|
|
149
|
+
granularity,
|
|
150
|
+
ignoreInvalidSymbols: true,
|
|
151
|
+
getCurrentTime: SynchronizedTime.now,
|
|
152
|
+
getCacheFromStorage: async (ticker) =>
|
|
153
|
+
this.#getCache({
|
|
154
|
+
currency,
|
|
155
|
+
granularity,
|
|
156
|
+
assetName: getAssetFromTicker(assets, ticker)?.name,
|
|
157
|
+
}),
|
|
158
|
+
ignoreCache,
|
|
159
|
+
runtimeCache: this.#runtimeCache,
|
|
160
|
+
getRuntimeCacheKey: ({ fiatTicker, granularity, assetTicker }) =>
|
|
161
|
+
this.#getRuntimeCacheKey({ fiatTicker, granularity, assetTicker }),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const pricesByAssetName = mapValues(assets, (asset) => {
|
|
165
|
+
const assetPricesMap = fetchedPricesMap.get(asset.ticker)
|
|
166
|
+
return assetPricesMap ? [...assetPricesMap] : []
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
this.#setCache({ currency, granularity, pricesByAssetName })
|
|
170
|
+
|
|
171
|
+
return transformPricesByAssetName(pricesByAssetName)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
update = async (granularity) => {
|
|
175
|
+
const parsedGranularity = parseGranularity(granularity)
|
|
176
|
+
const currency = this.#currency
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const prices = await this.#fetch({ currency, granularity })
|
|
180
|
+
this.emit('market-history', { currency, granularity: parsedGranularity, prices })
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(
|
|
183
|
+
'MarketHistoryMonitor: Failed to fetch rates',
|
|
184
|
+
currency,
|
|
185
|
+
parsedGranularity,
|
|
186
|
+
error
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#updateAll = () => {
|
|
192
|
+
return Promise.all([this.update('hour'), this.update('day')])
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#syncGranularity = async (granularity) => {
|
|
196
|
+
const parsedGranularity = parseGranularity(granularity)
|
|
197
|
+
const currency = this.#currency
|
|
198
|
+
const assets = this.assetsModule.getAssets()
|
|
199
|
+
|
|
200
|
+
const cachedPricesByAssetName = Object.fromEntries(
|
|
201
|
+
Object.keys(assets).map((assetName) => {
|
|
202
|
+
const key = this.#getCacheKey({ currency, assetName, granularity })
|
|
203
|
+
return [assetName, this.#runtimeCache.get(key)]
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const prices = transformPricesByAssetName(cachedPricesByAssetName)
|
|
208
|
+
this.emit('market-history', { currency, granularity: parsedGranularity, prices })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
sync = async () => {
|
|
212
|
+
this.#syncGranularity('day')
|
|
213
|
+
this.#syncGranularity('hour')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
invalidateStorage = async ({ remoteConfigClearCacheVersion }) => {
|
|
217
|
+
const clearCacheVersion = await this.#clearCacheAtom.get() // don't think we need to observe it considering this runs only on startup and set manually in config
|
|
218
|
+
return _invalidateStorageCache({
|
|
219
|
+
storage: this.storage,
|
|
220
|
+
clearRuntimeCache: () => {
|
|
221
|
+
this.#runtimeCache = new Map()
|
|
222
|
+
},
|
|
223
|
+
clearCacheVersion,
|
|
224
|
+
remoteConfigClearCacheVersion,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#listenRemoteConfigClearCacheVersionChanges = () => {
|
|
229
|
+
difference(this.#remoteConfigClearCacheAtom).observe(async ({ current, previous }) => {
|
|
230
|
+
if (previous !== undefined && current !== previous) {
|
|
231
|
+
await this.invalidateStorage({ remoteConfigClearCacheVersion: current })
|
|
232
|
+
await this.#updateAll()
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#setupTimers = () => {
|
|
238
|
+
const jitter = ms('3s')
|
|
239
|
+
const getCurrentTime = SynchronizedTime.now
|
|
240
|
+
const updateDayPrices = this.update.bind(this, 'day')
|
|
241
|
+
const updateHourPrices = this.update.bind(this, 'hour')
|
|
242
|
+
|
|
243
|
+
fetchPricesInterval({
|
|
244
|
+
func: updateDayPrices,
|
|
245
|
+
granularity: 'day',
|
|
246
|
+
jitter,
|
|
247
|
+
getCurrentTime,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
fetchPricesInterval({
|
|
251
|
+
func: updateHourPrices,
|
|
252
|
+
granularity: 'hour',
|
|
253
|
+
jitter,
|
|
254
|
+
getCurrentTime,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
start = async () => {
|
|
259
|
+
if (this.#started) {
|
|
260
|
+
await this.sync()
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.#started = true
|
|
265
|
+
|
|
266
|
+
const remoteConfigClearCacheVersion = await this.#remoteConfigClearCacheAtom.get()
|
|
267
|
+
await this.invalidateStorage({ remoteConfigClearCacheVersion })
|
|
268
|
+
this.#listenRemoteConfigClearCacheVersionChanges()
|
|
269
|
+
|
|
270
|
+
await this.#currencyAtom.observe((currency) => {
|
|
271
|
+
if (this.#currency) {
|
|
272
|
+
this.#currency = currency
|
|
273
|
+
this.#updateAll()
|
|
274
|
+
} else {
|
|
275
|
+
this.#currency = currency
|
|
276
|
+
this.#setupTimers()
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const createMarketHistoryMonitor = (args = {}) => new MarketHistoryMonitor({ ...args })
|
|
283
|
+
|
|
284
|
+
export default {
|
|
285
|
+
id: MODULE_ID,
|
|
286
|
+
factory: createMarketHistoryMonitor,
|
|
287
|
+
dependencies: [
|
|
288
|
+
'pricingClient',
|
|
289
|
+
'currencyAtom',
|
|
290
|
+
'storage',
|
|
291
|
+
'assetsModule',
|
|
292
|
+
'clearCacheAtom',
|
|
293
|
+
'remoteConfigClearCacheAtom',
|
|
294
|
+
'remoteConfigRefreshIntervalAtom',
|
|
295
|
+
],
|
|
296
|
+
}
|
package/package.json
CHANGED
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/market-history",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "module to fetch historical prices for assets",
|
|
5
5
|
"author": "Exodus Movement Inc",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"lint": "eslint .",
|
|
8
|
+
"lint": "eslint . --ignore-path ../../.gitignore",
|
|
9
9
|
"lint:fix": "yarn lint --fix",
|
|
10
10
|
"test": "jest"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"
|
|
14
|
-
"utils.js"
|
|
13
|
+
"module",
|
|
14
|
+
"utils.js",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"README.md",
|
|
17
|
+
"!**/__tests__"
|
|
15
18
|
],
|
|
16
19
|
"dependencies": {
|
|
17
20
|
"@exodus/basic-utils": "^1.0.0",
|
|
18
|
-
"@exodus/module": "^1.
|
|
19
|
-
"@exodus/price-api": "^2.
|
|
21
|
+
"@exodus/module": "^1.1.0",
|
|
22
|
+
"@exodus/price-api": "^3.2.1",
|
|
20
23
|
"ms": "^0.7.1"
|
|
21
24
|
},
|
|
22
25
|
"devDependencies": {
|
|
23
26
|
"@exodus/assets": "^8.0.72",
|
|
24
|
-
"@exodus/atoms": "^
|
|
25
|
-
"@exodus/storage-memory": "^1.0.0"
|
|
27
|
+
"@exodus/atoms": "^2.3.0",
|
|
28
|
+
"@exodus/storage-memory": "^1.0.0",
|
|
29
|
+
"eslint": "^8.33.0",
|
|
30
|
+
"events": "^3.3.0",
|
|
31
|
+
"jest": "^29.1.2"
|
|
26
32
|
},
|
|
27
33
|
"peerDependencies": {
|
|
28
34
|
"debug": ">= 3.0.0"
|
|
29
|
-
}
|
|
30
|
-
|
|
35
|
+
},
|
|
36
|
+
"gitHead": "37c209ea7fe3d24bc78fbb530f738a8ff3a87723"
|
|
37
|
+
}
|
package/utils.js
CHANGED
|
@@ -3,8 +3,17 @@ export const getAssetTickers = (assets) =>
|
|
|
3
3
|
.map((asset) => asset.ticker)
|
|
4
4
|
.sort()
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
let assetFromTickerCache = {}
|
|
7
|
+
export const getAssetFromTicker = (assets, ticker) => {
|
|
8
|
+
if (assetFromTickerCache[ticker]) return assetFromTickerCache[ticker]
|
|
9
|
+
return Object.values(assets).find((asset) => {
|
|
10
|
+
if (asset.ticker === ticker) {
|
|
11
|
+
assetFromTickerCache[ticker] = asset
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
})
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
export const parseGranularity = (granularity) => {
|
|
10
19
|
switch (granularity) {
|
package/index.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { mapValues, SynchronizedTime } from '@exodus/basic-utils'
|
|
2
|
-
import ExodusModule from '@exodus/module'
|
|
3
|
-
import { fetchHistoricalPrices, fetchPricesInterval } from '@exodus/price-api'
|
|
4
|
-
import ms from 'ms'
|
|
5
|
-
|
|
6
|
-
import { getAssetFromTicker, getAssetTickers, parseGranularity } from './utils'
|
|
7
|
-
|
|
8
|
-
const MARKET_HISTORY_CACHE_VERSION = 'v1'
|
|
9
|
-
|
|
10
|
-
const transformPriceEntries = (entries = []) => {
|
|
11
|
-
const closePrices = entries.map((entry) => [entry[0], entry[1].close])
|
|
12
|
-
return Object.fromEntries(closePrices)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const transformPricesByAssetName = (pricesByAssetName) =>
|
|
16
|
-
mapValues(pricesByAssetName, (entries) => transformPriceEntries(entries))
|
|
17
|
-
|
|
18
|
-
class MarketHistoryMonitor extends ExodusModule {
|
|
19
|
-
#pricingServer
|
|
20
|
-
#currencyAtom
|
|
21
|
-
#previousCurrency = null
|
|
22
|
-
|
|
23
|
-
constructor({ assetsModule, storage, currencyAtom, pricingServer }) {
|
|
24
|
-
super({ name: 'MarketHistoryMonitor' })
|
|
25
|
-
|
|
26
|
-
this.assetsModule = assetsModule
|
|
27
|
-
this.storage = storage
|
|
28
|
-
this.#pricingServer = pricingServer
|
|
29
|
-
|
|
30
|
-
this.#currencyAtom = currencyAtom
|
|
31
|
-
this.#currencyAtom.observe((value) => value && this.#onCurrencyChange(value))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
#started = false
|
|
35
|
-
|
|
36
|
-
#onCurrencyChange = async (value) => {
|
|
37
|
-
if (this.#previousCurrency === null) {
|
|
38
|
-
this.#previousCurrency = value
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this.#update('day')
|
|
43
|
-
this.#update('hour')
|
|
44
|
-
|
|
45
|
-
this.#previousCurrency = value
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
#getCacheKey = ({ currency, assetName, granularity }) => {
|
|
49
|
-
return `${currency}-${assetName}-${granularity}-${MARKET_HISTORY_CACHE_VERSION}`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
#setCache = async ({ currency, granularity, pricesByAssetName }) => {
|
|
53
|
-
const assets = this.assetsModule.getAssets()
|
|
54
|
-
|
|
55
|
-
const promises = Object.values(assets).map((asset) => {
|
|
56
|
-
const key = this.#getCacheKey({ currency, assetName: asset.name, granularity })
|
|
57
|
-
const values = pricesByAssetName[asset.name]
|
|
58
|
-
return this.storage.set(key, values)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
await Promise.all(promises)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
#getCache = async ({ currency, granularity, assetName }) => {
|
|
65
|
-
const key = this.#getCacheKey({ currency, assetName, granularity })
|
|
66
|
-
const cache = await this.storage.get(key)
|
|
67
|
-
return cache || []
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
#fetch = async ({ currency, granularity }) => {
|
|
71
|
-
const assets = this.assetsModule.getAssets()
|
|
72
|
-
const assetTickers = getAssetTickers(assets)
|
|
73
|
-
|
|
74
|
-
const pricesMap = await fetchHistoricalPrices({
|
|
75
|
-
api: (...args) => this.#pricingServer.historicalPrice(...args),
|
|
76
|
-
assetTickers,
|
|
77
|
-
fiatTicker: currency,
|
|
78
|
-
granularity,
|
|
79
|
-
ignoreInvalidSymbols: true,
|
|
80
|
-
getCurrentTime: SynchronizedTime.now,
|
|
81
|
-
getCacheFromStorage: async (ticker) =>
|
|
82
|
-
this.#getCache({
|
|
83
|
-
currency,
|
|
84
|
-
granularity,
|
|
85
|
-
assetName: getAssetFromTicker(assets, ticker)?.name,
|
|
86
|
-
}),
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const pricesByAssetName = mapValues(assets, (asset) => {
|
|
90
|
-
const assetPricesMap = pricesMap.get(asset.ticker)
|
|
91
|
-
return assetPricesMap ? [...assetPricesMap] : []
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
this.#setCache({ currency, granularity, pricesByAssetName })
|
|
95
|
-
|
|
96
|
-
return transformPricesByAssetName(pricesByAssetName)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
#getCurrency = async () => {
|
|
100
|
-
return this.#currencyAtom.get()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
#update = async (granularity) => {
|
|
104
|
-
const parsedGranularity = parseGranularity(granularity)
|
|
105
|
-
const currency = await this.#getCurrency()
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const prices = await this.#fetch({ currency, granularity })
|
|
109
|
-
this.emit('market-history', { currency, granularity: parsedGranularity, prices })
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error(
|
|
112
|
-
'MarketHistoryMonitor: Failed to fetch rates',
|
|
113
|
-
currency,
|
|
114
|
-
parsedGranularity,
|
|
115
|
-
error
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
#syncGranularity = async (granularity) => {
|
|
121
|
-
const parsedGranularity = parseGranularity(granularity)
|
|
122
|
-
const currency = await this.#getCurrency()
|
|
123
|
-
const assets = this.assetsModule.getAssets()
|
|
124
|
-
|
|
125
|
-
const keys = Object.keys(assets).map((assetName) =>
|
|
126
|
-
this.#getCacheKey({ currency, assetName, granularity })
|
|
127
|
-
)
|
|
128
|
-
const cachedPrices = await this.storage.batchGet(keys)
|
|
129
|
-
|
|
130
|
-
const cachedPricesByAssetName = Object.fromEntries(
|
|
131
|
-
Object.keys(assets).map((assetName, index) => [assetName, cachedPrices[index]])
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const prices = transformPricesByAssetName(cachedPricesByAssetName)
|
|
135
|
-
this.emit('market-history', { currency, granularity: parsedGranularity, prices })
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
sync = async () => {
|
|
139
|
-
this.#syncGranularity('day')
|
|
140
|
-
this.#syncGranularity('hour')
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
start = async () => {
|
|
144
|
-
if (this.#started) return
|
|
145
|
-
|
|
146
|
-
this.#started = true
|
|
147
|
-
|
|
148
|
-
const jitter = ms('3s')
|
|
149
|
-
const getCurrentTime = SynchronizedTime.now
|
|
150
|
-
const updateDayPrices = this.#update.bind(this, 'day')
|
|
151
|
-
const updateHourPrices = this.#update.bind(this, 'hour')
|
|
152
|
-
|
|
153
|
-
fetchPricesInterval({
|
|
154
|
-
func: updateDayPrices,
|
|
155
|
-
granularity: 'day',
|
|
156
|
-
jitter,
|
|
157
|
-
getCurrentTime,
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
fetchPricesInterval({
|
|
161
|
-
func: updateHourPrices,
|
|
162
|
-
granularity: 'hour',
|
|
163
|
-
jitter,
|
|
164
|
-
getCurrentTime,
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const createMarketHistoryMonitor = (args = {}) => new MarketHistoryMonitor({ ...args })
|
|
170
|
-
|
|
171
|
-
export default createMarketHistoryMonitor
|