@across-protocol/sdk 4.3.59-alpha.1 → 4.3.59

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.
@@ -1,7 +1,6 @@
1
1
  import assert from "assert";
2
2
  import _ from "lodash";
3
3
  import {
4
- ProposedRootBundle,
5
4
  SlowFillRequestWithBlock,
6
5
  SpokePoolClientsByChain,
7
6
  FillType,
@@ -14,7 +13,6 @@ import {
14
13
  BundleSlowFills,
15
14
  ExpiredDepositsToRefundV3,
16
15
  Clients,
17
- CombinedRefunds,
18
16
  FillWithBlock,
19
17
  Deposit,
20
18
  DepositWithBlock,
@@ -29,10 +27,8 @@ import {
29
27
  assign,
30
28
  fixedPointAdjustment,
31
29
  isDefined,
32
- toBN,
33
30
  forEachAsync,
34
31
  getBlockRangeForChain,
35
- getImpliedBundleBlockRanges,
36
32
  getRelayEventKey,
37
33
  isSlowFill,
38
34
  mapAsync,
@@ -42,7 +38,6 @@ import {
42
38
  duplicateEvent,
43
39
  invalidOutputToken,
44
40
  Address,
45
- getNetworkName,
46
41
  toBytes32,
47
42
  convertRelayDataParamsToBytes32,
48
43
  convertFillParamsToBytes32,
@@ -50,10 +45,7 @@ import {
50
45
  import winston from "winston";
51
46
  import {
52
47
  BundleDataSS,
53
- getEndBlockBuffers,
54
48
  getRefundInformationFromFill,
55
- getRefundsFromBundle,
56
- getWidestPossibleExpectedBlockRange,
57
49
  isChainDisabledAtBlock,
58
50
  prettyPrintV3SpokePoolEvents,
59
51
  V3DepositWithBlock,
@@ -370,346 +362,6 @@ export class BundleDataClient {
370
362
  return bundleData;
371
363
  }
372
364
 
373
- // @dev This function should probably be moved to the InventoryClient since it bypasses loadData completely now.
374
- async getPendingRefundsFromValidBundles(): Promise<CombinedRefunds[]> {
375
- const refunds = [];
376
- if (!this.clients.hubPoolClient.isUpdated) {
377
- throw new Error("BundleDataClient::getPendingRefundsFromValidBundles HubPoolClient not updated.");
378
- }
379
-
380
- const bundle = this.clients.hubPoolClient.getLatestFullyExecutedRootBundle(
381
- this.clients.hubPoolClient.latestHeightSearched
382
- );
383
- if (bundle !== undefined) {
384
- refunds.push(await this.getPendingRefundsFromBundle(bundle));
385
- } // No more valid bundles in history!
386
- return refunds;
387
- }
388
-
389
- // @dev This function should probably be moved to the InventoryClient since it bypasses loadData completely now.
390
- // Return refunds from input bundle.
391
- async getPendingRefundsFromBundle(bundle: ProposedRootBundle): Promise<CombinedRefunds> {
392
- const nextBundleMainnetStartBlock = this.clients.hubPoolClient.getNextBundleStartBlockNumber(
393
- this.chainIdListForBundleEvaluationBlockNumbers,
394
- this.clients.hubPoolClient.latestHeightSearched,
395
- this.clients.hubPoolClient.chainId
396
- );
397
- const chainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(nextBundleMainnetStartBlock);
398
-
399
- // Reconstruct latest bundle block range.
400
- const bundleEvaluationBlockRanges = getImpliedBundleBlockRanges(
401
- this.clients.hubPoolClient,
402
- this.clients.configStoreClient,
403
- bundle
404
- );
405
- let combinedRefunds: CombinedRefunds;
406
- // Here we don't call loadData because our fallback is to approximate refunds if we don't have arweave data, rather
407
- // than use the much slower loadData to compute all refunds. We don't need to consider slow fills or deposit
408
- // expiries here so we can skip some steps. We also don't need to compute LP fees as they should be small enough
409
- // so as not to affect this approximate refund count.
410
- const arweaveData = await this.loadArweaveData(bundleEvaluationBlockRanges);
411
- if (!isDefined(arweaveData)) {
412
- combinedRefunds = await this.getApproximateRefundsForBlockRange(chainIds, bundleEvaluationBlockRanges);
413
- } else {
414
- const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
415
- combinedRefunds = getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3);
416
- // If we don't have a spoke pool client for a chain, then we won't be able to deduct refunds correctly for this
417
- // chain. For most of the pending bundle's liveness period, these past refunds are already executed so this is
418
- // a reasonable assumption. This empty refund chain also matches what the alternative
419
- // `getApproximateRefundsForBlockRange` would return.
420
- Object.keys(combinedRefunds).forEach((chainId) => {
421
- if (!this.spokePoolClientManager.getClient(Number(chainId))) {
422
- delete combinedRefunds[Number(chainId)];
423
- }
424
- });
425
- }
426
-
427
- // The latest proposed bundle's refund leaves might have already been partially or entirely executed.
428
- // We have to deduct the executed amounts from the total refund amounts.
429
- return this.deductExecutedRefunds(combinedRefunds, bundle);
430
- }
431
-
432
- // @dev This helper function should probably be moved to the InventoryClient
433
- async getApproximateRefundsForBlockRange(chainIds: number[], blockRanges: number[][]): Promise<CombinedRefunds> {
434
- const refundsForChain: CombinedRefunds = {};
435
- const bundleEndBlockForMainnet = blockRanges[0][1];
436
- for (const chainId of chainIds) {
437
- const spokePoolClient = this.spokePoolClientManager.getClient(chainId);
438
- if (!isDefined(spokePoolClient)) {
439
- continue;
440
- }
441
- const chainIndex = chainIds.indexOf(chainId);
442
- // @dev This function does not account for pre-fill refunds as it is optimized for speed. The way to detect
443
- // pre-fill refunds is to load all deposits that are unmatched by fills in the spoke pool client's memory
444
- // and then query the FillStatus on-chain, but that might slow this function down too much. For now, we
445
- // will live with this expected inaccuracy as it should be small. The pre-fill would have to precede the deposit
446
- // by more than the caller's event lookback window which is expected to be unlikely.
447
- const fillsToCount = spokePoolClient.getFills().filter((fill) => {
448
- if (
449
- fill.blockNumber < blockRanges[chainIndex][0] ||
450
- fill.blockNumber > blockRanges[chainIndex][1] ||
451
- isZeroValueFillOrSlowFillRequest(fill) ||
452
- invalidOutputToken(fill)
453
- ) {
454
- return false;
455
- }
456
-
457
- const originSpokePoolClient = this.spokePoolClientManager.getClient(fill.originChainId);
458
- // If origin spoke pool client isn't defined, we can't validate it.
459
- if (!isDefined(originSpokePoolClient)) {
460
- return false;
461
- }
462
- const matchingDeposit = originSpokePoolClient.getDeposit(fill.depositId);
463
- const hasMatchingDeposit =
464
- matchingDeposit !== undefined && getRelayEventKey(fill) === getRelayEventKey(matchingDeposit);
465
- return hasMatchingDeposit;
466
- });
467
- await forEachAsync(fillsToCount, async (_fill) => {
468
- const originChain = getNetworkName(_fill.originChainId);
469
- const originSpokePoolClient = this.spokePoolClientManager.getClient(_fill.originChainId);
470
- assert(isDefined(originSpokePoolClient), `No SpokePoolClient for chain ${originChain}`);
471
- const matchingDeposit = originSpokePoolClient.getDeposit(_fill.depositId);
472
- assert(
473
- isDefined(matchingDeposit),
474
- `No ${originChain} deposit found for ${getNetworkName(_fill.destinationChainId)} fill ${_fill.depositId}`
475
- );
476
-
477
- const spokeClient = this.spokePoolClientManager.getClient(_fill.destinationChainId);
478
- assert(
479
- isDefined(spokeClient),
480
- `SpokePoolClient for ${getNetworkName(_fill.destinationChainId)} not found for fill.`
481
- );
482
-
483
- let provider;
484
- if (isEVMSpokePoolClient(spokeClient)) {
485
- provider = spokeClient.spokePool.provider;
486
- } else if (isSVMSpokePoolClient(spokeClient)) {
487
- provider = spokeClient.svmEventsClient.getRpc();
488
- }
489
- const fill = await verifyFillRepayment(
490
- _fill,
491
- provider!,
492
- matchingDeposit,
493
- this.clients.hubPoolClient,
494
- bundleEndBlockForMainnet
495
- );
496
- if (!isDefined(fill)) {
497
- return;
498
- }
499
- const { chainToSendRefundTo, repaymentToken } = getRefundInformationFromFill(
500
- {
501
- ...fill,
502
- fromLiteChain: matchingDeposit.fromLiteChain,
503
- },
504
- this.clients.hubPoolClient,
505
- bundleEndBlockForMainnet
506
- );
507
- // Assume that lp fees are 0 for the sake of speed. In the future we could batch compute
508
- // these or make hardcoded assumptions based on the origin-repayment chain direction. This might result
509
- // in slight over estimations of refunds, but its not clear whether underestimating or overestimating is
510
- // worst from the relayer's perspective.
511
- const { relayer, inputAmount: refundAmount } = fill;
512
- refundsForChain[chainToSendRefundTo] ??= {};
513
- refundsForChain[chainToSendRefundTo][repaymentToken.toBytes32()] ??= {};
514
- const existingRefundAmount =
515
- refundsForChain[chainToSendRefundTo][repaymentToken.toBytes32()][relayer.toBytes32()] ?? bnZero;
516
- refundsForChain[chainToSendRefundTo][repaymentToken.toBytes32()][relayer.toBytes32()] =
517
- existingRefundAmount.add(refundAmount);
518
- });
519
- }
520
- return refundsForChain;
521
- }
522
-
523
- getUpcomingDepositAmount(chainId: number, l2Token: Address, latestBlockToSearch: number): BigNumber {
524
- const spokePoolClient = this.spokePoolClientManager.getClient(chainId);
525
- if (!isDefined(spokePoolClient)) {
526
- return toBN(0);
527
- }
528
- return spokePoolClient
529
- .getDeposits()
530
- .filter((deposit) => deposit.blockNumber > latestBlockToSearch && deposit.inputToken.eq(l2Token))
531
- .reduce((acc, deposit) => {
532
- return acc.add(deposit.inputAmount);
533
- }, toBN(0));
534
- }
535
-
536
- // @dev This function should probably be moved to the InventoryClient since it bypasses loadData completely now.
537
- // Return refunds from the next valid bundle. This will contain any refunds that have been sent but are not included
538
- // in a valid bundle with all of its leaves executed. This contains refunds from:
539
- // - Bundles that passed liveness but have not had all of their pool rebalance leaves executed.
540
- // - Bundles that are pending liveness
541
- // - Fills sent after the pending, but not validated, bundle
542
- async getNextBundleRefunds(): Promise<CombinedRefunds[]> {
543
- const hubPoolClient = this.clients.hubPoolClient;
544
- const nextBundleMainnetStartBlock = hubPoolClient.getNextBundleStartBlockNumber(
545
- this.chainIdListForBundleEvaluationBlockNumbers,
546
- hubPoolClient.latestHeightSearched,
547
- hubPoolClient.chainId
548
- );
549
- const chainIds = this.clients.configStoreClient.getChainIdIndicesForBlock(nextBundleMainnetStartBlock);
550
- const combinedRefunds: CombinedRefunds[] = [];
551
-
552
- // @dev: If spoke pool client is undefined for a chain, then the end block will be null or undefined, which
553
- // should be handled gracefully and effectively cause this function to ignore refunds for the chain.
554
- let widestBundleBlockRanges = await getWidestPossibleExpectedBlockRange(
555
- chainIds,
556
- this.spokePoolClientManager.getSpokePoolClients(),
557
- getEndBlockBuffers(chainIds, this.blockRangeEndBlockBuffer),
558
- this.clients,
559
- this.clients.hubPoolClient.latestHeightSearched,
560
- this.clients.configStoreClient.getEnabledChains(this.clients.hubPoolClient.latestHeightSearched)
561
- );
562
- // Return block ranges for blocks after _pendingBlockRanges and up to widestBlockRanges.
563
- // If a chain is disabled or doesn't have a spoke pool client, return a range of 0
564
- function getBlockRangeDelta(_pendingBlockRanges: number[][]): number[][] {
565
- return widestBundleBlockRanges.map((blockRange, index) => {
566
- // If pending block range doesn't have an entry for the widest range, which is possible when a new chain
567
- // is added to the CHAIN_ID_INDICES list, then simply set the initial block range to the widest block range.
568
- // This will produce a block range delta of 0 where the returned range for this chain is [widest[1], widest[1]].
569
- const initialBlockRange = _pendingBlockRanges[index] ?? blockRange;
570
- // If chain is disabled, return disabled range
571
- if (initialBlockRange[0] === initialBlockRange[1]) {
572
- return initialBlockRange;
573
- }
574
- // If pending bundle end block exceeds widest end block or if widest end block is undefined
575
- // (which is possible if the spoke pool client for the chain is not defined), return an empty range since there are no
576
- // "new" events to consider for this chain.
577
- if (!isDefined(blockRange[1]) || initialBlockRange[1] >= blockRange[1]) {
578
- return [initialBlockRange[1], initialBlockRange[1]];
579
- }
580
- // If initialBlockRange][0] > widestBlockRange[0], then we'll ignore any blocks
581
- // between initialBlockRange[0] and widestBlockRange[0] (inclusive) for simplicity reasons. In practice
582
- // this should not happen.
583
- return [initialBlockRange[1] + 1, blockRange[1]];
584
- });
585
- }
586
-
587
- // If there is a pending bundle that has not been fully executed, then it should have arweave
588
- // data so we can load it from there.
589
- if (hubPoolClient.hasPendingProposal()) {
590
- const pendingBundleBlockRanges = getImpliedBundleBlockRanges(
591
- hubPoolClient,
592
- this.clients.configStoreClient,
593
- hubPoolClient.getLatestProposedRootBundle()
594
- );
595
- // Similar to getAppoximateRefundsForBlockRange, we'll skip the full bundle reconstruction if the arweave
596
- // data is undefined and use the much faster approximation method which doesn't consider LP fees which is
597
- // ok for this use case.
598
- const arweaveData = await this.loadArweaveData(pendingBundleBlockRanges);
599
- if (!isDefined(arweaveData)) {
600
- combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, pendingBundleBlockRanges));
601
- } else {
602
- const { bundleFillsV3, expiredDepositsToRefundV3 } = arweaveData;
603
- combinedRefunds.push(getRefundsFromBundle(bundleFillsV3, expiredDepositsToRefundV3));
604
- }
605
-
606
- // Shorten the widestBundleBlockRanges now to not double count the pending bundle blocks.
607
- widestBundleBlockRanges = getBlockRangeDelta(pendingBundleBlockRanges);
608
- }
609
-
610
- // Next, load all refunds sent after the last bundle proposal. This can be expensive so we'll skip the full
611
- // bundle reconstruction and make some simplifying assumptions:
612
- // - Only look up fills sent after the pending bundle's end blocks
613
- // - Skip LP fee computations and just assume the relayer is being refunded the full deposit.inputAmount
614
- const start = performance.now();
615
- combinedRefunds.push(await this.getApproximateRefundsForBlockRange(chainIds, widestBundleBlockRanges));
616
- this.logger.debug({
617
- at: "BundleDataClient#getNextBundleRefunds",
618
- message: `Loading approximate refunds for next bundle in ${Math.round(performance.now() - start) / 1000}s.`,
619
- blockRanges: JSON.stringify(widestBundleBlockRanges),
620
- });
621
- return combinedRefunds;
622
- }
623
-
624
- // @dev This helper function should probably be moved to the InventoryClient
625
- getExecutedRefunds(
626
- spokePoolClient: SpokePoolClient,
627
- relayerRefundRoot: string
628
- ): {
629
- [tokenAddress: string]: {
630
- [relayer: string]: BigNumber;
631
- };
632
- } {
633
- if (!isDefined(spokePoolClient)) {
634
- return {};
635
- }
636
- // @dev Search from right to left since there can be multiple root bundles with the same relayer refund root.
637
- // The caller should take caution if they're trying to use this function to find matching refunds for older
638
- // root bundles as opposed to more recent ones.
639
- const bundle = _.findLast(
640
- spokePoolClient.getRootBundleRelays(),
641
- (bundle) => bundle.relayerRefundRoot === relayerRefundRoot
642
- );
643
- if (bundle === undefined) {
644
- return {};
645
- }
646
-
647
- const executedRefundLeaves = spokePoolClient
648
- .getRelayerRefundExecutions()
649
- .filter((leaf) => leaf.rootBundleId === bundle.rootBundleId);
650
- const executedRefunds: { [tokenAddress: string]: { [relayer: string]: BigNumber } } = {};
651
- for (const refundLeaf of executedRefundLeaves) {
652
- const tokenAddress = refundLeaf.l2TokenAddress.toBytes32();
653
- const executedTokenRefunds = (executedRefunds[tokenAddress] ??= {});
654
-
655
- for (let i = 0; i < refundLeaf.refundAddresses.length; i++) {
656
- const relayer = refundLeaf.refundAddresses[i].toBytes32();
657
- const refundAmount = refundLeaf.refundAmounts[i];
658
-
659
- executedTokenRefunds[relayer] ??= bnZero;
660
- executedTokenRefunds[relayer] = executedTokenRefunds[relayer].add(refundAmount);
661
- }
662
- }
663
- return executedRefunds;
664
- }
665
-
666
- // @dev This helper function should probably be moved to the InventoryClient
667
- private deductExecutedRefunds(
668
- allRefunds: CombinedRefunds,
669
- bundleContainingRefunds: ProposedRootBundle
670
- ): CombinedRefunds {
671
- for (const chainIdStr of Object.keys(allRefunds)) {
672
- const chainId = Number(chainIdStr);
673
- const spokePoolClient = this.spokePoolClientManager.getClient(chainId);
674
- if (!isDefined(spokePoolClient)) {
675
- continue;
676
- }
677
- const executedRefunds = this.getExecutedRefunds(spokePoolClient, bundleContainingRefunds.relayerRefundRoot);
678
-
679
- for (const tokenAddress of Object.keys(allRefunds[chainId])) {
680
- const refunds = allRefunds[chainId][tokenAddress];
681
- if (executedRefunds[tokenAddress] === undefined || refunds === undefined) {
682
- continue;
683
- }
684
-
685
- for (const relayer of Object.keys(refunds)) {
686
- const executedAmount = executedRefunds[tokenAddress][relayer];
687
- if (executedAmount === undefined) {
688
- continue;
689
- }
690
- // Since there should only be a single executed relayer refund leaf for each relayer-token-chain combination,
691
- // we can deduct this refund and mark it as executed if the executed amount is > 0.
692
- refunds[relayer] = bnZero;
693
- }
694
- }
695
- }
696
- return allRefunds;
697
- }
698
-
699
- getRefundsFor(bundleRefunds: CombinedRefunds, relayer: Address, chainId: number, token: Address): BigNumber {
700
- if (!bundleRefunds[chainId] || !bundleRefunds[chainId][token.toBytes32()]) {
701
- return BigNumber.from(0);
702
- }
703
- const allRefunds = bundleRefunds[chainId][token.toBytes32()];
704
- return allRefunds && allRefunds[relayer.toBytes32()] ? allRefunds[relayer.toBytes32()] : BigNumber.from(0);
705
- }
706
-
707
- getTotalRefund(refunds: CombinedRefunds[], relayer: Address, chainId: number, refundToken: Address): BigNumber {
708
- return refunds.reduce((totalRefund, refunds) => {
709
- return totalRefund.add(this.getRefundsFor(refunds, relayer, chainId, refundToken));
710
- }, bnZero);
711
- }
712
-
713
365
  private async loadArweaveData(blockRangesForChains: number[][]): Promise<LoadDataReturnValue> {
714
366
  const arweaveKey = this.getArweaveBundleDataClientKey(blockRangesForChains);
715
367
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -111,7 +111,9 @@ export class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
111
111
  if (!results.every(isPromiseFulfilled)) {
112
112
  // Format the error so that it's very clear which providers failed and succeeded.
113
113
  const errorTexts = errors.map(([provider, errorText]) => formatProviderError(provider, errorText));
114
- const successfulProviderUrls = results.filter(isPromiseFulfilled).map((result) => result.value[0].connection.url);
114
+ const successfulProviderUrls = results
115
+ .filter(isPromiseFulfilled)
116
+ .map((result) => getOriginFromURL(result.value[0].connection.url));
115
117
  throw createSendErrorWithMessage(
116
118
  `Not enough providers succeeded. Errors:\n${errorTexts.join("\n")}\n` +
117
119
  `Successful Providers:\n${successfulProviderUrls.join("\n")}`,
@@ -130,7 +132,7 @@ export class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
130
132
  const getMismatchedProviders = (values: [ethers.providers.StaticJsonRpcProvider, unknown][]) => {
131
133
  return values
132
134
  .filter(([, result]) => !compareRpcResults(method, result, quorumResult))
133
- .map(([provider]) => provider.connection.url);
135
+ .map(([provider]) => getOriginFromURL(provider.connection.url));
134
136
  };
135
137
 
136
138
  const logQuorumMismatchOrFailureDetails = (
@@ -154,7 +156,7 @@ export class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
154
156
 
155
157
  const throwQuorumError = (fallbackValues?: [ethers.providers.StaticJsonRpcProvider, unknown][]) => {
156
158
  const errorTexts = errors.map(([provider, errorText]) => formatProviderError(provider, errorText));
157
- const successfulProviderUrls = values.map(([provider]) => provider.connection.url);
159
+ const successfulProviderUrls = values.map(([provider]) => getOriginFromURL(provider.connection.url));
158
160
  const mismatchedProviders = getMismatchedProviders([...values, ...(fallbackValues || [])]);
159
161
  logQuorumMismatchOrFailureDetails(method, params, successfulProviderUrls, mismatchedProviders, errors);
160
162
  throw new Error(
@@ -222,7 +224,7 @@ export class RetryProvider extends ethers.providers.StaticJsonRpcProvider {
222
224
  const mismatchedProviders = getMismatchedProviders([...values, ...fallbackValues]);
223
225
  const quorumProviders = [...values, ...fallbackValues]
224
226
  .filter(([, result]) => compareRpcResults(method, result, quorumResult))
225
- .map(([provider]) => provider.connection.url);
227
+ .map(([provider]) => getOriginFromURL(provider.connection.url));
226
228
  if (mismatchedProviders.length > 0 || errors.length > 0) {
227
229
  logQuorumMismatchOrFailureDetails(method, params, quorumProviders, mismatchedProviders, errors);
228
230
  }
@@ -2,7 +2,7 @@
2
2
  import assert from "assert";
3
3
  import { providers } from "ethers";
4
4
  import { isEqual } from "lodash";
5
- import { isDefined } from "../utils";
5
+ import { getOriginFromURL, isDefined } from "../utils";
6
6
  import { RPCProvider, RPCTransport } from "./types";
7
7
  import * as alchemy from "./alchemy";
8
8
  import * as infura from "./infura";
@@ -133,7 +133,7 @@ export interface RateLimitTask {
133
133
  * @returns The formatted error message.
134
134
  */
135
135
  export function formatProviderError(provider: providers.StaticJsonRpcProvider, rawErrorText: string) {
136
- return `Provider ${provider.connection.url} failed with error: ${rawErrorText}`;
136
+ return `Provider ${getOriginFromURL(provider.connection.url)} failed with error: ${rawErrorText}`;
137
137
  }
138
138
 
139
139
  export function createSendErrorWithMessage(message: string, sendError: Record<string, unknown>) {