@metamask-previews/bridge-controller 79.2.0-preview-6426280bb → 79.2.0-preview-c2ef75d18

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,81 @@ 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 = async (providerData) => {
558
+ const provider = formatProviderLabel(providerData);
559
+ if (isBatchSellRequest || tracedProviders.has(provider)) {
560
+ return;
561
+ }
562
+ tracedProviders.add(provider);
563
+ await traceWithoutImpact({
564
+ name: TraceName.QuoteProviderFirstResult,
565
+ startTime: quoteTraceStartTime,
533
566
  data: {
567
+ provider,
568
+ request_id: quoteTraceRequestId,
569
+ swap_type: getSwapType(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
534
570
  srcChainId: formatChainIdToCaip(firstQuoteRequest.srcChainId),
535
571
  destChainId: formatChainIdToCaip(firstQuoteRequest.destChainId),
572
+ result: 'success',
536
573
  },
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);
574
+ });
575
+ };
576
+ try {
577
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
578
+ // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
579
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
580
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
581
+ // Use SSE if enabled and return early
582
+ if (shouldStream || isBatchSellRequest) {
583
+ const quoteCount = await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, {
584
+ quoteRequests,
585
+ featureId: context.feature_id,
586
+ jwt,
587
+ selectedAccount,
588
+ signal: quoteAbortSignal,
589
+ traceProviderFirstResult,
590
+ });
591
+ if (quoteAbortSignal.aborted) {
592
+ traceResult = 'cancelled';
545
593
  return;
546
594
  }
595
+ traceResult = quoteCount > 0 ? 'success' : 'no_quotes';
596
+ }
597
+ else {
547
598
  // Otherwise use regular fetch
548
- const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
599
+ const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, quoteAbortSignal);
600
+ for (const quote of quotes) {
601
+ await traceProviderFirstResult(quote.quote);
602
+ }
549
603
  this.update((state) => {
550
604
  // Set the initial load time if this is the first fetch
551
605
  if (state.quotesRefreshCount ===
552
606
  DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
553
607
  __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
554
- state.quotesInitialLoadTime =
555
- Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
608
+ state.quotesInitialLoadTime = Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
556
609
  }
557
610
  state.quotes = quotes.map(toQuoteResponseV2);
558
611
  state.quotesLoadingStatus = RequestStatus.FETCHED;
559
612
  });
560
- });
613
+ traceResult = quotes.length > 0 ? 'success' : 'no_quotes';
614
+ }
561
615
  }
562
616
  catch (error) {
563
617
  // Reset the quotes list if the fetch fails to avoid showing stale quotes
@@ -565,14 +619,8 @@ export class BridgeController extends StaticIntervalPollingController() {
565
619
  state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
566
620
  });
567
621
  // 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)) {
622
+ if (isExpectedQuoteAbort(error, quoteAbortSignal)) {
623
+ traceResult = 'cancelled';
576
624
  // Exit the function early to prevent other state updates
577
625
  return;
578
626
  }
@@ -597,6 +645,22 @@ export class BridgeController extends StaticIntervalPollingController() {
597
645
  this.trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesError, context);
598
646
  console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
599
647
  }
648
+ finally {
649
+ await traceWithoutImpact({
650
+ name: traceName,
651
+ startTime: quoteTraceStartTime,
652
+ data: {
653
+ srcChainId: formatChainIdToCaip(firstQuoteRequest.srcChainId),
654
+ destChainId: formatChainIdToCaip(firstQuoteRequest.destChainId),
655
+ ...(!isBatchSellRequest && {
656
+ request_id: quoteTraceRequestId,
657
+ swap_type: getSwapType(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
658
+ feature_id: context.feature_id,
659
+ result: traceResult,
660
+ }),
661
+ },
662
+ });
663
+ }
600
664
  // Update refresh count after fetching, validation and fee calculation have completed
601
665
  this.update((state) => {
602
666
  state.quotesRefreshCount += 1;
@@ -611,7 +675,7 @@ export class BridgeController extends StaticIntervalPollingController() {
611
675
  this.stopAllPolling();
612
676
  }
613
677
  });
614
- _BridgeController_handleQuoteStreaming.set(this, async (quoteRequests, featureId, jwt, selectedAccount) => {
678
+ _BridgeController_handleQuoteStreaming.set(this, async ({ quoteRequests, featureId, jwt, selectedAccount, signal, traceProviderFirstResult, }) => {
615
679
  /**
616
680
  * Tracks the number of valid quotes received from the current stream, which is used
617
681
  * to determine when to clear the quotes list and set the initial load time
@@ -622,9 +686,10 @@ export class BridgeController extends StaticIntervalPollingController() {
622
686
  * before setting quotesLoadingStatus to FETCHED
623
687
  */
624
688
  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, {
689
+ 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
690
  onQuoteValidationFailure: (validationFailures) => __classPrivateFieldGet(this, _BridgeController_trackQuoteValidationFailures, "f").call(this, validationFailures, featureId),
627
691
  onValidQuoteReceived: async (quote) => {
692
+ await traceProviderFirstResult(quote.quote);
628
693
  const feeAppendPromise = (async () => {
629
694
  const quotesWithFees = await appendFeesToQuotes(quote.chainId, [quote], this.messenger, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
630
695
  if (quotesWithFees.length > 0) {
@@ -688,6 +753,7 @@ export class BridgeController extends StaticIntervalPollingController() {
688
753
  });
689
754
  },
690
755
  }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
756
+ return validQuotesCounter;
691
757
  });
692
758
  _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
693
759
  if (!isSolanaChainId(srcChainId) || !snapId) {