@metamask/ramps-controller 6.0.0 → 7.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.0.0]
11
+
12
+ ### Added
13
+
14
+ - Update payment method delay type to match API response structure ([#7845](https://github.com/MetaMask/core/pull/7845))
15
+ - Add automatic quote polling with `startQuotePolling()`, `stopQuotePolling()`, and `setSelectedQuote()` methods, with auto-selection when a single quote is returned ([#7824](https://github.com/MetaMask/core/pull/7824))
16
+
17
+ ### Changed
18
+
19
+ - **BREAKING:** Require provider selection for quote polling and update quotes API endpoint to `/v2/quotes` ([#7846](https://github.com/MetaMask/core/pull/7846))
20
+
10
21
  ## [6.0.0]
11
22
 
12
23
  ### Changed
@@ -46,14 +57,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
57
  ### Added
47
58
 
48
59
  - Add sync trigger methods to RampsController ([#7662](https://github.com/MetaMask/core/pull/7662))
49
-
50
60
  - Export `RampAction` type for `'buy' | 'sell'` ramp actions ([#7663](https://github.com/MetaMask/core/pull/7663))
51
61
  - Add payment methods support with `getPaymentMethods()` method, `paymentMethods` and `selectedPaymentMethod` state ([#7665](https://github.com/MetaMask/core/pull/7665))
52
62
 
53
63
  ### Changed
54
64
 
55
65
  - Evict expired cache entries based on TTL in addition to size-based eviction ([#7674](https://github.com/MetaMask/core/pull/7674))
56
-
57
66
  - Update `getTokens()` to use v2 API endpoint and support optional provider parameter ([#7664](https://github.com/MetaMask/core/pull/7664))
58
67
 
59
68
  ## [4.0.0]
@@ -61,19 +70,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
61
70
  ### Added
62
71
 
63
72
  - Add `preferredProvider` state and `setPreferredProvider()` method to RampsController ([#7617](https://github.com/MetaMask/core/pull/7617))
64
-
65
73
  - Export `UserRegion` type ([#7646](https://github.com/MetaMask/core/pull/7646))
66
-
67
74
  - Add `defaultAmount` and `quickAmounts` fields to the `Country` type ([#7645](https://github.com/MetaMask/core/pull/7645))
68
-
69
75
  - Add `providers` state and `getProviders()` method to RampsController. Providers are automatically fetched on init and when the region changes ([#7652](https://github.com/MetaMask/core/pull/7652))
70
76
 
71
77
  ### Changed
72
78
 
73
79
  - **BREAKING:** Change `userRegion` from `string | null` to `UserRegion | null`. Access region code via `userRegion.regionCode`. ([#7646](https://github.com/MetaMask/core/pull/7646))
74
-
75
80
  - Update `getCountries()` endpoint to use v2 API (`v2/regions/countries`) ([#7645](https://github.com/MetaMask/core/pull/7645))
76
-
77
81
  - Add `getApiPath()` helper function for versioned API paths with v2 default ([#7645](https://github.com/MetaMask/core/pull/7645))
78
82
 
79
83
  ### Removed
@@ -89,7 +93,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
89
93
  ### Changed
90
94
 
91
95
  - **BREAKING:** Rename `geolocation` to `userRegion` and `updateGeolocation()` to `updateUserRegion()` in RampsController ([#7563](https://github.com/MetaMask/core/pull/7563))
92
-
93
96
  - Bump `@metamask/controller-utils` from `^11.17.0` to `^11.18.0` ([#7583](https://github.com/MetaMask/core/pull/7583))
94
97
 
95
98
  ## [2.1.0]
@@ -97,11 +100,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
97
100
  ### Added
98
101
 
99
102
  - Add eligibility state ([#7539](https://github.com/MetaMask/core/pull/7539))
100
-
101
103
  - Add `createRequestSelector` utility function for creating memoized selectors for RampsController request states ([#7554](https://github.com/MetaMask/core/pull/7554))
102
-
103
104
  - Add request caching infrastructure with TTL, deduplication, and abort support ([#7536](https://github.com/MetaMask/core/pull/7536))
104
-
105
105
  - Add `init()` and `setUserRegion()` methods to RampsController ([#7563](https://github.com/MetaMask/core/pull/7563))
106
106
 
107
107
  ### Changed
@@ -128,7 +128,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
128
128
  - Add `OnRampService` for interacting with the OnRamp API
129
129
  - Add geolocation detection via IP address lookup
130
130
 
131
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@6.0.0...HEAD
131
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@7.0.0...HEAD
132
+ [7.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@6.0.0...@metamask/ramps-controller@7.0.0
132
133
  [6.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@5.1.0...@metamask/ramps-controller@6.0.0
133
134
  [5.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@5.0.0...@metamask/ramps-controller@5.1.0
134
135
  [5.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@4.1.0...@metamask/ramps-controller@5.0.0
@@ -10,7 +10,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
11
11
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
12
12
  };
13
- var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_quotePollingInterval, _RampsController_quotePollingOptions, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_restartPollingIfActive, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.RampsController = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -119,7 +119,7 @@ function getDefaultRampsControllerState() {
119
119
  providers: createDefaultResourceState([], null),
120
120
  tokens: createDefaultResourceState(null, null),
121
121
  paymentMethods: createDefaultResourceState([], null),
122
- quotes: createDefaultResourceState(null),
122
+ quotes: createDefaultResourceState(null, null),
123
123
  requests: {},
124
124
  };
125
125
  }
@@ -149,6 +149,7 @@ function resetDependentResources(state, options) {
149
149
  state.paymentMethods.isLoading = false;
150
150
  state.paymentMethods.error = null;
151
151
  state.quotes.data = null;
152
+ state.quotes.selected = null;
152
153
  state.quotes.isLoading = false;
153
154
  state.quotes.error = null;
154
155
  }
@@ -260,6 +261,16 @@ class RampsController extends base_controller_1.BaseController {
260
261
  * Used so isLoading is only cleared when the last request for that resource finishes.
261
262
  */
262
263
  _RampsController_pendingResourceCount.set(this, new Map());
264
+ /**
265
+ * Interval ID for automatic quote polling.
266
+ * Set when startQuotePolling() is called, cleared when stopQuotePolling() is called.
267
+ */
268
+ _RampsController_quotePollingInterval.set(this, null);
269
+ /**
270
+ * Options used for quote polling (walletAddress, amount, redirectUrl).
271
+ * Stored so polling can be restarted when dependencies change.
272
+ */
273
+ _RampsController_quotePollingOptions.set(this, null);
263
274
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
264
275
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
265
276
  }
@@ -415,6 +426,9 @@ class RampsController extends base_controller_1.BaseController {
415
426
  if (regionChanged) {
416
427
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
417
428
  }
429
+ if (regionChanged) {
430
+ this.stopQuotePolling();
431
+ }
418
432
  this.update((state) => {
419
433
  if (regionChanged) {
420
434
  resetDependentResources(state);
@@ -450,6 +464,7 @@ class RampsController extends base_controller_1.BaseController {
450
464
  */
451
465
  setSelectedProvider(providerId) {
452
466
  if (providerId === null) {
467
+ this.stopQuotePolling();
453
468
  this.update((state) => {
454
469
  state.providers.selected = null;
455
470
  state.paymentMethods.data = [];
@@ -473,8 +488,13 @@ class RampsController extends base_controller_1.BaseController {
473
488
  state.providers.selected = provider;
474
489
  state.paymentMethods.data = [];
475
490
  state.paymentMethods.selected = null;
491
+ state.quotes.selected = null;
476
492
  });
477
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
493
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }).then(() => {
494
+ // Restart quote polling after payment methods are fetched
495
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
496
+ return undefined;
497
+ }));
478
498
  }
479
499
  /**
480
500
  * Initializes the controller by fetching the user's region from geolocation.
@@ -570,6 +590,7 @@ class RampsController extends base_controller_1.BaseController {
570
590
  */
571
591
  setSelectedToken(assetId) {
572
592
  if (!assetId) {
593
+ this.stopQuotePolling();
573
594
  this.update((state) => {
574
595
  state.tokens.selected = null;
575
596
  state.paymentMethods.data = [];
@@ -594,8 +615,13 @@ class RampsController extends base_controller_1.BaseController {
594
615
  state.tokens.selected = token;
595
616
  state.paymentMethods.data = [];
596
617
  state.paymentMethods.selected = null;
618
+ state.quotes.selected = null;
597
619
  });
598
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }));
620
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }).then(() => {
621
+ // Restart quote polling after payment methods are fetched
622
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
623
+ return undefined;
624
+ }));
599
625
  }
600
626
  /**
601
627
  * Fetches the list of providers for a given region.
@@ -735,6 +761,8 @@ class RampsController extends base_controller_1.BaseController {
735
761
  this.update((state) => {
736
762
  state.paymentMethods.selected = paymentMethod;
737
763
  });
764
+ // Restart quote polling if active
765
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_restartPollingIfActive).call(this);
738
766
  }
739
767
  /**
740
768
  * Fetches quotes from all providers for a given set of parameters.
@@ -821,6 +849,106 @@ class RampsController extends base_controller_1.BaseController {
821
849
  });
822
850
  return response;
823
851
  }
852
+ /**
853
+ * Starts automatic quote polling with a 15-second refresh interval.
854
+ * Fetches quotes immediately and then every 15 seconds.
855
+ * If the response contains exactly one quote, it is auto-selected.
856
+ * If multiple quotes are returned, the existing selection is preserved if still valid.
857
+ *
858
+ * @param options - Parameters for fetching quotes.
859
+ * @param options.walletAddress - The destination wallet address.
860
+ * @param options.amount - The amount (in fiat for buy, crypto for sell).
861
+ * @param options.redirectUrl - Optional redirect URL after order completion.
862
+ * @throws If required dependencies (region, token, provider, payment method) are not set.
863
+ */
864
+ startQuotePolling(options) {
865
+ // Validate required dependencies
866
+ const regionCode = this.state.userRegion?.regionCode;
867
+ const token = this.state.tokens.selected;
868
+ const provider = this.state.providers.selected;
869
+ const paymentMethod = this.state.paymentMethods.selected;
870
+ if (!regionCode) {
871
+ throw new Error('Region is required. Cannot start quote polling without valid region information.');
872
+ }
873
+ if (!token) {
874
+ throw new Error('Token is required. Cannot start quote polling without a selected token.');
875
+ }
876
+ if (!provider) {
877
+ throw new Error('Provider is required. Cannot start quote polling without a selected provider.');
878
+ }
879
+ if (!paymentMethod) {
880
+ throw new Error('Payment method is required. Cannot start quote polling without a selected payment method.');
881
+ }
882
+ // Stop any existing polling first
883
+ this.stopQuotePolling();
884
+ // Store options for restarts (must be after stop to avoid being cleared)
885
+ __classPrivateFieldSet(this, _RampsController_quotePollingOptions, options, "f");
886
+ // Define the fetch function
887
+ const fetchQuotes = () => {
888
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getQuotes({
889
+ assetId: token.assetId,
890
+ amount: options.amount,
891
+ walletAddress: options.walletAddress,
892
+ redirectUrl: options.redirectUrl,
893
+ paymentMethods: [paymentMethod.id],
894
+ provider: provider.id,
895
+ forceRefresh: true,
896
+ }).then((response) => {
897
+ // Auto-select logic: only when exactly one quote is returned
898
+ this.update((state) => {
899
+ if (response.success.length === 1) {
900
+ state.quotes.selected = response.success[0];
901
+ }
902
+ else {
903
+ // Keep existing selection if still valid, but update with fresh data
904
+ const currentSelection = state.quotes.selected;
905
+ if (currentSelection) {
906
+ const freshQuote = response.success.find((quote) => quote.provider === currentSelection.provider &&
907
+ quote.quote.paymentMethod ===
908
+ currentSelection.quote.paymentMethod);
909
+ // Update with fresh quote data, or clear if no longer valid
910
+ state.quotes.selected = freshQuote ?? null;
911
+ }
912
+ }
913
+ });
914
+ return undefined;
915
+ }));
916
+ };
917
+ // Fetch immediately
918
+ fetchQuotes();
919
+ // Set up 15-second polling
920
+ __classPrivateFieldSet(this, _RampsController_quotePollingInterval, setInterval(fetchQuotes, 15000), "f");
921
+ }
922
+ /**
923
+ * Stops automatic quote polling.
924
+ * Does not clear quotes data or selection, only stops the interval.
925
+ */
926
+ stopQuotePolling() {
927
+ if (__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f") !== null) {
928
+ clearInterval(__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f"));
929
+ __classPrivateFieldSet(this, _RampsController_quotePollingInterval, null, "f");
930
+ }
931
+ __classPrivateFieldSet(this, _RampsController_quotePollingOptions, null, "f");
932
+ }
933
+ /**
934
+ * Manually sets the selected quote.
935
+ *
936
+ * @param quote - The quote to select, or null to clear the selection.
937
+ */
938
+ setSelectedQuote(quote) {
939
+ this.update((state) => {
940
+ state.quotes.selected = quote;
941
+ });
942
+ }
943
+ /**
944
+ * Cleans up controller resources.
945
+ * Stops any active quote polling to prevent memory leaks.
946
+ * Should be called when the controller is no longer needed.
947
+ */
948
+ destroy() {
949
+ this.stopQuotePolling();
950
+ super.destroy();
951
+ }
824
952
  /**
825
953
  * Extracts the widget URL from a quote for redirect providers.
826
954
  * Returns the widget URL if available, or null if the quote doesn't have one.
@@ -833,7 +961,7 @@ class RampsController extends base_controller_1.BaseController {
833
961
  }
834
962
  }
835
963
  exports.RampsController = RampsController;
836
- _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
964
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_quotePollingInterval = new WeakMap(), _RampsController_quotePollingOptions = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
837
965
  const types = [
838
966
  'providers',
839
967
  'tokens',
@@ -849,12 +977,25 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
849
977
  delete requests[cacheKey];
850
978
  });
851
979
  }, _RampsController_cleanupState = function _RampsController_cleanupState() {
980
+ this.stopQuotePolling();
852
981
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
853
982
  this.update((state) => resetDependentResources(state, {
854
983
  clearUserRegionData: true,
855
984
  }));
856
985
  }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
857
986
  promise.catch((_error) => undefined);
987
+ }, _RampsController_restartPollingIfActive = function _RampsController_restartPollingIfActive() {
988
+ if (__classPrivateFieldGet(this, _RampsController_quotePollingInterval, "f") !== null && __classPrivateFieldGet(this, _RampsController_quotePollingOptions, "f")) {
989
+ const options = __classPrivateFieldGet(this, _RampsController_quotePollingOptions, "f");
990
+ this.stopQuotePolling();
991
+ try {
992
+ this.startQuotePolling(options);
993
+ }
994
+ catch {
995
+ // Dependencies not met yet, polling will need to be manually restarted
996
+ // when dependencies are available
997
+ }
998
+ }
858
999
  }, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
859
1000
  this.update((state) => {
860
1001
  const resource = state[resourceType];