@metamask/ramps-controller 15.0.0 → 15.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.
@@ -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_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
15
  exports.RampsController = exports.getInternalOrderCode = exports.normalizeProviderCode = exports.getDefaultRampsControllerState = exports.RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
@@ -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,
@@ -441,8 +441,13 @@ class RampsController extends base_controller_1.BaseController {
441
441
  * controller. Missing properties will be filled in with defaults.
442
442
  * @param args.requestCacheTTL - Time to live for cached requests in milliseconds.
443
443
  * @param args.requestCacheMaxSize - Maximum number of entries in the request cache.
444
+ * @param args.getProviderScope - Optional callback returning the current
445
+ * provider-class scope for fiat quote widening. Defaults to `off`.
446
+ * @param args.getDefaultRedirectUrl - Optional callback returning the default
447
+ * redirect URL used for the widened in-app quote fetch when the caller omits
448
+ * `redirectUrl`. Defaults to a callback returning `undefined`.
444
449
  */
445
- constructor({ messenger, state = {}, requestCacheTTL = RequestCache_1.DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = RequestCache_1.DEFAULT_REQUEST_CACHE_MAX_SIZE, }) {
450
+ constructor({ messenger, state = {}, requestCacheTTL = RequestCache_1.DEFAULT_REQUEST_CACHE_TTL, requestCacheMaxSize = RequestCache_1.DEFAULT_REQUEST_CACHE_MAX_SIZE, getProviderScope, getDefaultRedirectUrl, }) {
446
451
  super({
447
452
  messenger,
448
453
  metadata: rampsControllerMetadata,
@@ -463,6 +468,17 @@ class RampsController extends base_controller_1.BaseController {
463
468
  * Maximum number of entries in the request cache.
464
469
  */
465
470
  _RampsController_requestCacheMaxSize.set(this, void 0);
471
+ /**
472
+ * Resolves the current provider-class scope for fiat quote widening. Defaults
473
+ * to `() => 'off'` (native-only) when no callback is injected.
474
+ */
475
+ _RampsController_getProviderScope.set(this, void 0);
476
+ /**
477
+ * Resolves the default redirect URL for the widened in-app quote fetch when
478
+ * the caller omits `redirectUrl`. Defaults to `() => undefined` when no
479
+ * callback is injected.
480
+ */
481
+ _RampsController_getDefaultRedirectUrl.set(this, void 0);
466
482
  /**
467
483
  * Map of pending requests for deduplication.
468
484
  * Key is the cache key, value is the pending request with abort controller.
@@ -484,6 +500,8 @@ class RampsController extends base_controller_1.BaseController {
484
500
  _RampsController_initPromise.set(this, null);
485
501
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
486
502
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
503
+ __classPrivateFieldSet(this, _RampsController_getProviderScope, getProviderScope ?? (() => 'off'), "f");
504
+ __classPrivateFieldSet(this, _RampsController_getDefaultRedirectUrl, getDefaultRedirectUrl ?? (() => undefined), "f");
487
505
  this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
488
506
  }
489
507
  /**
@@ -721,8 +739,10 @@ class RampsController extends base_controller_1.BaseController {
721
739
  * This should be called once at app startup to set up the initial region.
722
740
  *
723
741
  * 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.
742
+ * Force-refetches the countries catalog on startup (bypassing the in-session
743
+ * request cache) so region preset amounts stay current. The catalog is not
744
+ * persisted, so a cold start always re-fetches it regardless. Skips
745
+ * geolocation when userRegion already exists.
726
746
  *
727
747
  * @param options - Options for cache behavior. forceRefresh bypasses idempotency and re-runs the full flow.
728
748
  * @returns Promise that resolves when initialization is complete.
@@ -759,6 +779,7 @@ class RampsController extends base_controller_1.BaseController {
759
779
  this.update((state) => {
760
780
  state.countries.data = Array.isArray(countries) ? [...countries] : [];
761
781
  });
782
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncUserRegionFromCountriesCatalog).call(this);
762
783
  return countries;
763
784
  }
764
785
  /**
@@ -995,7 +1016,19 @@ class RampsController extends base_controller_1.BaseController {
995
1016
  if (normalizedAssetIdForValidation === '') {
996
1017
  throw new Error('assetId is required.');
997
1018
  }
1019
+ // When a non-`off` provider scope is active, widen the native-only
1020
+ // auto-selection path to every supporting provider and pick the best in-app
1021
+ // quote from the results (in-app vs external is only knowable per-quote via
1022
+ // `buyWidget.browser`). Only the auto-select/restrict path that MM Pay's
1023
+ // `getRampsQuote` uses is affected; explicit-`providers` callers and the
1024
+ // plain all-provider path are untouched.
1025
+ const providerScope = __classPrivateFieldGet(this, _RampsController_getProviderScope, "f").call(this);
1026
+ const widenToInAppProviders = providerScope !== 'off' &&
1027
+ !options.providers &&
1028
+ (options.autoSelectProvider === true ||
1029
+ options.restrictToKnownOrNativeProviders === true);
998
1030
  let providersToUse;
1031
+ let inAppProviderCatalog = this.state.providers.data;
999
1032
  if (options.providers) {
1000
1033
  providersToUse = options.restrictToKnownOrNativeProviders
1001
1034
  ? await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_filterProviderIdsBySupport).call(this, {
@@ -1005,6 +1038,17 @@ class RampsController extends base_controller_1.BaseController {
1005
1038
  })
1006
1039
  : options.providers;
1007
1040
  }
1041
+ else if (widenToInAppProviders) {
1042
+ // `#getSupportingProvidersForRegion` also hydrates the provider catalog
1043
+ // when controller state is empty, so all-provider quoting cannot silently
1044
+ // return zero providers here.
1045
+ const { supporting } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
1046
+ assetId: normalizedAssetIdForValidation,
1047
+ region: regionToUse,
1048
+ });
1049
+ inAppProviderCatalog = supporting;
1050
+ providersToUse = supporting.map((provider) => provider.id);
1051
+ }
1008
1052
  else if (options.autoSelectProvider ||
1009
1053
  options.restrictToKnownOrNativeProviders) {
1010
1054
  // The restriction flag implies resolution: it must narrow the provider
@@ -1034,8 +1078,12 @@ class RampsController extends base_controller_1.BaseController {
1034
1078
  // Under headless-buy gating, an empty resolved provider list means no
1035
1079
  // eligible (native/supporting) provider exists. Return an empty response
1036
1080
  // rather than passing `[]` to the service, which omits the provider filter
1037
- // and would quote every provider.
1038
- if (options.restrictToKnownOrNativeProviders &&
1081
+ // and would quote every provider. This also guards the widened in-app path:
1082
+ // a caller may trigger widening with `autoSelectProvider` alone (no
1083
+ // `restrictToKnownOrNativeProviders`), and an empty supporting set must not
1084
+ // fall through to unfiltered quotes from providers that do not support the
1085
+ // asset.
1086
+ if ((options.restrictToKnownOrNativeProviders || widenToInAppProviders) &&
1039
1087
  providersToUse.length === 0) {
1040
1088
  return { success: [], sorted: [], error: [], customActions: [] };
1041
1089
  }
@@ -1043,6 +1091,12 @@ class RampsController extends base_controller_1.BaseController {
1043
1091
  const normalizedFiat = fiatToUse.toLowerCase().trim();
1044
1092
  const normalizedAssetId = normalizedAssetIdForValidation;
1045
1093
  const normalizedWalletAddress = options.walletAddress.trim();
1094
+ // The quotes API only embeds a `buyURL`/`buyWidget` when a `redirectUrl` is
1095
+ // present, so on the widened in-app path (where MM Pay omits one) supply the
1096
+ // injected default so aggregator quotes carry a usable widget URL. An
1097
+ // explicit caller `redirectUrl` always wins, and scope `off` never injects.
1098
+ const effectiveRedirectUrl = options.redirectUrl ??
1099
+ (widenToInAppProviders ? __classPrivateFieldGet(this, _RampsController_getDefaultRedirectUrl, "f").call(this) : undefined);
1046
1100
  const cacheKey = (0, RequestCache_1.createCacheKey)('getQuotes', [
1047
1101
  normalizedRegion,
1048
1102
  normalizedFiat,
@@ -1051,7 +1105,7 @@ class RampsController extends base_controller_1.BaseController {
1051
1105
  normalizedWalletAddress,
1052
1106
  [...paymentMethodsToUse].sort().join(','),
1053
1107
  [...providersToUse].sort().join(','),
1054
- options.redirectUrl,
1108
+ effectiveRedirectUrl,
1055
1109
  action,
1056
1110
  ]);
1057
1111
  const params = {
@@ -1062,15 +1116,45 @@ class RampsController extends base_controller_1.BaseController {
1062
1116
  walletAddress: normalizedWalletAddress,
1063
1117
  paymentMethods: paymentMethodsToUse,
1064
1118
  providers: providersToUse,
1065
- redirectUrl: options.redirectUrl,
1119
+ redirectUrl: effectiveRedirectUrl,
1066
1120
  action,
1067
1121
  };
1068
- return this.executeRequest(cacheKey, async () => {
1122
+ const response = await this.executeRequest(cacheKey, async () => {
1069
1123
  return this.messenger.call('RampsService:getQuotes', params);
1070
1124
  }, {
1071
1125
  forceRefresh: options.forceRefresh,
1072
1126
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
1073
1127
  });
1128
+ if (!widenToInAppProviders) {
1129
+ return response;
1130
+ }
1131
+ // Reduce the widened multi-provider result to the single best in-app quote
1132
+ // and place it at `success[0]`, since single-pick consumers
1133
+ // (`getRampsQuote` -> `success?.[0]`) rely on index 0 while `success[]`
1134
+ // order is server-defined rather than ranked.
1135
+ const selectedQuote = __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_pickInAppQuote).call(this, response, {
1136
+ scope: providerScope,
1137
+ amount: options.amount,
1138
+ fiat: normalizedFiat,
1139
+ providers: inAppProviderCatalog,
1140
+ });
1141
+ if (!selectedQuote) {
1142
+ // No usable in-app quote: surface "no quote" rather than leaking an
1143
+ // external/custom quote to the single-pick consumer.
1144
+ return {
1145
+ success: [],
1146
+ sorted: response.sorted,
1147
+ error: response.error,
1148
+ customActions: response.customActions,
1149
+ };
1150
+ }
1151
+ return {
1152
+ ...response,
1153
+ success: [
1154
+ selectedQuote,
1155
+ ...response.success.filter((quote) => quote !== selectedQuote),
1156
+ ],
1157
+ };
1074
1158
  }
1075
1159
  // === ORDER MANAGEMENT ===
1076
1160
  /**
@@ -1687,7 +1771,7 @@ class RampsController extends base_controller_1.BaseController {
1687
1771
  }
1688
1772
  }
1689
1773
  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() {
1774
+ _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
1775
  for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
1692
1776
  __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
1693
1777
  const generation = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
@@ -1773,20 +1857,103 @@ _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheM
1773
1857
  }
1774
1858
  });
1775
1859
  }, _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
- }
1860
+ // Force-refetch the catalog on startup so region preset amounts stay
1861
+ // current, bypassing the in-session request cache. The catalog is not
1862
+ // persisted, so a cold start always re-fetches it regardless.
1863
+ await this.getCountries({ ...options, forceRefresh: true });
1781
1864
  // Always prefer the user's persisted region. Geolocation is only used to
1782
1865
  // seed the initial value; once the user (or a prior init) has set a region
1783
1866
  // we must respect that choice — even on forceRefresh.
1784
- let regionCode = this.state.userRegion?.regionCode;
1785
- regionCode ?? (regionCode = await this.messenger.call('RampsService:getGeolocation'));
1867
+ const persistedRegionCode = this.state.userRegion?.regionCode;
1868
+ const regionCode = persistedRegionCode ??
1869
+ (await this.messenger.call('RampsService:getGeolocation'));
1786
1870
  if (!regionCode) {
1787
1871
  throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
1788
1872
  }
1873
+ // For an already-persisted region, getCountries() has already re-synced it
1874
+ // from the fresh catalog (see #syncUserRegionFromCountriesCatalog). Calling
1875
+ // setUserRegion here would re-validate against that catalog and, if it is
1876
+ // momentarily empty or no longer lists the region (e.g. a transient/partial
1877
+ // catalog response or a region with no current provider coverage), throw and
1878
+ // wipe the persisted region via #cleanupState. Preserve the existing region
1879
+ // instead; only resolve a brand-new region (from geolocation) strictly.
1880
+ if (persistedRegionCode) {
1881
+ return;
1882
+ }
1789
1883
  await this.setUserRegion(regionCode, options);
1884
+ }, _RampsController_syncUserRegionFromCountriesCatalog = function _RampsController_syncUserRegionFromCountriesCatalog() {
1885
+ const regionCode = this.state.userRegion?.regionCode;
1886
+ if (!regionCode) {
1887
+ return;
1888
+ }
1889
+ const countriesData = this.state.countries.data;
1890
+ if (!countriesData.length) {
1891
+ return;
1892
+ }
1893
+ const userRegion = findRegionFromCode(regionCode, countriesData);
1894
+ if (!userRegion) {
1895
+ return;
1896
+ }
1897
+ this.update((state) => {
1898
+ state.userRegion = userRegion;
1899
+ });
1900
+ }, _RampsController_pickInAppQuote = function _RampsController_pickInAppQuote(response, { scope, amount, fiat, providers, }) {
1901
+ const providerByCode = new Map(providers.map((provider) => [
1902
+ normalizeProviderCode(provider.id),
1903
+ provider,
1904
+ ]));
1905
+ const customActionProviderCodes = new Set(response.customActions.map((action) => normalizeProviderCode(action.buy.providerId)));
1906
+ const fitsProviderLimits = (quote) => {
1907
+ const provider = providerByCode.get(normalizeProviderCode(quote.provider));
1908
+ const limit = provider?.limits?.fiat?.[fiat]?.[quote.quote.paymentMethod];
1909
+ if (!limit) {
1910
+ // No published limits for this provider/payment method: treat as
1911
+ // eligible and let the provider enforce limits at checkout.
1912
+ return true;
1913
+ }
1914
+ return amount >= limit.minAmount && amount <= limit.maxAmount;
1915
+ };
1916
+ const isEligible = (quote) => {
1917
+ // `all` (Phase 2) skips the in-app-only exclusions; both scopes still
1918
+ // enforce provider limits up front.
1919
+ if (scope !== 'all') {
1920
+ const providerCode = normalizeProviderCode(quote.provider);
1921
+ if (customActionProviderCodes.has(providerCode)) {
1922
+ return false;
1923
+ }
1924
+ // Defensive: the wire may carry an inline `isCustomAction` flag that is
1925
+ // absent from the published `Quote` type.
1926
+ if (quote.quote.isCustomAction === true) {
1927
+ return false;
1928
+ }
1929
+ if (quote.quote.buyWidget?.browser === 'IN_APP_OS_BROWSER') {
1930
+ return false;
1931
+ }
1932
+ }
1933
+ return fitsProviderLimits(quote);
1934
+ };
1935
+ const candidates = response.success.filter(isEligible);
1936
+ if (candidates.length === 0) {
1937
+ return undefined;
1938
+ }
1939
+ const candidateByCode = new Map(candidates.map((quote) => [normalizeProviderCode(quote.provider), quote]));
1940
+ const pickBySortOrder = (sortBy) => {
1941
+ const order = response.sorted.find((entry) => entry.sortBy === sortBy)?.ids;
1942
+ if (!order) {
1943
+ return undefined;
1944
+ }
1945
+ for (const providerId of order) {
1946
+ const match = candidateByCode.get(normalizeProviderCode(providerId));
1947
+ if (match) {
1948
+ return match;
1949
+ }
1950
+ }
1951
+ return undefined;
1952
+ };
1953
+ // Reliability first, then price, then the first surviving candidate.
1954
+ return (pickBySortOrder('reliability') ??
1955
+ pickBySortOrder('price') ??
1956
+ candidates[0]);
1790
1957
  }, _RampsController_getSupportingProvidersForRegion =
1791
1958
  /**
1792
1959
  * Returns the region's providers that support the given asset, plus the full