@metamask-previews/bridge-controller 79.2.0-preview-3c77cf4 → 79.3.0-preview-5cf810ca6

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.
@@ -16,6 +16,7 @@ import { Contract } from "@ethersproject/contracts";
16
16
  import { Web3Provider } from "@ethersproject/providers";
17
17
  import { abiERC20 } from "@metamask/metamask-eth-abis";
18
18
  import { StaticIntervalPollingController } from "@metamask/polling-controller";
19
+ import { v4 as uuid } from "uuid";
19
20
  import { toQuoteResponseV2 } from "./coercers/quote-response-v1-to-v2.mjs";
20
21
  import { BRIDGE_CONTROLLER_NAME, BRIDGE_PROD_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE, METABRIDGE_ETHEREUM_ADDRESS, REFRESH_INTERVAL_MS } from "./constants/bridge.mjs";
21
22
  import { CHAIN_IDS } from "./constants/chains.mjs";
@@ -29,7 +30,7 @@ import { formatAddressToCaipReference, formatChainIdToCaip, formatChainIdToHex }
29
30
  import { getBridgeFeatureFlags, hasMinimumRequiredVersion } from "./utils/feature-flags.mjs";
30
31
  import { fetchAssetPrices, fetchBridgeQuotes, fetchBridgeQuoteStream, fetchBatchSellTrades } from "./utils/fetch.mjs";
31
32
  import { AbortReason, BatchSellMetricsEventName, MetaMetricsSwapsEventSource, MetricsActionType, UnifiedSwapBridgeEventName } from "./utils/metrics/constants.mjs";
32
- import { formatProviderLabel, getAccountHardwareType, getRequestParams, getSwapTypeFromQuote, isCustomSlippage, toInputChangedPropertyKey, toInputChangedPropertyValue } from "./utils/metrics/properties.mjs";
33
+ import { formatProviderLabel, getAccountHardwareType, getRequestParams, getSwapType, getSwapTypeFromQuote, isCustomSlippage, toInputChangedPropertyKey, toInputChangedPropertyValue } from "./utils/metrics/properties.mjs";
33
34
  import { appendFeesToQuotes } from "./utils/quote-fees.mjs";
34
35
  import { getMinimumBalanceForRentExemptionInLamports } from "./utils/snaps.mjs";
35
36
  import { sortQuotes } from "./utils/sort-quotes.mjs";
@@ -126,6 +127,18 @@ const metadata = {
126
127
  usedInUi: true,
127
128
  },
128
129
  };
130
+ const QUOTE_ABORT_REASONS = new Set(Object.values(AbortReason));
131
+ const isExpectedQuoteAbort = (error, signal) => {
132
+ if (signal?.aborted) {
133
+ return true;
134
+ }
135
+ if (QUOTE_ABORT_REASONS.has(String(error))) {
136
+ return true;
137
+ }
138
+ const errorText = error instanceof Error ? `${error.name} ${error.message}` : String(error);
139
+ return (errorText.includes('AbortError') ||
140
+ errorText.includes('FetchRequestCanceledException'));
141
+ };
129
142
  const MESSENGER_EXPOSED_METHODS = [
130
143
  'updateBridgeQuoteRequestParams',
131
144
  'fetchQuotes',
@@ -501,6 +514,9 @@ export class BridgeController extends StaticIntervalPollingController() {
501
514
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(AbortReason.NewQuoteRequest);
502
515
  __classPrivateFieldGet(this, _BridgeController_batchSellTradesAbortController, "f")?.abort(AbortReason.NewQuoteRequest);
503
516
  __classPrivateFieldSet(this, _BridgeController_abortController, new AbortController(), "f");
517
+ const quoteTraceStartTime = Date.now();
518
+ const quoteTraceRequestId = uuid();
519
+ const quoteAbortSignal = __classPrivateFieldGet(this, _BridgeController_abortController, "f").signal;
504
520
  __classPrivateFieldGet(this, _BridgeController_fetchAssetExchangeRates, "f").call(this, quoteRequests).catch((error) => console.warn('Failed to fetch asset exchange rates', error));
505
521
  this.trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesRequested, context);
506
522
  const { sse, maxRefreshCount } = getBridgeFeatureFlags(this.messenger);
@@ -521,43 +537,84 @@ export class BridgeController extends StaticIntervalPollingController() {
521
537
  }
522
538
  });
523
539
  const jwt = await __classPrivateFieldGet(this, _BridgeController_getJwt, "f").call(this);
524
- try {
525
- const [firstQuoteRequest] = quoteRequests;
526
- const unifiedSwapTraceName = isCrossChain(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId)
540
+ const [firstQuoteRequest] = quoteRequests;
541
+ let traceResult = 'error';
542
+ let traceName = TraceName.BatchSellQuotesFetched;
543
+ if (!isBatchSellRequest) {
544
+ traceName = isCrossChain(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId)
527
545
  ? TraceName.BridgeQuotesFetched
528
546
  : TraceName.SwapQuotesFetched;
529
- await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, {
530
- name: isBatchSellRequest
531
- ? TraceName.BatchSellQuotesFetched
532
- : unifiedSwapTraceName,
547
+ }
548
+ const tracedProviders = new Set();
549
+ const traceWithoutImpact = async (request) => {
550
+ try {
551
+ await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, request, () => undefined);
552
+ }
553
+ catch {
554
+ // Telemetry failures must not affect quote fetching or state updates.
555
+ }
556
+ };
557
+ const traceProviderFirstResult = (providerData) => {
558
+ const provider = formatProviderLabel(providerData);
559
+ if (isBatchSellRequest || tracedProviders.has(provider)) {
560
+ return;
561
+ }
562
+ tracedProviders.add(provider);
563
+ // Provider telemetry must not delay quote processing.
564
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
565
+ traceWithoutImpact({
566
+ name: TraceName.QuoteProviderFirstResult,
567
+ startTime: quoteTraceStartTime,
533
568
  data: {
569
+ provider,
570
+ feature_id: context.feature_id,
571
+ request_id: quoteTraceRequestId,
572
+ swap_type: getSwapType(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
534
573
  srcChainId: formatChainIdToCaip(firstQuoteRequest.srcChainId),
535
574
  destChainId: formatChainIdToCaip(firstQuoteRequest.destChainId),
575
+ result: 'success',
536
576
  },
537
- }, async () => {
538
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
539
- // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
540
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
541
- __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
542
- // Use SSE if enabled and return early
543
- if (shouldStream || isBatchSellRequest) {
544
- await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, quoteRequests, context.feature_id, jwt, selectedAccount);
577
+ });
578
+ };
579
+ try {
580
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
581
+ // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
582
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
583
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
584
+ // Use SSE if enabled and return early
585
+ if (shouldStream || isBatchSellRequest) {
586
+ const quoteCount = await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, {
587
+ quoteRequests,
588
+ featureId: context.feature_id,
589
+ jwt,
590
+ selectedAccount,
591
+ signal: quoteAbortSignal,
592
+ traceProviderFirstResult,
593
+ });
594
+ if (quoteAbortSignal.aborted) {
595
+ traceResult = 'cancelled';
545
596
  return;
546
597
  }
598
+ traceResult = quoteCount > 0 ? 'success' : 'no_quotes';
599
+ }
600
+ else {
547
601
  // Otherwise use regular fetch
548
- const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
602
+ const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, quoteAbortSignal);
603
+ for (const quote of quotes) {
604
+ traceProviderFirstResult(quote.quote);
605
+ }
549
606
  this.update((state) => {
550
607
  // Set the initial load time if this is the first fetch
551
608
  if (state.quotesRefreshCount ===
552
609
  DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
553
610
  __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
554
- state.quotesInitialLoadTime =
555
- Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
611
+ state.quotesInitialLoadTime = Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
556
612
  }
557
613
  state.quotes = quotes.map(toQuoteResponseV2);
558
614
  state.quotesLoadingStatus = RequestStatus.FETCHED;
559
615
  });
560
- });
616
+ traceResult = quotes.length > 0 ? 'success' : 'no_quotes';
617
+ }
561
618
  }
562
619
  catch (error) {
563
620
  // Reset the quotes list if the fetch fails to avoid showing stale quotes
@@ -565,14 +622,8 @@ export class BridgeController extends StaticIntervalPollingController() {
565
622
  state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
566
623
  });
567
624
  // Ignore abort errors
568
- if (error.toString().includes('AbortError') ||
569
- error.toString().includes('FetchRequestCanceledException') ||
570
- [
571
- AbortReason.ResetState,
572
- AbortReason.NewQuoteRequest,
573
- AbortReason.QuoteRequestUpdated,
574
- AbortReason.TransactionSubmitted,
575
- ].includes(error)) {
625
+ if (isExpectedQuoteAbort(error, quoteAbortSignal)) {
626
+ traceResult = 'cancelled';
576
627
  // Exit the function early to prevent other state updates
577
628
  return;
578
629
  }
@@ -597,6 +648,21 @@ export class BridgeController extends StaticIntervalPollingController() {
597
648
  this.trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesError, context);
598
649
  console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
599
650
  }
651
+ finally {
652
+ await traceWithoutImpact({
653
+ name: traceName,
654
+ startTime: quoteTraceStartTime,
655
+ data: {
656
+ srcChainId: formatChainIdToCaip(firstQuoteRequest.srcChainId),
657
+ destChainId: formatChainIdToCaip(firstQuoteRequest.destChainId),
658
+ ...(!isBatchSellRequest && {
659
+ request_id: quoteTraceRequestId,
660
+ feature_id: context.feature_id,
661
+ result: traceResult,
662
+ }),
663
+ },
664
+ });
665
+ }
600
666
  // Update refresh count after fetching, validation and fee calculation have completed
601
667
  this.update((state) => {
602
668
  state.quotesRefreshCount += 1;
@@ -611,7 +677,7 @@ export class BridgeController extends StaticIntervalPollingController() {
611
677
  this.stopAllPolling();
612
678
  }
613
679
  });
614
- _BridgeController_handleQuoteStreaming.set(this, async (quoteRequests, featureId, jwt, selectedAccount) => {
680
+ _BridgeController_handleQuoteStreaming.set(this, async ({ quoteRequests, featureId, jwt, selectedAccount, signal, traceProviderFirstResult, }) => {
615
681
  /**
616
682
  * Tracks the number of valid quotes received from the current stream, which is used
617
683
  * to determine when to clear the quotes list and set the initial load time
@@ -622,13 +688,14 @@ export class BridgeController extends StaticIntervalPollingController() {
622
688
  * before setting quotesLoadingStatus to FETCHED
623
689
  */
624
690
  const pendingFeeAppendPromises = new Set();
625
- await fetchBridgeQuoteStream(__classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), quoteRequests, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal, featureId, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), jwt, __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL, {
691
+ await fetchBridgeQuoteStream(__classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), quoteRequests, signal, featureId, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), jwt, __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL, {
626
692
  onQuoteValidationFailure: (validationFailures) => __classPrivateFieldGet(this, _BridgeController_trackQuoteValidationFailures, "f").call(this, validationFailures, featureId),
627
693
  onValidQuoteReceived: async (quote) => {
628
694
  const feeAppendPromise = (async () => {
629
695
  const quotesWithFees = await appendFeesToQuotes(quote.chainId, [quote], this.messenger, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
630
696
  if (quotesWithFees.length > 0) {
631
697
  validQuotesCounter += 1;
698
+ traceProviderFirstResult(quote.quote);
632
699
  }
633
700
  this.update((state) => {
634
701
  // Clear previous quotes and quotes load time when first quote in the current
@@ -688,6 +755,7 @@ export class BridgeController extends StaticIntervalPollingController() {
688
755
  });
689
756
  },
690
757
  }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
758
+ return validQuotesCounter;
691
759
  });
692
760
  _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
693
761
  if (!isSolanaChainId(srcChainId) || !snapId) {