@metamask/ramps-controller 5.1.0 → 6.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.
@@ -1,15 +1,15 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
1
6
  var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
7
  if (kind === "m") throw new TypeError("Private method is not writable");
3
8
  if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
9
  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");
5
10
  return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
11
  };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_updateRequestState;
12
+ var _RampsController_instances, _RampsController_requestCacheTTL, _RampsController_requestCacheMaxSize, _RampsController_pendingRequests, _RampsController_pendingResourceCount, _RampsController_clearPendingResourceCountForDependentResources, _RampsController_removeRequestState, _RampsController_cleanupState, _RampsController_fireAndForget, _RampsController_updateResourceField, _RampsController_setResourceLoading, _RampsController_setResourceError, _RampsController_updateRequestState;
13
13
  import { BaseController } from "@metamask/base-controller";
14
14
  import { DEFAULT_REQUEST_CACHE_TTL, DEFAULT_REQUEST_CACHE_MAX_SIZE, createCacheKey, isCacheExpired, createLoadingState, createSuccessState, createErrorState, RequestStatus } from "./RequestCache.mjs";
15
15
  // === GENERAL ===
@@ -19,6 +19,19 @@ import { DEFAULT_REQUEST_CACHE_TTL, DEFAULT_REQUEST_CACHE_MAX_SIZE, createCacheK
19
19
  * when composed with other controllers.
20
20
  */
21
21
  export const controllerName = 'RampsController';
22
+ /**
23
+ * RampsService action types that RampsController calls via the messenger.
24
+ * Any host (e.g. mobile) that creates a RampsController messenger must delegate
25
+ * these actions from the root messenger so the controller can function.
26
+ */
27
+ export const RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = [
28
+ 'RampsService:getGeolocation',
29
+ 'RampsService:getCountries',
30
+ 'RampsService:getTokens',
31
+ 'RampsService:getProviders',
32
+ 'RampsService:getPaymentMethods',
33
+ 'RampsService:getQuotes',
34
+ ];
22
35
  /**
23
36
  * Default TTL for quotes requests (15 seconds).
24
37
  * Quotes are time-sensitive and should have a shorter cache duration.
@@ -34,12 +47,6 @@ const rampsControllerMetadata = {
34
47
  includeInStateLogs: true,
35
48
  usedInUi: true,
36
49
  },
37
- selectedProvider: {
38
- persist: false,
39
- includeInDebugSnapshot: true,
40
- includeInStateLogs: true,
41
- usedInUi: true,
42
- },
43
50
  countries: {
44
51
  persist: true,
45
52
  includeInDebugSnapshot: true,
@@ -58,24 +65,12 @@ const rampsControllerMetadata = {
58
65
  includeInStateLogs: true,
59
66
  usedInUi: true,
60
67
  },
61
- selectedToken: {
62
- persist: false,
63
- includeInDebugSnapshot: true,
64
- includeInStateLogs: true,
65
- usedInUi: true,
66
- },
67
68
  paymentMethods: {
68
69
  persist: false,
69
70
  includeInDebugSnapshot: true,
70
71
  includeInStateLogs: true,
71
72
  usedInUi: true,
72
73
  },
73
- selectedPaymentMethod: {
74
- persist: false,
75
- includeInDebugSnapshot: true,
76
- includeInStateLogs: true,
77
- usedInUi: true,
78
- },
79
74
  quotes: {
80
75
  persist: false,
81
76
  includeInDebugSnapshot: true,
@@ -89,6 +84,23 @@ const rampsControllerMetadata = {
89
84
  usedInUi: true,
90
85
  },
91
86
  };
87
+ /**
88
+ * Creates a default resource state object.
89
+ *
90
+ * @template TData - The type of the resource data.
91
+ * @template TSelected - The type of the selected item.
92
+ * @param data - The initial data value.
93
+ * @param selected - The initial selected value.
94
+ * @returns A ResourceState object with default loading and error values.
95
+ */
96
+ function createDefaultResourceState(data, selected = null) {
97
+ return {
98
+ data,
99
+ selected,
100
+ isLoading: false,
101
+ error: null,
102
+ };
103
+ }
92
104
  /**
93
105
  * Constructs the default {@link RampsController} state. This allows
94
106
  * consumers to provide a partial state object when initializing the controller
@@ -100,17 +112,42 @@ const rampsControllerMetadata = {
100
112
  export function getDefaultRampsControllerState() {
101
113
  return {
102
114
  userRegion: null,
103
- selectedProvider: null,
104
- countries: [],
105
- providers: [],
106
- tokens: null,
107
- selectedToken: null,
108
- paymentMethods: [],
109
- selectedPaymentMethod: null,
110
- quotes: null,
115
+ countries: createDefaultResourceState([]),
116
+ providers: createDefaultResourceState([], null),
117
+ tokens: createDefaultResourceState(null, null),
118
+ paymentMethods: createDefaultResourceState([], null),
119
+ quotes: createDefaultResourceState(null),
111
120
  requests: {},
112
121
  };
113
122
  }
123
+ /**
124
+ * Resets region-dependent resources (userRegion, providers, tokens, paymentMethods, quotes).
125
+ * Mutates state in place; use from within controller update() for atomic updates.
126
+ *
127
+ * @param state - The state object to mutate.
128
+ * @param options - Options for the reset.
129
+ * @param options.clearUserRegionData - When true, sets userRegion to null (e.g. for full cleanup).
130
+ */
131
+ function resetDependentResources(state, options) {
132
+ if (options?.clearUserRegionData) {
133
+ state.userRegion = null;
134
+ }
135
+ state.providers.selected = null;
136
+ state.providers.data = [];
137
+ state.providers.isLoading = false;
138
+ state.providers.error = null;
139
+ state.tokens.selected = null;
140
+ state.tokens.data = null;
141
+ state.tokens.isLoading = false;
142
+ state.tokens.error = null;
143
+ state.paymentMethods.data = [];
144
+ state.paymentMethods.selected = null;
145
+ state.paymentMethods.isLoading = false;
146
+ state.paymentMethods.error = null;
147
+ state.quotes.data = null;
148
+ state.quotes.isLoading = false;
149
+ state.quotes.error = null;
150
+ }
114
151
  // === HELPER FUNCTIONS ===
115
152
  /**
116
153
  * Finds a country and state from a region code string.
@@ -169,6 +206,15 @@ function findRegionFromCode(regionCode, countries) {
169
206
  * Manages cryptocurrency on/off ramps functionality.
170
207
  */
171
208
  export class RampsController extends BaseController {
209
+ /**
210
+ * Clears the pending resource count map. Used only in tests to exercise the
211
+ * defensive path when get() returns undefined in the finally block.
212
+ *
213
+ * @internal
214
+ */
215
+ clearPendingResourceCountForTest() {
216
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").clear();
217
+ }
172
218
  /**
173
219
  * Constructs a new {@link RampsController}.
174
220
  *
@@ -205,6 +251,11 @@ export class RampsController extends BaseController {
205
251
  * Key is the cache key, value is the pending request with abort controller.
206
252
  */
207
253
  _RampsController_pendingRequests.set(this, new Map());
254
+ /**
255
+ * Count of in-flight requests per resource type.
256
+ * Used so isLoading is only cleared when the last request for that resource finishes.
257
+ */
258
+ _RampsController_pendingResourceCount.set(this, new Map());
208
259
  __classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
209
260
  __classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
210
261
  }
@@ -236,8 +287,18 @@ export class RampsController extends BaseController {
236
287
  // Create abort controller for this request
237
288
  const abortController = new AbortController();
238
289
  const lastFetchedAt = Date.now();
290
+ const { resourceType } = options ?? {};
239
291
  // Update state to loading
240
292
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createLoadingState());
293
+ // Set resource-level loading state (only on cache miss). Ref-count so concurrent
294
+ // requests for the same resource type (different cache keys) keep isLoading true.
295
+ if (resourceType) {
296
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
297
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, count + 1);
298
+ if (count === 0) {
299
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, true);
300
+ }
301
+ }
241
302
  // Create the fetch promise
242
303
  const promise = (async () => {
243
304
  try {
@@ -247,6 +308,15 @@ export class RampsController extends BaseController {
247
308
  throw new Error('Request was aborted');
248
309
  }
249
310
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createSuccessState(data, lastFetchedAt));
311
+ if (resourceType) {
312
+ // We need the extra logic because there are two situations where we’re allowed to clear the error:
313
+ // No callback → always clear
314
+ // Callback present → clear only when isResultCurrent() returns true.
315
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
316
+ if (isCurrent) {
317
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, null);
318
+ }
319
+ }
250
320
  return data;
251
321
  }
252
322
  catch (error) {
@@ -254,8 +324,14 @@ export class RampsController extends BaseController {
254
324
  if (abortController.signal.aborted) {
255
325
  throw error;
256
326
  }
257
- const errorMessage = error?.message;
258
- __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt));
327
+ const errorMessage = error?.message ?? 'Unknown error';
328
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateRequestState).call(this, cacheKey, createErrorState(errorMessage, lastFetchedAt));
329
+ if (resourceType) {
330
+ const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
331
+ if (isCurrent) {
332
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceError).call(this, resourceType, errorMessage);
333
+ }
334
+ }
259
335
  throw error;
260
336
  }
261
337
  finally {
@@ -264,6 +340,18 @@ export class RampsController extends BaseController {
264
340
  if (currentPending?.abortController === abortController) {
265
341
  __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
266
342
  }
343
+ // Clear resource-level loading state only when no requests for this resource remain
344
+ if (resourceType) {
345
+ const count = __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").get(resourceType) ?? 0;
346
+ const next = Math.max(0, count - 1);
347
+ if (next === 0) {
348
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
349
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_setResourceLoading).call(this, resourceType, false);
350
+ }
351
+ else {
352
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").set(resourceType, next);
353
+ }
354
+ }
267
355
  }
268
356
  })();
269
357
  // Store pending request for deduplication
@@ -306,37 +394,40 @@ export class RampsController extends BaseController {
306
394
  async setUserRegion(region, options) {
307
395
  const normalizedRegion = region.toLowerCase().trim();
308
396
  try {
309
- const { countries } = this.state;
310
- if (!countries || countries.length === 0) {
397
+ const countriesData = this.state.countries.data;
398
+ if (!countriesData || countriesData.length === 0) {
311
399
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
312
400
  throw new Error('No countries found. Cannot set user region without valid country information.');
313
401
  }
314
- const userRegion = findRegionFromCode(normalizedRegion, countries);
402
+ const userRegion = findRegionFromCode(normalizedRegion, countriesData);
315
403
  if (!userRegion) {
316
404
  __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_cleanupState).call(this);
317
405
  throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
318
406
  }
319
- // Only cleanup state if region is actually changing
320
407
  const regionChanged = normalizedRegion !== this.state.userRegion?.regionCode;
321
- // Set the new region atomically with cleanup to avoid intermediate null state
408
+ const needsRefetch = regionChanged ||
409
+ !this.state.tokens.data ||
410
+ this.state.providers.data.length === 0;
411
+ if (regionChanged) {
412
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
413
+ }
322
414
  this.update((state) => {
323
415
  if (regionChanged) {
324
- state.selectedProvider = null;
325
- state.selectedToken = null;
326
- state.tokens = null;
327
- state.providers = [];
328
- state.paymentMethods = [];
329
- state.selectedPaymentMethod = null;
330
- state.quotes = null;
416
+ resetDependentResources(state);
331
417
  }
332
418
  state.userRegion = userRegion;
333
419
  });
334
- // Only trigger fetches if region changed or if data is missing
335
- if (regionChanged || !this.state.tokens) {
336
- this.triggerGetTokens(userRegion.regionCode, 'buy', options);
337
- }
338
- if (regionChanged || this.state.providers.length === 0) {
339
- this.triggerGetProviders(userRegion.regionCode, options);
420
+ if (needsRefetch) {
421
+ const refetchPromises = [];
422
+ if (regionChanged || !this.state.tokens.data) {
423
+ refetchPromises.push(this.getTokens(userRegion.regionCode, 'buy', options));
424
+ }
425
+ if (regionChanged || this.state.providers.data.length === 0) {
426
+ refetchPromises.push(this.getProviders(userRegion.regionCode, options));
427
+ }
428
+ if (refetchPromises.length > 0) {
429
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, Promise.all(refetchPromises));
430
+ }
340
431
  }
341
432
  return userRegion;
342
433
  }
@@ -356,9 +447,9 @@ export class RampsController extends BaseController {
356
447
  setSelectedProvider(providerId) {
357
448
  if (providerId === null) {
358
449
  this.update((state) => {
359
- state.selectedProvider = null;
360
- state.paymentMethods = [];
361
- state.selectedPaymentMethod = null;
450
+ state.providers.selected = null;
451
+ state.paymentMethods.data = [];
452
+ state.paymentMethods.selected = null;
362
453
  });
363
454
  return;
364
455
  }
@@ -366,7 +457,7 @@ export class RampsController extends BaseController {
366
457
  if (!regionCode) {
367
458
  throw new Error('Region is required. Cannot set selected provider without valid region information.');
368
459
  }
369
- const { providers } = this.state;
460
+ const providers = this.state.providers.data;
370
461
  if (!providers || providers.length === 0) {
371
462
  throw new Error('Providers not loaded. Cannot set selected provider before providers are fetched.');
372
463
  }
@@ -375,16 +466,11 @@ export class RampsController extends BaseController {
375
466
  throw new Error(`Provider with ID "${providerId}" not found in available providers.`);
376
467
  }
377
468
  this.update((state) => {
378
- state.selectedProvider = provider;
379
- state.paymentMethods = [];
380
- state.selectedPaymentMethod = null;
381
- });
382
- // fetch payment methods for the new provider
383
- // this is needed because you can change providers without changing the token
384
- // (getPaymentMethods will use state as its default)
385
- this.triggerGetPaymentMethods(regionCode, {
386
- provider: provider.id,
469
+ state.providers.selected = provider;
470
+ state.paymentMethods.data = [];
471
+ state.paymentMethods.selected = null;
387
472
  });
473
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { provider: provider.id }));
388
474
  }
389
475
  /**
390
476
  * Initializes the controller by fetching the user's region from geolocation.
@@ -410,8 +496,8 @@ export class RampsController extends BaseController {
410
496
  if (!regionCode) {
411
497
  throw new Error('Region code is required. Cannot hydrate state without valid region information.');
412
498
  }
413
- this.triggerGetTokens(regionCode, 'buy', options);
414
- this.triggerGetProviders(regionCode, options);
499
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getTokens(regionCode, 'buy', options));
500
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getProviders(regionCode, options));
415
501
  }
416
502
  /**
417
503
  * Fetches the list of supported countries.
@@ -425,9 +511,9 @@ export class RampsController extends BaseController {
425
511
  const cacheKey = createCacheKey('getCountries', []);
426
512
  const countries = await this.executeRequest(cacheKey, async () => {
427
513
  return this.messenger.call('RampsService:getCountries');
428
- }, options);
514
+ }, { ...options, resourceType: 'countries' });
429
515
  this.update((state) => {
430
- state.countries = countries;
516
+ state.countries.data = countries;
431
517
  });
432
518
  return countries;
433
519
  }
@@ -456,11 +542,16 @@ export class RampsController extends BaseController {
456
542
  return this.messenger.call('RampsService:getTokens', normalizedRegion, action, {
457
543
  provider: options?.provider,
458
544
  });
459
- }, options);
545
+ }, {
546
+ ...options,
547
+ resourceType: 'tokens',
548
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
549
+ this.state.userRegion?.regionCode === normalizedRegion,
550
+ });
460
551
  this.update((state) => {
461
552
  const userRegionCode = state.userRegion?.regionCode;
462
553
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
463
- state.tokens = tokens;
554
+ state.tokens.data = tokens;
464
555
  }
465
556
  });
466
557
  return tokens;
@@ -476,9 +567,9 @@ export class RampsController extends BaseController {
476
567
  setSelectedToken(assetId) {
477
568
  if (!assetId) {
478
569
  this.update((state) => {
479
- state.selectedToken = null;
480
- state.paymentMethods = [];
481
- state.selectedPaymentMethod = null;
570
+ state.tokens.selected = null;
571
+ state.paymentMethods.data = [];
572
+ state.paymentMethods.selected = null;
482
573
  });
483
574
  return;
484
575
  }
@@ -486,7 +577,7 @@ export class RampsController extends BaseController {
486
577
  if (!regionCode) {
487
578
  throw new Error('Region is required. Cannot set selected token without valid region information.');
488
579
  }
489
- const { tokens } = this.state;
580
+ const tokens = this.state.tokens.data;
490
581
  if (!tokens) {
491
582
  throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
492
583
  }
@@ -496,13 +587,11 @@ export class RampsController extends BaseController {
496
587
  throw new Error(`Token with asset ID "${assetId}" not found in available tokens.`);
497
588
  }
498
589
  this.update((state) => {
499
- state.selectedToken = token;
500
- state.paymentMethods = [];
501
- state.selectedPaymentMethod = null;
502
- });
503
- this.triggerGetPaymentMethods(regionCode, {
504
- assetId: token.assetId,
590
+ state.tokens.selected = token;
591
+ state.paymentMethods.data = [];
592
+ state.paymentMethods.selected = null;
505
593
  });
594
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_fireAndForget).call(this, this.getPaymentMethods(regionCode, { assetId: token.assetId }));
506
595
  }
507
596
  /**
508
597
  * Fetches the list of providers for a given region.
@@ -536,11 +625,16 @@ export class RampsController extends BaseController {
536
625
  fiat: options?.fiat,
537
626
  payments: options?.payments,
538
627
  });
539
- }, options);
628
+ }, {
629
+ ...options,
630
+ resourceType: 'providers',
631
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
632
+ this.state.userRegion?.regionCode === normalizedRegion,
633
+ });
540
634
  this.update((state) => {
541
635
  const userRegionCode = state.userRegion?.regionCode;
542
636
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
543
- state.providers = providers;
637
+ state.providers.data = providers;
544
638
  }
545
639
  });
546
640
  return { providers };
@@ -559,8 +653,8 @@ export class RampsController extends BaseController {
559
653
  async getPaymentMethods(region, options) {
560
654
  const regionCode = region ?? this.state.userRegion?.regionCode ?? null;
561
655
  const fiatToUse = options?.fiat ?? this.state.userRegion?.country?.currency ?? null;
562
- const assetIdToUse = options?.assetId ?? this.state.selectedToken?.assetId ?? '';
563
- const providerToUse = options?.provider ?? this.state.selectedProvider?.id ?? '';
656
+ const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
657
+ const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
564
658
  if (!regionCode) {
565
659
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
566
660
  }
@@ -582,21 +676,31 @@ export class RampsController extends BaseController {
582
676
  assetId: assetIdToUse,
583
677
  provider: providerToUse,
584
678
  });
585
- }, options);
679
+ }, {
680
+ ...options,
681
+ resourceType: 'paymentMethods',
682
+ isResultCurrent: () => {
683
+ const regionMatch = this.state.userRegion?.regionCode === undefined ||
684
+ this.state.userRegion?.regionCode === normalizedRegion;
685
+ const tokenMatch = (this.state.tokens.selected?.assetId ?? '') === assetIdToUse;
686
+ const providerMatch = (this.state.providers.selected?.id ?? '') === providerToUse;
687
+ return regionMatch && tokenMatch && providerMatch;
688
+ },
689
+ });
586
690
  this.update((state) => {
587
- const currentAssetId = state.selectedToken?.assetId ?? '';
588
- const currentProviderId = state.selectedProvider?.id ?? '';
691
+ const currentAssetId = state.tokens.selected?.assetId ?? '';
692
+ const currentProviderId = state.providers.selected?.id ?? '';
589
693
  const tokenSelectionUnchanged = assetIdToUse === currentAssetId;
590
694
  const providerSelectionUnchanged = providerToUse === currentProviderId;
591
695
  // this is a race condition check to ensure that the selected token and provider in state are the same as the tokens we're requesting for
592
696
  // ex: if the user rapidly changes the token or provider, the in-flight payment methods might not be valid
593
697
  // so this check will ensure that the payment methods are still valid for the token and provider that were requested
594
698
  if (tokenSelectionUnchanged && providerSelectionUnchanged) {
595
- state.paymentMethods = response.payments;
699
+ state.paymentMethods.data = response.payments;
596
700
  // this will auto-select the first payment method if the selected payment method is not in the new payment methods
597
- const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.selectedPaymentMethod?.id);
701
+ const currentSelectionStillValid = response.payments.some((pm) => pm.id === state.paymentMethods.selected?.id);
598
702
  if (!currentSelectionStillValid) {
599
- state.selectedPaymentMethod = response.payments[0] ?? null;
703
+ state.paymentMethods.selected = response.payments[0] ?? null;
600
704
  }
601
705
  }
602
706
  });
@@ -612,11 +716,11 @@ export class RampsController extends BaseController {
612
716
  setSelectedPaymentMethod(paymentMethodId) {
613
717
  if (!paymentMethodId) {
614
718
  this.update((state) => {
615
- state.selectedPaymentMethod = null;
719
+ state.paymentMethods.selected = null;
616
720
  });
617
721
  return;
618
722
  }
619
- const { paymentMethods } = this.state;
723
+ const paymentMethods = this.state.paymentMethods.data;
620
724
  if (!paymentMethods || paymentMethods.length === 0) {
621
725
  throw new Error('Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.');
622
726
  }
@@ -625,7 +729,7 @@ export class RampsController extends BaseController {
625
729
  throw new Error(`Payment method with ID "${paymentMethodId}" not found in available payment methods.`);
626
730
  }
627
731
  this.update((state) => {
628
- state.selectedPaymentMethod = paymentMethod;
732
+ state.paymentMethods.selected = paymentMethod;
629
733
  });
630
734
  }
631
735
  /**
@@ -650,7 +754,7 @@ export class RampsController extends BaseController {
650
754
  const regionToUse = options.region ?? this.state.userRegion?.regionCode;
651
755
  const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
652
756
  const paymentMethodsToUse = options.paymentMethods ??
653
- this.state.paymentMethods.map((pm) => pm.id);
757
+ this.state.paymentMethods.data.map((pm) => pm.id);
654
758
  const action = options.action ?? 'buy';
655
759
  if (!regionToUse) {
656
760
  throw new Error('Region is required. Either provide a region parameter or ensure userRegion is set in controller state.');
@@ -701,11 +805,14 @@ export class RampsController extends BaseController {
701
805
  }, {
702
806
  forceRefresh: options.forceRefresh,
703
807
  ttl: options.ttl ?? DEFAULT_QUOTES_TTL,
808
+ resourceType: 'quotes',
809
+ isResultCurrent: () => this.state.userRegion?.regionCode === undefined ||
810
+ this.state.userRegion?.regionCode === normalizedRegion,
704
811
  });
705
812
  this.update((state) => {
706
813
  const userRegionCode = state.userRegion?.regionCode;
707
814
  if (userRegionCode === undefined || userRegionCode === normalizedRegion) {
708
- state.quotes = response;
815
+ state.quotes.data = response;
709
816
  }
710
817
  });
711
818
  return response;
@@ -720,107 +827,40 @@ export class RampsController extends BaseController {
720
827
  getWidgetUrl(quote) {
721
828
  return quote.quote?.widgetUrl ?? null;
722
829
  }
723
- // ============================================================
724
- // Sync Trigger Methods
725
- // These fire-and-forget methods are for use in React effects.
726
- // Errors are stored in state and available via selectors.
727
- // ============================================================
728
- /**
729
- * Triggers setting the user region without throwing.
730
- *
731
- * @param region - The region code to set (e.g., "US-CA").
732
- * @param options - Options for cache behavior.
733
- */
734
- triggerSetUserRegion(region, options) {
735
- this.setUserRegion(region, options).catch(() => {
736
- // Error stored in state
737
- });
738
- }
739
- /**
740
- * Triggers fetching countries without throwing.
741
- *
742
- * @param options - Options for cache behavior.
743
- */
744
- triggerGetCountries(options) {
745
- this.getCountries(options).catch(() => {
746
- // Error stored in state
747
- });
748
- }
749
- /**
750
- * Triggers fetching tokens without throwing.
751
- *
752
- * @param region - The region code. If not provided, uses userRegion from state.
753
- * @param action - The ramp action type ('buy' or 'sell').
754
- * @param options - Options for cache behavior.
755
- */
756
- triggerGetTokens(region, action = 'buy', options) {
757
- this.getTokens(region, action, options).catch(() => {
758
- // Error stored in state
759
- });
760
- }
761
- /**
762
- * Triggers fetching providers without throwing.
763
- *
764
- * @param region - The region code. If not provided, uses userRegion from state.
765
- * @param options - Options for cache behavior and query filters.
766
- */
767
- triggerGetProviders(region, options) {
768
- this.getProviders(region, options).catch(() => {
769
- // Error stored in state
770
- });
771
- }
772
- /**
773
- * Triggers fetching payment methods without throwing.
774
- *
775
- * @param region - User's region code (e.g., "us", "fr", "us-ny").
776
- * @param options - Query parameters for filtering payment methods.
777
- * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency.
778
- * @param options.assetId - CAIP-19 cryptocurrency identifier.
779
- * @param options.provider - Provider ID path.
780
- */
781
- triggerGetPaymentMethods(region, options) {
782
- this.getPaymentMethods(region, options).catch(() => {
783
- // Error stored in state
784
- });
785
- }
786
- /**
787
- * Triggers fetching quotes without throwing.
788
- *
789
- * @param options - The parameters for fetching quotes.
790
- * @param options.region - User's region code. If not provided, uses userRegion from state.
791
- * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency.
792
- * @param options.assetId - CAIP-19 cryptocurrency identifier.
793
- * @param options.amount - The amount (in fiat for buy, crypto for sell).
794
- * @param options.walletAddress - The destination wallet address.
795
- * @param options.paymentMethods - Array of payment method IDs. If not provided, uses paymentMethods from state.
796
- * @param options.provider - Optional provider ID to filter quotes.
797
- * @param options.redirectUrl - Optional redirect URL after order completion.
798
- * @param options.action - The ramp action type. Defaults to 'buy'.
799
- * @param options.forceRefresh - Whether to bypass cache.
800
- * @param options.ttl - Custom TTL for this request.
801
- */
802
- triggerGetQuotes(options) {
803
- this.getQuotes(options).catch(() => {
804
- // Error stored in state
805
- });
806
- }
807
830
  }
808
- _RampsController_requestCacheTTL = new WeakMap(), _RampsController_requestCacheMaxSize = new WeakMap(), _RampsController_pendingRequests = new WeakMap(), _RampsController_instances = new WeakSet(), _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
831
+ _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() {
832
+ const types = [
833
+ 'providers',
834
+ 'tokens',
835
+ 'paymentMethods',
836
+ 'quotes',
837
+ ];
838
+ for (const resourceType of types) {
839
+ __classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
840
+ }
841
+ }, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
809
842
  this.update((state) => {
810
843
  const requests = state.requests;
811
844
  delete requests[cacheKey];
812
845
  });
813
846
  }, _RampsController_cleanupState = function _RampsController_cleanupState() {
847
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
848
+ this.update((state) => resetDependentResources(state, {
849
+ clearUserRegionData: true,
850
+ }));
851
+ }, _RampsController_fireAndForget = function _RampsController_fireAndForget(promise) {
852
+ promise.catch((_error) => undefined);
853
+ }, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
814
854
  this.update((state) => {
815
- state.userRegion = null;
816
- state.selectedProvider = null;
817
- state.selectedToken = null;
818
- state.tokens = null;
819
- state.providers = [];
820
- state.paymentMethods = [];
821
- state.selectedPaymentMethod = null;
822
- state.quotes = null;
855
+ const resource = state[resourceType];
856
+ if (resource) {
857
+ resource[field] = value;
858
+ }
823
859
  });
860
+ }, _RampsController_setResourceLoading = function _RampsController_setResourceLoading(resourceType, loading) {
861
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'isLoading', loading);
862
+ }, _RampsController_setResourceError = function _RampsController_setResourceError(resourceType, error) {
863
+ __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'error', error);
824
864
  }, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
825
865
  const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
826
866
  const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");