@across-protocol/sdk 4.3.50 → 4.3.52

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.
@@ -10,6 +10,7 @@ import {
10
10
  PoolRebalanceLeaf,
11
11
  Refund,
12
12
  RunningBalances,
13
+ SpokePoolClientsByChain,
13
14
  } from "../../../interfaces";
14
15
  import {
15
16
  bnZero,
@@ -18,6 +19,9 @@ import {
18
19
  count2DDictionaryValues,
19
20
  count3DDictionaryValues,
20
21
  toAddressType,
22
+ getImpliedBundleBlockRanges,
23
+ isDefined,
24
+ EvmAddress,
21
25
  } from "../../../utils";
22
26
  import {
23
27
  addLastRunningBalance,
@@ -28,6 +32,7 @@ import {
28
32
  } from "./PoolRebalanceUtils";
29
33
  import { AcrossConfigStoreClient } from "../../AcrossConfigStoreClient";
30
34
  import { HubPoolClient } from "../../HubPoolClient";
35
+ import { BundleDataClient } from "../../BundleDataClient";
31
36
  import { buildPoolRebalanceLeafTree } from "./MerkleTreeUtils";
32
37
 
33
38
  // and expired deposits.
@@ -114,7 +119,22 @@ export function getEndBlockBuffers(
114
119
  return chainIdListForBundleEvaluationBlockNumbers.map((chainId: number) => blockRangeEndBlockBuffer[chainId] ?? 0);
115
120
  }
116
121
 
117
- export function _buildPoolRebalanceRoot(
122
+ /*
123
+ * @notice Constructs a new pool rebalance root given the input bundle data.
124
+ * @dev It is assumed that the input bundle data corresponds to the block ranges of the input mainnetBundleEndBlock.
125
+ * If the mainnetBundleEndBlock does not correspond to any historical or pending root bundle, then the output pool rebalance
126
+ * root will be constructed under the assumption that the pending root bundle is valid and passes liveness.
127
+ * @param latestMainnetBlock The latest mainnet block number.
128
+ * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
129
+ * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
130
+ * @param bundleFillsV3 Fill bundle data.
131
+ * @param bundleSlowFillsV3 Slow fill bundle data.
132
+ * @param unexecutableSlowFills Expired slow fill bundle data.
133
+ * @param expiredDepositsToRefundV3 Expired deposit bundle data.
134
+ * @param clients Clients required to construct a new pool rebalance root.
135
+ * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
136
+ */
137
+ export async function _buildPoolRebalanceRoot(
118
138
  latestMainnetBlock: number,
119
139
  mainnetBundleEndBlock: number,
120
140
  bundleV3Deposits: BundleDepositsV3,
@@ -122,9 +142,235 @@ export function _buildPoolRebalanceRoot(
122
142
  bundleSlowFillsV3: BundleSlowFills,
123
143
  unexecutableSlowFills: BundleExcessSlowFills,
124
144
  expiredDepositsToRefundV3: ExpiredDepositsToRefundV3,
125
- clients: { hubPoolClient: HubPoolClient; configStoreClient: AcrossConfigStoreClient },
145
+ clients: {
146
+ hubPoolClient: HubPoolClient;
147
+ configStoreClient: AcrossConfigStoreClient;
148
+ bundleDataClient: BundleDataClient;
149
+ spokePoolClients: SpokePoolClientsByChain;
150
+ },
151
+ maxL1TokenCountOverride?: number
152
+ ): Promise<PoolRebalanceRoot> {
153
+ // If there is a pending proposal and the mainnet bundle end block is greater than the pending proposal's mainnet end block, then this pool rebalance root is being built during the liveness
154
+ // of a different root bundle, so running balance calculations will be slightly different.
155
+ if (
156
+ clients.hubPoolClient.hasPendingProposal() &&
157
+ clients.hubPoolClient.getPendingRootBundle()!.bundleEvaluationBlockNumbers[0] < mainnetBundleEndBlock
158
+ ) {
159
+ return await _buildOptimisticPoolRebalanceRoot(
160
+ latestMainnetBlock,
161
+ mainnetBundleEndBlock,
162
+ bundleV3Deposits,
163
+ bundleFillsV3,
164
+ bundleSlowFillsV3,
165
+ unexecutableSlowFills,
166
+ expiredDepositsToRefundV3,
167
+ clients,
168
+ maxL1TokenCountOverride
169
+ );
170
+ }
171
+ // Otherwise, we can synchronously reconstruct a historical pool rebalance root from the input data.
172
+ return _buildHistoricalPoolRebalanceRoot(
173
+ latestMainnetBlock,
174
+ mainnetBundleEndBlock,
175
+ bundleV3Deposits,
176
+ bundleFillsV3,
177
+ bundleSlowFillsV3,
178
+ unexecutableSlowFills,
179
+ expiredDepositsToRefundV3,
180
+ clients,
181
+ maxL1TokenCountOverride
182
+ );
183
+ }
184
+
185
+ /*
186
+ * @notice Constructs a new pool rebalance root given historical bundle data.
187
+ * @param latestMainnetBlock The latest mainnet block number.
188
+ * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
189
+ * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
190
+ * @param bundleFillsV3 Fill bundle data.
191
+ * @param bundleSlowFillsV3 Slow fill bundle data.
192
+ * @param unexecutableSlowFills Expired slow fill bundle data.
193
+ * @param expiredDepositsToRefundV3 Expired deposit bundle data.
194
+ * @param clients Clients required to construct a new pool rebalance root.
195
+ * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
196
+ */
197
+ export function _buildHistoricalPoolRebalanceRoot(
198
+ latestMainnetBlock: number,
199
+ mainnetBundleEndBlock: number,
200
+ bundleV3Deposits: BundleDepositsV3,
201
+ bundleFillsV3: BundleFillsV3,
202
+ bundleSlowFillsV3: BundleSlowFills,
203
+ unexecutableSlowFills: BundleExcessSlowFills,
204
+ expiredDepositsToRefundV3: ExpiredDepositsToRefundV3,
205
+ clients: {
206
+ hubPoolClient: HubPoolClient;
207
+ configStoreClient: AcrossConfigStoreClient;
208
+ },
126
209
  maxL1TokenCountOverride?: number
127
210
  ): PoolRebalanceRoot {
211
+ const { runningBalances, realizedLpFees, chainWithRefundsOnly } = _getMarginalRunningBalances(
212
+ mainnetBundleEndBlock,
213
+ bundleV3Deposits,
214
+ bundleFillsV3,
215
+ bundleSlowFillsV3,
216
+ unexecutableSlowFills,
217
+ expiredDepositsToRefundV3,
218
+ clients
219
+ );
220
+ addLastRunningBalance(latestMainnetBlock, runningBalances, clients.hubPoolClient);
221
+ const leaves: PoolRebalanceLeaf[] = constructPoolRebalanceLeaves(
222
+ mainnetBundleEndBlock,
223
+ runningBalances,
224
+ realizedLpFees,
225
+ Array.from(chainWithRefundsOnly).filter((chainId) => !Object.keys(runningBalances).includes(chainId.toString())),
226
+ clients.configStoreClient,
227
+ maxL1TokenCountOverride
228
+ );
229
+ return {
230
+ runningBalances,
231
+ realizedLpFees,
232
+ leaves,
233
+ tree: buildPoolRebalanceLeafTree(leaves),
234
+ };
235
+ }
236
+
237
+ /*
238
+ * @notice Constructs a new pool rebalance root given the input bundle data. This function assumes there is a pending root bundle which will eventually clear liveness.
239
+ * @param latestMainnetBlock The latest mainnet block number.
240
+ * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
241
+ * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
242
+ * @param bundleFillsV3 Fill bundle data.
243
+ * @param bundleSlowFillsV3 Slow fill bundle data.
244
+ * @param unexecutableSlowFills Expired slow fill bundle data.
245
+ * @param expiredDepositsToRefundV3 Expired deposit bundle data.
246
+ * @param clients Clients required to construct a new pool rebalance root.
247
+ * @maxL1TokenCountOverride Optional parameter to cap the number of tokens in a single pool rebalance leaf.
248
+ */
249
+ export async function _buildOptimisticPoolRebalanceRoot(
250
+ latestMainnetBlock: number,
251
+ mainnetBundleEndBlock: number,
252
+ bundleV3Deposits: BundleDepositsV3,
253
+ bundleFillsV3: BundleFillsV3,
254
+ bundleSlowFillsV3: BundleSlowFills,
255
+ unexecutableSlowFills: BundleExcessSlowFills,
256
+ expiredDepositsToRefundV3: ExpiredDepositsToRefundV3,
257
+ clients: {
258
+ hubPoolClient: HubPoolClient;
259
+ configStoreClient: AcrossConfigStoreClient;
260
+ bundleDataClient: BundleDataClient;
261
+ spokePoolClients: SpokePoolClientsByChain;
262
+ },
263
+ maxL1TokenCountOverride?: number
264
+ ): Promise<PoolRebalanceRoot> {
265
+ const { runningBalances, realizedLpFees, chainWithRefundsOnly } = _getMarginalRunningBalances(
266
+ mainnetBundleEndBlock,
267
+ bundleV3Deposits,
268
+ bundleFillsV3,
269
+ bundleSlowFillsV3,
270
+ unexecutableSlowFills,
271
+ expiredDepositsToRefundV3,
272
+ clients
273
+ );
274
+ // Get the pool rebalance root for the pending bundle so that we may account for its calculated running balances.
275
+ // @dev It is safe to index the hub pool client's proposed root bundles here since there is guaranteed to be a pending proposal in this code block.
276
+ const mostRecentProposedRootBundle = clients.hubPoolClient.getLatestProposedRootBundle();
277
+ const blockRangesForChains = getImpliedBundleBlockRanges(
278
+ clients.hubPoolClient,
279
+ clients.configStoreClient,
280
+ mostRecentProposedRootBundle
281
+ );
282
+ // We are loading data from a pending root bundle which should be well into liveness, so we want to use arweave if possible.
283
+ const pendingRootBundleData = await clients.bundleDataClient.loadData(
284
+ blockRangesForChains,
285
+ clients.spokePoolClients,
286
+ true
287
+ );
288
+ // Build the pool rebalance root for the pending root bundle.
289
+ const { leaves, tree } = _buildHistoricalPoolRebalanceRoot(
290
+ latestMainnetBlock,
291
+ blockRangesForChains[0][1],
292
+ pendingRootBundleData.bundleDepositsV3,
293
+ pendingRootBundleData.bundleFillsV3,
294
+ pendingRootBundleData.bundleSlowFillsV3,
295
+ pendingRootBundleData.unexecutableSlowFills,
296
+ pendingRootBundleData.expiredDepositsToRefundV3,
297
+ clients,
298
+ maxL1TokenCountOverride
299
+ );
300
+ // Assert that the rebuilt pool rebalance root matches the pending root bundle's value. If it does not, then we likely misconstructed the pending root bundle and should throw.
301
+ assert(tree.getHexRoot() === mostRecentProposedRootBundle.poolRebalanceRoot);
302
+
303
+ // Only add marginal pending running balances if there is already an entry in `runningBalances`. If there is no entry in `runningBalances`, then
304
+ // The running balance for this entry was unchanged since the last root bundle.
305
+ Object.keys(runningBalances).forEach((_repaymentChainId) => {
306
+ Object.keys(runningBalances[Number(_repaymentChainId)]).forEach((_l1TokenAddress) => {
307
+ const repaymentChainId = Number(_repaymentChainId);
308
+ const l1TokenAddress = EvmAddress.from(_l1TokenAddress);
309
+ const pendingPoolRebalanceLeaf = leaves.find(
310
+ (leaf) => leaf.chainId === repaymentChainId && leaf.l1Tokens.some((l1Token) => l1Token.eq(l1TokenAddress))
311
+ );
312
+ // If the pending pool rebalance root has running balances defined, then add it to `runningBalances`.
313
+ if (isDefined(pendingPoolRebalanceLeaf)) {
314
+ const pendingLeafTokenIdx = pendingPoolRebalanceLeaf.l1Tokens.findIndex((l1Token) =>
315
+ l1Token.eq(l1TokenAddress)
316
+ );
317
+ assert(pendingLeafTokenIdx !== -1);
318
+ const pendingRunningBalanceAmount = pendingPoolRebalanceLeaf.runningBalances[pendingLeafTokenIdx];
319
+ if (!pendingRunningBalanceAmount.eq(bnZero)) {
320
+ updateRunningBalance(runningBalances, repaymentChainId, _l1TokenAddress, pendingRunningBalanceAmount);
321
+ }
322
+ } else {
323
+ // Otherwise, add the last running balance for this token.
324
+ const { runningBalance: lastExecutedBundleRunningBalance } =
325
+ clients.hubPoolClient.getRunningBalanceBeforeBlockForChain(
326
+ latestMainnetBlock,
327
+ repaymentChainId,
328
+ l1TokenAddress
329
+ );
330
+ if (!lastExecutedBundleRunningBalance.eq(bnZero)) {
331
+ updateRunningBalance(runningBalances, repaymentChainId, _l1TokenAddress, lastExecutedBundleRunningBalance);
332
+ }
333
+ }
334
+ });
335
+ });
336
+ const poolRebalanceLeaves: PoolRebalanceLeaf[] = constructPoolRebalanceLeaves(
337
+ mainnetBundleEndBlock,
338
+ runningBalances,
339
+ realizedLpFees,
340
+ Array.from(chainWithRefundsOnly).filter((chainId) => !Object.keys(runningBalances).includes(chainId.toString())),
341
+ clients.configStoreClient,
342
+ maxL1TokenCountOverride
343
+ );
344
+ return {
345
+ runningBalances,
346
+ realizedLpFees,
347
+ leaves: poolRebalanceLeaves,
348
+ tree: buildPoolRebalanceLeafTree(poolRebalanceLeaves),
349
+ };
350
+ }
351
+
352
+ /*
353
+ * @notice Gets the running balance amounts derived from the input bundle data.
354
+ * @param mainnetBundleEndBlock The end block number of the block range corresponding to the bundle data.
355
+ * @param bundleV3Deposits Deposit bundle data for the implied block range given by the mainnetBundleEndBlock.
356
+ * @param bundleFillsV3 Fill bundle data.
357
+ * @param bundleSlowFillsV3 Slow fill bundle data.
358
+ * @param unexecutableSlowFills Expired slow fill bundle data.
359
+ * @param expiredDepositsToRefundV3 Expired deposit bundle data.
360
+ * @param clients Clients required to construct a new pool rebalance root.
361
+ */
362
+ export function _getMarginalRunningBalances(
363
+ mainnetBundleEndBlock: number,
364
+ bundleV3Deposits: BundleDepositsV3,
365
+ bundleFillsV3: BundleFillsV3,
366
+ bundleSlowFillsV3: BundleSlowFills,
367
+ unexecutableSlowFills: BundleExcessSlowFills,
368
+ expiredDepositsToRefundV3: ExpiredDepositsToRefundV3,
369
+ clients: {
370
+ hubPoolClient: HubPoolClient;
371
+ configStoreClient: AcrossConfigStoreClient;
372
+ }
373
+ ): { chainWithRefundsOnly: Set<number>; realizedLpFees: RunningBalances; runningBalances: RunningBalances } {
128
374
  // Running balances are the amount of tokens that we need to send to each SpokePool to pay for all instant and
129
375
  // slow relay refunds. They are decreased by the amount of funds already held by the SpokePool. Balances are keyed
130
376
  // by the SpokePool's network and L1 token equivalent of the L2 token to refund.
@@ -296,24 +542,5 @@ export function _buildPoolRebalanceRoot(
296
542
  });
297
543
  });
298
544
  });
299
-
300
- // Add to the running balance value from the last valid root bundle proposal for {chainId, l1Token}
301
- // combination if found.
302
- addLastRunningBalance(latestMainnetBlock, runningBalances, clients.hubPoolClient);
303
-
304
- const leaves: PoolRebalanceLeaf[] = constructPoolRebalanceLeaves(
305
- mainnetBundleEndBlock,
306
- runningBalances,
307
- realizedLpFees,
308
- Array.from(chainWithRefundsOnly).filter((chainId) => !Object.keys(runningBalances).includes(chainId.toString())),
309
- clients.configStoreClient,
310
- maxL1TokenCountOverride
311
- );
312
-
313
- return {
314
- runningBalances,
315
- realizedLpFees,
316
- leaves,
317
- tree: buildPoolRebalanceLeafTree(leaves),
318
- };
545
+ return { runningBalances, chainWithRefundsOnly, realizedLpFees };
319
546
  }
@@ -29,6 +29,12 @@ type PriceCache = {
29
29
  };
30
30
  };
31
31
 
32
+ type PriceCacheBySymbol = {
33
+ [symbol: string]: {
34
+ [currency: string]: Omit<CoinGeckoPrice, "address">;
35
+ };
36
+ };
37
+
32
38
  type CGTokenPrice = {
33
39
  [currency: string]: number;
34
40
  last_updated_at: number;
@@ -61,6 +67,7 @@ export type HistoricPriceChartData = {
61
67
  export class Coingecko {
62
68
  private static instance: Coingecko | undefined;
63
69
  private prices: PriceCache;
70
+ private pricesBySymbol: PriceCacheBySymbol;
64
71
  private _maxPriceAge = 300; // seconds
65
72
 
66
73
  // Retry configuration.
@@ -103,6 +110,7 @@ export class Coingecko {
103
110
  private readonly customPlatformIdMap?: Record<number, string>
104
111
  ) {
105
112
  this.prices = {};
113
+ this.pricesBySymbol = {};
106
114
  }
107
115
 
108
116
  protected async getPlatformId(chainId: number): Promise<string> {
@@ -281,6 +289,29 @@ export class Coingecko {
281
289
  return [tokenPrice.timestamp.toString(), tokenPrice.price];
282
290
  }
283
291
 
292
+ async getCurrentPriceBySymbol(symbol: string, currency = "usd"): Promise<[string, number]> {
293
+ let tokenPrice = this.getCachedSymbolPrice(symbol, currency);
294
+ if (tokenPrice === undefined) {
295
+ const result = await this.call<Record<string, CGTokenPrice>>(
296
+ `simple/price?symbols=${symbol}&vs_currencies=${currency}&include_last_updated_at=true`
297
+ );
298
+ const cgPrice = result?.[symbol.toLowerCase()];
299
+ if (cgPrice === undefined || !cgPrice?.[currency]) {
300
+ const errMsg = `Failed to retrieve ${symbol}/${currency} price via Coingecko API`;
301
+ this.logger.debug({
302
+ at: "Coingecko#getCurrentPriceBySymbol",
303
+ message: errMsg,
304
+ });
305
+ throw new Error(errMsg);
306
+ } else {
307
+ this.updatePriceCacheBySymbol(cgPrice, symbol, currency);
308
+ }
309
+ }
310
+ tokenPrice = this.getCachedSymbolPrice(symbol, currency);
311
+ assert(tokenPrice !== undefined);
312
+ return [tokenPrice.timestamp.toString(), tokenPrice.price];
313
+ }
314
+
284
315
  // Return an array of spot prices for an array of collateral addresses in one async call. Note we might in future
285
316
  // This was adapted from packages/merkle-distributor/kpi-options-helpers/calculate-uma-tvl.ts
286
317
  async getContractPrices(
@@ -381,6 +412,11 @@ export class Coingecko {
381
412
  return this.prices[platform_id][currency];
382
413
  }
383
414
 
415
+ protected getPriceCacheBySymbol(symbol: string): { [currency: string]: Omit<CoinGeckoPrice, "address"> } {
416
+ if (this.pricesBySymbol[symbol] === undefined) this.pricesBySymbol[symbol] = {};
417
+ return this.pricesBySymbol[symbol];
418
+ }
419
+
384
420
  protected getCachedAddressPrice(
385
421
  contractAddress: string,
386
422
  currency: string,
@@ -409,6 +445,16 @@ export class Coingecko {
409
445
  }
410
446
  }
411
447
 
448
+ protected getCachedSymbolPrice(symbol: string, currency: string): Omit<CoinGeckoPrice, "address"> | undefined {
449
+ const priceCache = this.getPriceCacheBySymbol(symbol);
450
+ const now: number = msToS(Date.now());
451
+ const tokenPrice: Omit<CoinGeckoPrice, "address"> | undefined = priceCache[currency];
452
+ if (tokenPrice === undefined || tokenPrice.timestamp + this.maxPriceAge <= now) {
453
+ return undefined;
454
+ }
455
+ return tokenPrice;
456
+ }
457
+
412
458
  protected updatePriceCache(cgPrice: CGTokenPrice, contractAddress: string, currency: string, platform_id: string) {
413
459
  const priceCache = this.getPriceCache(currency, platform_id);
414
460
  if (priceCache[contractAddress] === undefined) {
@@ -433,6 +479,29 @@ export class Coingecko {
433
479
  }
434
480
  }
435
481
 
482
+ protected updatePriceCacheBySymbol(cgPrice: CGTokenPrice, symbol: string, currency: string) {
483
+ const priceCache = this.getPriceCacheBySymbol(symbol);
484
+ if (priceCache[currency] === undefined) {
485
+ priceCache[currency] = { price: 0, timestamp: 0 };
486
+ }
487
+ if (cgPrice.last_updated_at > priceCache[currency].timestamp) {
488
+ priceCache[currency] = {
489
+ price: cgPrice[currency],
490
+ timestamp: cgPrice.last_updated_at,
491
+ };
492
+ this.logger.debug({
493
+ at: "Coingecko#updatePriceCacheBySymbol",
494
+ message: `Updated ${symbol}/${currency} token price cache.`,
495
+ });
496
+ } else {
497
+ this.logger.debug({
498
+ at: "Coingecko#updatePriceCacheBySymbol",
499
+ message: `No new price available for symbol ${symbol}.`,
500
+ token: cgPrice,
501
+ });
502
+ }
503
+ }
504
+
436
505
  private async _callBasic(path: string, timeout?: number) {
437
506
  const url = `${this.host}/${path}`;
438
507