@metamask/bridge-controller 50.0.0 → 52.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 +22 -0
  11. package/dist/selectors.d.cts.map +1 -1
  12. package/dist/selectors.d.mts +22 -0
  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 +3 -0
  59. package/dist/utils/validators.d.cts.map +1 -1
  60. package/dist/utils/validators.d.mts +3 -0
  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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [52.0.0]
11
+
12
+ ### Changed
13
+
14
+ - **BREAKING:** Bump peer dependency `@metamask/assets-controllers` from `^80.0.0` to `^81.0.0` ([#6834](https://github.com/MetaMask/core/pull/6834))
15
+
16
+ ## [51.0.0]
17
+
18
+ ### Added
19
+
20
+ - Introduce server‑sent events quote streaming and integrates incremental quote updates into the bridge controller polling flow ([#6760](https://github.com/MetaMask/core/pull/6760))
21
+ - Add private `handleQuoteStreaming` method that calls `getQuoteStream` when the `sseEnabled` flag is enabled in LaunchDarkly
22
+ - Reuse existing polling, metrics and validation utilities when processing server-sent quotes
23
+ - Add dependency on `@microsoft/fetch-event-source` at `^2.0.1` ([#6760](https://github.com/MetaMask/core/pull/6760))
24
+ - Note that clients need to patch this library such that it rejects instead of resolving when the quote request is cancelled. This preserves the controller's expected request cancellation behavior
25
+
26
+ ### Changed
27
+
28
+ - Extract some logic from bridge-controller and move them to utility files for better readability ([#6760](https://github.com/MetaMask/core/pull/6760))
29
+
30
+ ### Removed
31
+
32
+ - Remove cache options from spot-prices and getQuote api calls since they are only required by the extension client ([#6760](https://github.com/MetaMask/core/pull/6760))
33
+
34
+ ### Fixed
35
+
36
+ - Pass abortSignal to fetchAssetPricesForCurrency in order to cancel exchange rate fetching when quote parameters change ([#6760](https://github.com/MetaMask/core/pull/6760))
37
+
10
38
  ## [50.0.0]
11
39
 
12
40
  ### Changed
@@ -696,7 +724,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
696
724
 
697
725
  - Initial release ([#5317](https://github.com/MetaMask/core/pull/5317))
698
726
 
699
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@50.0.0...HEAD
727
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@52.0.0...HEAD
728
+ [52.0.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@51.0.0...@metamask/bridge-controller@52.0.0
729
+ [51.0.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@50.0.0...@metamask/bridge-controller@51.0.0
700
730
  [50.0.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@49.0.1...@metamask/bridge-controller@50.0.0
701
731
  [49.0.1]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@49.0.0...@metamask/bridge-controller@49.0.1
702
732
  [49.0.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@48.0.0...@metamask/bridge-controller@49.0.0
@@ -10,16 +10,14 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  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");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- 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;
13
+ 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;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.BridgeController = void 0;
16
16
  const contracts_1 = require("@ethersproject/contracts");
17
17
  const providers_1 = require("@ethersproject/providers");
18
18
  const metamask_eth_abis_1 = require("@metamask/metamask-eth-abis");
19
19
  const polling_controller_1 = require("@metamask/polling-controller");
20
- const utils_1 = require("@metamask/utils");
21
20
  const bridge_1 = require("./constants/bridge.cjs");
22
- const chains_1 = require("./constants/chains.cjs");
23
21
  const traces_1 = require("./constants/traces.cjs");
24
22
  const selectors_1 = require("./selectors.cjs");
25
23
  const types_1 = require("./types.cjs");
@@ -32,8 +30,8 @@ const fetch_1 = require("./utils/fetch.cjs");
32
30
  const constants_1 = require("./utils/metrics/constants.cjs");
33
31
  const properties_1 = require("./utils/metrics/properties.cjs");
34
32
  const quote_1 = require("./utils/quote.cjs");
33
+ const quote_fees_1 = require("./utils/quote-fees.cjs");
35
34
  const snaps_1 = require("./utils/snaps.cjs");
36
- const validators_1 = require("./utils/validators.cjs");
37
35
  const metadata = {
38
36
  quoteRequest: {
39
37
  includeInStateLogs: true,
@@ -115,31 +113,14 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
115
113
  await __classPrivateFieldGet(this, _BridgeController_fetchBridgeQuotes, "f").call(this, pollingInput);
116
114
  };
117
115
  this.updateBridgeQuoteRequestParams = async (paramsToUpdate, context) => {
118
- this.stopAllPolling();
119
- __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(constants_1.AbortReason.QuoteRequestUpdated);
120
116
  __classPrivateFieldGet(this, _BridgeController_trackInputChangedEvents, "f").call(this, paramsToUpdate);
117
+ this.resetState(constants_1.AbortReason.QuoteRequestUpdated);
121
118
  const updatedQuoteRequest = {
122
119
  ...bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest,
123
120
  ...paramsToUpdate,
124
121
  };
125
122
  this.update((state) => {
126
123
  state.quoteRequest = updatedQuoteRequest;
127
- state.quotes = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
128
- state.quotesLastFetched =
129
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLastFetched;
130
- state.quotesLoadingStatus =
131
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesLoadingStatus;
132
- state.quoteFetchError = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError;
133
- state.quotesRefreshCount =
134
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount;
135
- state.quotesInitialLoadTime =
136
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesInitialLoadTime;
137
- // Reset required minimum balance if the source chain is not Solana
138
- if (updatedQuoteRequest.srcChainId &&
139
- !(0, bridge_2.isSolanaChainId)(updatedQuoteRequest.srcChainId)) {
140
- state.minimumBalanceForRentExemptionInLamports =
141
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.minimumBalanceForRentExemptionInLamports;
142
- }
143
124
  });
144
125
  await __classPrivateFieldGet(this, _BridgeController_fetchAssetExchangeRates, "f").call(this, updatedQuoteRequest).catch((error) => console.warn('Failed to fetch asset exchange rates', error));
145
126
  if ((0, quote_1.isValidQuoteRequest)(updatedQuoteRequest)) {
@@ -156,10 +137,16 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
156
137
  insufficientBal = true;
157
138
  }
158
139
  else {
159
- // Otherwise query the src token balance from the RPC provider
160
- insufficientBal =
161
- paramsToUpdate.insufficientBal ??
162
- !(await __classPrivateFieldGet(this, _BridgeController_hasSufficientBalance, "f").call(this, updatedQuoteRequest));
140
+ try {
141
+ // Otherwise query the src token balance from the RPC provider
142
+ insufficientBal =
143
+ paramsToUpdate.insufficientBal ??
144
+ !(await __classPrivateFieldGet(this, _BridgeController_hasSufficientBalance, "f").call(this, updatedQuoteRequest));
145
+ }
146
+ catch (error) {
147
+ console.warn('Failed to fetch balance', error);
148
+ insufficientBal = true;
149
+ }
163
150
  }
164
151
  const networkClientId = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getSelectedNetworkClientId).call(this);
165
152
  // Set refresh rate based on the source chain before starting polling
@@ -194,17 +181,8 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
194
181
  ? { ...quoteRequest, ...quoteRequestOverrides }
195
182
  : quoteRequest, abortSignal, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), __classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? bridge_1.BRIDGE_PROD_API_BASE_URL, featureId, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
196
183
  __classPrivateFieldGet(this, _BridgeController_trackResponseValidationFailures, "f").call(this, validationFailures);
197
- const quotesWithL1GasFees = await __classPrivateFieldGet(this, _BridgeController_appendL1GasFees, "f").call(this, baseQuotes);
198
- const quotesWithNonEvmFees = await __classPrivateFieldGet(this, _BridgeController_appendNonEvmFees, "f").call(this, baseQuotes, quoteRequest.walletAddress);
199
- const quotesWithFees = quotesWithL1GasFees ?? quotesWithNonEvmFees ?? baseQuotes;
200
- // Sort perps quotes by increasing estimated processing time (fastest first)
201
- if (featureId === validators_1.FeatureId.PERPS) {
202
- return quotesWithFees.sort((a, b) => {
203
- return (a.estimatedProcessingTimeInSeconds -
204
- b.estimatedProcessingTimeInSeconds);
205
- });
206
- }
207
- return quotesWithFees;
184
+ const quotesWithFees = await (0, quote_fees_1.appendFeesToQuotes)(baseQuotes, this.messagingSystem, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, quoteRequest.walletAddress));
185
+ return (0, quote_1.sortQuotes)(quotesWithFees, featureId);
208
186
  };
209
187
  _BridgeController_trackResponseValidationFailures.set(this, (validationFailures) => {
210
188
  if (validationFailures.length === 0) {
@@ -255,6 +233,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
255
233
  clientId: __classPrivateFieldGet(this, _BridgeController_clientId, "f"),
256
234
  clientVersion: __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"),
257
235
  fetchFn: __classPrivateFieldGet(this, _BridgeController_fetchFn, "f"),
236
+ signal: __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal,
258
237
  });
259
238
  const exchangeRates = (0, assets_1.toExchangeRates)(currency, pricesByAssetId);
260
239
  this.update((state) => {
@@ -265,10 +244,6 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
265
244
  });
266
245
  });
267
246
  _BridgeController_hasSufficientBalance.set(this, async (quoteRequest) => {
268
- // Only check balance for EVM chains
269
- if ((0, bridge_2.isNonEvmChainId)(quoteRequest.srcChainId)) {
270
- return true;
271
- }
272
247
  const srcChainIdInHex = (0, caip_formatters_1.formatChainIdToHex)(quoteRequest.srcChainId);
273
248
  const provider = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getSelectedNetworkClient).call(this)?.provider;
274
249
  const normalizedSrcTokenAddress = (0, caip_formatters_1.formatAddressToCaipReference)(quoteRequest.srcTokenAddress);
@@ -282,8 +257,8 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
282
257
  this.stopAllPolling();
283
258
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(reason);
284
259
  };
285
- this.resetState = () => {
286
- this.stopPollingForQuotes(constants_1.AbortReason.ResetState);
260
+ this.resetState = (reason = constants_1.AbortReason.ResetState) => {
261
+ this.stopPollingForQuotes(reason);
287
262
  this.update((state) => {
288
263
  // Cannot do direct assignment to state, i.e. state = {... }, need to manually assign each field
289
264
  state.quoteRequest = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quoteRequest;
@@ -320,10 +295,13 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
320
295
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort('New quote request');
321
296
  __classPrivateFieldSet(this, _BridgeController_abortController, new AbortController(), "f");
322
297
  this.trackUnifiedSwapBridgeEvent(constants_1.UnifiedSwapBridgeEventName.QuotesRequested, context);
298
+ const { sseEnabled, maxRefreshCount } = (0, feature_flags_1.getBridgeFeatureFlags)(this.messagingSystem);
299
+ const shouldStream = Boolean(sseEnabled);
323
300
  this.update((state) => {
324
- state.quotesLoadingStatus = types_1.RequestStatus.LOADING;
325
301
  state.quoteRequest = updatedQuoteRequest;
326
302
  state.quoteFetchError = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quoteFetchError;
303
+ state.quotesLastFetched = Date.now();
304
+ state.quotesLoadingStatus = types_1.RequestStatus.LOADING;
327
305
  });
328
306
  try {
329
307
  await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, {
@@ -335,22 +313,36 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
335
313
  destChainId: (0, caip_formatters_1.formatChainIdToCaip)(updatedQuoteRequest.destChainId),
336
314
  },
337
315
  }, async () => {
316
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, updatedQuoteRequest.walletAddress);
338
317
  // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
339
318
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
340
- __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, updatedQuoteRequest.srcChainId);
341
- const quotes = await this.fetchQuotes(updatedQuoteRequest,
342
- // AbortController is always defined by this line, because we assign it a few lines above,
343
- // not sure why Jest thinks it's not
344
- // Linters accurately say that it's defined
345
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
346
- __classPrivateFieldGet(this, _BridgeController_abortController, "f").signal);
319
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, updatedQuoteRequest.srcChainId, selectedAccount.metadata?.snap?.id);
320
+ // Use SSE if enabled and return early
321
+ if (shouldStream) {
322
+ await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, updatedQuoteRequest, selectedAccount);
323
+ return;
324
+ }
325
+ // Otherwise use regular fetch
326
+ const quotes = await this.fetchQuotes(updatedQuoteRequest, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
347
327
  this.update((state) => {
328
+ // Set the initial load time if this is the first fetch
329
+ if (state.quotesRefreshCount ===
330
+ bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
331
+ __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
332
+ state.quotesInitialLoadTime =
333
+ Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
334
+ }
348
335
  state.quotes = quotes;
349
336
  state.quotesLoadingStatus = types_1.RequestStatus.FETCHED;
350
337
  });
351
338
  });
352
339
  }
353
340
  catch (error) {
341
+ // Reset the quotes list if the fetch fails to avoid showing stale quotes
342
+ this.update((state) => {
343
+ state.quotes = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
344
+ });
345
+ // Ignore abort errors
354
346
  const isAbortError = error.name === 'AbortError';
355
347
  if (isAbortError ||
356
348
  [
@@ -361,141 +353,89 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
361
353
  // Exit the function early to prevent other state updates
362
354
  return;
363
355
  }
356
+ // Update loading status and error message
364
357
  this.update((state) => {
365
- state.quoteFetchError =
366
- error instanceof Error ? error.message : (error?.toString() ?? null);
358
+ // The error object reference is not guaranteed to exist on mobile so reading
359
+ // the message directly could cause an error.
360
+ let errorMessage;
361
+ try {
362
+ errorMessage =
363
+ error?.message ?? error.toString();
364
+ }
365
+ catch {
366
+ // Intentionally empty
367
+ }
368
+ finally {
369
+ state.quoteFetchError = errorMessage ?? 'Unknown error';
370
+ }
367
371
  state.quotesLoadingStatus = types_1.RequestStatus.ERROR;
368
- state.quotes = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
369
372
  });
373
+ // Track event and log error
370
374
  this.trackUnifiedSwapBridgeEvent(constants_1.UnifiedSwapBridgeEventName.QuotesError, context);
371
- console.log('Failed to fetch bridge quotes', error);
375
+ console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
372
376
  }
373
- const bridgeFeatureFlags = (0, feature_flags_1.getBridgeFeatureFlags)(this.messagingSystem);
374
- const { maxRefreshCount } = bridgeFeatureFlags;
377
+ // Update refresh count after fetching, validation and fee calculation have completed
378
+ this.update((state) => {
379
+ state.quotesRefreshCount += 1;
380
+ });
375
381
  // Stop polling if the maximum number of refreshes has been reached
376
382
  if (updatedQuoteRequest.insufficientBal ||
377
383
  (!updatedQuoteRequest.insufficientBal &&
378
384
  this.state.quotesRefreshCount >= maxRefreshCount)) {
379
385
  this.stopAllPolling();
380
386
  }
381
- // Update quote fetching stats
382
- const quotesLastFetched = Date.now();
383
- this.update((state) => {
384
- state.quotesInitialLoadTime =
385
- state.quotesRefreshCount === 0 && __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")
386
- ? quotesLastFetched - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")
387
- : this.state.quotesInitialLoadTime;
388
- state.quotesLastFetched = quotesLastFetched;
389
- state.quotesRefreshCount += 1;
390
- });
391
- });
392
- _BridgeController_appendL1GasFees.set(this, async (quotes) => {
393
- // Indicates whether some of the quotes are not for optimism or base
394
- const hasInvalidQuotes = quotes.some(({ quote }) => {
395
- const chainId = (0, caip_formatters_1.formatChainIdToCaip)(quote.srcChainId);
396
- return ![chains_1.CHAIN_IDS.OPTIMISM, chains_1.CHAIN_IDS.BASE]
397
- .map(caip_formatters_1.formatChainIdToCaip)
398
- .includes(chainId);
399
- });
400
- // Only append L1 gas fees if all quotes are for either optimism or base
401
- if (hasInvalidQuotes) {
402
- return undefined;
403
- }
404
- const l1GasFeePromises = Promise.allSettled(quotes.map(async (quoteResponse) => {
405
- const { quote, trade, approval } = quoteResponse;
406
- const chainId = (0, utils_1.numberToHex)(quote.srcChainId);
407
- const getTxParams = (txData) => ({
408
- from: txData.from,
409
- to: txData.to,
410
- value: txData.value,
411
- data: txData.data,
412
- gasLimit: txData.gasLimit?.toString(),
413
- });
414
- const approvalL1GasFees = approval
415
- ? await __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f").call(this, {
416
- transactionParams: getTxParams(approval),
417
- chainId,
418
- })
419
- : '0x0';
420
- const tradeL1GasFees = await __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f").call(this, {
421
- transactionParams: getTxParams(trade),
422
- chainId,
423
- });
424
- if (approvalL1GasFees === undefined || tradeL1GasFees === undefined) {
425
- return undefined;
426
- }
427
- return {
428
- ...quoteResponse,
429
- l1GasFeesInHexWei: (0, bridge_2.sumHexes)(approvalL1GasFees, tradeL1GasFees),
430
- };
431
- }));
432
- const quotesWithL1GasFees = (await l1GasFeePromises).reduce((acc, result) => {
433
- if (result.status === 'fulfilled' && result.value) {
434
- acc.push(result.value);
435
- }
436
- else if (result.status === 'rejected') {
437
- console.error('Error calculating L1 gas fees for quote', result.reason);
438
- }
439
- return acc;
440
- }, []);
441
- return quotesWithL1GasFees;
442
387
  });
443
- _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, (srcChainId) => {
444
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this);
445
- return (0, bridge_2.isSolanaChainId)(srcChainId) && selectedAccount?.metadata?.snap?.id
446
- ? this.messagingSystem
447
- .call('SnapController:handleRequest', (0, snaps_1.getMinimumBalanceForRentExemptionRequest)(selectedAccount.metadata.snap?.id)) // eslint-disable-next-line promise/always-return
448
- .then((result) => {
388
+ _BridgeController_handleQuoteStreaming.set(this, async (updatedQuoteRequest, selectedAccount) => {
389
+ /**
390
+ * Tracks the number of valid quotes received from the current stream, which is used
391
+ * to determine when to clear the quotes list and set the initial load time
392
+ */
393
+ let validQuotesCounter = 0;
394
+ await (0, fetch_1.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_1.BRIDGE_PROD_API_BASE_URL, {
395
+ onValidationFailure: __classPrivateFieldGet(this, _BridgeController_trackResponseValidationFailures, "f"),
396
+ onValidQuoteReceived: async (quote) => {
397
+ const quotesWithFees = await (0, quote_fees_1.appendFeesToQuotes)([quote], this.messagingSystem, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
398
+ if (quotesWithFees.length > 0) {
399
+ validQuotesCounter += 1;
400
+ }
449
401
  this.update((state) => {
450
- state.minimumBalanceForRentExemptionInLamports = String(result);
402
+ // Clear previous quotes and quotes load time when first quote in the current
403
+ // polling loop is received
404
+ // This enables clients to continue showing the previous quotes while new
405
+ // quotes are loading
406
+ // Note: If there are no valid quotes until the 2nd fetch, quotesInitialLoadTime will be > refreshRate
407
+ if (validQuotesCounter === 1) {
408
+ state.quotes = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
409
+ if (!state.quotesInitialLoadTime && __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
410
+ // Set the initial load time after the first quote is received
411
+ state.quotesInitialLoadTime =
412
+ Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
413
+ }
414
+ }
415
+ state.quotes = [...state.quotes, ...quotesWithFees];
451
416
  });
452
- })
453
- .catch((error) => {
454
- console.error('Error setting minimum balance for rent exemption', error);
417
+ },
418
+ onClose: () => {
455
419
  this.update((state) => {
456
- state.minimumBalanceForRentExemptionInLamports =
457
- bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.minimumBalanceForRentExemptionInLamports;
420
+ // If there are no valid quotes in the current stream, clear the quotes list
421
+ // to remove quotes from the previous stream
422
+ if (validQuotesCounter === 0) {
423
+ state.quotes = bridge_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
424
+ }
425
+ state.quotesLoadingStatus = types_1.RequestStatus.FETCHED;
458
426
  });
459
- })
460
- : undefined;
427
+ },
428
+ }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
461
429
  });
462
- /**
463
- * Appends transaction fees for non-EVM chains to quotes
464
- *
465
- * @param quotes - Array of quote responses to append fees to
466
- * @param walletAddress - The wallet address for which the quotes were requested
467
- * @returns Array of quotes with fees appended, or undefined if quotes are for EVM chains
468
- */
469
- _BridgeController_appendNonEvmFees.set(this, async (quotes, walletAddress) => {
470
- if (quotes.some(({ quote: { srcChainId } }) => !(0, bridge_2.isNonEvmChainId)(srcChainId))) {
471
- return undefined;
430
+ _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
431
+ if (!(0, bridge_2.isSolanaChainId)(srcChainId) || !snapId) {
432
+ return;
472
433
  }
473
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, walletAddress);
474
- const nonEvmFeePromises = Promise.allSettled(quotes.map(async (quoteResponse) => {
475
- const { trade, quote } = quoteResponse;
476
- if (selectedAccount?.metadata?.snap?.id && typeof trade === 'string') {
477
- const scope = (0, caip_formatters_1.formatChainIdToCaip)(quote.srcChainId);
478
- const response = (await this.messagingSystem.call('SnapController:handleRequest', (0, snaps_1.computeFeeRequest)(selectedAccount.metadata.snap?.id, trade, selectedAccount.id, scope)));
479
- const baseFee = response?.find((fee) => fee.type === 'base');
480
- // Store fees in native units as returned by the snap (e.g., SOL, BTC)
481
- const feeInNative = baseFee?.asset?.amount || '0';
482
- return {
483
- ...quoteResponse,
484
- nonEvmFeesInNative: feeInNative,
485
- };
486
- }
487
- return quoteResponse;
488
- }));
489
- const quotesWithNonEvmFees = (await nonEvmFeePromises).reduce((acc, result) => {
490
- if (result.status === 'fulfilled' && result.value) {
491
- acc.push(result.value);
492
- }
493
- else if (result.status === 'rejected') {
494
- console.error('Error calculating non-EVM fees for quote', result.reason);
495
- }
496
- return acc;
497
- }, []);
498
- return quotesWithNonEvmFees;
434
+ const minimumBalanceForRentExemptionInLamports = await (0, snaps_1.getMinimumBalanceForRentExemptionInLamports)(snapId, this.messagingSystem);
435
+ this.update((state) => {
436
+ state.minimumBalanceForRentExemptionInLamports =
437
+ minimumBalanceForRentExemptionInLamports;
438
+ });
499
439
  });
500
440
  _BridgeController_getRequestParams.set(this, () => {
501
441
  const srcChainIdCaip = (0, caip_formatters_1.formatChainIdToCaip)(this.state.quoteRequest.srcChainId ||
@@ -661,7 +601,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
661
601
  }
662
602
  }
663
603
  exports.BridgeController = BridgeController;
664
- _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) {
604
+ _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) {
665
605
  const addressToUse = walletAddress ?? this.state.quoteRequest.walletAddress;
666
606
  if (!addressToUse) {
667
607
  throw new Error('Account address is required');