@metamask/ramps-controller 10.0.0 → 10.2.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,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [10.2.0]
11
+
12
+ ### Fixed
13
+
14
+ - `setSelectedProvider` no longer fetches payment methods when the selected token is explicitly not supported by the new provider, preventing empty payment method state with no user feedback ([#8103](https://github.com/MetaMask/core/pull/8103))
15
+
16
+ ## [10.1.0]
17
+
18
+ ### Added
19
+
20
+ - Added `orders: RampsOrder[]` to controller state with persistence, along with crud methods([#8045](https://github.com/MetaMask/core/pull/8045))
21
+ - Added `apiMessage` property to `TransakApiError` to surface human-readable error messages from the Transak API (e.g. OTP rate-limit cooldown) ([#8072](https://github.com/MetaMask/core/pull/8072))
22
+ - Added `RampsController:orderStatusChanged` event, published when a polled order's status transitions ([#8045](https://github.com/MetaMask/core/pull/8045))
23
+ - Add messenger actions for `RampsController:setSelectedToken`, `RampsController:getQuotes`, and `RampsController:getOrder`, register their handlers in `RampsController`, and export the action types from the package index ([#8081](https://github.com/MetaMask/core/pull/8081))
24
+
10
25
  ## [10.0.0]
11
26
 
12
27
  ### Changed
@@ -183,7 +198,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
183
198
  - Add `OnRampService` for interacting with the OnRamp API
184
199
  - Add geolocation detection via IP address lookup
185
200
 
186
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.0.0...HEAD
201
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.2.0...HEAD
202
+ [10.2.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.1.0...@metamask/ramps-controller@10.2.0
203
+ [10.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.0.0...@metamask/ramps-controller@10.1.0
187
204
  [10.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@9.0.0...@metamask/ramps-controller@10.0.0
188
205
  [9.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@8.1.0...@metamask/ramps-controller@9.0.0
189
206
  [8.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@8.0.0...@metamask/ramps-controller@8.1.0
@@ -10,10 +10,11 @@ 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_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_syncTransakAuthOnError;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_orderPollingMeta, _RampsController_orderPollingTimer, _RampsController_isPolling, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_registerActionHandlers, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError;
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");
17
+ const RampsService_1 = require("./RampsService.cjs");
17
18
  const RequestCache_1 = require("./RequestCache.cjs");
18
19
  // === GENERAL ===
19
20
  /**
@@ -113,6 +114,12 @@ const rampsControllerMetadata = {
113
114
  includeInStateLogs: false,
114
115
  usedInUi: true,
115
116
  },
117
+ orders: {
118
+ persist: true,
119
+ includeInDebugSnapshot: true,
120
+ includeInStateLogs: true,
121
+ usedInUi: true,
122
+ },
116
123
  };
117
124
  /**
118
125
  * Creates a default resource state object.
@@ -155,6 +162,7 @@ function getDefaultRampsControllerState() {
155
162
  kycRequirement: createDefaultResourceState(null),
156
163
  },
157
164
  },
165
+ orders: [],
158
166
  };
159
167
  }
160
168
  exports.getDefaultRampsControllerState = getDefaultRampsControllerState;
@@ -242,6 +250,21 @@ function findRegionFromCode(regionCode, countries) {
242
250
  regionCode: normalizedCode,
243
251
  };
244
252
  }
253
+ // === ORDER POLLING CONSTANTS ===
254
+ const TERMINAL_ORDER_STATUSES = new Set([
255
+ RampsService_1.RampsOrderStatus.Completed,
256
+ RampsService_1.RampsOrderStatus.Failed,
257
+ RampsService_1.RampsOrderStatus.Cancelled,
258
+ RampsService_1.RampsOrderStatus.IdExpired,
259
+ ]);
260
+ const PENDING_ORDER_STATUSES = new Set([
261
+ RampsService_1.RampsOrderStatus.Pending,
262
+ RampsService_1.RampsOrderStatus.Created,
263
+ RampsService_1.RampsOrderStatus.Unknown,
264
+ RampsService_1.RampsOrderStatus.Precreated,
265
+ ]);
266
+ const DEFAULT_POLLING_INTERVAL_MS = 30000;
267
+ const MAX_ERROR_COUNT = 5;
245
268
  // === CONTROLLER DEFINITION ===
246
269
  /**
247
270
  * Manages cryptocurrency on/off ramps functionality.
@@ -297,8 +320,12 @@ class RampsController extends base_controller_1.BaseController {
297
320
  * Used so isLoading is only cleared when the last request for that resource finishes.
298
321
  */
299
322
  _RampsController_pendingResourceCount.set(this, new Map());
323
+ _RampsController_orderPollingMeta.set(this, new Map());
324
+ _RampsController_orderPollingTimer.set(this, null);
325
+ _RampsController_isPolling.set(this, false);
300
326
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
301
327
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
328
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_registerActionHandlers).call(this);
302
329
  }
303
330
  /**
304
331
  * Executes a request with caching, deduplication, and at most one in-flight
@@ -518,11 +545,21 @@ class RampsController extends base_controller_1.BaseController {
518
545
  if (!provider) {
519
546
  throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
520
547
  }
548
+ const selectedToken = this.state.tokens.selected;
549
+ const supportedCryptos = provider.supportedCryptoCurrencies;
550
+ // Only fetch payment methods if the selected token is supported by the new
551
+ // provider. If it isn't, the payment methods request would fail or return
552
+ // empty for the wrong reason; the UI will show the Token Not Available modal
553
+ // so the user can change token or pick a different provider.
554
+ const assetId = selectedToken?.assetId;
555
+ const tokenSupportedByProvider = !(assetId && supportedCryptos?.[assetId] === false);
521
556
  this.update((state) => {
522
557
  state.providers.selected = provider;
523
558
  resetResource(state, 'paymentMethods');
524
559
  });
525
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
560
+ if (tokenSupportedByProvider) {
561
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
562
+ }
526
563
  }
527
564
  /**
528
565
  * Initializes the controller by fetching the user's region from geolocation.
@@ -842,11 +879,69 @@ class RampsController extends base_controller_1.BaseController {
842
879
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
843
880
  });
844
881
  }
882
+ // === ORDER MANAGEMENT ===
883
+ /**
884
+ * Adds or updates a V2 order in controller state.
885
+ * If an order with the same providerOrderId already exists, the incoming
886
+ * fields are merged on top of the existing order so that fields not present
887
+ * in the update (e.g. paymentDetails from the Transak API) are preserved.
888
+ *
889
+ * @param order - The RampsOrder to add or update.
890
+ */
891
+ addOrder(order) {
892
+ this.update((state) => {
893
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === order.providerOrderId);
894
+ if (idx === -1) {
895
+ state.orders.push(order);
896
+ }
897
+ else {
898
+ state.orders[idx] = {
899
+ ...state.orders[idx],
900
+ ...order,
901
+ };
902
+ }
903
+ });
904
+ }
905
+ /**
906
+ * Removes a V2 order from controller state by providerOrderId.
907
+ *
908
+ * @param providerOrderId - The provider order ID to remove.
909
+ */
910
+ removeOrder(providerOrderId) {
911
+ this.update((state) => {
912
+ state.orders = state.orders.filter((order) => order.providerOrderId !== providerOrderId);
913
+ });
914
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(providerOrderId);
915
+ }
916
+ /**
917
+ * Starts polling all pending V2 orders at a fixed interval.
918
+ * Each poll cycle iterates orders with non-terminal statuses,
919
+ * respects pollingSecondsMinimum and backoff from error count.
920
+ */
921
+ startOrderPolling() {
922
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
923
+ return;
924
+ }
925
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, setInterval(() => {
926
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
927
+ }, DEFAULT_POLLING_INTERVAL_MS), "f");
928
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
929
+ }
930
+ /**
931
+ * Stops order polling and clears the interval.
932
+ */
933
+ stopOrderPolling() {
934
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
935
+ clearInterval(__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f"));
936
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, null, "f");
937
+ }
938
+ }
845
939
  /**
846
940
  * Cleans up controller resources.
847
941
  * Should be called when the controller is no longer needed.
848
942
  */
849
943
  destroy() {
944
+ this.stopOrderPolling();
850
945
  super.destroy();
851
946
  }
852
947
  /**
@@ -880,7 +975,17 @@ class RampsController extends base_controller_1.BaseController {
880
975
  * @returns The unified order data.
881
976
  */
882
977
  async getOrder(providerCode, orderCode, wallet) {
883
- return await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
978
+ const order = await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
979
+ this.update((state) => {
980
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === orderCode);
981
+ if (idx !== -1) {
982
+ state.orders[idx] = {
983
+ ...state.orders[idx],
984
+ ...order,
985
+ };
986
+ }
987
+ });
988
+ return order;
884
989
  }
885
990
  /**
886
991
  * Extracts an order from a provider callback URL.
@@ -1292,7 +1397,7 @@ class RampsController extends base_controller_1.BaseController {
1292
1397
  }
1293
1398
  }
1294
1399
  exports.RampsController = RampsController;
1295
- _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() {
1400
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_orderPollingMeta = new WeakMap(), _RampsController_orderPollingTimer = new WeakMap(), _RampsController_isPolling = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1296
1401
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1297
1402
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1298
1403
  }
@@ -1305,6 +1410,10 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1305
1410
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_removeRequestState).call(this, cacheKey);
1306
1411
  }
1307
1412
  }
1413
+ }, _RampsController_registerActionHandlers = function _RampsController_registerActionHandlers() {
1414
+ this.messenger.registerActionHandler('RampsController:getOrder', this.getOrder.bind(this));
1415
+ this.messenger.registerActionHandler('RampsController:getQuotes', this.getQuotes.bind(this));
1416
+ this.messenger.registerActionHandler('RampsController:setSelectedToken', this.setSelectedToken.bind(this));
1308
1417
  }, _RampsController_mutateRequests = function _RampsController_mutateRequests(fn) {
1309
1418
  this.update((state) => {
1310
1419
  const requests = state.requests;
@@ -1375,6 +1484,81 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1375
1484
  }
1376
1485
  }
1377
1486
  });
1487
+ }, _RampsController_refreshOrder =
1488
+ /**
1489
+ * Refreshes a single order via the V2 API and updates it in state.
1490
+ * Publishes orderStatusChanged if the status transitioned.
1491
+ *
1492
+ * @param order - The order to refresh (needs provider and providerOrderId).
1493
+ */
1494
+ async function _RampsController_refreshOrder(order) {
1495
+ const providerCode = order.provider?.id ?? '';
1496
+ if (!providerCode || !order.providerOrderId || !order.walletAddress) {
1497
+ return;
1498
+ }
1499
+ const providerCodeSegment = providerCode.replace('/providers/', '');
1500
+ const previousStatus = order.status;
1501
+ try {
1502
+ const updatedOrder = await this.getOrder(providerCodeSegment, order.providerOrderId, order.walletAddress);
1503
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1504
+ lastTimeFetched: 0,
1505
+ errorCount: 0,
1506
+ };
1507
+ if (updatedOrder.status === RampsService_1.RampsOrderStatus.Unknown) {
1508
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1509
+ }
1510
+ else {
1511
+ meta.errorCount = 0;
1512
+ }
1513
+ meta.lastTimeFetched = Date.now();
1514
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1515
+ if (previousStatus !== updatedOrder.status &&
1516
+ previousStatus !== undefined) {
1517
+ this.messenger.publish('RampsController:orderStatusChanged', {
1518
+ order: updatedOrder,
1519
+ previousStatus,
1520
+ });
1521
+ }
1522
+ if (TERMINAL_ORDER_STATUSES.has(updatedOrder.status)) {
1523
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(order.providerOrderId);
1524
+ }
1525
+ }
1526
+ catch {
1527
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1528
+ lastTimeFetched: 0,
1529
+ errorCount: 0,
1530
+ };
1531
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1532
+ meta.lastTimeFetched = Date.now();
1533
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1534
+ }
1535
+ }, _RampsController_pollPendingOrders = async function _RampsController_pollPendingOrders() {
1536
+ if (__classPrivateFieldGet(this, _RampsController_isPolling, "f")) {
1537
+ return;
1538
+ }
1539
+ __classPrivateFieldSet(this, _RampsController_isPolling, true, "f");
1540
+ try {
1541
+ const pendingOrders = this.state.orders.filter((order) => PENDING_ORDER_STATUSES.has(order.status));
1542
+ const now = Date.now();
1543
+ await Promise.allSettled(pendingOrders.map(async (order) => {
1544
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId);
1545
+ if (meta) {
1546
+ const backoffMs = meta.errorCount > 0
1547
+ ? Math.min(DEFAULT_POLLING_INTERVAL_MS *
1548
+ Math.pow(2, meta.errorCount - 1), 5 * 60 * 1000)
1549
+ : 0;
1550
+ const pollingMinMs = (order.pollingSecondsMinimum ?? 0) * 1000;
1551
+ const minWait = Math.max(backoffMs, pollingMinMs);
1552
+ if (now - meta.lastTimeFetched < minWait) {
1553
+ return;
1554
+ }
1555
+ }
1556
+ await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_refreshOrder).call(this, order);
1557
+ }));
1558
+ }
1559
+ finally {
1560
+ __classPrivateFieldSet(this, _RampsController_isPolling, false, "f");
1561
+ }
1378
1562
  }, _RampsController_syncTransakAuthOnError = function _RampsController_syncTransakAuthOnError(error) {
1379
1563
  if (error instanceof Error &&
1380
1564
  'httpStatus' in error &&