@paraswap/dex-lib 2.14.0 → 2.14.2

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.
Files changed (107) hide show
  1. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  2. package/.idea/misc.xml +6 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/.vscode/launch.json +21 -0
  6. package/build/config.js +23 -5
  7. package/build/config.js.map +1 -1
  8. package/build/dex/aave-v3/tokens/arbitrum.json +50 -0
  9. package/build/dex/aave-v3/tokens/avalanche.json +50 -0
  10. package/build/dex/aave-v3/tokens/fantom.json +62 -0
  11. package/build/dex/aave-v3/tokens/mainnet.json +44 -0
  12. package/build/dex/aave-v3/tokens/optimism.json +50 -0
  13. package/build/dex/aave-v3/tokens/polygon.json +98 -0
  14. package/build/dex/balancer-v2/VirtualBoostedPool.d.ts +79 -0
  15. package/build/dex/balancer-v2/VirtualBoostedPool.js +382 -0
  16. package/build/dex/balancer-v2/VirtualBoostedPool.js.map +1 -0
  17. package/build/dex/generic-rfq/e2e-test-config.d.ts +9 -0
  18. package/build/dex/generic-rfq/e2e-test-config.js +33 -0
  19. package/build/dex/generic-rfq/e2e-test-config.js.map +1 -0
  20. package/build/dex/generic-rfq/rate-fetcher.js +2 -0
  21. package/build/dex/generic-rfq/rate-fetcher.js.map +1 -1
  22. package/build/dex/generic-rfq/validators.d.ts +1 -1
  23. package/build/dex/generic-rfq/validators.js +36 -26
  24. package/build/dex/generic-rfq/validators.js.map +1 -1
  25. package/build/dex/index.js +2 -2
  26. package/build/dex/index.js.map +1 -1
  27. package/build/dex/simple-exchange.d.ts +1 -0
  28. package/build/dex/simple-exchange.js +2 -0
  29. package/build/dex/simple-exchange.js.map +1 -1
  30. package/build/dex/test-aave-v3/config.d.ts +4 -0
  31. package/build/dex/test-aave-v3/config.js +15 -0
  32. package/build/dex/test-aave-v3/config.js.map +1 -0
  33. package/build/dex/test-aave-v3/test-aave-v3-pool.d.ts +39 -0
  34. package/build/dex/test-aave-v3/test-aave-v3-pool.js +63 -0
  35. package/build/dex/test-aave-v3/test-aave-v3-pool.js.map +1 -0
  36. package/build/dex/test-aave-v3/test-aave-v3.d.ts +37 -0
  37. package/build/dex/test-aave-v3/test-aave-v3.js +126 -0
  38. package/build/dex/test-aave-v3/test-aave-v3.js.map +1 -0
  39. package/build/dex/test-aave-v3/types.d.ts +6 -0
  40. package/build/dex/test-aave-v3/types.js +3 -0
  41. package/build/dex/test-aave-v3/types.js.map +1 -0
  42. package/build/lib/decoders.d.ts +2 -1
  43. package/build/lib/decoders.js +8 -1
  44. package/build/lib/decoders.js.map +1 -1
  45. package/build/lib/fetcher/fetcher.js +3 -0
  46. package/build/lib/fetcher/fetcher.js.map +1 -1
  47. package/build/lib/multi-wrapper.d.ts +2 -1
  48. package/build/lib/multi-wrapper.js.map +1 -1
  49. package/build/lib/tokens/balancer-fetcher.js +4 -5
  50. package/build/lib/tokens/balancer-fetcher.js.map +1 -1
  51. package/build/lib/tokens/utils.d.ts +2 -2
  52. package/build/lib/tokens/utils.js +1 -1
  53. package/build/lib/tokens/utils.js.map +1 -1
  54. package/build/types.d.ts +3 -0
  55. package/dex-template/__DexName__-e2e.test.ts.template +1 -0
  56. package/dex-template/__DexName__-events.test.ts.template +1 -0
  57. package/dex-template/__DexName__-integration.test.ts.template +1 -0
  58. package/dex-template/__DexName__-pool.ts.template +8 -5
  59. package/package.json +1 -1
  60. package/scripts/generate-bytecode.ts +2 -0
  61. package/src/abi/woo-fi-v2/IntegrationHelper.abi.json +116 -0
  62. package/src/abi/{woo-fi/Wooracle.abi.json → woo-fi-v2/WooOracleV2.abi.json} +148 -50
  63. package/src/abi/{woo-fi/WooPP.abi.json → woo-fi-v2/WooPPV2.abi.json} +201 -169
  64. package/src/config.ts +27 -5
  65. package/src/dex/balancer-v2/balancer-v2-e2e.test.ts +0 -1
  66. package/src/dex/generic-rfq/e2e-test-config.ts +37 -0
  67. package/src/dex/generic-rfq/generic-rfq-e2e.test.ts +63 -258
  68. package/src/dex/generic-rfq/generic-rfq-local-e2e.test.ts +121 -0
  69. package/src/dex/generic-rfq/rate-fetcher.ts +2 -0
  70. package/src/dex/generic-rfq/validators.test.ts +6 -32
  71. package/src/dex/generic-rfq/validators.ts +57 -32
  72. package/src/dex/index.ts +2 -2
  73. package/src/dex/simple-exchange.ts +3 -0
  74. package/src/dex/woo-fi-v2/config.ts +87 -0
  75. package/src/dex/woo-fi-v2/constants.ts +22 -0
  76. package/src/dex/woo-fi-v2/decoders.ts +45 -0
  77. package/src/dex/woo-fi-v2/types.ts +63 -0
  78. package/src/dex/woo-fi-v2/utils.ts +19 -0
  79. package/src/dex/woo-fi-v2/woo-fi-v2-e2e.test.ts +216 -0
  80. package/src/dex/woo-fi-v2/woo-fi-v2-integration.test.ts +495 -0
  81. package/src/dex/woo-fi-v2/woo-fi-v2-math.ts +195 -0
  82. package/src/dex/woo-fi-v2/woo-fi-v2-pool.ts +112 -0
  83. package/src/dex/woo-fi-v2/woo-fi-v2.ts +487 -0
  84. package/src/lib/decoders.ts +12 -1
  85. package/src/lib/fetcher/fetcher.ts +6 -0
  86. package/src/lib/log-messages-suppressor.ts +173 -0
  87. package/src/lib/multi-wrapper.ts +2 -1
  88. package/src/lib/stateful-rpc-poller/state-polling-manager.ts +320 -0
  89. package/src/lib/stateful-rpc-poller/stateful-rpc-poller.ts +604 -0
  90. package/src/lib/stateful-rpc-poller/types.ts +66 -0
  91. package/src/lib/stateful-rpc-poller/utils.ts +23 -0
  92. package/src/lib/tokens/balancer-fetcher.ts +5 -6
  93. package/src/lib/tokens/utils.ts +5 -3
  94. package/src/types.ts +4 -0
  95. package/tests/constants-e2e.ts +10 -3
  96. package/tests/tenderly-simulation.ts +1 -1
  97. package/tests/utils-e2e.ts +61 -48
  98. package/tests/utils.ts +2 -0
  99. package/src/abi/woo-fi/WooFeeManager.abi.json +0 -318
  100. package/src/dex/woo-fi/config.ts +0 -141
  101. package/src/dex/woo-fi/constants.ts +0 -21
  102. package/src/dex/woo-fi/types.ts +0 -55
  103. package/src/dex/woo-fi/woo-fi-decimal-math.ts +0 -45
  104. package/src/dex/woo-fi/woo-fi-e2e.test.ts +0 -183
  105. package/src/dex/woo-fi/woo-fi-integration.test.ts +0 -416
  106. package/src/dex/woo-fi/woo-fi-math.ts +0 -527
  107. package/src/dex/woo-fi/woo-fi.ts +0 -693
@@ -0,0 +1,604 @@
1
+ import { Logger, LogLevels } from '../../types';
2
+ import { IDexHelper } from '../../dex-helper';
3
+ import {
4
+ IStatefulRpcPoller,
5
+ ObjWithUpdateInfo,
6
+ PollingManagerControllersCb,
7
+ StateSources,
8
+ } from './types';
9
+ import { MultiCallParams, MultiResult } from '../multi-wrapper';
10
+ import { CACHE_PREFIX } from '../../constants';
11
+ import { uint256DecodeToNumber } from '../decoders';
12
+ import { assert } from 'ts-essentials';
13
+ import { LogMessagesSuppressor, MessageInfo } from '../log-messages-suppressor';
14
+ import { Utils } from '../../utils';
15
+ import { getIdentifierKeyForRpcPoller } from './utils';
16
+
17
+ const StatefulRPCPollerMessages = {
18
+ ERROR_FETCHING_STATE_FROM_CACHE: `Unexpected error while fetching state from Cache`,
19
+ ERROR_FETCHING_STATE_FROM_RPC: `Unexpected error while fetching state from RPC`,
20
+ ERROR_SAVING_LIQUIDITY_IN_CACHE: `Unexpected error while saving liquidity in Cache`,
21
+ ERROR_SAVING_STATE_IN_CACHE: `Unexpected error while saving state in Cache`,
22
+
23
+ FALLBACK_TO_RPC: `Failed to retrieve updated state from cache. Falling back to RPC`,
24
+
25
+ LIQUIDITY_INFO_IS_OUTDATED:
26
+ `Liquidity info in USD is outdated. Wil be updating ` +
27
+ `every pool that is outdated to not degrade performance`,
28
+ } as const;
29
+
30
+ const DEFAULT_LIQUIDITY_UPDATE_PERIOD_MS = 2 * 60 * 1000;
31
+ const DEFAULT_STATE_INIT_RETRY_MS = 1000;
32
+
33
+ export abstract class StatefulRpcPoller<State, M>
34
+ implements IStatefulRpcPoller<State, M>
35
+ {
36
+ static StatefulRPCPollerMessages: Record<
37
+ keyof typeof StatefulRPCPollerMessages,
38
+ MessageInfo<typeof StatefulRPCPollerMessages>
39
+ > = {
40
+ ERROR_FETCHING_STATE_FROM_CACHE: {
41
+ key: 'ERROR_FETCHING_STATE_FROM_CACHE',
42
+ message: StatefulRPCPollerMessages.ERROR_FETCHING_STATE_FROM_CACHE,
43
+ logLevel: 'error',
44
+ },
45
+ ERROR_FETCHING_STATE_FROM_RPC: {
46
+ key: 'ERROR_FETCHING_STATE_FROM_RPC',
47
+ message: StatefulRPCPollerMessages.ERROR_FETCHING_STATE_FROM_RPC,
48
+ logLevel: 'error',
49
+ },
50
+ ERROR_SAVING_LIQUIDITY_IN_CACHE: {
51
+ key: 'ERROR_SAVING_LIQUIDITY_IN_CACHE',
52
+ message: StatefulRPCPollerMessages.ERROR_SAVING_LIQUIDITY_IN_CACHE,
53
+ logLevel: 'error',
54
+ },
55
+ ERROR_SAVING_STATE_IN_CACHE: {
56
+ key: 'ERROR_SAVING_STATE_IN_CACHE',
57
+ message: StatefulRPCPollerMessages.ERROR_SAVING_STATE_IN_CACHE,
58
+ logLevel: 'error',
59
+ },
60
+ LIQUIDITY_INFO_IS_OUTDATED: {
61
+ key: 'LIQUIDITY_INFO_IS_OUTDATED',
62
+ message: StatefulRPCPollerMessages.LIQUIDITY_INFO_IS_OUTDATED,
63
+ logLevel: 'error',
64
+ },
65
+ FALLBACK_TO_RPC: {
66
+ key: 'FALLBACK_TO_RPC',
67
+ message: StatefulRPCPollerMessages.FALLBACK_TO_RPC,
68
+ logLevel: 'warn',
69
+ },
70
+ };
71
+
72
+ // The current state and its block number
73
+ // Derived classes should not set these directly, and instead use setState()
74
+ protected _stateWithUpdateInfo?: ObjWithUpdateInfo<State>;
75
+
76
+ // This values is used to determine if current pool will participate in update or not
77
+ // We don't want to keep track of state for pools without liquidity
78
+ protected _liquidityInUSDWithUpdateInfo: ObjWithUpdateInfo<number> = {
79
+ value: 0,
80
+ blockNumber: 0,
81
+ lastUpdatedAtMs: 0,
82
+ };
83
+
84
+ readonly cacheStateKey: string;
85
+ readonly cacheLiquidityMapKey: string;
86
+
87
+ // Store here encoded calls for blockNumber, blockTimestamp etc.
88
+ protected _cachedMultiCallData?: [
89
+ MultiCallParams<number>,
90
+ ...MultiCallParams<M>[],
91
+ ];
92
+
93
+ protected _getBlockNumberMultiCall: MultiCallParams<number>;
94
+
95
+ readonly entityName: string;
96
+
97
+ protected logMessagesSuppressor: LogMessagesSuppressor<
98
+ typeof StatefulRPCPollerMessages
99
+ >;
100
+
101
+ // This flag is changed only via isPoolParticipateInUpdates setter
102
+ // It is used to determine if we should update state or not
103
+ protected _isPoolParticipateInUpdates: boolean = true;
104
+
105
+ protected _isPoolInTheMiddleOfUpdate: boolean = false;
106
+
107
+ readonly identifierKey: string;
108
+
109
+ protected logger: Logger;
110
+
111
+ constructor(
112
+ readonly dexKey: string,
113
+ poolIdentifier: string,
114
+ protected dexHelper: IDexHelper,
115
+
116
+ // If liquidity is less than this value, we will not update state
117
+ protected liquidityThresholdForUpdate: number,
118
+ // If for some reason liquidity update is broken, for this delay we will still
119
+ // continue to serve update state
120
+ protected liquidityUpdateAllowedDelayMs: number,
121
+
122
+ // For some pools we don't worry about liquidity and want always to keep state updated
123
+ protected isLiquidityTracked: boolean,
124
+
125
+ // Polling manager callbacks. They are useful when you want
126
+ // to give some change information in reverse way.
127
+ // For example, you changed liquidity state, notify manager to
128
+ // not poll that particular pools
129
+ protected managerCbControllers: PollingManagerControllersCb,
130
+
131
+ // If the state is outdated more than this amount of blocks, we will not use this state anymore
132
+ protected maxAllowedStateDelayInBlocks: number = dexHelper.config.data
133
+ .rpcPollingMaxAllowedStateDelayInBlocks,
134
+
135
+ // When cross this threshold we will trigger update. The idea is this number is less than
136
+ // maxAllowedStateDelayToUpdate, so we trigger update before state become invalid.
137
+ // If blocksBackToTriggerUpdate = maxAllowedStateDelayInBlocks, we will do our best
138
+ // effort to keep up with a tip of chain: trigger update on every new block
139
+ protected blocksBackToTriggerUpdate: number = dexHelper.config.data
140
+ .rpcPollingBlocksBackToTriggerUpdate,
141
+
142
+ protected liquidityUpdatePeriodMs = DEFAULT_LIQUIDITY_UPDATE_PERIOD_MS,
143
+ ) {
144
+ // Don't make it too custom, like adding poolIdentifier. It will break log suppressor
145
+ // If we are really need to do that, update log Suppressor to handle that case
146
+ // The idea is to have one entity name per Dex level, not pool level
147
+ this.entityName = `StatefulPoller-${this.dexKey}-${this.dexHelper.config.data.network}`;
148
+
149
+ // I made it a little bit different from poolIdentifier, because usually
150
+ // pool identifier doesn't contain network information and it may occasionally
151
+ // collide across chains, though that scenario is very unlikely to happen
152
+ // For consumers they still can use poolIdentifier to get instance, but since
153
+ // we always give network information, it is better we can hide this implementation detail
154
+ this.identifierKey = getIdentifierKeyForRpcPoller(
155
+ poolIdentifier,
156
+ this.network,
157
+ );
158
+
159
+ this.cacheLiquidityMapKey =
160
+ `${CACHE_PREFIX}_${this.network}_${this.dexKey}_liquidity_usd`.toLowerCase();
161
+ this.cacheStateKey =
162
+ `${CACHE_PREFIX}_${this.network}_${this.dexKey}_states`.toLowerCase();
163
+
164
+ this.logger = this.dexHelper.getLogger(`${this.entityName}`);
165
+
166
+ assert(
167
+ this.maxAllowedStateDelayInBlocks >= 0,
168
+ `${this.dexKey}-${this.network}-${this.identifierKey}: ` +
169
+ `Max allowed state delay must be >= 0. Received ${this.maxAllowedStateDelayInBlocks}`,
170
+ );
171
+
172
+ assert(
173
+ this.blocksBackToTriggerUpdate >= 0,
174
+ `${this.dexKey}-${this.network}-${this.identifierKey}: ` +
175
+ `Blocks back to trigger update must be >= 0. Received ${this.blocksBackToTriggerUpdate}`,
176
+ );
177
+
178
+ // You can not go more blocks back than you allow to be delayed
179
+ assert(
180
+ this.blocksBackToTriggerUpdate <= this.maxAllowedStateDelayInBlocks,
181
+ `${this.dexKey}-${this.network}-${this.identifierKey}: ` +
182
+ `Blocks back to trigger update must be <= maxAllowedStateDelayInBlocks. ` +
183
+ `Received ${this.blocksBackToTriggerUpdate} > ${this.maxAllowedStateDelayInBlocks}`,
184
+ );
185
+
186
+ this._getBlockNumberMultiCall = {
187
+ target: this.dexHelper.multiContract.options.address,
188
+ callData: this.dexHelper.multiContract.methods
189
+ .getBlockNumber()
190
+ .encodeABI(),
191
+ decodeFunction: uint256DecodeToNumber,
192
+ };
193
+
194
+ this.logMessagesSuppressor = LogMessagesSuppressor.getLogSuppressorInstance<
195
+ typeof StatefulRPCPollerMessages
196
+ >(
197
+ this.entityName,
198
+ StatefulRpcPoller.StatefulRPCPollerMessages,
199
+ this.logger,
200
+ );
201
+
202
+ // This is done to not rely on manual initialize pool call.
203
+ // Any time new pool initialized, it is already automatically registered in polling manager
204
+ this.managerCbControllers.registerPendingPool(this);
205
+ }
206
+
207
+ get network() {
208
+ return this.dexHelper.config.data.network;
209
+ }
210
+
211
+ protected get isMaster() {
212
+ return !this.dexHelper.config.isSlave;
213
+ }
214
+
215
+ protected _isStateOutdated(
216
+ checkForBlockNumber: number,
217
+ stateValidBlockNumber: number,
218
+ stateValidityBlockNumDelay: number,
219
+ ): boolean {
220
+ return (
221
+ checkForBlockNumber - stateValidBlockNumber > stateValidityBlockNumDelay
222
+ );
223
+ }
224
+
225
+ protected _isStateOutdatedForUse(
226
+ checkForBlockNumber: number,
227
+ stateValidBlockNumber: number,
228
+ ): boolean {
229
+ return this._isStateOutdated(
230
+ checkForBlockNumber,
231
+ stateValidBlockNumber,
232
+ this.maxAllowedStateDelayInBlocks,
233
+ );
234
+ }
235
+
236
+ isTimeToTriggerUpdate(blocknumber: number): boolean {
237
+ return this._isStateOutdated(
238
+ blocknumber,
239
+ this._stateWithUpdateInfo?.blockNumber || 0,
240
+ this.blocksBackToTriggerUpdate,
241
+ );
242
+ }
243
+
244
+ protected async _retrieveStateWithChecks(
245
+ retriever: () => Promise<ObjWithUpdateInfo<State> | null>,
246
+ blockNumber: number | 'latest',
247
+ source: StateSources,
248
+ ) {
249
+ const state = await retriever();
250
+
251
+ if (state) {
252
+ if (
253
+ blockNumber === 'latest' ||
254
+ !this._isStateOutdatedForUse(blockNumber, state.blockNumber)
255
+ ) {
256
+ return state;
257
+ } else {
258
+ this._immediateLogMessage(
259
+ `State from ${source} is outdated. Valid for number ${
260
+ state.blockNumber
261
+ }, but requested for ${blockNumber}. Diff ${
262
+ blockNumber - state.blockNumber
263
+ } blocks`,
264
+ 'trace',
265
+ );
266
+ }
267
+ } else {
268
+ if (source === StateSources.LOCAL_MEMORY) {
269
+ this._immediateLogMessage(
270
+ `State is not initialized in memory`,
271
+ 'error',
272
+ );
273
+ } else {
274
+ this._immediateLogMessage(
275
+ `State from ${source} is not available`,
276
+ 'trace',
277
+ );
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+
283
+ async getState(
284
+ blockNumber: number | 'latest' = 'latest',
285
+ forInitialization: boolean = false,
286
+ // If we found that state in memory is outdated, then we save it for future use
287
+ saveInMemory: boolean = true,
288
+ ): Promise<ObjWithUpdateInfo<State> | null> {
289
+ // If it is for initialization purposes, we for sure doesn't have state in memory
290
+ if (!forInitialization) {
291
+ // Try to get with least effort from local memory
292
+ const localState = await this._retrieveStateWithChecks(
293
+ async () => this._stateWithUpdateInfo ?? null,
294
+ blockNumber,
295
+ StateSources.LOCAL_MEMORY,
296
+ );
297
+ if (localState !== null) {
298
+ return localState;
299
+ }
300
+ }
301
+
302
+ // If we failed to get from memory. Try to fetch state from cache
303
+ try {
304
+ const cacheState = await this._retrieveStateWithChecks(
305
+ async () => (await this.fetchStateFromCache()) ?? null,
306
+ blockNumber,
307
+ StateSources.CACHE,
308
+ );
309
+ if (cacheState !== null) {
310
+ if (saveInMemory) {
311
+ // We want to save latest available state in memory for future use
312
+ // This await shouldn't slow down because if we are slave, we won't save state in cache
313
+ await this.setState(
314
+ cacheState.value,
315
+ cacheState.blockNumber,
316
+ cacheState.lastUpdatedAtMs,
317
+ );
318
+ }
319
+ return cacheState;
320
+ }
321
+ } catch (e) {
322
+ this._logMessageWithSuppression(`ERROR_FETCHING_STATE_FROM_CACHE`, e);
323
+ }
324
+
325
+ this._logMessageWithSuppression('FALLBACK_TO_RPC');
326
+
327
+ // As a last step. If we failed everything above, try to fetch from RPC
328
+ try {
329
+ const rpcState = await this._retrieveStateWithChecks(
330
+ async () => (await this.fetchLatestStateFromRpc()) ?? null,
331
+ blockNumber,
332
+ StateSources.RPC,
333
+ );
334
+ if (rpcState !== null) {
335
+ if (saveInMemory) {
336
+ // We want to save latest available state in memory for future use
337
+ // This await shouldn't slow down because if we are slave, we won't save state in cache
338
+ await this.setState(
339
+ rpcState.value,
340
+ rpcState.blockNumber,
341
+ rpcState.lastUpdatedAtMs,
342
+ );
343
+ return rpcState;
344
+ }
345
+ }
346
+ } catch (e) {
347
+ this._logMessageWithSuppression('ERROR_FETCHING_STATE_FROM_RPC', e);
348
+ }
349
+
350
+ // If nothing works, then we can not do anything here and skip this pool
351
+ return null;
352
+ }
353
+
354
+ protected _logMessageWithSuppression(
355
+ msgKey: keyof typeof StatefulRPCPollerMessages,
356
+ ...args: unknown[]
357
+ ) {
358
+ this.logMessagesSuppressor.logMessage(msgKey, this.identifierKey, ...args);
359
+ }
360
+
361
+ protected _immediateLogMessage(
362
+ message: string,
363
+ level: LogLevels,
364
+ ...args: unknown[]
365
+ ) {
366
+ this.logger[level](
367
+ `${message}. ${args.map(a => Utils.Serialize(a)).join('.')}`,
368
+ );
369
+ }
370
+
371
+ get isPoolParticipateInUpdates(): boolean {
372
+ return this._isPoolParticipateInUpdates;
373
+ }
374
+
375
+ set isPoolParticipateInUpdates(value: boolean) {
376
+ // If we change state update status, we always keep relevant info in manager
377
+ value
378
+ ? this.managerCbControllers.enableStateTracking(this.identifierKey)
379
+ : this.managerCbControllers.disableStateTracking(this.identifierKey);
380
+
381
+ this._isPoolParticipateInUpdates = value;
382
+ }
383
+
384
+ get isPoolInTheMiddleOfUpdate(): boolean {
385
+ return this._isPoolInTheMiddleOfUpdate;
386
+ }
387
+
388
+ set isPoolInTheMiddleOfUpdate(value: boolean) {
389
+ this._isPoolInTheMiddleOfUpdate = value;
390
+ }
391
+
392
+ // For the time of implementing this class, I don't need multi step state fetch,
393
+ // when we need to sequentially fetch data to get full state. This is true for CurveV1Factory and WooFi
394
+ // Later we may consider having more complicated generateState mechanism.
395
+ protected abstract _getFetchStateMultiCalls(): MultiCallParams<M>[];
396
+
397
+ getFetchStateWithBlockInfoMultiCalls(): [
398
+ MultiCallParams<number>,
399
+ ...MultiCallParams<M>[],
400
+ ] {
401
+ if (this._cachedMultiCallData === undefined) {
402
+ const stateMultiCalls = this._getFetchStateMultiCalls();
403
+ this._cachedMultiCallData = [
404
+ this._getBlockNumberMultiCall,
405
+ ...stateMultiCalls,
406
+ ];
407
+ }
408
+
409
+ return this._cachedMultiCallData;
410
+ }
411
+
412
+ protected abstract _parseStateFromMultiResults(multiOutputs: M[]): State;
413
+
414
+ parseStateFromMultiResultsWithBlockInfo(
415
+ multiOutputs: [MultiResult<number>, ...MultiResult<M>[]],
416
+ lastUpdatedAtMs: number,
417
+ ): ObjWithUpdateInfo<State> {
418
+ // By abstract I mean for abstract method which must be implemented
419
+ const [blockNumber, ...outputsForAbstract] = multiOutputs.map((m, i) => {
420
+ if (!m.success) {
421
+ throw new Error(
422
+ `${this.entityName} failed to get multicall with index ${i}`,
423
+ );
424
+ }
425
+
426
+ return m.returnData;
427
+ }) as [number, ...M[]];
428
+
429
+ return {
430
+ value: this._parseStateFromMultiResults(outputsForAbstract),
431
+ blockNumber,
432
+ lastUpdatedAtMs,
433
+ };
434
+ }
435
+
436
+ async fetchLatestStateFromRpc(): Promise<ObjWithUpdateInfo<State> | null> {
437
+ const multiCalls = this.getFetchStateWithBlockInfoMultiCalls();
438
+ try {
439
+ const lastUpdatedAtMs = Date.now();
440
+ const aggregatedResults = (await this.dexHelper.multiWrapper.tryAggregate<
441
+ number | M
442
+ >(true, multiCalls as MultiCallParams<M | number>[])) as [
443
+ MultiResult<number>,
444
+ ...MultiResult<M>[],
445
+ ];
446
+
447
+ return this.parseStateFromMultiResultsWithBlockInfo(
448
+ aggregatedResults,
449
+ lastUpdatedAtMs,
450
+ );
451
+ } catch (e) {
452
+ this._logMessageWithSuppression('ERROR_FETCHING_STATE_FROM_RPC', e);
453
+ }
454
+
455
+ return null;
456
+ }
457
+
458
+ async setState(
459
+ state: State,
460
+ blockNumber: number,
461
+ lastUpdatedAtMs: number,
462
+ ): Promise<void> {
463
+ if (this._stateWithUpdateInfo === undefined) {
464
+ this._stateWithUpdateInfo = {
465
+ value: state,
466
+ blockNumber,
467
+ lastUpdatedAtMs,
468
+ };
469
+ } else {
470
+ this._stateWithUpdateInfo.value = state;
471
+ this._stateWithUpdateInfo.blockNumber = blockNumber;
472
+ this._stateWithUpdateInfo.lastUpdatedAtMs = lastUpdatedAtMs;
473
+ }
474
+
475
+ // Master version must keep cache up to date
476
+ if (this.isMaster) {
477
+ await this._saveStateInCache();
478
+ }
479
+ }
480
+
481
+ protected async _saveStateInCache(): Promise<boolean> {
482
+ try {
483
+ await this.dexHelper.cache.hset(
484
+ this.cacheStateKey,
485
+ this.identifierKey,
486
+ Utils.Serialize(this._stateWithUpdateInfo),
487
+ );
488
+ this._immediateLogMessage(
489
+ `State successfully saved in cache hashmap: ${this.cacheStateKey}, key=${this.identifierKey}`,
490
+ 'debug',
491
+ );
492
+ return true;
493
+ } catch (e) {
494
+ this._logMessageWithSuppression('ERROR_SAVING_STATE_IN_CACHE', e);
495
+ }
496
+ return false;
497
+ }
498
+
499
+ protected async _saveLiquidityInCache(): Promise<boolean> {
500
+ try {
501
+ await this.dexHelper.cache.setex(
502
+ this.dexKey,
503
+ this.network,
504
+ this.cacheLiquidityMapKey,
505
+ this._getExpiryTimeForCachedLiquidity(),
506
+ JSON.stringify(this._liquidityInUSDWithUpdateInfo),
507
+ );
508
+ return true;
509
+ } catch (e) {
510
+ this._logMessageWithSuppression('ERROR_SAVING_LIQUIDITY_IN_CACHE', e);
511
+ }
512
+ return false;
513
+ }
514
+
515
+ async fetchStateFromCache(): Promise<ObjWithUpdateInfo<State> | null> {
516
+ const resultUnparsed = await this.dexHelper.cache.hget(
517
+ this.cacheStateKey,
518
+ this.identifierKey,
519
+ );
520
+
521
+ if (resultUnparsed !== null) {
522
+ return Utils.Parse(resultUnparsed) as ObjWithUpdateInfo<State>;
523
+ }
524
+
525
+ return null;
526
+ }
527
+
528
+ protected async _fetchLiquidityFromCache(): Promise<ObjWithUpdateInfo<number> | null> {
529
+ const resultUnparsed = await this.dexHelper.cache.get(
530
+ this.dexKey,
531
+ this.network,
532
+ this.cacheStateKey,
533
+ );
534
+
535
+ if (resultUnparsed !== null) {
536
+ return Utils.Parse(resultUnparsed) as ObjWithUpdateInfo<number>;
537
+ }
538
+
539
+ return null;
540
+ }
541
+
542
+ async setLiquidity(
543
+ newLiquidityInUSD: number,
544
+ lastUpdatedAtMs: number,
545
+ blockNumber?: number,
546
+ ): Promise<void> {
547
+ this._liquidityInUSDWithUpdateInfo.value = newLiquidityInUSD;
548
+ this._liquidityInUSDWithUpdateInfo.lastUpdatedAtMs = lastUpdatedAtMs;
549
+ this._liquidityInUSDWithUpdateInfo.blockNumber = blockNumber || 0;
550
+
551
+ this._adjustIsStateToBeUpdated();
552
+
553
+ if (this.isMaster) {
554
+ await this._saveLiquidityInCache();
555
+ }
556
+ }
557
+
558
+ protected _adjustIsStateToBeUpdated() {
559
+ if (this.isLiquidityTracked) {
560
+ if (
561
+ Date.now() - this._liquidityInUSDWithUpdateInfo.lastUpdatedAtMs >
562
+ this.liquidityUpdateAllowedDelayMs
563
+ ) {
564
+ this._logMessageWithSuppression(
565
+ 'LIQUIDITY_INFO_IS_OUTDATED',
566
+ `Last updated at ${this._liquidityInUSDWithUpdateInfo.lastUpdatedAtMs}`,
567
+ );
568
+ this.isPoolParticipateInUpdates = true;
569
+ } else {
570
+ this.isPoolParticipateInUpdates =
571
+ this._liquidityInUSDWithUpdateInfo.value >=
572
+ this.liquidityThresholdForUpdate;
573
+ }
574
+ }
575
+ }
576
+
577
+ protected _getExpiryTimeForCachedLiquidity() {
578
+ // Give it 10 minutes margin to recover
579
+ return Math.floor(this.liquidityUpdatePeriodMs / 1000) + 10 * 60 * 1000;
580
+ }
581
+
582
+ async initializeState(): Promise<void> {
583
+ try {
584
+ const state = await this.fetchLatestStateFromRpc();
585
+
586
+ if (state === null) {
587
+ this._immediateLogMessage(
588
+ `initializePool: pool=${this.identifierKey} from ${this.dexKey} state is null. Retry in ${DEFAULT_STATE_INIT_RETRY_MS} ms`,
589
+ 'error',
590
+ );
591
+ } else {
592
+ this.setState(state.value, state.blockNumber, state.lastUpdatedAtMs);
593
+ return;
594
+ }
595
+ } catch (e) {
596
+ this._immediateLogMessage(
597
+ `initializePool: pool=${this.identifierKey} from ${this.dexKey} failed to initialize state from RPC. Retry in ${DEFAULT_STATE_INIT_RETRY_MS} ms`,
598
+ 'error',
599
+ );
600
+ }
601
+
602
+ setTimeout(() => this.initializeState(), DEFAULT_STATE_INIT_RETRY_MS);
603
+ }
604
+ }
@@ -0,0 +1,66 @@
1
+ import { MultiCallParams, MultiResult } from '../multi-wrapper';
2
+
3
+ export type ObjWithUpdateInfo<T> = {
4
+ value: T;
5
+ blockNumber: number;
6
+ lastUpdatedAtMs: number;
7
+ };
8
+
9
+ export interface IStatefulRpcPoller<State, M> {
10
+ identifierKey: string;
11
+ dexKey: string;
12
+
13
+ // For example, if particular pool has enough liquidity, or maybe it is paused?
14
+ isPoolParticipateInUpdates: boolean;
15
+
16
+ isPoolInTheMiddleOfUpdate: boolean;
17
+
18
+ network: number;
19
+
20
+ isTimeToTriggerUpdate(blockNumber: number): boolean;
21
+
22
+ getFetchStateWithBlockInfoMultiCalls(): [
23
+ MultiCallParams<number>,
24
+ ...MultiCallParams<M>[],
25
+ ];
26
+
27
+ parseStateFromMultiResultsWithBlockInfo(
28
+ multiOutputs: [MultiResult<number>, ...MultiResult<M>[]],
29
+ lastUpdatedAtMs: number,
30
+ ): ObjWithUpdateInfo<State>;
31
+
32
+ setState(
33
+ state: State,
34
+ blockNumber: number,
35
+ lastUpdatedAtMs: number,
36
+ ): Promise<void>;
37
+
38
+ fetchStateFromCache(): Promise<ObjWithUpdateInfo<State> | null>;
39
+
40
+ fetchLatestStateFromRpc(): Promise<ObjWithUpdateInfo<State> | null>;
41
+
42
+ // This function must not throw any errors
43
+ initializeState(): Promise<void>;
44
+
45
+ getState(blockNumber: number): Promise<ObjWithUpdateInfo<State> | null>;
46
+
47
+ setLiquidity(
48
+ newLiquidityInUSD: number,
49
+ lastUpdatedAtMs: number,
50
+ blockNumber?: number,
51
+ ): Promise<void>;
52
+ }
53
+
54
+ export type PollingManagerControllersCb = {
55
+ enableStateTracking: (identifierKey: string) => void;
56
+ disableStateTracking: (identifierKey: string) => void;
57
+ registerPendingPool: <T, M>(
58
+ statefulRpcPoller: IStatefulRpcPoller<T, M>,
59
+ ) => void;
60
+ };
61
+
62
+ export enum StateSources {
63
+ LOCAL_MEMORY = 'local_memory',
64
+ CACHE = 'cache',
65
+ RPC = 'rpc',
66
+ }
@@ -0,0 +1,23 @@
1
+ import type { StatePollingManager } from './state-polling-manager';
2
+ import { PollingManagerControllersCb } from './types';
3
+
4
+ export function getIdentifierKeyForRpcPoller(
5
+ poolIdentifier: string,
6
+ network: number,
7
+ ): string {
8
+ return `${network}_${poolIdentifier}`.toLowerCase();
9
+ }
10
+
11
+ // Helper function to extract relevant call backs for later use
12
+ export function pollingManagerCbExtractor(
13
+ statePollingManager: StatePollingManager,
14
+ ): PollingManagerControllersCb {
15
+ return {
16
+ enableStateTracking:
17
+ statePollingManager.enableStateTracking.bind(statePollingManager),
18
+ disableStateTracking:
19
+ statePollingManager.disableStateTracking.bind(statePollingManager),
20
+ registerPendingPool:
21
+ statePollingManager.registerPendingPool.bind(statePollingManager),
22
+ };
23
+ }