@metamask/ramps-controller 15.0.0 → 16.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.
@@ -10,9 +10,9 @@ 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_pendingResourceGeneration, _RampsController_orderPollingMeta, _RampsController_orderPollingTimer, _RampsController_isPolling, _RampsController_initPromise, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_runInit, _RampsController_getSupportingProvidersForRegion, _RampsController_filterProviderIdsBySupport, _RampsController_resolveProviderIdsForQuote, _RampsController_getPreferredProviderIdsFromOrders, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError, _RampsController_getNormalizedTransakError;
13
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_getProviderScope, _RampsController_getDefaultRedirectUrl, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_pendingResourceGeneration, _RampsController_orderPollingMeta, _RampsController_orderPollingTimer, _RampsController_isPolling, _RampsController_initPromise, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_abortDependentRequests, _RampsController_mutateRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_requireRegion, _RampsController_isRegionCurrent, _RampsController_isTokenCurrent, _RampsController_isProviderCurrent, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState, _RampsController_runInit, _RampsController_syncUserRegionFromCountriesCatalog, _RampsController_pickInAppQuote, _RampsController_getSupportingProvidersForRegion, _RampsController_filterProviderIdsBySupport, _RampsController_resolveProviderIdsForQuote, _RampsController_getPreferredProviderIdsFromOrders, _RampsController_refreshOrder, _RampsController_pollPendingOrders, _RampsController_syncTransakAuthOnError, _RampsController_getNormalizedTransakError;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.RampsController = exports.getInternalOrderCode = exports.normalizeProviderCode = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
15
+ exports.RampsController = exports.getInternalOrderCode = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
17
  const controller_utils_1 = require("@metamask/controller-utils");
18
18
  const rampsErrorCodes_1 = require("./rampsErrorCodes.cjs");
@@ -135,7 +135,7 @@ const rampsControllerMetadata = {
135
135
  usedInUi: true,
136
136
  },
137
137
  countries: {
138
- persist: true,
138
+ persist: false,
139
139
  includeInDebugSnapshot: true,
140
140
  includeInStateLogs: true,
141
141
  usedInUi: true,
@@ -329,10 +329,6 @@ function findRegionFromCode(regionCode, countries) {
329
329
  regionCode: normalizedCode,
330
330
  };
331
331
  }
332
- function normalizeProviderCode(providerCode) {
333
- return providerCode.replace(/^\/providers\//u, '');
334
- }
335
- exports.normalizeProviderCode = normalizeProviderCode;
336
332
  /**
337
333
  * Returns the internal MetaMask order code used for state lookups and polling.
338
334
  * Prefers the code embedded in the canonical order `id` path over `providerOrderId`,
@@ -441,8 +437,13 @@ class RampsController extends base_controller_1.BaseController {
441
437
  * controller. Missing properties will be filled in with defaults.
442
438
  * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.
443
439
  * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.
440
+ * @param args.getProviderScope - Optional callback returning the current
441
+ * provider-class scope for fiat quote widening. Defaults to `off`.
442
+ * @param args.getDefaultRedirectUrl - Optional callback returning the default
443
+ * redirect URL used for the widened in-app quote fetch when the caller omits
444
+ * `redirectUrl`. Defaults to a callback returning `undefined`.
444
445
  */
445
- constructor({ messenger, state = {}, requestCacheTTL = RequestCache_1.DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = RequestCache_1.DEFAULT_REQUEST_CACHE_MAX_SIZE, }) {
446
+ constructor({ messenger, state = {}, requestCacheTTL = RequestCache_1.DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = RequestCache_1.DEFAULT_REQUEST_CACHE_MAX_SIZE, getProviderScope, getDefaultRedirectUrl, }) {
446
447
  super({
447
448
  messenger,
448
449
  metadata: rampsControllerMetadata,
@@ -463,6 +464,17 @@ class RampsController extends base_controller_1.BaseController {
463
464
  * Maximum number of entries in the request cache.
464
465
  */
465
466
  _RampsController_requestCacheMaxSize.set(this, void 0);
467
+ /**
468
+ * Resolves the current provider-class scope for fiat quote widening. Defaults
469
+ * to `() => 'off'` (native-only) when no callback is injected.
470
+ */
471
+ _RampsController_getProviderScope.set(this, void 0);
472
+ /**
473
+ * Resolves the default redirect URL for the widened in-app quote fetch when
474
+ * the caller omits `redirectUrl`. Defaults to `() => undefined` when no
475
+ * callback is injected.
476
+ */
477
+ _RampsController_getDefaultRedirectUrl.set(this, void 0);
466
478
  /**
467
479
  * Map of pending requests for deduplication.
468
480
  * Key is the cache key, value is the pending request with abort controller.
@@ -484,6 +496,8 @@ class RampsController extends base_controller_1.BaseController {
484
496
  _RampsController_initPromise.set(this, null);
485
497
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
486
498
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
499
+ __classPrivateFieldSet(this, _RampsController_getProviderScope, getProviderScope ?? (() => 'off'), "f");
500
+ __classPrivateFieldSet(this, _RampsController_getDefaultRedirectUrl, getDefaultRedirectUrl ?? (() => undefined), "f");
487
501
  this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
488
502
  }
489
503
  /**
@@ -721,8 +735,10 @@ class RampsController extends base_controller_1.BaseController {
721
735
  * This should be called once at app startup to set up the initial region.
722
736
  *
723
737
  * Idempotent: subsequent calls return the same promise unless forceRefresh is set.
724
- * Skips getCountries when countries are already loaded; skips geolocation when
725
- * userRegion already exists.
738
+ * Force-refetches the countries catalog on startup (bypassing the in-session
739
+ * request cache) so region preset amounts stay current. The catalog is not
740
+ * persisted, so a cold start always re-fetches it regardless. Skips
741
+ * geolocation when userRegion already exists.
726
742
  *
727
743
  * @param options - Options for cache behavior. forceRefresh bypasses idempotency and re-runs the full flow.
728
744
  * @returns Promise that resolves when initialization is complete.
@@ -759,6 +775,7 @@ class RampsController extends base_controller_1.BaseController {
759
775
  this.update((state) => {
760
776
  state.countries.data = Array.isArray(countries) ? [...countries] : [];
761
777
  });
778
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncUserRegionFromCountriesCatalog).call(this);
762
779
  return countries;
763
780
  }
764
781
  /**
@@ -995,7 +1012,19 @@ class RampsController extends base_controller_1.BaseController {
995
1012
  if (normalizedAssetIdForValidation === '') {
996
1013
  throw new Error('assetId is required.');
997
1014
  }
1015
+ // When a non-`off` provider scope is active, widen the native-only
1016
+ // auto-selection path to every supporting provider and pick the best in-app
1017
+ // quote from the results (in-app vs external is only knowable per-quote via
1018
+ // `buyWidget.browser`). Only the auto-select/restrict path that MM Pay's
1019
+ // `getRampsQuote` uses is affected; explicit-`providers` callers and the
1020
+ // plain all-provider path are untouched.
1021
+ const providerScope = __classPrivateFieldGet(this, _RampsController_getProviderScope, "f").call(this);
1022
+ const widenToInAppProviders = providerScope !== 'off' &&
1023
+ !options.providers &&
1024
+ (options.autoSelectProvider === true ||
1025
+ options.restrictToKnownOrNativeProviders === true);
998
1026
  let providersToUse;
1027
+ let inAppProviderCatalog = this.state.providers.data;
999
1028
  if (options.providers) {
1000
1029
  providersToUse = options.restrictToKnownOrNativeProviders
1001
1030
  ? await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_filterProviderIdsBySupport).call(this, {
@@ -1005,6 +1034,17 @@ class RampsController extends base_controller_1.BaseController {
1005
1034
  })
1006
1035
  : options.providers;
1007
1036
  }
1037
+ else if (widenToInAppProviders) {
1038
+ // `#getSupportingProvidersForRegion` also hydrates the provider catalog
1039
+ // when controller state is empty, so all-provider quoting cannot silently
1040
+ // return zero providers here.
1041
+ const { supporting } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
1042
+ assetId: normalizedAssetIdForValidation,
1043
+ region: regionToUse,
1044
+ });
1045
+ inAppProviderCatalog = supporting;
1046
+ providersToUse = supporting.map((provider) => provider.id);
1047
+ }
1008
1048
  else if (options.autoSelectProvider ||
1009
1049
  options.restrictToKnownOrNativeProviders) {
1010
1050
  // The restriction flag implies resolution: it must narrow the provider
@@ -1034,8 +1074,12 @@ class RampsController extends base_controller_1.BaseController {
1034
1074
  // Under headless-buy gating, an empty resolved provider list means no
1035
1075
  // eligible (native/supporting) provider exists. Return an empty response
1036
1076
  // rather than passing `[]` to the service, which omits the provider filter
1037
- // and would quote every provider.
1038
- if (options.restrictToKnownOrNativeProviders &&
1077
+ // and would quote every provider. This also guards the widened in-app path:
1078
+ // a caller may trigger widening with `autoSelectProvider` alone (no
1079
+ // `restrictToKnownOrNativeProviders`), and an empty supporting set must not
1080
+ // fall through to unfiltered quotes from providers that do not support the
1081
+ // asset.
1082
+ if ((options.restrictToKnownOrNativeProviders || widenToInAppProviders) &&
1039
1083
  providersToUse.length === 0) {
1040
1084
  return { success: [], sorted: [], error: [], customActions: [] };
1041
1085
  }
@@ -1043,6 +1087,12 @@ class RampsController extends base_controller_1.BaseController {
1043
1087
  const normalizedFiat = fiatToUse.toLowerCase().trim();
1044
1088
  const normalizedAssetId = normalizedAssetIdForValidation;
1045
1089
  const normalizedWalletAddress = options.walletAddress.trim();
1090
+ // The quotes API only embeds a `buyURL`/`buyWidget` when a `redirectUrl` is
1091
+ // present, so on the widened in-app path (where MM Pay omits one) supply the
1092
+ // injected default so aggregator quotes carry a usable widget URL. An
1093
+ // explicit caller `redirectUrl` always wins, and scope `off` never injects.
1094
+ const effectiveRedirectUrl = options.redirectUrl ??
1095
+ (widenToInAppProviders ? __classPrivateFieldGet(this, _RampsController_getDefaultRedirectUrl, "f").call(this) : undefined);
1046
1096
  const cacheKey = (0, RequestCache_1.createCacheKey)('getQuotes', [
1047
1097
  normalizedRegion,
1048
1098
  normalizedFiat,
@@ -1051,7 +1101,7 @@ class RampsController extends base_controller_1.BaseController {
1051
1101
  normalizedWalletAddress,
1052
1102
  [...paymentMethodsToUse].sort().join(','),
1053
1103
  [...providersToUse].sort().join(','),
1054
- options.redirectUrl,
1104
+ effectiveRedirectUrl,
1055
1105
  action,
1056
1106
  ]);
1057
1107
  const params = {
@@ -1062,15 +1112,45 @@ class RampsController extends base_controller_1.BaseController {
1062
1112
  walletAddress: normalizedWalletAddress,
1063
1113
  paymentMethods: paymentMethodsToUse,
1064
1114
  providers: providersToUse,
1065
- redirectUrl: options.redirectUrl,
1115
+ redirectUrl: effectiveRedirectUrl,
1066
1116
  action,
1067
1117
  };
1068
- return this.executeRequest(cacheKey, async () => {
1118
+ const response = await this.executeRequest(cacheKey, async () => {
1069
1119
  return this.messenger.call('RampsService:getQuotes', params);
1070
1120
  }, {
1071
1121
  forceRefresh: options.forceRefresh,
1072
1122
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
1073
1123
  });
1124
+ if (!widenToInAppProviders) {
1125
+ return response;
1126
+ }
1127
+ // Reduce the widened multi-provider result to the single best in-app quote
1128
+ // and place it at `success[0]`, since single-pick consumers
1129
+ // (`getRampsQuote` -> `success?.[0]`) rely on index 0 while `success[]`
1130
+ // order is server-defined rather than ranked.
1131
+ const selectedQuote = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pickInAppQuote).call(this, response, {
1132
+ scope: providerScope,
1133
+ amount: options.amount,
1134
+ fiat: normalizedFiat,
1135
+ providers: inAppProviderCatalog,
1136
+ });
1137
+ if (!selectedQuote) {
1138
+ // No usable in-app quote: surface "no quote" rather than leaking an
1139
+ // external/custom quote to the single-pick consumer.
1140
+ return {
1141
+ success: [],
1142
+ sorted: response.sorted,
1143
+ error: response.error,
1144
+ customActions: response.customActions,
1145
+ };
1146
+ }
1147
+ return {
1148
+ ...response,
1149
+ success: [
1150
+ selectedQuote,
1151
+ ...response.success.filter((quote) => quote !== selectedQuote),
1152
+ ],
1153
+ };
1074
1154
  }
1075
1155
  // === ORDER MANAGEMENT ===
1076
1156
  /**
@@ -1169,7 +1249,7 @@ class RampsController extends base_controller_1.BaseController {
1169
1249
  *
1170
1250
  * @param params - Object containing order identifiers and wallet info.
1171
1251
  * @param params.orderId - Full order ID (e.g. "/providers/paypal/orders/abc123") or order code.
1172
- * @param params.providerCode - Provider code (e.g. "paypal", "transak"), with or without /providers/ prefix.
1252
+ * @param params.providerCode - Canonical provider code (e.g. "paypal", "transak").
1173
1253
  * @param params.walletAddress - Wallet address for the order.
1174
1254
  * @param params.chainId - Optional chain ID for the order.
1175
1255
  */
@@ -1179,11 +1259,10 @@ class RampsController extends base_controller_1.BaseController {
1179
1259
  if (!orderCode?.trim()) {
1180
1260
  return;
1181
1261
  }
1182
- const normalizedProviderCode = normalizeProviderCode(providerCode);
1183
1262
  const stubOrder = {
1184
1263
  providerOrderId: orderCode,
1185
1264
  provider: {
1186
- id: `/providers/${normalizedProviderCode}`,
1265
+ id: providerCode,
1187
1266
  name: '',
1188
1267
  environmentType: '',
1189
1268
  description: '',
@@ -1687,7 +1766,7 @@ class RampsController extends base_controller_1.BaseController {
1687
1766
  }
1688
1767
  }
1689
1768
  exports.RampsController = RampsController;
1690
- _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_pendingResourceGeneration = new WeakMap(), _RampsController_orderPollingMeta = new WeakMap(), _RampsController_orderPollingTimer = new WeakMap(), _RampsController_isPolling = new WeakMap(), _RampsController_initPromise = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1769
+ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_getProviderScope = new WeakMap(), _RampsController_getDefaultRedirectUrl = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_pendingResourceCount = new WeakMap(), _RampsController_pendingResourceGeneration = new WeakMap(), _RampsController_orderPollingMeta = new WeakMap(), _RampsController_orderPollingTimer = new WeakMap(), _RampsController_isPolling = new WeakMap(), _RampsController_initPromise = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_clearPendingResourceCountForDependentResources = function _RampsController_clearPendingResourceCountForDependentResources() {
1691
1770
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1692
1771
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1693
1772
  const generation = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
@@ -1773,20 +1852,100 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1773
1852
  }
1774
1853
  });
1775
1854
  }, _RampsController_runInit = async function _RampsController_runInit(options) {
1776
- const forceRefresh = options?.forceRefresh === true;
1777
- const hasCountries = this.state.countries.data.length > 0;
1778
- if (forceRefresh || !hasCountries) {
1779
- await this.getCountries(options);
1780
- }
1855
+ // Force-refetch the catalog on startup so region preset amounts stay
1856
+ // current, bypassing the in-session request cache. The catalog is not
1857
+ // persisted, so a cold start always re-fetches it regardless.
1858
+ await this.getCountries({ ...options, forceRefresh: true });
1781
1859
  // Always prefer the user's persisted region. Geolocation is only used to
1782
1860
  // seed the initial value; once the user (or a prior init) has set a region
1783
1861
  // we must respect that choice — even on forceRefresh.
1784
- let regionCode = this.state.userRegion?.regionCode;
1785
- regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1862
+ const persistedRegionCode = this.state.userRegion?.regionCode;
1863
+ const regionCode = persistedRegionCode ??
1864
+ (await this.messenger.call('RampsService:getGeolocation'));
1786
1865
  if (!regionCode) {
1787
1866
  throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
1788
1867
  }
1868
+ // For an already-persisted region, getCountries() has already re-synced it
1869
+ // from the fresh catalog (see #syncUserRegionFromCountriesCatalog). Calling
1870
+ // setUserRegion here would re-validate against that catalog and, if it is
1871
+ // momentarily empty or no longer lists the region (e.g. a transient/partial
1872
+ // catalog response or a region with no current provider coverage), throw and
1873
+ // wipe the persisted region via #cleanupState. Preserve the existing region
1874
+ // instead; only resolve a brand-new region (from geolocation) strictly.
1875
+ if (persistedRegionCode) {
1876
+ return;
1877
+ }
1789
1878
  await this.setUserRegion(regionCode, options);
1879
+ }, _RampsController_syncUserRegionFromCountriesCatalog = function _RampsController_syncUserRegionFromCountriesCatalog() {
1880
+ const regionCode = this.state.userRegion?.regionCode;
1881
+ if (!regionCode) {
1882
+ return;
1883
+ }
1884
+ const countriesData = this.state.countries.data;
1885
+ if (!countriesData.length) {
1886
+ return;
1887
+ }
1888
+ const userRegion = findRegionFromCode(regionCode, countriesData);
1889
+ if (!userRegion) {
1890
+ return;
1891
+ }
1892
+ this.update((state) => {
1893
+ state.userRegion = userRegion;
1894
+ });
1895
+ }, _RampsController_pickInAppQuote = function _RampsController_pickInAppQuote(response, { scope, amount, fiat, providers, }) {
1896
+ const providerByCode = new Map(providers.map((provider) => [provider.id, provider]));
1897
+ const customActionProviderCodes = new Set(response.customActions.map((action) => action.buy.providerId));
1898
+ const fitsProviderLimits = (quote) => {
1899
+ const provider = providerByCode.get(quote.provider);
1900
+ const limit = provider?.limits?.fiat?.[fiat]?.[quote.quote.paymentMethod];
1901
+ if (!limit) {
1902
+ // No published limits for this provider/payment method: treat as
1903
+ // eligible and let the provider enforce limits at checkout.
1904
+ return true;
1905
+ }
1906
+ return amount >= limit.minAmount && amount <= limit.maxAmount;
1907
+ };
1908
+ const isEligible = (quote) => {
1909
+ // `all` (Phase 2) skips the in-app-only exclusions; both scopes still
1910
+ // enforce provider limits up front.
1911
+ if (scope !== 'all') {
1912
+ const providerCode = quote.provider;
1913
+ if (customActionProviderCodes.has(providerCode)) {
1914
+ return false;
1915
+ }
1916
+ // Defensive: the wire may carry an inline `isCustomAction` flag that is
1917
+ // absent from the published `Quote` type.
1918
+ if (quote.quote.isCustomAction === true) {
1919
+ return false;
1920
+ }
1921
+ if (quote.quote.buyWidget?.browser === 'IN_APP_OS_BROWSER') {
1922
+ return false;
1923
+ }
1924
+ }
1925
+ return fitsProviderLimits(quote);
1926
+ };
1927
+ const candidates = response.success.filter(isEligible);
1928
+ if (candidates.length === 0) {
1929
+ return undefined;
1930
+ }
1931
+ const candidateByCode = new Map(candidates.map((quote) => [quote.provider, quote]));
1932
+ const pickBySortOrder = (sortBy) => {
1933
+ const order = response.sorted.find((entry) => entry.sortBy === sortBy)?.ids;
1934
+ if (!order) {
1935
+ return undefined;
1936
+ }
1937
+ for (const providerId of order) {
1938
+ const match = candidateByCode.get(providerId);
1939
+ if (match) {
1940
+ return match;
1941
+ }
1942
+ }
1943
+ return undefined;
1944
+ };
1945
+ // Reliability first, then price, then the first surviving candidate.
1946
+ return (pickBySortOrder('reliability') ??
1947
+ pickBySortOrder('price') ??
1948
+ candidates[0]);
1790
1949
  }, _RampsController_getSupportingProvidersForRegion =
1791
1950
  /**
1792
1951
  * Returns the region's providers that support the given asset, plus the full