@metamask/bridge-controller 49.0.1 → 51.0.0

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/dist/bridge-controller.cjs +109 -169
  3. package/dist/bridge-controller.cjs.map +1 -1
  4. package/dist/bridge-controller.d.cts +5 -4
  5. package/dist/bridge-controller.d.cts.map +1 -1
  6. package/dist/bridge-controller.d.mts +5 -4
  7. package/dist/bridge-controller.d.mts.map +1 -1
  8. package/dist/bridge-controller.mjs +113 -173
  9. package/dist/bridge-controller.mjs.map +1 -1
  10. package/dist/selectors.d.cts +100 -78
  11. package/dist/selectors.d.cts.map +1 -1
  12. package/dist/selectors.d.mts +100 -78
  13. package/dist/selectors.d.mts.map +1 -1
  14. package/dist/types.cjs.map +1 -1
  15. package/dist/types.d.cts +21 -10
  16. package/dist/types.d.cts.map +1 -1
  17. package/dist/types.d.mts +21 -10
  18. package/dist/types.d.mts.map +1 -1
  19. package/dist/types.mjs.map +1 -1
  20. package/dist/utils/feature-flags.d.cts +1 -0
  21. package/dist/utils/feature-flags.d.cts.map +1 -1
  22. package/dist/utils/feature-flags.d.mts +1 -0
  23. package/dist/utils/feature-flags.d.mts.map +1 -1
  24. package/dist/utils/fetch.cjs +97 -19
  25. package/dist/utils/fetch.cjs.map +1 -1
  26. package/dist/utils/fetch.d.cts +23 -2
  27. package/dist/utils/fetch.d.cts.map +1 -1
  28. package/dist/utils/fetch.d.mts +23 -2
  29. package/dist/utils/fetch.d.mts.map +1 -1
  30. package/dist/utils/fetch.mjs +95 -18
  31. package/dist/utils/fetch.mjs.map +1 -1
  32. package/dist/utils/quote-fees.cjs +120 -0
  33. package/dist/utils/quote-fees.cjs.map +1 -0
  34. package/dist/utils/quote-fees.d.cts +18 -0
  35. package/dist/utils/quote-fees.d.cts.map +1 -0
  36. package/dist/utils/quote-fees.d.mts +18 -0
  37. package/dist/utils/quote-fees.d.mts.map +1 -0
  38. package/dist/utils/quote-fees.mjs +116 -0
  39. package/dist/utils/quote-fees.mjs.map +1 -0
  40. package/dist/utils/quote.cjs +12 -1
  41. package/dist/utils/quote.cjs.map +1 -1
  42. package/dist/utils/quote.d.cts +2 -0
  43. package/dist/utils/quote.d.cts.map +1 -1
  44. package/dist/utils/quote.d.mts +2 -0
  45. package/dist/utils/quote.d.mts.map +1 -1
  46. package/dist/utils/quote.mjs +10 -0
  47. package/dist/utils/quote.mjs.map +1 -1
  48. package/dist/utils/snaps.cjs +19 -1
  49. package/dist/utils/snaps.cjs.map +1 -1
  50. package/dist/utils/snaps.d.cts +9 -0
  51. package/dist/utils/snaps.d.cts.map +1 -1
  52. package/dist/utils/snaps.d.mts +9 -0
  53. package/dist/utils/snaps.d.mts.map +1 -1
  54. package/dist/utils/snaps.mjs +17 -0
  55. package/dist/utils/snaps.mjs.map +1 -1
  56. package/dist/utils/validators.cjs +1 -0
  57. package/dist/utils/validators.cjs.map +1 -1
  58. package/dist/utils/validators.d.cts +10 -7
  59. package/dist/utils/validators.d.cts.map +1 -1
  60. package/dist/utils/validators.d.mts +10 -7
  61. package/dist/utils/validators.d.mts.map +1 -1
  62. package/dist/utils/validators.mjs +1 -0
  63. package/dist/utils/validators.mjs.map +1 -1
  64. package/package.json +4 -3
@@ -9,28 +9,26 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _BridgeController_instances, _BridgeController_abortController, _BridgeController_quotesFirstFetched, _BridgeController_clientId, _BridgeController_clientVersion, _BridgeController_getLayer1GasFee, _BridgeController_fetchFn, _BridgeController_trackMetaMetricsFn, _BridgeController_trace, _BridgeController_config, _BridgeController_trackResponseValidationFailures, _BridgeController_getExchangeRateSources, _BridgeController_fetchAssetExchangeRates, _BridgeController_hasSufficientBalance, _BridgeController_fetchBridgeQuotes, _BridgeController_appendL1GasFees, _BridgeController_setMinimumBalanceForRentExemptionInLamports, _BridgeController_appendNonEvmFees, _BridgeController_getMultichainSelectedAccount, _BridgeController_getSelectedNetworkClientId, _BridgeController_getSelectedNetworkClient, _BridgeController_getRequestParams, _BridgeController_getRequestMetadata, _BridgeController_getQuoteFetchData, _BridgeController_getEventProperties, _BridgeController_trackInputChangedEvents;
12
+ var _BridgeController_instances, _BridgeController_abortController, _BridgeController_quotesFirstFetched, _BridgeController_clientId, _BridgeController_clientVersion, _BridgeController_getLayer1GasFee, _BridgeController_fetchFn, _BridgeController_trackMetaMetricsFn, _BridgeController_trace, _BridgeController_config, _BridgeController_trackResponseValidationFailures, _BridgeController_getExchangeRateSources, _BridgeController_fetchAssetExchangeRates, _BridgeController_hasSufficientBalance, _BridgeController_fetchBridgeQuotes, _BridgeController_handleQuoteStreaming, _BridgeController_setMinimumBalanceForRentExemptionInLamports, _BridgeController_getMultichainSelectedAccount, _BridgeController_getSelectedNetworkClientId, _BridgeController_getSelectedNetworkClient, _BridgeController_getRequestParams, _BridgeController_getRequestMetadata, _BridgeController_getQuoteFetchData, _BridgeController_getEventProperties, _BridgeController_trackInputChangedEvents;
13
13
  import { Contract } from "@ethersproject/contracts";
14
14
  import { Web3Provider } from "@ethersproject/providers";
15
15
  import { abiERC20 } from "@metamask/metamask-eth-abis";
16
16
  import { StaticIntervalPollingController } from "@metamask/polling-controller";
17
- import { numberToHex } from "@metamask/utils";
18
17
  import { BRIDGE_CONTROLLER_NAME, BRIDGE_PROD_API_BASE_URL, DEFAULT_BRIDGE_CONTROLLER_STATE, METABRIDGE_CHAIN_TO_ADDRESS_MAP, REFRESH_INTERVAL_MS } from "./constants/bridge.mjs";
19
- import { CHAIN_IDS } from "./constants/chains.mjs";
20
18
  import { TraceName } from "./constants/traces.mjs";
21
19
  import { selectIsAssetExchangeRateInState } from "./selectors.mjs";
22
20
  import { RequestStatus } from "./types.mjs";
23
21
  import { getAssetIdsForToken, toExchangeRates } from "./utils/assets.mjs";
24
22
  import { hasSufficientBalance } from "./utils/balance.mjs";
25
- import { getDefaultBridgeControllerState, isCrossChain, isNonEvmChainId, isSolanaChainId, sumHexes } from "./utils/bridge.mjs";
23
+ import { getDefaultBridgeControllerState, isCrossChain, isNonEvmChainId, isSolanaChainId } from "./utils/bridge.mjs";
26
24
  import { formatAddressToCaipReference, formatChainIdToCaip, formatChainIdToHex } from "./utils/caip-formatters.mjs";
27
25
  import { getBridgeFeatureFlags } from "./utils/feature-flags.mjs";
28
- import { fetchAssetPrices, fetchBridgeQuotes } from "./utils/fetch.mjs";
26
+ import { fetchAssetPrices, fetchBridgeQuotes, fetchBridgeQuoteStream } from "./utils/fetch.mjs";
29
27
  import { AbortReason, MetricsActionType, UnifiedSwapBridgeEventName } from "./utils/metrics/constants.mjs";
30
28
  import { formatProviderLabel, getRequestParams, getSwapTypeFromQuote, isCustomSlippage, isHardwareWallet, toInputChangedPropertyKey, toInputChangedPropertyValue } from "./utils/metrics/properties.mjs";
31
- import { isValidQuoteRequest } from "./utils/quote.mjs";
32
- import { computeFeeRequest, getMinimumBalanceForRentExemptionRequest } from "./utils/snaps.mjs";
33
- import { FeatureId } from "./utils/validators.mjs";
29
+ import { isValidQuoteRequest, sortQuotes } from "./utils/quote.mjs";
30
+ import { appendFeesToQuotes } from "./utils/quote-fees.mjs";
31
+ import { getMinimumBalanceForRentExemptionInLamports } from "./utils/snaps.mjs";
34
32
  const metadata = {
35
33
  quoteRequest: {
36
34
  includeInStateLogs: true,
@@ -112,31 +110,14 @@ export class BridgeController extends StaticIntervalPollingController() {
112
110
  await __classPrivateFieldGet(this, _BridgeController_fetchBridgeQuotes, "f").call(this, pollingInput);
113
111
  };
114
112
  this.updateBridgeQuoteRequestParams = async (paramsToUpdate, context) => {
115
- this.stopAllPolling();
116
- __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(AbortReason.QuoteRequestUpdated);
117
113
  __classPrivateFieldGet(this, _BridgeController_trackInputChangedEvents, "f").call(this, paramsToUpdate);
114
+ this.resetState(AbortReason.QuoteRequestUpdated);
118
115
  const updatedQuoteRequest = {
119
116
  ...DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest,
120
117
  ...paramsToUpdate,
121
118
  };
122
119
  this.update((state) => {
123
120
  state.quoteRequest = updatedQuoteRequest;
124
- state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
125
- state.quotesLastFetched =
126
- DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched;
127
- state.quotesLoadingStatus =
128
- DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus;
129
- state.quoteFetchError = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError;
130
- state.quotesRefreshCount =
131
- DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount;
132
- state.quotesInitialLoadTime =
133
- DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime;
134
- // Reset required minimum balance if the source chain is not Solana
135
- if (updatedQuoteRequest.srcChainId &&
136
- !isSolanaChainId(updatedQuoteRequest.srcChainId)) {
137
- state.minimumBalanceForRentExemptionInLamports =
138
- DEFAULT_BRIDGE_CONTROLLER_STATE.minimumBalanceForRentExemptionInLamports;
139
- }
140
121
  });
141
122
  await __classPrivateFieldGet(this, _BridgeController_fetchAssetExchangeRates, "f").call(this, updatedQuoteRequest).catch((error) => console.warn('Failed to fetch asset exchange rates', error));
142
123
  if (isValidQuoteRequest(updatedQuoteRequest)) {
@@ -153,10 +134,16 @@ export class BridgeController extends StaticIntervalPollingController() {
153
134
  insufficientBal = true;
154
135
  }
155
136
  else {
156
- // Otherwise query the src token balance from the RPC provider
157
- insufficientBal =
158
- paramsToUpdate.insufficientBal ??
159
- !(await __classPrivateFieldGet(this, _BridgeController_hasSufficientBalance, "f").call(this, updatedQuoteRequest));
137
+ try {
138
+ // Otherwise query the src token balance from the RPC provider
139
+ insufficientBal =
140
+ paramsToUpdate.insufficientBal ??
141
+ !(await __classPrivateFieldGet(this, _BridgeController_hasSufficientBalance, "f").call(this, updatedQuoteRequest));
142
+ }
143
+ catch (error) {
144
+ console.warn('Failed to fetch balance', error);
145
+ insufficientBal = true;
146
+ }
160
147
  }
161
148
  const networkClientId = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getSelectedNetworkClientId).call(this);
162
149
  // Set refresh rate based on the source chain before starting polling
@@ -191,17 +178,8 @@ export class BridgeController extends StaticIntervalPollingController() {
191
178
  ? { ...quoteRequest, ...quoteRequestOverrides }
192
179
  : quoteRequest, abortSignal, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), __classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL, featureId, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
193
180
  __classPrivateFieldGet(this, _BridgeController_trackResponseValidationFailures, "f").call(this, validationFailures);
194
- const quotesWithL1GasFees = await __classPrivateFieldGet(this, _BridgeController_appendL1GasFees, "f").call(this, baseQuotes);
195
- const quotesWithNonEvmFees = await __classPrivateFieldGet(this, _BridgeController_appendNonEvmFees, "f").call(this, baseQuotes, quoteRequest.walletAddress);
196
- const quotesWithFees = quotesWithL1GasFees ?? quotesWithNonEvmFees ?? baseQuotes;
197
- // Sort perps quotes by increasing estimated processing time (fastest first)
198
- if (featureId === FeatureId.PERPS) {
199
- return quotesWithFees.sort((a, b) => {
200
- return (a.estimatedProcessingTimeInSeconds -
201
- b.estimatedProcessingTimeInSeconds);
202
- });
203
- }
204
- return quotesWithFees;
181
+ const quotesWithFees = await appendFeesToQuotes(baseQuotes, this.messagingSystem, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, quoteRequest.walletAddress));
182
+ return sortQuotes(quotesWithFees, featureId);
205
183
  };
206
184
  _BridgeController_trackResponseValidationFailures.set(this, (validationFailures) => {
207
185
  if (validationFailures.length === 0) {
@@ -252,6 +230,7 @@ export class BridgeController extends StaticIntervalPollingController() {
252
230
  clientId: __classPrivateFieldGet(this, _BridgeController_clientId, "f"),
253
231
  clientVersion: __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"),
254
232
  fetchFn: __classPrivateFieldGet(this, _BridgeController_fetchFn, "f"),
233
+ signal: __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal,
255
234
  });
256
235
  const exchangeRates = toExchangeRates(currency, pricesByAssetId);
257
236
  this.update((state) => {
@@ -262,10 +241,6 @@ export class BridgeController extends StaticIntervalPollingController() {
262
241
  });
263
242
  });
264
243
  _BridgeController_hasSufficientBalance.set(this, async (quoteRequest) => {
265
- // Only check balance for EVM chains
266
- if (isNonEvmChainId(quoteRequest.srcChainId)) {
267
- return true;
268
- }
269
244
  const srcChainIdInHex = formatChainIdToHex(quoteRequest.srcChainId);
270
245
  const provider = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getSelectedNetworkClient).call(this)?.provider;
271
246
  const normalizedSrcTokenAddress = formatAddressToCaipReference(quoteRequest.srcTokenAddress);
@@ -279,8 +254,8 @@ export class BridgeController extends StaticIntervalPollingController() {
279
254
  this.stopAllPolling();
280
255
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(reason);
281
256
  };
282
- this.resetState = () => {
283
- this.stopPollingForQuotes(AbortReason.ResetState);
257
+ this.resetState = (reason = AbortReason.ResetState) => {
258
+ this.stopPollingForQuotes(reason);
284
259
  this.update((state) => {
285
260
  // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field
286
261
  state.quoteRequest = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest;
@@ -317,10 +292,13 @@ export class BridgeController extends StaticIntervalPollingController() {
317
292
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort('New quote request');
318
293
  __classPrivateFieldSet(this, _BridgeController_abortController, new AbortController(), "f");
319
294
  this.trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesRequested, context);
295
+ const { sseEnabled, maxRefreshCount } = getBridgeFeatureFlags(this.messagingSystem);
296
+ const shouldStream = Boolean(sseEnabled);
320
297
  this.update((state) => {
321
- state.quotesLoadingStatus = RequestStatus.LOADING;
322
298
  state.quoteRequest = updatedQuoteRequest;
323
299
  state.quoteFetchError = DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError;
300
+ state.quotesLastFetched = Date.now();
301
+ state.quotesLoadingStatus = RequestStatus.LOADING;
324
302
  });
325
303
  try {
326
304
  await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, {
@@ -332,22 +310,36 @@ export class BridgeController extends StaticIntervalPollingController() {
332
310
  destChainId: formatChainIdToCaip(updatedQuoteRequest.destChainId),
333
311
  },
334
312
  }, async () => {
313
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, updatedQuoteRequest.walletAddress);
335
314
  // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
336
315
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
337
- __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, updatedQuoteRequest.srcChainId);
338
- const quotes = await this.fetchQuotes(updatedQuoteRequest,
339
- // AbortController is always defined by this line, because we assign it a few lines above,
340
- // not sure why Jest thinks it's not
341
- // Linters accurately say that it's defined
342
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
343
- __classPrivateFieldGet(this, _BridgeController_abortController, "f").signal);
316
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, updatedQuoteRequest.srcChainId, selectedAccount.metadata?.snap?.id);
317
+ // Use SSE if enabled and return early
318
+ if (shouldStream) {
319
+ await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, updatedQuoteRequest, selectedAccount);
320
+ return;
321
+ }
322
+ // Otherwise use regular fetch
323
+ const quotes = await this.fetchQuotes(updatedQuoteRequest, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
344
324
  this.update((state) => {
325
+ // Set the initial load time if this is the first fetch
326
+ if (state.quotesRefreshCount ===
327
+ DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
328
+ __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
329
+ state.quotesInitialLoadTime =
330
+ Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
331
+ }
345
332
  state.quotes = quotes;
346
333
  state.quotesLoadingStatus = RequestStatus.FETCHED;
347
334
  });
348
335
  });
349
336
  }
350
337
  catch (error) {
338
+ // Reset the quotes list if the fetch fails to avoid showing stale quotes
339
+ this.update((state) => {
340
+ state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
341
+ });
342
+ // Ignore abort errors
351
343
  const isAbortError = error.name === 'AbortError';
352
344
  if (isAbortError ||
353
345
  [
@@ -358,141 +350,89 @@ export class BridgeController extends StaticIntervalPollingController() {
358
350
  // Exit the function early to prevent other state updates
359
351
  return;
360
352
  }
353
+ // Update loading status and error message
361
354
  this.update((state) => {
362
- state.quoteFetchError =
363
- error instanceof Error ? error.message : (error?.toString() ?? null);
355
+ // The error object reference is not guaranteed to exist on mobile so reading
356
+ // the message directly could cause an error.
357
+ let errorMessage;
358
+ try {
359
+ errorMessage =
360
+ error?.message ?? error.toString();
361
+ }
362
+ catch {
363
+ // Intentionally empty
364
+ }
365
+ finally {
366
+ state.quoteFetchError = errorMessage ?? 'Unknown error';
367
+ }
364
368
  state.quotesLoadingStatus = RequestStatus.ERROR;
365
- state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
366
369
  });
370
+ // Track event and log error
367
371
  this.trackUnifiedSwapBridgeEvent(UnifiedSwapBridgeEventName.QuotesError, context);
368
- console.log('Failed to fetch bridge quotes', error);
372
+ console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
369
373
  }
370
- const bridgeFeatureFlags = getBridgeFeatureFlags(this.messagingSystem);
371
- const { maxRefreshCount } = bridgeFeatureFlags;
374
+ // Update refresh count after fetching, validation and fee calculation have completed
375
+ this.update((state) => {
376
+ state.quotesRefreshCount += 1;
377
+ });
372
378
  // Stop polling if the maximum number of refreshes has been reached
373
379
  if (updatedQuoteRequest.insufficientBal ||
374
380
  (!updatedQuoteRequest.insufficientBal &&
375
381
  this.state.quotesRefreshCount >= maxRefreshCount)) {
376
382
  this.stopAllPolling();
377
383
  }
378
- // Update quote fetching stats
379
- const quotesLastFetched = Date.now();
380
- this.update((state) => {
381
- state.quotesInitialLoadTime =
382
- state.quotesRefreshCount === 0 && __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")
383
- ? quotesLastFetched - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")
384
- : this.state.quotesInitialLoadTime;
385
- state.quotesLastFetched = quotesLastFetched;
386
- state.quotesRefreshCount += 1;
387
- });
388
- });
389
- _BridgeController_appendL1GasFees.set(this, async (quotes) => {
390
- // Indicates whether some of the quotes are not for optimism or base
391
- const hasInvalidQuotes = quotes.some(({ quote }) => {
392
- const chainId = formatChainIdToCaip(quote.srcChainId);
393
- return ![CHAIN_IDS.OPTIMISM, CHAIN_IDS.BASE]
394
- .map(formatChainIdToCaip)
395
- .includes(chainId);
396
- });
397
- // Only append L1 gas fees if all quotes are for either optimism or base
398
- if (hasInvalidQuotes) {
399
- return undefined;
400
- }
401
- const l1GasFeePromises = Promise.allSettled(quotes.map(async (quoteResponse) => {
402
- const { quote, trade, approval } = quoteResponse;
403
- const chainId = numberToHex(quote.srcChainId);
404
- const getTxParams = (txData) => ({
405
- from: txData.from,
406
- to: txData.to,
407
- value: txData.value,
408
- data: txData.data,
409
- gasLimit: txData.gasLimit?.toString(),
410
- });
411
- const approvalL1GasFees = approval
412
- ? await __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f").call(this, {
413
- transactionParams: getTxParams(approval),
414
- chainId,
415
- })
416
- : '0x0';
417
- const tradeL1GasFees = await __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f").call(this, {
418
- transactionParams: getTxParams(trade),
419
- chainId,
420
- });
421
- if (approvalL1GasFees === undefined || tradeL1GasFees === undefined) {
422
- return undefined;
423
- }
424
- return {
425
- ...quoteResponse,
426
- l1GasFeesInHexWei: sumHexes(approvalL1GasFees, tradeL1GasFees),
427
- };
428
- }));
429
- const quotesWithL1GasFees = (await l1GasFeePromises).reduce((acc, result) => {
430
- if (result.status === 'fulfilled' && result.value) {
431
- acc.push(result.value);
432
- }
433
- else if (result.status === 'rejected') {
434
- console.error('Error calculating L1 gas fees for quote', result.reason);
435
- }
436
- return acc;
437
- }, []);
438
- return quotesWithL1GasFees;
439
384
  });
440
- _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, (srcChainId) => {
441
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this);
442
- return isSolanaChainId(srcChainId) && selectedAccount?.metadata?.snap?.id
443
- ? this.messagingSystem
444
- .call('SnapController:handleRequest', getMinimumBalanceForRentExemptionRequest(selectedAccount.metadata.snap?.id)) // eslint-disable-next-line promise/always-return
445
- .then((result) => {
385
+ _BridgeController_handleQuoteStreaming.set(this, async (updatedQuoteRequest, selectedAccount) => {
386
+ /**
387
+ * Tracks the number of valid quotes received from the current stream, which is used
388
+ * to determine when to clear the quotes list and set the initial load time
389
+ */
390
+ let validQuotesCounter = 0;
391
+ await fetchBridgeQuoteStream(__classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), updatedQuoteRequest, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? BRIDGE_PROD_API_BASE_URL, {
392
+ onValidationFailure: __classPrivateFieldGet(this, _BridgeController_trackResponseValidationFailures, "f"),
393
+ onValidQuoteReceived: async (quote) => {
394
+ const quotesWithFees = await appendFeesToQuotes([quote], this.messagingSystem, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
395
+ if (quotesWithFees.length > 0) {
396
+ validQuotesCounter += 1;
397
+ }
446
398
  this.update((state) => {
447
- state.minimumBalanceForRentExemptionInLamports = String(result);
399
+ // Clear previous quotes and quotes load time when first quote in the current
400
+ // polling loop is received
401
+ // This enables clients to continue showing the previous quotes while new
402
+ // quotes are loading
403
+ // Note: If there are no valid quotes until the 2nd fetch, quotesInitialLoadTime will be > refreshRate
404
+ if (validQuotesCounter === 1) {
405
+ state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
406
+ if (!state.quotesInitialLoadTime && __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
407
+ // Set the initial load time after the first quote is received
408
+ state.quotesInitialLoadTime =
409
+ Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
410
+ }
411
+ }
412
+ state.quotes = [...state.quotes, ...quotesWithFees];
448
413
  });
449
- })
450
- .catch((error) => {
451
- console.error('Error setting minimum balance for rent exemption', error);
414
+ },
415
+ onClose: () => {
452
416
  this.update((state) => {
453
- state.minimumBalanceForRentExemptionInLamports =
454
- DEFAULT_BRIDGE_CONTROLLER_STATE.minimumBalanceForRentExemptionInLamports;
417
+ // If there are no valid quotes in the current stream, clear the quotes list
418
+ // to remove quotes from the previous stream
419
+ if (validQuotesCounter === 0) {
420
+ state.quotes = DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
421
+ }
422
+ state.quotesLoadingStatus = RequestStatus.FETCHED;
455
423
  });
456
- })
457
- : undefined;
424
+ },
425
+ }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
458
426
  });
459
- /**
460
- * Appends transaction fees for non-EVM chains to quotes
461
- *
462
- * @param quotes - Array of quote responses to append fees to
463
- * @param walletAddress - The wallet address for which the quotes were requested
464
- * @returns Array of quotes with fees appended, or undefined if quotes are for EVM chains
465
- */
466
- _BridgeController_appendNonEvmFees.set(this, async (quotes, walletAddress) => {
467
- if (quotes.some(({ quote: { srcChainId } }) => !isNonEvmChainId(srcChainId))) {
468
- return undefined;
427
+ _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
428
+ if (!isSolanaChainId(srcChainId) || !snapId) {
429
+ return;
469
430
  }
470
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, walletAddress);
471
- const nonEvmFeePromises = Promise.allSettled(quotes.map(async (quoteResponse) => {
472
- const { trade, quote } = quoteResponse;
473
- if (selectedAccount?.metadata?.snap?.id && typeof trade === 'string') {
474
- const scope = formatChainIdToCaip(quote.srcChainId);
475
- const response = (await this.messagingSystem.call('SnapController:handleRequest', computeFeeRequest(selectedAccount.metadata.snap?.id, trade, selectedAccount.id, scope)));
476
- const baseFee = response?.find((fee) => fee.type === 'base');
477
- // Store fees in native units as returned by the snap (e.g., SOL, BTC)
478
- const feeInNative = baseFee?.asset?.amount || '0';
479
- return {
480
- ...quoteResponse,
481
- nonEvmFeesInNative: feeInNative,
482
- };
483
- }
484
- return quoteResponse;
485
- }));
486
- const quotesWithNonEvmFees = (await nonEvmFeePromises).reduce((acc, result) => {
487
- if (result.status === 'fulfilled' && result.value) {
488
- acc.push(result.value);
489
- }
490
- else if (result.status === 'rejected') {
491
- console.error('Error calculating non-EVM fees for quote', result.reason);
492
- }
493
- return acc;
494
- }, []);
495
- return quotesWithNonEvmFees;
431
+ const minimumBalanceForRentExemptionInLamports = await getMinimumBalanceForRentExemptionInLamports(snapId, this.messagingSystem);
432
+ this.update((state) => {
433
+ state.minimumBalanceForRentExemptionInLamports =
434
+ minimumBalanceForRentExemptionInLamports;
435
+ });
496
436
  });
497
437
  _BridgeController_getRequestParams.set(this, () => {
498
438
  const srcChainIdCaip = formatChainIdToCaip(this.state.quoteRequest.srcChainId ||
@@ -657,7 +597,7 @@ export class BridgeController extends StaticIntervalPollingController() {
657
597
  this.messagingSystem.registerActionHandler(`${BRIDGE_CONTROLLER_NAME}:fetchQuotes`, this.fetchQuotes.bind(this));
658
598
  }
659
599
  }
660
- _BridgeController_abortController = new WeakMap(), _BridgeController_quotesFirstFetched = new WeakMap(), _BridgeController_clientId = new WeakMap(), _BridgeController_clientVersion = new WeakMap(), _BridgeController_getLayer1GasFee = new WeakMap(), _BridgeController_fetchFn = new WeakMap(), _BridgeController_trackMetaMetricsFn = new WeakMap(), _BridgeController_trace = new WeakMap(), _BridgeController_config = new WeakMap(), _BridgeController_trackResponseValidationFailures = new WeakMap(), _BridgeController_getExchangeRateSources = new WeakMap(), _BridgeController_fetchAssetExchangeRates = new WeakMap(), _BridgeController_hasSufficientBalance = new WeakMap(), _BridgeController_fetchBridgeQuotes = new WeakMap(), _BridgeController_appendL1GasFees = new WeakMap(), _BridgeController_setMinimumBalanceForRentExemptionInLamports = new WeakMap(), _BridgeController_appendNonEvmFees = new WeakMap(), _BridgeController_getRequestParams = new WeakMap(), _BridgeController_getRequestMetadata = new WeakMap(), _BridgeController_getQuoteFetchData = new WeakMap(), _BridgeController_getEventProperties = new WeakMap(), _BridgeController_trackInputChangedEvents = new WeakMap(), _BridgeController_instances = new WeakSet(), _BridgeController_getMultichainSelectedAccount = function _BridgeController_getMultichainSelectedAccount(walletAddress) {
600
+ _BridgeController_abortController = new WeakMap(), _BridgeController_quotesFirstFetched = new WeakMap(), _BridgeController_clientId = new WeakMap(), _BridgeController_clientVersion = new WeakMap(), _BridgeController_getLayer1GasFee = new WeakMap(), _BridgeController_fetchFn = new WeakMap(), _BridgeController_trackMetaMetricsFn = new WeakMap(), _BridgeController_trace = new WeakMap(), _BridgeController_config = new WeakMap(), _BridgeController_trackResponseValidationFailures = new WeakMap(), _BridgeController_getExchangeRateSources = new WeakMap(), _BridgeController_fetchAssetExchangeRates = new WeakMap(), _BridgeController_hasSufficientBalance = new WeakMap(), _BridgeController_fetchBridgeQuotes = new WeakMap(), _BridgeController_handleQuoteStreaming = new WeakMap(), _BridgeController_setMinimumBalanceForRentExemptionInLamports = new WeakMap(), _BridgeController_getRequestParams = new WeakMap(), _BridgeController_getRequestMetadata = new WeakMap(), _BridgeController_getQuoteFetchData = new WeakMap(), _BridgeController_getEventProperties = new WeakMap(), _BridgeController_trackInputChangedEvents = new WeakMap(), _BridgeController_instances = new WeakSet(), _BridgeController_getMultichainSelectedAccount = function _BridgeController_getMultichainSelectedAccount(walletAddress) {
661
601
  const addressToUse = walletAddress ?? this.state.quoteRequest.walletAddress;
662
602
  if (!addressToUse) {
663
603
  throw new Error('Account address is required');