@metamask/ramps-controller 10.0.0 → 10.1.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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [10.1.0]
11
+
12
+ ### Added
13
+
14
+ - Added `orders: RampsOrder[]` to controller state with persistence, along with crud methods([#8045](https://github.com/MetaMask/core/pull/8045))
15
+ - 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))
16
+ - Added `RampsController:orderStatusChanged` event, published when a polled order's status transitions ([#8045](https://github.com/MetaMask/core/pull/8045))
17
+ - 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))
18
+
10
19
  ## [10.0.0]
11
20
 
12
21
  ### Changed
@@ -183,7 +192,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
183
192
  - Add `OnRampService` for interacting with the OnRamp API
184
193
  - Add geolocation detection via IP address lookup
185
194
 
186
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.0.0...HEAD
195
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.1.0...HEAD
196
+ [10.1.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@10.0.0...@metamask/ramps-controller@10.1.0
187
197
  [10.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@9.0.0...@metamask/ramps-controller@10.0.0
188
198
  [9.0.0]: https://github.com/MetaMask/core/compare/@metamask/ramps-controller@8.1.0...@metamask/ramps-controller@9.0.0
189
199
  [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
@@ -842,11 +869,69 @@ class RampsController extends base_controller_1.BaseController {
842
869
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
843
870
  });
844
871
  }
872
+ // === ORDER MANAGEMENT ===
873
+ /**
874
+ * Adds or updates a V2 order in controller state.
875
+ * If an order with the same providerOrderId already exists, the incoming
876
+ * fields are merged on top of the existing order so that fields not present
877
+ * in the update (e.g. paymentDetails from the Transak API) are preserved.
878
+ *
879
+ * @param order - The RampsOrder to add or update.
880
+ */
881
+ addOrder(order) {
882
+ this.update((state) => {
883
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === order.providerOrderId);
884
+ if (idx === -1) {
885
+ state.orders.push(order);
886
+ }
887
+ else {
888
+ state.orders[idx] = {
889
+ ...state.orders[idx],
890
+ ...order,
891
+ };
892
+ }
893
+ });
894
+ }
895
+ /**
896
+ * Removes a V2 order from controller state by providerOrderId.
897
+ *
898
+ * @param providerOrderId - The provider order ID to remove.
899
+ */
900
+ removeOrder(providerOrderId) {
901
+ this.update((state) => {
902
+ state.orders = state.orders.filter((order) => order.providerOrderId !== providerOrderId);
903
+ });
904
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(providerOrderId);
905
+ }
906
+ /**
907
+ * Starts polling all pending V2 orders at a fixed interval.
908
+ * Each poll cycle iterates orders with non-terminal statuses,
909
+ * respects pollingSecondsMinimum and backoff from error count.
910
+ */
911
+ startOrderPolling() {
912
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
913
+ return;
914
+ }
915
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, setInterval(() => {
916
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
917
+ }, DEFAULT_POLLING_INTERVAL_MS), "f");
918
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
919
+ }
920
+ /**
921
+ * Stops order polling and clears the interval.
922
+ */
923
+ stopOrderPolling() {
924
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
925
+ clearInterval(__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f"));
926
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, null, "f");
927
+ }
928
+ }
845
929
  /**
846
930
  * Cleans up controller resources.
847
931
  * Should be called when the controller is no longer needed.
848
932
  */
849
933
  destroy() {
934
+ this.stopOrderPolling();
850
935
  super.destroy();
851
936
  }
852
937
  /**
@@ -880,7 +965,17 @@ class RampsController extends base_controller_1.BaseController {
880
965
  * @returns The unified order data.
881
966
  */
882
967
  async getOrder(providerCode, orderCode, wallet) {
883
- return await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
968
+ const order = await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
969
+ this.update((state) => {
970
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === orderCode);
971
+ if (idx !== -1) {
972
+ state.orders[idx] = {
973
+ ...state.orders[idx],
974
+ ...order,
975
+ };
976
+ }
977
+ });
978
+ return order;
884
979
  }
885
980
  /**
886
981
  * Extracts an order from a provider callback URL.
@@ -1292,7 +1387,7 @@ class RampsController extends base_controller_1.BaseController {
1292
1387
  }
1293
1388
  }
1294
1389
  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() {
1390
+ _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
1391
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1297
1392
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1298
1393
  }
@@ -1305,6 +1400,10 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1305
1400
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_removeRequestState).call(this, cacheKey);
1306
1401
  }
1307
1402
  }
1403
+ }, _RampsController_registerActionHandlers = function _RampsController_registerActionHandlers() {
1404
+ this.messenger.registerActionHandler('RampsController:getOrder', this.getOrder.bind(this));
1405
+ this.messenger.registerActionHandler('RampsController:getQuotes', this.getQuotes.bind(this));
1406
+ this.messenger.registerActionHandler('RampsController:setSelectedToken', this.setSelectedToken.bind(this));
1308
1407
  }, _RampsController_mutateRequests = function _RampsController_mutateRequests(fn) {
1309
1408
  this.update((state) => {
1310
1409
  const requests = state.requests;
@@ -1375,6 +1474,81 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1375
1474
  }
1376
1475
  }
1377
1476
  });
1477
+ }, _RampsController_refreshOrder =
1478
+ /**
1479
+ * Refreshes a single order via the V2 API and updates it in state.
1480
+ * Publishes orderStatusChanged if the status transitioned.
1481
+ *
1482
+ * @param order - The order to refresh (needs provider and providerOrderId).
1483
+ */
1484
+ async function _RampsController_refreshOrder(order) {
1485
+ const providerCode = order.provider?.id ?? '';
1486
+ if (!providerCode || !order.providerOrderId || !order.walletAddress) {
1487
+ return;
1488
+ }
1489
+ const providerCodeSegment = providerCode.replace('/providers/', '');
1490
+ const previousStatus = order.status;
1491
+ try {
1492
+ const updatedOrder = await this.getOrder(providerCodeSegment, order.providerOrderId, order.walletAddress);
1493
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1494
+ lastTimeFetched: 0,
1495
+ errorCount: 0,
1496
+ };
1497
+ if (updatedOrder.status === RampsService_1.RampsOrderStatus.Unknown) {
1498
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1499
+ }
1500
+ else {
1501
+ meta.errorCount = 0;
1502
+ }
1503
+ meta.lastTimeFetched = Date.now();
1504
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1505
+ if (previousStatus !== updatedOrder.status &&
1506
+ previousStatus !== undefined) {
1507
+ this.messenger.publish('RampsController:orderStatusChanged', {
1508
+ order: updatedOrder,
1509
+ previousStatus,
1510
+ });
1511
+ }
1512
+ if (TERMINAL_ORDER_STATUSES.has(updatedOrder.status)) {
1513
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(order.providerOrderId);
1514
+ }
1515
+ }
1516
+ catch {
1517
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1518
+ lastTimeFetched: 0,
1519
+ errorCount: 0,
1520
+ };
1521
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1522
+ meta.lastTimeFetched = Date.now();
1523
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1524
+ }
1525
+ }, _RampsController_pollPendingOrders = async function _RampsController_pollPendingOrders() {
1526
+ if (__classPrivateFieldGet(this, _RampsController_isPolling, "f")) {
1527
+ return;
1528
+ }
1529
+ __classPrivateFieldSet(this, _RampsController_isPolling, true, "f");
1530
+ try {
1531
+ const pendingOrders = this.state.orders.filter((order) => PENDING_ORDER_STATUSES.has(order.status));
1532
+ const now = Date.now();
1533
+ await Promise.allSettled(pendingOrders.map(async (order) => {
1534
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId);
1535
+ if (meta) {
1536
+ const backoffMs = meta.errorCount > 0
1537
+ ? Math.min(DEFAULT_POLLING_INTERVAL_MS *
1538
+ Math.pow(2, meta.errorCount - 1), 5 * 60 * 1000)
1539
+ : 0;
1540
+ const pollingMinMs = (order.pollingSecondsMinimum ?? 0) * 1000;
1541
+ const minWait = Math.max(backoffMs, pollingMinMs);
1542
+ if (now - meta.lastTimeFetched < minWait) {
1543
+ return;
1544
+ }
1545
+ }
1546
+ await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_refreshOrder).call(this, order);
1547
+ }));
1548
+ }
1549
+ finally {
1550
+ __classPrivateFieldSet(this, _RampsController_isPolling, false, "f");
1551
+ }
1378
1552
  }, _RampsController_syncTransakAuthOnError = function _RampsController_syncTransakAuthOnError(error) {
1379
1553
  if (error instanceof Error &&
1380
1554
  'httpStatus' in error &&