@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.
- package/CHANGELOG.md +4 -0
- package/dist/bridge-controller.cjs +95 -29
- package/dist/bridge-controller.cjs.map +1 -1
- package/dist/bridge-controller.d.cts +2 -2
- package/dist/bridge-controller.d.cts.map +1 -1
- package/dist/bridge-controller.d.mts +2 -2
- package/dist/bridge-controller.d.mts.map +1 -1
- package/dist/bridge-controller.mjs +96 -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 +1 -1
|
@@ -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
|
-
|
|
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 = 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
|
-
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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,
|
|
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
|
|
569
|
-
|
|
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,
|
|
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) {
|