@metamask-previews/ramps-controller 10.0.0-preview-ee1fa5d01 → 10.0.0-preview-1d2746c

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 (36) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/RampsController.cjs +234 -3
  3. package/dist/RampsController.cjs.map +1 -1
  4. package/dist/RampsController.d.cts +53 -5
  5. package/dist/RampsController.d.cts.map +1 -1
  6. package/dist/RampsController.d.mts +53 -5
  7. package/dist/RampsController.d.mts.map +1 -1
  8. package/dist/RampsController.mjs +234 -3
  9. package/dist/RampsController.mjs.map +1 -1
  10. package/dist/RampsService.cjs.map +1 -1
  11. package/dist/RampsService.d.cts +14 -0
  12. package/dist/RampsService.d.cts.map +1 -1
  13. package/dist/RampsService.d.mts +14 -0
  14. package/dist/RampsService.d.mts.map +1 -1
  15. package/dist/RampsService.mjs.map +1 -1
  16. package/dist/TransakService-method-action-types.cjs.map +1 -1
  17. package/dist/TransakService-method-action-types.d.cts +13 -1
  18. package/dist/TransakService-method-action-types.d.cts.map +1 -1
  19. package/dist/TransakService-method-action-types.d.mts +13 -1
  20. package/dist/TransakService-method-action-types.d.mts.map +1 -1
  21. package/dist/TransakService-method-action-types.mjs.map +1 -1
  22. package/dist/TransakService.cjs +82 -6
  23. package/dist/TransakService.cjs.map +1 -1
  24. package/dist/TransakService.d.cts +28 -3
  25. package/dist/TransakService.d.cts.map +1 -1
  26. package/dist/TransakService.d.mts +28 -3
  27. package/dist/TransakService.d.mts.map +1 -1
  28. package/dist/TransakService.mjs +82 -6
  29. package/dist/TransakService.mjs.map +1 -1
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +4 -4
  32. package/dist/index.d.cts.map +1 -1
  33. package/dist/index.d.mts +4 -4
  34. package/dist/index.d.mts.map +1 -1
  35. package/dist/index.mjs.map +1 -1
  36. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Added `orders: RampsOrder[]` to controller state with persistence, along with crud methods([#8045](https://github.com/MetaMask/core/pull/8045))
13
+ - Added `apiMessage` property to `TransakApiError` to surface human-readable error messages from the Transak API (e.g. OTP rate-limit cooldown)
14
+ - Added `RampsController:orderStatusChanged` event, published when a polled order's status transitions ([#8045](https://github.com/MetaMask/core/pull/8045))
15
+ - Added real-time order status tracking for Transak orders via Pusher WebSockets ([#8075](https://github.com/MetaMask/core/pull/8075))
16
+ - Added `TransakService:orderUpdate` event to notify when WebSocket events are received
17
+ - Added `subscribeToOrder()`, `unsubscribeFromOrder()`, and `disconnectWebSocket()` methods to `TransakService`
18
+ - Added `subscribeToTransakOrderUpdates()` method to `RampsController` for bootstrapping WebSocket subscriptions on app restart
19
+ - Added `PusherFactory`, `PusherLike`, and `ChannelLike` types for dependency injection
20
+ - Exported new action types: `TransakServiceSubscribeToOrderAction`, `TransakServiceUnsubscribeFromOrderAction`, `TransakServiceDisconnectWebSocketAction`
21
+
10
22
  ## [10.0.0]
11
23
 
12
24
  ### Changed
@@ -10,11 +10,13 @@ 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_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_isTransakOrder, _RampsController_subscribeTransakOrder, _RampsController_unsubscribeTransakOrder, _RampsController_handleTransakOrderUpdate, _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");
19
+ const TransakService_1 = require("./TransakService.cjs");
18
20
  // === GENERAL ===
19
21
  /**
20
22
  * The name of the {@link RampsController}, used to namespace the
@@ -61,6 +63,9 @@ exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = [
61
63
  'TransakService:cancelOrder',
62
64
  'TransakService:cancelAllActiveOrders',
63
65
  'TransakService:getActiveOrders',
66
+ 'TransakService:subscribeToOrder',
67
+ 'TransakService:unsubscribeFromOrder',
68
+ 'TransakService:disconnectWebSocket',
64
69
  ];
65
70
  /**
66
71
  * Default TTL for quotes requests (15 seconds).
@@ -113,6 +118,12 @@ const rampsControllerMetadata = {
113
118
  includeInStateLogs: false,
114
119
  usedInUi: true,
115
120
  },
121
+ orders: {
122
+ persist: true,
123
+ includeInDebugSnapshot: true,
124
+ includeInStateLogs: true,
125
+ usedInUi: true,
126
+ },
116
127
  };
117
128
  /**
118
129
  * Creates a default resource state object.
@@ -155,6 +166,7 @@ function getDefaultRampsControllerState() {
155
166
  kycRequirement: createDefaultResourceState(null),
156
167
  },
157
168
  },
169
+ orders: [],
158
170
  };
159
171
  }
160
172
  exports.getDefaultRampsControllerState = getDefaultRampsControllerState;
@@ -242,6 +254,21 @@ function findRegionFromCode(regionCode, countries) {
242
254
  regionCode: normalizedCode,
243
255
  };
244
256
  }
257
+ // === ORDER POLLING CONSTANTS ===
258
+ const TERMINAL_ORDER_STATUSES = new Set([
259
+ RampsService_1.RampsOrderStatus.Completed,
260
+ RampsService_1.RampsOrderStatus.Failed,
261
+ RampsService_1.RampsOrderStatus.Cancelled,
262
+ RampsService_1.RampsOrderStatus.IdExpired,
263
+ ]);
264
+ const PENDING_ORDER_STATUSES = new Set([
265
+ RampsService_1.RampsOrderStatus.Pending,
266
+ RampsService_1.RampsOrderStatus.Created,
267
+ RampsService_1.RampsOrderStatus.Unknown,
268
+ RampsService_1.RampsOrderStatus.Precreated,
269
+ ]);
270
+ const DEFAULT_POLLING_INTERVAL_MS = 30000;
271
+ const MAX_ERROR_COUNT = 5;
245
272
  // === CONTROLLER DEFINITION ===
246
273
  /**
247
274
  * Manages cryptocurrency on/off ramps functionality.
@@ -297,8 +324,12 @@ class RampsController extends base_controller_1.BaseController {
297
324
  * Used so isLoading is only cleared when the last request for that resource finishes.
298
325
  */
299
326
  _RampsController_pendingResourceCount.set(this, new Map());
327
+ _RampsController_orderPollingMeta.set(this, new Map());
328
+ _RampsController_orderPollingTimer.set(this, null);
329
+ _RampsController_isPolling.set(this, false);
300
330
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
301
331
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
332
+ this.messenger.subscribe('TransakService:orderUpdate', __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_handleTransakOrderUpdate).bind(this));
302
333
  }
303
334
  /**
304
335
  * Executes a request with caching, deduplication, and at most one in-flight
@@ -842,11 +873,83 @@ class RampsController extends base_controller_1.BaseController {
842
873
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
843
874
  });
844
875
  }
876
+ // === ORDER MANAGEMENT ===
877
+ /**
878
+ * Adds or updates a V2 order in controller state.
879
+ * If an order with the same providerOrderId already exists, the incoming
880
+ * fields are merged on top of the existing order so that fields not present
881
+ * in the update (e.g. paymentDetails from the Transak API) are preserved.
882
+ *
883
+ * @param order - The RampsOrder to add or update.
884
+ */
885
+ addOrder(order) {
886
+ this.update((state) => {
887
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === order.providerOrderId);
888
+ if (idx === -1) {
889
+ state.orders.push(order);
890
+ }
891
+ else {
892
+ state.orders[idx] = {
893
+ ...state.orders[idx],
894
+ ...order,
895
+ };
896
+ }
897
+ });
898
+ if (__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTransakOrder).call(this, order) &&
899
+ PENDING_ORDER_STATUSES.has(order.status)) {
900
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_subscribeTransakOrder).call(this, order.providerOrderId);
901
+ }
902
+ }
903
+ /**
904
+ * Removes a V2 order from controller state by providerOrderId.
905
+ *
906
+ * @param providerOrderId - The provider order ID to remove.
907
+ */
908
+ removeOrder(providerOrderId) {
909
+ const order = this.state.orders.find((existing) => existing.providerOrderId === providerOrderId);
910
+ this.update((state) => {
911
+ state.orders = state.orders.filter((existing) => existing.providerOrderId !== providerOrderId);
912
+ });
913
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(providerOrderId);
914
+ if (order && __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTransakOrder).call(this, order)) {
915
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_unsubscribeTransakOrder).call(this, providerOrderId);
916
+ }
917
+ }
918
+ /**
919
+ * Starts polling all pending V2 orders at a fixed interval.
920
+ * Each poll cycle iterates orders with non-terminal statuses,
921
+ * respects pollingSecondsMinimum and backoff from error count.
922
+ */
923
+ startOrderPolling() {
924
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
925
+ return;
926
+ }
927
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, setInterval(() => {
928
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
929
+ }, DEFAULT_POLLING_INTERVAL_MS), "f");
930
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pollPendingOrders).call(this).catch(() => undefined);
931
+ }
932
+ /**
933
+ * Stops order polling and clears the interval.
934
+ */
935
+ stopOrderPolling() {
936
+ if (__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f")) {
937
+ clearInterval(__classPrivateFieldGet(this, _RampsController_orderPollingTimer, "f"));
938
+ __classPrivateFieldSet(this, _RampsController_orderPollingTimer, null, "f");
939
+ }
940
+ }
845
941
  /**
846
942
  * Cleans up controller resources.
847
943
  * Should be called when the controller is no longer needed.
848
944
  */
849
945
  destroy() {
946
+ this.stopOrderPolling();
947
+ try {
948
+ this.messenger.call('TransakService:disconnectWebSocket');
949
+ }
950
+ catch {
951
+ // TransakService may not be registered yet during tests
952
+ }
850
953
  super.destroy();
851
954
  }
852
955
  /**
@@ -880,7 +983,17 @@ class RampsController extends base_controller_1.BaseController {
880
983
  * @returns The unified order data.
881
984
  */
882
985
  async getOrder(providerCode, orderCode, wallet) {
883
- return await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
986
+ const order = await this.messenger.call('RampsService:getOrder', providerCode, orderCode, wallet);
987
+ this.update((state) => {
988
+ const idx = state.orders.findIndex((existing) => existing.providerOrderId === orderCode);
989
+ if (idx !== -1) {
990
+ state.orders[idx] = {
991
+ ...state.orders[idx],
992
+ ...order,
993
+ };
994
+ }
995
+ });
996
+ return order;
884
997
  }
885
998
  /**
886
999
  * Extracts an order from a provider callback URL.
@@ -896,6 +1009,16 @@ class RampsController extends base_controller_1.BaseController {
896
1009
  async getOrderFromCallback(providerCode, callbackUrl, wallet) {
897
1010
  return await this.messenger.call('RampsService:getOrderFromCallback', providerCode, callbackUrl, wallet);
898
1011
  }
1012
+ /**
1013
+ * Subscribes to WebSocket channels for all pending Transak orders.
1014
+ * Called on startup to resume real-time tracking for orders already in state.
1015
+ */
1016
+ subscribeToTransakOrderUpdates() {
1017
+ const pendingTransakOrders = this.state.orders.filter((order) => __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTransakOrder).call(this, order) && PENDING_ORDER_STATUSES.has(order.status));
1018
+ for (const order of pendingTransakOrders) {
1019
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_subscribeTransakOrder).call(this, order.providerOrderId);
1020
+ }
1021
+ }
899
1022
  /**
900
1023
  * Sets the Transak API key used for all Transak API requests.
901
1024
  *
@@ -1292,7 +1415,7 @@ class RampsController extends base_controller_1.BaseController {
1292
1415
  }
1293
1416
  }
1294
1417
  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() {
1418
+ _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
1419
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1297
1420
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1298
1421
  }
@@ -1375,6 +1498,114 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1375
1498
  }
1376
1499
  }
1377
1500
  });
1501
+ }, _RampsController_refreshOrder =
1502
+ /**
1503
+ * Refreshes a single order via the V2 API and updates it in state.
1504
+ * Publishes orderStatusChanged if the status transitioned.
1505
+ *
1506
+ * @param order - The order to refresh (needs provider and providerOrderId).
1507
+ */
1508
+ async function _RampsController_refreshOrder(order) {
1509
+ const providerCode = order.provider?.id ?? '';
1510
+ if (!providerCode || !order.providerOrderId || !order.walletAddress) {
1511
+ return;
1512
+ }
1513
+ const providerCodeSegment = providerCode.replace('/providers/', '');
1514
+ const previousStatus = order.status;
1515
+ try {
1516
+ const updatedOrder = await this.getOrder(providerCodeSegment, order.providerOrderId, order.walletAddress);
1517
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1518
+ lastTimeFetched: 0,
1519
+ errorCount: 0,
1520
+ };
1521
+ if (updatedOrder.status === RampsService_1.RampsOrderStatus.Unknown) {
1522
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1523
+ }
1524
+ else {
1525
+ meta.errorCount = 0;
1526
+ }
1527
+ meta.lastTimeFetched = Date.now();
1528
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1529
+ if (previousStatus !== updatedOrder.status &&
1530
+ previousStatus !== undefined) {
1531
+ this.messenger.publish('RampsController:orderStatusChanged', {
1532
+ order: updatedOrder,
1533
+ previousStatus,
1534
+ });
1535
+ }
1536
+ if (TERMINAL_ORDER_STATUSES.has(updatedOrder.status)) {
1537
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(order.providerOrderId);
1538
+ if (__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTransakOrder).call(this, order)) {
1539
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_unsubscribeTransakOrder).call(this, order.providerOrderId);
1540
+ }
1541
+ }
1542
+ }
1543
+ catch {
1544
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
1545
+ lastTimeFetched: 0,
1546
+ errorCount: 0,
1547
+ };
1548
+ meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
1549
+ meta.lastTimeFetched = Date.now();
1550
+ __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
1551
+ }
1552
+ }, _RampsController_pollPendingOrders = async function _RampsController_pollPendingOrders() {
1553
+ if (__classPrivateFieldGet(this, _RampsController_isPolling, "f")) {
1554
+ return;
1555
+ }
1556
+ __classPrivateFieldSet(this, _RampsController_isPolling, true, "f");
1557
+ try {
1558
+ const pendingOrders = this.state.orders.filter((order) => PENDING_ORDER_STATUSES.has(order.status));
1559
+ const now = Date.now();
1560
+ await Promise.allSettled(pendingOrders.map(async (order) => {
1561
+ const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId);
1562
+ if (meta) {
1563
+ const backoffMs = meta.errorCount > 0
1564
+ ? Math.min(DEFAULT_POLLING_INTERVAL_MS *
1565
+ Math.pow(2, meta.errorCount - 1), 5 * 60 * 1000)
1566
+ : 0;
1567
+ const pollingMinMs = (order.pollingSecondsMinimum ?? 0) * 1000;
1568
+ const minWait = Math.max(backoffMs, pollingMinMs);
1569
+ if (now - meta.lastTimeFetched < minWait) {
1570
+ return;
1571
+ }
1572
+ }
1573
+ await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_refreshOrder).call(this, order);
1574
+ }));
1575
+ }
1576
+ finally {
1577
+ __classPrivateFieldSet(this, _RampsController_isPolling, false, "f");
1578
+ }
1579
+ }, _RampsController_isTransakOrder = function _RampsController_isTransakOrder(order) {
1580
+ const providerId = order.provider?.id ?? '';
1581
+ return providerId.includes('transak-native');
1582
+ }, _RampsController_subscribeTransakOrder = function _RampsController_subscribeTransakOrder(providerOrderId) {
1583
+ const transakOrderId = TransakService_1.TransakOrderIdTransformer.extractTransakOrderId(providerOrderId);
1584
+ try {
1585
+ this.messenger.call('TransakService:subscribeToOrder', transakOrderId);
1586
+ }
1587
+ catch {
1588
+ // WebSocket subscription is best-effort; polling is the fallback
1589
+ }
1590
+ }, _RampsController_unsubscribeTransakOrder = function _RampsController_unsubscribeTransakOrder(providerOrderId) {
1591
+ const transakOrderId = TransakService_1.TransakOrderIdTransformer.extractTransakOrderId(providerOrderId);
1592
+ try {
1593
+ this.messenger.call('TransakService:unsubscribeFromOrder', transakOrderId);
1594
+ }
1595
+ catch {
1596
+ // Best-effort cleanup
1597
+ }
1598
+ }, _RampsController_handleTransakOrderUpdate = function _RampsController_handleTransakOrderUpdate(data) {
1599
+ const order = this.state.orders.find((existing) => {
1600
+ if (!__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isTransakOrder).call(this, existing)) {
1601
+ return false;
1602
+ }
1603
+ const orderId = TransakService_1.TransakOrderIdTransformer.extractTransakOrderId(existing.providerOrderId);
1604
+ return orderId === data.transakOrderId;
1605
+ });
1606
+ if (order) {
1607
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_refreshOrder).call(this, order).catch(() => undefined);
1608
+ }
1378
1609
  }, _RampsController_syncTransakAuthOnError = function _RampsController_syncTransakAuthOnError(error) {
1379
1610
  if (error instanceof Error &&
1380
1611
  'httpStatus' in error &&