@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.
- package/CHANGELOG.md +9 -2
- package/dist/bridge-controller.cjs +97 -29
- package/dist/bridge-controller.cjs.map +1 -1
- package/dist/bridge-controller.d.cts.map +1 -1
- package/dist/bridge-controller.d.mts.map +1 -1
- package/dist/bridge-controller.mjs +98 -30
- package/dist/bridge-controller.mjs.map +1 -1
- package/dist/constants/traces.cjs +1 -0
- package/dist/constants/traces.cjs.map +1 -1
- package/dist/constants/traces.d.cts +1 -0
- package/dist/constants/traces.d.cts.map +1 -1
- package/dist/constants/traces.d.mts +1 -0
- package/dist/constants/traces.d.mts.map +1 -1
- package/dist/constants/traces.mjs +1 -0
- package/dist/constants/traces.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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,
|
|
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
|
|
569
|
-
|
|
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,
|
|
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) {
|