@metamask/ramps-controller 20.2.0 → 21.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.
- package/CHANGELOG.md +30 -1
- package/dist/NeoBankService-method-action-types.d.ts +126 -0
- package/dist/NeoBankService-method-action-types.d.ts.map +1 -0
- package/dist/{TransakService-method-action-types.mjs → NeoBankService-method-action-types.js} +1 -1
- package/dist/NeoBankService-method-action-types.js.map +1 -0
- package/dist/NeoBankService.d.ts +191 -0
- package/dist/NeoBankService.d.ts.map +1 -0
- package/dist/NeoBankService.js +380 -0
- package/dist/NeoBankService.js.map +1 -0
- package/dist/{RampsController-method-action-types.d.mts → RampsController-method-action-types.d.ts} +93 -3
- package/dist/RampsController-method-action-types.d.ts.map +1 -0
- package/dist/{RampsController-method-action-types.mjs → RampsController-method-action-types.js} +1 -1
- package/dist/RampsController-method-action-types.js.map +1 -0
- package/dist/{RampsController.d.cts → RampsController.d.ts} +164 -17
- package/dist/RampsController.d.ts.map +1 -0
- package/dist/{RampsController.mjs → RampsController.js} +1064 -582
- package/dist/RampsController.js.map +1 -0
- package/dist/{RampsService-method-action-types.d.cts → RampsService-method-action-types.d.ts} +2 -2
- package/dist/RampsService-method-action-types.d.ts.map +1 -0
- package/dist/{RampsService-method-action-types.mjs → RampsService-method-action-types.js} +1 -1
- package/dist/RampsService-method-action-types.js.map +1 -0
- package/dist/{RampsService.d.mts → RampsService.d.ts} +5 -5
- package/dist/RampsService.d.ts.map +1 -0
- package/dist/{RampsService.mjs → RampsService.js} +159 -147
- package/dist/RampsService.js.map +1 -0
- package/dist/{RequestCache.d.cts → RequestCache.d.ts} +3 -3
- package/dist/RequestCache.d.ts.map +1 -0
- package/dist/{RequestCache.mjs → RequestCache.js} +1 -1
- package/dist/RequestCache.js.map +1 -0
- package/dist/{TransakService-method-action-types.d.cts → TransakService-method-action-types.d.ts} +2 -2
- package/dist/TransakService-method-action-types.d.ts.map +1 -0
- package/dist/TransakService-method-action-types.js +6 -0
- package/dist/TransakService-method-action-types.js.map +1 -0
- package/dist/{TransakService.d.mts → TransakService.d.ts} +6 -6
- package/dist/TransakService.d.ts.map +1 -0
- package/dist/TransakService.js +746 -0
- package/dist/TransakService.js.map +1 -0
- package/dist/autoramp-types.d.ts +22 -0
- package/dist/autoramp-types.d.ts.map +1 -0
- package/dist/autoramp-types.js +2 -0
- package/dist/autoramp-types.js.map +1 -0
- package/dist/autorampAccount.d.ts +126 -0
- package/dist/autorampAccount.d.ts.map +1 -0
- package/dist/autorampAccount.js +147 -0
- package/dist/autorampAccount.js.map +1 -0
- package/dist/{client-identity.d.cts → client-identity.d.ts} +1 -1
- package/dist/client-identity.d.ts.map +1 -0
- package/dist/{client-identity.mjs → client-identity.js} +1 -1
- package/dist/client-identity.js.map +1 -0
- package/dist/{errorNormalization.d.mts → errorNormalization.d.ts} +1 -1
- package/dist/errorNormalization.d.ts.map +1 -0
- package/dist/{errorNormalization.mjs → errorNormalization.js} +1 -1
- package/dist/errorNormalization.js.map +1 -0
- package/dist/{featureFlags.d.mts → featureFlags.d.ts} +2 -2
- package/dist/featureFlags.d.ts.map +1 -0
- package/dist/{featureFlags.mjs → featureFlags.js} +1 -1
- package/dist/featureFlags.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/{orderStatus.d.mts → orderStatus.d.ts} +2 -2
- package/dist/orderStatus.d.ts.map +1 -0
- package/dist/{orderStatus.mjs → orderStatus.js} +2 -2
- package/dist/orderStatus.js.map +1 -0
- package/dist/ownership-message.d.ts +22 -0
- package/dist/ownership-message.d.ts.map +1 -0
- package/dist/ownership-message.js +22 -0
- package/dist/ownership-message.js.map +1 -0
- package/dist/{paymentMethodMerge.d.cts → paymentMethodMerge.d.ts} +2 -2
- package/dist/paymentMethodMerge.d.ts.map +1 -0
- package/dist/{paymentMethodMerge.mjs → paymentMethodMerge.js} +1 -1
- package/dist/paymentMethodMerge.js.map +1 -0
- package/dist/{providerAvailability.d.mts → providerAvailability.d.ts} +2 -2
- package/dist/providerAvailability.d.ts.map +1 -0
- package/dist/{providerAvailability.mjs → providerAvailability.js} +1 -1
- package/dist/providerAvailability.js.map +1 -0
- package/dist/{quoteClassification.d.cts → quoteClassification.d.ts} +2 -2
- package/dist/quoteClassification.d.ts.map +1 -0
- package/dist/{quoteClassification.mjs → quoteClassification.js} +1 -1
- package/dist/quoteClassification.js.map +1 -0
- package/dist/{rampsErrorCodes.d.cts → rampsErrorCodes.d.ts} +2 -2
- package/dist/rampsErrorCodes.d.ts.map +1 -0
- package/dist/{rampsErrorCodes.mjs → rampsErrorCodes.js} +1 -1
- package/dist/rampsErrorCodes.js.map +1 -0
- package/dist/{selectors.d.mts → selectors.d.ts} +3 -3
- package/dist/selectors.d.ts.map +1 -0
- package/dist/{selectors.mjs → selectors.js} +2 -2
- package/dist/selectors.js.map +1 -0
- package/dist/{transakApiErrorUtils.d.cts → transakApiErrorUtils.d.ts} +1 -1
- package/dist/transakApiErrorUtils.d.ts.map +1 -0
- package/dist/{transakApiErrorUtils.mjs → transakApiErrorUtils.js} +3 -3
- package/dist/transakApiErrorUtils.js.map +1 -0
- package/dist/{transakErrorCodes.d.cts → transakErrorCodes.d.ts} +3 -3
- package/dist/transakErrorCodes.d.ts.map +1 -0
- package/dist/{transakErrorCodes.mjs → transakErrorCodes.js} +1 -1
- package/dist/transakErrorCodes.js.map +1 -0
- package/dist/wallet-registration-machine.d.ts +81 -0
- package/dist/wallet-registration-machine.d.ts.map +1 -0
- package/dist/wallet-registration-machine.js +139 -0
- package/dist/wallet-registration-machine.js.map +1 -0
- package/dist/wallet-registration-service.d.ts +144 -0
- package/dist/wallet-registration-service.d.ts.map +1 -0
- package/dist/wallet-registration-service.js +335 -0
- package/dist/wallet-registration-service.js.map +1 -0
- package/package.json +17 -21
- package/dist/RampsController-method-action-types.cjs +0 -7
- package/dist/RampsController-method-action-types.cjs.map +0 -1
- package/dist/RampsController-method-action-types.d.cts +0 -668
- package/dist/RampsController-method-action-types.d.cts.map +0 -1
- package/dist/RampsController-method-action-types.d.mts.map +0 -1
- package/dist/RampsController-method-action-types.mjs.map +0 -1
- package/dist/RampsController.cjs +0 -2429
- package/dist/RampsController.cjs.map +0 -1
- package/dist/RampsController.d.cts.map +0 -1
- package/dist/RampsController.d.mts +0 -821
- package/dist/RampsController.d.mts.map +0 -1
- package/dist/RampsController.mjs.map +0 -1
- package/dist/RampsService-method-action-types.cjs +0 -7
- package/dist/RampsService-method-action-types.cjs.map +0 -1
- package/dist/RampsService-method-action-types.d.cts.map +0 -1
- package/dist/RampsService-method-action-types.d.mts +0 -171
- package/dist/RampsService-method-action-types.d.mts.map +0 -1
- package/dist/RampsService-method-action-types.mjs.map +0 -1
- package/dist/RampsService.cjs +0 -708
- package/dist/RampsService.cjs.map +0 -1
- package/dist/RampsService.d.cts +0 -944
- package/dist/RampsService.d.cts.map +0 -1
- package/dist/RampsService.d.mts.map +0 -1
- package/dist/RampsService.mjs.map +0 -1
- package/dist/RequestCache.cjs +0 -103
- package/dist/RequestCache.cjs.map +0 -1
- package/dist/RequestCache.d.cts.map +0 -1
- package/dist/RequestCache.d.mts +0 -110
- package/dist/RequestCache.d.mts.map +0 -1
- package/dist/RequestCache.mjs.map +0 -1
- package/dist/TransakService-method-action-types.cjs +0 -7
- package/dist/TransakService-method-action-types.cjs.map +0 -1
- package/dist/TransakService-method-action-types.d.cts.map +0 -1
- package/dist/TransakService-method-action-types.d.mts +0 -140
- package/dist/TransakService-method-action-types.d.mts.map +0 -1
- package/dist/TransakService-method-action-types.mjs.map +0 -1
- package/dist/TransakService.cjs +0 -743
- package/dist/TransakService.cjs.map +0 -1
- package/dist/TransakService.d.cts +0 -370
- package/dist/TransakService.d.cts.map +0 -1
- package/dist/TransakService.d.mts.map +0 -1
- package/dist/TransakService.mjs +0 -734
- package/dist/TransakService.mjs.map +0 -1
- package/dist/client-identity.cjs +0 -28
- package/dist/client-identity.cjs.map +0 -1
- package/dist/client-identity.d.cts.map +0 -1
- package/dist/client-identity.d.mts +0 -24
- package/dist/client-identity.d.mts.map +0 -1
- package/dist/client-identity.mjs.map +0 -1
- package/dist/errorNormalization.cjs +0 -84
- package/dist/errorNormalization.cjs.map +0 -1
- package/dist/errorNormalization.d.cts +0 -56
- package/dist/errorNormalization.d.cts.map +0 -1
- package/dist/errorNormalization.d.mts.map +0 -1
- package/dist/errorNormalization.mjs.map +0 -1
- package/dist/featureFlags.cjs +0 -144
- package/dist/featureFlags.cjs.map +0 -1
- package/dist/featureFlags.d.cts +0 -82
- package/dist/featureFlags.d.cts.map +0 -1
- package/dist/featureFlags.d.mts.map +0 -1
- package/dist/featureFlags.mjs.map +0 -1
- package/dist/index.cjs +0 -63
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -26
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts +0 -26
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -14
- package/dist/index.mjs.map +0 -1
- package/dist/orderStatus.cjs +0 -34
- package/dist/orderStatus.cjs.map +0 -1
- package/dist/orderStatus.d.cts +0 -18
- package/dist/orderStatus.d.cts.map +0 -1
- package/dist/orderStatus.d.mts.map +0 -1
- package/dist/orderStatus.mjs.map +0 -1
- package/dist/paymentMethodMerge.cjs +0 -51
- package/dist/paymentMethodMerge.cjs.map +0 -1
- package/dist/paymentMethodMerge.d.cts.map +0 -1
- package/dist/paymentMethodMerge.d.mts +0 -26
- package/dist/paymentMethodMerge.d.mts.map +0 -1
- package/dist/paymentMethodMerge.mjs.map +0 -1
- package/dist/providerAvailability.cjs +0 -134
- package/dist/providerAvailability.cjs.map +0 -1
- package/dist/providerAvailability.d.cts +0 -101
- package/dist/providerAvailability.d.cts.map +0 -1
- package/dist/providerAvailability.d.mts.map +0 -1
- package/dist/providerAvailability.mjs.map +0 -1
- package/dist/quoteClassification.cjs +0 -44
- package/dist/quoteClassification.cjs.map +0 -1
- package/dist/quoteClassification.d.cts.map +0 -1
- package/dist/quoteClassification.d.mts +0 -33
- package/dist/quoteClassification.d.mts.map +0 -1
- package/dist/quoteClassification.mjs.map +0 -1
- package/dist/rampsErrorCodes.cjs +0 -11
- package/dist/rampsErrorCodes.cjs.map +0 -1
- package/dist/rampsErrorCodes.d.cts.map +0 -1
- package/dist/rampsErrorCodes.d.mts +0 -9
- package/dist/rampsErrorCodes.d.mts.map +0 -1
- package/dist/rampsErrorCodes.mjs.map +0 -1
- package/dist/selectors.cjs +0 -85
- package/dist/selectors.cjs.map +0 -1
- package/dist/selectors.d.cts +0 -78
- package/dist/selectors.d.cts.map +0 -1
- package/dist/selectors.d.mts.map +0 -1
- package/dist/selectors.mjs.map +0 -1
- package/dist/transakApiErrorUtils.cjs +0 -15
- package/dist/transakApiErrorUtils.cjs.map +0 -1
- package/dist/transakApiErrorUtils.d.cts.map +0 -1
- package/dist/transakApiErrorUtils.d.mts +0 -3
- package/dist/transakApiErrorUtils.d.mts.map +0 -1
- package/dist/transakApiErrorUtils.mjs.map +0 -1
- package/dist/transakErrorCodes.cjs +0 -14
- package/dist/transakErrorCodes.cjs.map +0 -1
- package/dist/transakErrorCodes.d.cts.map +0 -1
- package/dist/transakErrorCodes.d.mts +0 -12
- package/dist/transakErrorCodes.d.mts.map +0 -1
- package/dist/transakErrorCodes.mjs.map +0 -1
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import { BrokenCircuitError } from "@metamask/controller-utils";
|
|
15
|
-
import { getHeadlessProviderAllowlist, isHeadlessAllProvidersEnabled, normalizeHeadlessProviderId } from "./featureFlags.mjs";
|
|
16
|
-
import { PENDING_ORDER_STATUSES, TERMINAL_ORDER_STATUSES } from "./orderStatus.mjs";
|
|
17
|
-
import { mergePaymentMethodsById, pickPaymentMethod } from "./paymentMethodMerge.mjs";
|
|
18
|
-
import { getProvidersServingAsset, normalizeRampsAssetId, providerServesAsset } from "./providerAvailability.mjs";
|
|
19
|
-
import { RAMPS_ERROR_CODES } from "./rampsErrorCodes.mjs";
|
|
20
|
-
import { RampsOrderStatus } from "./RampsService.mjs";
|
|
21
|
-
import { DEFAULT_REQUEST_CACHE_TTL, DEFAULT_REQUEST_CACHE_MAX_SIZE, createCacheKey, isCacheExpired, createLoadingState, createSuccessState, createErrorState, RequestStatus } from "./RequestCache.mjs";
|
|
1
|
+
import { BaseController } from '@metamask/base-controller';
|
|
2
|
+
import { BrokenCircuitError } from '@metamask/controller-utils';
|
|
3
|
+
import { applyAutorampRemoteStatus, createAutorampAccount, markAutorampNotified, } from './autorampAccount.js';
|
|
4
|
+
import { getHeadlessProviderAllowlist, isHeadlessAllProvidersEnabled, normalizeHeadlessProviderId, } from './featureFlags.js';
|
|
5
|
+
import { PENDING_ORDER_STATUSES, TERMINAL_ORDER_STATUSES, } from './orderStatus.js';
|
|
6
|
+
import { buildOwnershipMessage } from './ownership-message.js';
|
|
7
|
+
import { mergePaymentMethodsById, pickPaymentMethod, } from './paymentMethodMerge.js';
|
|
8
|
+
import { getProvidersServingAsset, normalizeRampsAssetId, providerServesAsset, } from './providerAvailability.js';
|
|
9
|
+
import { RAMPS_ERROR_CODES } from './rampsErrorCodes.js';
|
|
10
|
+
import { RampsOrderStatus } from './RampsService.js';
|
|
11
|
+
import { DEFAULT_REQUEST_CACHE_TTL, DEFAULT_REQUEST_CACHE_MAX_SIZE, createCacheKey, isCacheExpired, createLoadingState, createSuccessState, createErrorState, RequestStatus, } from './RequestCache.js';
|
|
12
|
+
import { createInitialState as createInitialWalletRegistrationState, transition as transitionWalletRegistration, } from './wallet-registration-machine.js';
|
|
13
|
+
import { createIdempotencyKey, WalletRegistrationError, } from './wallet-registration-service.js';
|
|
22
14
|
// === GENERAL ===
|
|
23
15
|
/**
|
|
24
16
|
* The name of the {@link RampsController}, used to namespace the
|
|
@@ -67,7 +59,36 @@ export const RAMPS_CONTROLLER_REQUIRED_SERVICE_ACTIONS = [
|
|
|
67
59
|
'TransakService:cancelOrder',
|
|
68
60
|
'TransakService:cancelAllActiveOrders',
|
|
69
61
|
'TransakService:getActiveOrders',
|
|
62
|
+
'NeoBankService:getAutoramp',
|
|
63
|
+
'NeoBankService:createAutoramp',
|
|
64
|
+
'NeoBankService:getCustomerByExternalId',
|
|
65
|
+
'NeoBankService:getWalletRegistrationStatus',
|
|
66
|
+
'NeoBankService:registerSelfHostedWallet',
|
|
70
67
|
];
|
|
68
|
+
/**
|
|
69
|
+
* Every external controller action RampsController calls via the messenger,
|
|
70
|
+
* which hosts must delegate from the root messenger.
|
|
71
|
+
* `AuthenticationController:getSessionProfile` resolves the vendor customer
|
|
72
|
+
* identity from Profile Sync, and `KeyringController:signPersonalMessage` signs
|
|
73
|
+
* the EIP-191 ownership proof for Money Account self-hosted wallet
|
|
74
|
+
* registration; both are only exercised by the autoramp paths.
|
|
75
|
+
*/
|
|
76
|
+
export const RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS = [
|
|
77
|
+
'AuthenticationController:getSessionProfile',
|
|
78
|
+
'KeyringController:signPersonalMessage',
|
|
79
|
+
'RemoteFeatureFlagController:getState',
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* Distinguishes an already-materialized {@link AutorampAccount} from the
|
|
83
|
+
* create-fields shape accepted by {@link RampsController.addAutoramp}.
|
|
84
|
+
*
|
|
85
|
+
* @param value - Full account or create fields.
|
|
86
|
+
* @returns Whether the value already carries the derived account fields.
|
|
87
|
+
*/
|
|
88
|
+
function isFullAutorampAccount(value) {
|
|
89
|
+
return (typeof value.updatedAt === 'number' &&
|
|
90
|
+
value.lastSeenStatus !== undefined);
|
|
91
|
+
}
|
|
71
92
|
/**
|
|
72
93
|
* Default TTL for quotes requests (15 seconds).
|
|
73
94
|
* Quotes are time-sensitive and should have a shorter cache duration.
|
|
@@ -179,6 +200,12 @@ const rampsControllerMetadata = {
|
|
|
179
200
|
includeInStateLogs: true,
|
|
180
201
|
usedInUi: true,
|
|
181
202
|
},
|
|
203
|
+
autoramps: {
|
|
204
|
+
persist: true,
|
|
205
|
+
includeInDebugSnapshot: true,
|
|
206
|
+
includeInStateLogs: true,
|
|
207
|
+
usedInUi: true,
|
|
208
|
+
},
|
|
182
209
|
providerAutoSelected: {
|
|
183
210
|
persist: true,
|
|
184
211
|
includeInDebugSnapshot: true,
|
|
@@ -228,6 +255,7 @@ export function getDefaultRampsControllerState() {
|
|
|
228
255
|
},
|
|
229
256
|
},
|
|
230
257
|
orders: [],
|
|
258
|
+
autoramps: [],
|
|
231
259
|
providerAutoSelected: false,
|
|
232
260
|
};
|
|
233
261
|
}
|
|
@@ -352,7 +380,7 @@ export function getInternalOrderCode(orderOrId) {
|
|
|
352
380
|
return providerOrderId;
|
|
353
381
|
}
|
|
354
382
|
// === ORDER POLLING CONSTANTS ===
|
|
355
|
-
const DEFAULT_POLLING_INTERVAL_MS =
|
|
383
|
+
const DEFAULT_POLLING_INTERVAL_MS = 30_000;
|
|
356
384
|
const MAX_ERROR_COUNT = 5;
|
|
357
385
|
// === CONTROLLER DEFINITION ===
|
|
358
386
|
const MESSENGER_EXPOSED_METHODS = [
|
|
@@ -373,6 +401,14 @@ const MESSENGER_EXPOSED_METHODS = [
|
|
|
373
401
|
'getQuotes',
|
|
374
402
|
'addOrder',
|
|
375
403
|
'removeOrder',
|
|
404
|
+
'addAutoramp',
|
|
405
|
+
'createAutoramp',
|
|
406
|
+
'removeAutoramp',
|
|
407
|
+
'registerMoneyAccountWallet',
|
|
408
|
+
'markAutorampAsNotified',
|
|
409
|
+
'applyAutorampStatusFromPush',
|
|
410
|
+
'refreshAutoramp',
|
|
411
|
+
'refreshAutoramps',
|
|
376
412
|
'startOrderPolling',
|
|
377
413
|
'stopOrderPolling',
|
|
378
414
|
'getBuyWidgetData',
|
|
@@ -428,6 +464,33 @@ function contextStillMatches(state, context) {
|
|
|
428
464
|
(state.providers.selected?.id.trim() ?? '') === context.providerId);
|
|
429
465
|
}
|
|
430
466
|
export class RampsController extends BaseController {
|
|
467
|
+
/**
|
|
468
|
+
* Default TTL for cached requests.
|
|
469
|
+
*/
|
|
470
|
+
#requestCacheTTL;
|
|
471
|
+
/**
|
|
472
|
+
* Maximum number of entries in the request cache.
|
|
473
|
+
*/
|
|
474
|
+
#requestCacheMaxSize;
|
|
475
|
+
/**
|
|
476
|
+
* Map of pending requests for deduplication.
|
|
477
|
+
* Key is the cache key, value is the pending request with abort controller.
|
|
478
|
+
*/
|
|
479
|
+
#pendingRequests = new Map();
|
|
480
|
+
/**
|
|
481
|
+
* Count of in-flight requests per resource type.
|
|
482
|
+
* Used so isLoading is only cleared when the last request for that resource finishes.
|
|
483
|
+
*/
|
|
484
|
+
#pendingResourceCount = new Map();
|
|
485
|
+
/**
|
|
486
|
+
* Monotonic generation per resource type used to invalidate stale in-flight
|
|
487
|
+
* requests after region/token/provider dependent-resource resets.
|
|
488
|
+
*/
|
|
489
|
+
#pendingResourceGeneration = new Map();
|
|
490
|
+
#orderPollingMeta = new Map();
|
|
491
|
+
#orderPollingTimer = null;
|
|
492
|
+
#isPolling = false;
|
|
493
|
+
#initPromise = null;
|
|
431
494
|
/**
|
|
432
495
|
* Clears the pending resource count map. Used only in tests to exercise the
|
|
433
496
|
* defensive path when get() returns undefined in the finally block.
|
|
@@ -435,7 +498,24 @@ export class RampsController extends BaseController {
|
|
|
435
498
|
* @internal
|
|
436
499
|
*/
|
|
437
500
|
clearPendingResourceCountForTest() {
|
|
438
|
-
|
|
501
|
+
this.#pendingResourceCount.clear();
|
|
502
|
+
}
|
|
503
|
+
#clearPendingResourceCountForDependentResources() {
|
|
504
|
+
for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
|
|
505
|
+
this.#pendingResourceCount.delete(resourceType);
|
|
506
|
+
const generation = this.#pendingResourceGeneration.get(resourceType) ?? 0;
|
|
507
|
+
this.#pendingResourceGeneration.set(resourceType, generation + 1);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
#abortDependentRequests() {
|
|
511
|
+
for (const [cacheKey, pending] of this.#pendingRequests.entries()) {
|
|
512
|
+
if (pending.resourceType &&
|
|
513
|
+
DEPENDENT_RESOURCE_KEYS_SET.has(pending.resourceType)) {
|
|
514
|
+
pending.abortController.abort();
|
|
515
|
+
this.#pendingRequests.delete(cacheKey);
|
|
516
|
+
this.#removeRequestState(cacheKey);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
439
519
|
}
|
|
440
520
|
/**
|
|
441
521
|
* Constructs a new {@link RampsController}.
|
|
@@ -459,38 +539,39 @@ export class RampsController extends BaseController {
|
|
|
459
539
|
requests: {},
|
|
460
540
|
},
|
|
461
541
|
});
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
* Default TTL for cached requests.
|
|
465
|
-
*/
|
|
466
|
-
_RampsController_requestCacheTTL.set(this, void 0);
|
|
467
|
-
/**
|
|
468
|
-
* Maximum number of entries in the request cache.
|
|
469
|
-
*/
|
|
470
|
-
_RampsController_requestCacheMaxSize.set(this, void 0);
|
|
471
|
-
/**
|
|
472
|
-
* Map of pending requests for deduplication.
|
|
473
|
-
* Key is the cache key, value is the pending request with abort controller.
|
|
474
|
-
*/
|
|
475
|
-
_RampsController_pendingRequests.set(this, new Map());
|
|
476
|
-
/**
|
|
477
|
-
* Count of in-flight requests per resource type.
|
|
478
|
-
* Used so isLoading is only cleared when the last request for that resource finishes.
|
|
479
|
-
*/
|
|
480
|
-
_RampsController_pendingResourceCount.set(this, new Map());
|
|
481
|
-
/**
|
|
482
|
-
* Monotonic generation per resource type used to invalidate stale in-flight
|
|
483
|
-
* requests after region/token/provider dependent-resource resets.
|
|
484
|
-
*/
|
|
485
|
-
_RampsController_pendingResourceGeneration.set(this, new Map());
|
|
486
|
-
_RampsController_orderPollingMeta.set(this, new Map());
|
|
487
|
-
_RampsController_orderPollingTimer.set(this, null);
|
|
488
|
-
_RampsController_isPolling.set(this, false);
|
|
489
|
-
_RampsController_initPromise.set(this, null);
|
|
490
|
-
__classPrivateFieldSet(this, _RampsController_requestCacheTTL, requestCacheTTL, "f");
|
|
491
|
-
__classPrivateFieldSet(this, _RampsController_requestCacheMaxSize, requestCacheMaxSize, "f");
|
|
542
|
+
this.#requestCacheTTL = requestCacheTTL;
|
|
543
|
+
this.#requestCacheMaxSize = requestCacheMaxSize;
|
|
492
544
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
493
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Resolves the Headless Buy all-providers feature flag: whether widening is
|
|
548
|
+
* enabled, and the provider-id allowlist the widened pick is restricted to
|
|
549
|
+
* (if the flag's object payload carries one).
|
|
550
|
+
*
|
|
551
|
+
* Reads `RemoteFeatureFlagController` state through the messenger on every
|
|
552
|
+
* call, so a remote flag fetch or a local dev override takes effect at
|
|
553
|
+
* runtime without reconstructing the controller. The single read keeps the
|
|
554
|
+
* enabled bit and the allowlist consistent even if the flag changes while a
|
|
555
|
+
* quote request is in flight. Key lookup, local-override merging, and value
|
|
556
|
+
* coercion live in the shared `isHeadlessAllProvidersEnabled` /
|
|
557
|
+
* `getHeadlessProviderAllowlist` helpers so UI consumers resolve the flag
|
|
558
|
+
* identically. Fails closed: when `RemoteFeatureFlagController:getState` is
|
|
559
|
+
* not wired up, quoting stays native-only.
|
|
560
|
+
*
|
|
561
|
+
* @returns The enabled bit and the optional provider-id allowlist.
|
|
562
|
+
*/
|
|
563
|
+
#resolveAllProvidersFlag() {
|
|
564
|
+
try {
|
|
565
|
+
const remoteFeatureFlagState = this.messenger.call('RemoteFeatureFlagController:getState');
|
|
566
|
+
return {
|
|
567
|
+
enabled: isHeadlessAllProvidersEnabled(remoteFeatureFlagState),
|
|
568
|
+
allowlist: getHeadlessProviderAllowlist(remoteFeatureFlagState),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return { enabled: false };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
494
575
|
/**
|
|
495
576
|
* Executes a request with caching, deduplication, and at most one in-flight
|
|
496
577
|
* request per resource type.
|
|
@@ -517,11 +598,11 @@ export class RampsController extends BaseController {
|
|
|
517
598
|
*/
|
|
518
599
|
async executeRequest(cacheKey, fetcher, options) {
|
|
519
600
|
// Get TTL for verifying cache expiration
|
|
520
|
-
const ttl = options?.ttl ??
|
|
601
|
+
const ttl = options?.ttl ?? this.#requestCacheTTL;
|
|
521
602
|
// DEDUPLICATION:
|
|
522
603
|
// Check if a request is already in flight for this cache key
|
|
523
604
|
// If so, return the original promise for that request
|
|
524
|
-
const pending =
|
|
605
|
+
const pending = this.#pendingRequests.get(cacheKey);
|
|
525
606
|
if (pending) {
|
|
526
607
|
return pending.promise;
|
|
527
608
|
}
|
|
@@ -539,17 +620,17 @@ export class RampsController extends BaseController {
|
|
|
539
620
|
const lastFetchedAt = Date.now();
|
|
540
621
|
const { resourceType } = options ?? {};
|
|
541
622
|
const resourceGeneration = resourceType
|
|
542
|
-
? (
|
|
623
|
+
? (this.#pendingResourceGeneration.get(resourceType) ?? 0)
|
|
543
624
|
: undefined;
|
|
544
625
|
// Update state to loading
|
|
545
|
-
|
|
626
|
+
this.#updateRequestState(cacheKey, createLoadingState());
|
|
546
627
|
// Set resource-level loading state (only on cache miss). Ref-count so concurrent
|
|
547
628
|
// requests for the same resource type (different cache keys) keep isLoading true.
|
|
548
629
|
if (resourceType) {
|
|
549
|
-
const count =
|
|
550
|
-
|
|
630
|
+
const count = this.#pendingResourceCount.get(resourceType) ?? 0;
|
|
631
|
+
this.#pendingResourceCount.set(resourceType, count + 1);
|
|
551
632
|
if (count === 0) {
|
|
552
|
-
|
|
633
|
+
this.#setResourceLoading(resourceType, true);
|
|
553
634
|
}
|
|
554
635
|
}
|
|
555
636
|
// Create the fetch promise
|
|
@@ -559,11 +640,11 @@ export class RampsController extends BaseController {
|
|
|
559
640
|
if (abortController.signal.aborted) {
|
|
560
641
|
throw new Error('Request was aborted');
|
|
561
642
|
}
|
|
562
|
-
|
|
643
|
+
this.#updateRequestState(cacheKey, createSuccessState(data, lastFetchedAt));
|
|
563
644
|
if (resourceType) {
|
|
564
645
|
const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
|
|
565
646
|
if (isCurrent) {
|
|
566
|
-
|
|
647
|
+
this.#setResourceError(resourceType, null);
|
|
567
648
|
}
|
|
568
649
|
}
|
|
569
650
|
return data;
|
|
@@ -573,38 +654,38 @@ export class RampsController extends BaseController {
|
|
|
573
654
|
throw error;
|
|
574
655
|
}
|
|
575
656
|
const { errorInfo, normalizedError } = getNormalizedRampsError(error);
|
|
576
|
-
|
|
657
|
+
this.#updateRequestState(cacheKey, createErrorState(errorInfo.message, lastFetchedAt, errorInfo.errorKey));
|
|
577
658
|
if (resourceType) {
|
|
578
659
|
const isCurrent = !options?.isResultCurrent || options.isResultCurrent();
|
|
579
660
|
if (isCurrent) {
|
|
580
|
-
|
|
661
|
+
this.#setResourceError(resourceType, errorInfo);
|
|
581
662
|
}
|
|
582
663
|
}
|
|
583
664
|
throw normalizedError;
|
|
584
665
|
}
|
|
585
666
|
finally {
|
|
586
|
-
if (
|
|
667
|
+
if (this.#pendingRequests.get(cacheKey)?.abortController ===
|
|
587
668
|
abortController) {
|
|
588
|
-
|
|
669
|
+
this.#pendingRequests.delete(cacheKey);
|
|
589
670
|
}
|
|
590
671
|
// Clear resource-level loading state only when no requests for this resource remain
|
|
591
672
|
if (resourceType && !abortController.signal.aborted) {
|
|
592
|
-
const currentGeneration =
|
|
673
|
+
const currentGeneration = this.#pendingResourceGeneration.get(resourceType) ?? 0;
|
|
593
674
|
if (currentGeneration === resourceGeneration) {
|
|
594
|
-
const count =
|
|
675
|
+
const count = this.#pendingResourceCount.get(resourceType) ?? 0;
|
|
595
676
|
const next = Math.max(0, count - 1);
|
|
596
677
|
if (next === 0) {
|
|
597
|
-
|
|
598
|
-
|
|
678
|
+
this.#pendingResourceCount.delete(resourceType);
|
|
679
|
+
this.#setResourceLoading(resourceType, false);
|
|
599
680
|
}
|
|
600
681
|
else {
|
|
601
|
-
|
|
682
|
+
this.#pendingResourceCount.set(resourceType, next);
|
|
602
683
|
}
|
|
603
684
|
}
|
|
604
685
|
}
|
|
605
686
|
}
|
|
606
687
|
})();
|
|
607
|
-
|
|
688
|
+
this.#pendingRequests.set(cacheKey, {
|
|
608
689
|
promise,
|
|
609
690
|
abortController,
|
|
610
691
|
resourceType,
|
|
@@ -618,15 +699,92 @@ export class RampsController extends BaseController {
|
|
|
618
699
|
* @returns True if a request was aborted.
|
|
619
700
|
*/
|
|
620
701
|
abortRequest(cacheKey) {
|
|
621
|
-
const pending =
|
|
702
|
+
const pending = this.#pendingRequests.get(cacheKey);
|
|
622
703
|
if (pending) {
|
|
623
704
|
pending.abortController.abort();
|
|
624
|
-
|
|
625
|
-
|
|
705
|
+
this.#pendingRequests.delete(cacheKey);
|
|
706
|
+
this.#removeRequestState(cacheKey);
|
|
626
707
|
return true;
|
|
627
708
|
}
|
|
628
709
|
return false;
|
|
629
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Mutates state.requests inside update(); cast is centralized here.
|
|
713
|
+
*
|
|
714
|
+
* @param fn - Callback that mutates the requests record.
|
|
715
|
+
*/
|
|
716
|
+
#mutateRequests(fn) {
|
|
717
|
+
this.update((state) => {
|
|
718
|
+
const requests = state.requests;
|
|
719
|
+
fn(requests);
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
#removeRequestState(cacheKey) {
|
|
723
|
+
this.#mutateRequests((requests) => {
|
|
724
|
+
delete requests[cacheKey];
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
#cleanupState() {
|
|
728
|
+
this.#abortDependentRequests();
|
|
729
|
+
this.#clearPendingResourceCountForDependentResources();
|
|
730
|
+
this.update((state) => resetDependentResources(state, { clearUserRegionData: true }));
|
|
731
|
+
}
|
|
732
|
+
#requireRegion() {
|
|
733
|
+
const regionCode = this.state.userRegion?.regionCode;
|
|
734
|
+
if (!regionCode) {
|
|
735
|
+
throw new Error('Region is required. Cannot proceed without valid region information.');
|
|
736
|
+
}
|
|
737
|
+
return regionCode;
|
|
738
|
+
}
|
|
739
|
+
#isRegionCurrent(normalizedRegion) {
|
|
740
|
+
const current = this.state.userRegion?.regionCode;
|
|
741
|
+
return current === undefined || current === normalizedRegion;
|
|
742
|
+
}
|
|
743
|
+
#isTokenCurrent(normalizedAssetId) {
|
|
744
|
+
const current = this.state.tokens.selected?.assetId ?? '';
|
|
745
|
+
return current === normalizedAssetId;
|
|
746
|
+
}
|
|
747
|
+
#isProviderCurrent(normalizedProviderId) {
|
|
748
|
+
const current = this.state.providers.selected?.id ?? '';
|
|
749
|
+
return current === normalizedProviderId;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Updates a single field (isLoading or error) on a resource state.
|
|
753
|
+
* All resources share the same ResourceState structure, so we use
|
|
754
|
+
* dynamic property access to avoid duplicating switch statements.
|
|
755
|
+
*
|
|
756
|
+
* @param resourceType - The type of resource.
|
|
757
|
+
* @param field - The field to update ('isLoading' or 'error').
|
|
758
|
+
* @param value - The value to set.
|
|
759
|
+
*/
|
|
760
|
+
#updateResourceField(resourceType, field, value) {
|
|
761
|
+
this.update((state) => {
|
|
762
|
+
const resource = getResourceState(state, resourceType);
|
|
763
|
+
resource[field] = value;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Sets the loading state for a resource type.
|
|
768
|
+
*
|
|
769
|
+
* @param resourceType - The type of resource.
|
|
770
|
+
* @param loading - Whether the resource is loading.
|
|
771
|
+
*/
|
|
772
|
+
#setResourceLoading(resourceType, loading) {
|
|
773
|
+
this.#updateResourceField(resourceType, 'isLoading', loading);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Sets the error state for a resource type.
|
|
777
|
+
*
|
|
778
|
+
* @param resourceType - The type of resource.
|
|
779
|
+
* @param errorInfo - The error info, or null to clear.
|
|
780
|
+
*/
|
|
781
|
+
#setResourceError(resourceType, errorInfo) {
|
|
782
|
+
this.update((state) => {
|
|
783
|
+
const resource = getResourceState(state, resourceType);
|
|
784
|
+
resource.error = errorInfo?.message ?? null;
|
|
785
|
+
resource.errorKey = errorInfo?.errorKey ?? null;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
630
788
|
/**
|
|
631
789
|
* Gets the state of a specific cached request.
|
|
632
790
|
*
|
|
@@ -636,6 +794,42 @@ export class RampsController extends BaseController {
|
|
|
636
794
|
getRequestState(cacheKey) {
|
|
637
795
|
return this.state.requests[cacheKey];
|
|
638
796
|
}
|
|
797
|
+
/**
|
|
798
|
+
* Updates the state for a specific request.
|
|
799
|
+
*
|
|
800
|
+
* @param cacheKey - The cache key.
|
|
801
|
+
* @param requestState - The new state for the request.
|
|
802
|
+
*/
|
|
803
|
+
#updateRequestState(cacheKey, requestState) {
|
|
804
|
+
const maxSize = this.#requestCacheMaxSize;
|
|
805
|
+
const ttl = this.#requestCacheTTL;
|
|
806
|
+
this.#mutateRequests((requests) => {
|
|
807
|
+
requests[cacheKey] = requestState;
|
|
808
|
+
const keys = Object.keys(requests);
|
|
809
|
+
for (const key of keys) {
|
|
810
|
+
const entry = requests[key];
|
|
811
|
+
if (entry?.status === RequestStatus.SUCCESS &&
|
|
812
|
+
isCacheExpired(entry, ttl)) {
|
|
813
|
+
delete requests[key];
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const remainingKeys = Object.keys(requests);
|
|
817
|
+
if (remainingKeys.length > maxSize) {
|
|
818
|
+
const sortedKeys = remainingKeys.sort((a, b) => {
|
|
819
|
+
const aTime = requests[a]?.timestamp ?? 0;
|
|
820
|
+
const bTime = requests[b]?.timestamp ?? 0;
|
|
821
|
+
return aTime - bTime;
|
|
822
|
+
});
|
|
823
|
+
const entriesToRemove = remainingKeys.length - maxSize;
|
|
824
|
+
for (let i = 0; i < entriesToRemove; i++) {
|
|
825
|
+
const keyToRemove = sortedKeys[i];
|
|
826
|
+
if (keyToRemove) {
|
|
827
|
+
delete requests[keyToRemove];
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
639
833
|
/**
|
|
640
834
|
* Sets the user's region manually (without fetching geolocation).
|
|
641
835
|
* This allows users to override the detected region.
|
|
@@ -649,12 +843,12 @@ export class RampsController extends BaseController {
|
|
|
649
843
|
try {
|
|
650
844
|
const countriesData = this.state.countries.data;
|
|
651
845
|
if (!countriesData || countriesData.length === 0) {
|
|
652
|
-
|
|
846
|
+
this.#cleanupState();
|
|
653
847
|
throw new Error('No countries found. Cannot set user region without valid country information.');
|
|
654
848
|
}
|
|
655
849
|
const userRegion = findRegionFromCode(normalizedRegion, countriesData);
|
|
656
850
|
if (!userRegion) {
|
|
657
|
-
|
|
851
|
+
this.#cleanupState();
|
|
658
852
|
throw new Error(`Region "${normalizedRegion}" not found in countries data. Cannot set user region without valid country information.`);
|
|
659
853
|
}
|
|
660
854
|
const regionChanged = Boolean(options?.forceRefresh) ||
|
|
@@ -665,7 +859,7 @@ export class RampsController extends BaseController {
|
|
|
665
859
|
// user taps France → Finland → France quickly). Instead we let
|
|
666
860
|
// old requests complete naturally; isResultCurrent guards in
|
|
667
861
|
// getProviders/getPaymentMethods discard stale results.
|
|
668
|
-
|
|
862
|
+
this.#clearPendingResourceCountForDependentResources();
|
|
669
863
|
}
|
|
670
864
|
this.update((state) => {
|
|
671
865
|
if (regionChanged) {
|
|
@@ -676,7 +870,7 @@ export class RampsController extends BaseController {
|
|
|
676
870
|
return userRegion;
|
|
677
871
|
}
|
|
678
872
|
catch (error) {
|
|
679
|
-
|
|
873
|
+
this.#cleanupState();
|
|
680
874
|
throw error;
|
|
681
875
|
}
|
|
682
876
|
}
|
|
@@ -701,7 +895,7 @@ export class RampsController extends BaseController {
|
|
|
701
895
|
});
|
|
702
896
|
return;
|
|
703
897
|
}
|
|
704
|
-
|
|
898
|
+
this.#requireRegion();
|
|
705
899
|
// If a full Provider object is passed, store it directly (avoids
|
|
706
900
|
// depending on state.providers.data being populated).
|
|
707
901
|
if (typeof providerOrId !== 'string') {
|
|
@@ -781,21 +975,68 @@ export class RampsController extends BaseController {
|
|
|
781
975
|
* @returns Promise that resolves when initialization is complete.
|
|
782
976
|
*/
|
|
783
977
|
async init(options) {
|
|
784
|
-
if (!options?.forceRefresh &&
|
|
785
|
-
return
|
|
978
|
+
if (!options?.forceRefresh && this.#initPromise !== null) {
|
|
979
|
+
return this.#initPromise;
|
|
786
980
|
}
|
|
787
981
|
if (options?.forceRefresh) {
|
|
788
|
-
|
|
982
|
+
this.#initPromise = null;
|
|
789
983
|
}
|
|
790
|
-
const initPromise =
|
|
791
|
-
if (
|
|
792
|
-
|
|
984
|
+
const initPromise = this.#runInit(options).then(() => undefined, (error) => {
|
|
985
|
+
if (this.#initPromise === initPromise) {
|
|
986
|
+
this.#initPromise = null;
|
|
793
987
|
}
|
|
794
988
|
throw error;
|
|
795
989
|
});
|
|
796
|
-
|
|
990
|
+
this.#initPromise = initPromise;
|
|
797
991
|
return initPromise;
|
|
798
992
|
}
|
|
993
|
+
async #runInit(options) {
|
|
994
|
+
// Force-refetch the catalog on startup so region preset amounts stay
|
|
995
|
+
// current, bypassing the in-session request cache. The catalog is not
|
|
996
|
+
// persisted, so a cold start always re-fetches it regardless.
|
|
997
|
+
await this.getCountries({ ...options, forceRefresh: true });
|
|
998
|
+
// Always prefer the user's persisted region. Geolocation is only used to
|
|
999
|
+
// seed the initial value; once the user (or a prior init) has set a region
|
|
1000
|
+
// we must respect that choice — even on forceRefresh.
|
|
1001
|
+
const persistedRegionCode = this.state.userRegion?.regionCode;
|
|
1002
|
+
const regionCode = persistedRegionCode ??
|
|
1003
|
+
(await this.messenger.call('RampsService:getGeolocation'));
|
|
1004
|
+
if (!regionCode) {
|
|
1005
|
+
throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
|
|
1006
|
+
}
|
|
1007
|
+
// For an already-persisted region, getCountries() has already re-synced it
|
|
1008
|
+
// from the fresh catalog (see #syncUserRegionFromCountriesCatalog). Calling
|
|
1009
|
+
// setUserRegion here would re-validate against that catalog and, if it is
|
|
1010
|
+
// momentarily empty or no longer lists the region (e.g. a transient/partial
|
|
1011
|
+
// catalog response or a region with no current provider coverage), throw and
|
|
1012
|
+
// wipe the persisted region via #cleanupState. Preserve the existing region
|
|
1013
|
+
// instead; only resolve a brand-new region (from geolocation) strictly.
|
|
1014
|
+
if (persistedRegionCode) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
await this.setUserRegion(regionCode, options);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Re-applies `userRegion` from the current countries catalog so preset
|
|
1021
|
+
* amounts and support flags stay in sync after a catalog refresh.
|
|
1022
|
+
*/
|
|
1023
|
+
#syncUserRegionFromCountriesCatalog() {
|
|
1024
|
+
const regionCode = this.state.userRegion?.regionCode;
|
|
1025
|
+
if (!regionCode) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const countriesData = this.state.countries.data;
|
|
1029
|
+
if (!countriesData.length) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const userRegion = findRegionFromCode(regionCode, countriesData);
|
|
1033
|
+
if (!userRegion) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
this.update((state) => {
|
|
1037
|
+
state.userRegion = userRegion;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
799
1040
|
/**
|
|
800
1041
|
* Fetches the list of supported countries.
|
|
801
1042
|
* The API returns countries with support information for both buy and sell actions.
|
|
@@ -812,7 +1053,7 @@ export class RampsController extends BaseController {
|
|
|
812
1053
|
this.update((state) => {
|
|
813
1054
|
state.countries.data = Array.isArray(countries) ? [...countries] : [];
|
|
814
1055
|
});
|
|
815
|
-
|
|
1056
|
+
this.#syncUserRegionFromCountriesCatalog();
|
|
816
1057
|
return countries;
|
|
817
1058
|
}
|
|
818
1059
|
/**
|
|
@@ -826,7 +1067,7 @@ export class RampsController extends BaseController {
|
|
|
826
1067
|
* @returns The tokens response containing topTokens and allTokens.
|
|
827
1068
|
*/
|
|
828
1069
|
async getTokens(region, action = 'buy', options) {
|
|
829
|
-
const regionToUse = region ??
|
|
1070
|
+
const regionToUse = region ?? this.#requireRegion();
|
|
830
1071
|
const normalizedRegion = regionToUse.toLowerCase().trim();
|
|
831
1072
|
const cacheKey = createCacheKey('getTokens', [
|
|
832
1073
|
normalizedRegion,
|
|
@@ -840,7 +1081,7 @@ export class RampsController extends BaseController {
|
|
|
840
1081
|
}, {
|
|
841
1082
|
...options,
|
|
842
1083
|
resourceType: 'tokens',
|
|
843
|
-
isResultCurrent: () =>
|
|
1084
|
+
isResultCurrent: () => this.#isRegionCurrent(normalizedRegion),
|
|
844
1085
|
});
|
|
845
1086
|
this.update((state) => {
|
|
846
1087
|
const userRegionCode = state.userRegion?.regionCode;
|
|
@@ -865,7 +1106,7 @@ export class RampsController extends BaseController {
|
|
|
865
1106
|
});
|
|
866
1107
|
return;
|
|
867
1108
|
}
|
|
868
|
-
|
|
1109
|
+
this.#requireRegion();
|
|
869
1110
|
const tokens = this.state.tokens.data;
|
|
870
1111
|
if (!tokens) {
|
|
871
1112
|
throw new Error('Tokens not loaded. Cannot set selected token before tokens are fetched.');
|
|
@@ -891,7 +1132,7 @@ export class RampsController extends BaseController {
|
|
|
891
1132
|
* @returns The providers response containing providers array.
|
|
892
1133
|
*/
|
|
893
1134
|
async getProviders(region, options) {
|
|
894
|
-
const regionToUse = region ??
|
|
1135
|
+
const regionToUse = region ?? this.#requireRegion();
|
|
895
1136
|
const normalizedRegion = regionToUse.toLowerCase().trim();
|
|
896
1137
|
const cacheKey = createCacheKey('getProviders', [
|
|
897
1138
|
normalizedRegion,
|
|
@@ -908,7 +1149,7 @@ export class RampsController extends BaseController {
|
|
|
908
1149
|
}, {
|
|
909
1150
|
...options,
|
|
910
1151
|
resourceType: 'providers',
|
|
911
|
-
isResultCurrent: () =>
|
|
1152
|
+
isResultCurrent: () => this.#isRegionCurrent(normalizedRegion),
|
|
912
1153
|
});
|
|
913
1154
|
const { providers } = response;
|
|
914
1155
|
this.update((state) => {
|
|
@@ -930,7 +1171,7 @@ export class RampsController extends BaseController {
|
|
|
930
1171
|
* @returns The payment methods response containing payments array.
|
|
931
1172
|
*/
|
|
932
1173
|
async getPaymentMethods(region, options) {
|
|
933
|
-
const regionCode = region ??
|
|
1174
|
+
const regionCode = region ?? this.#requireRegion();
|
|
934
1175
|
const assetIdToUse = options?.assetId ?? this.state.tokens.selected?.assetId ?? '';
|
|
935
1176
|
const providerToUse = options?.provider ?? this.state.providers.selected?.id ?? '';
|
|
936
1177
|
const normalizedRegion = regionCode.toLowerCase().trim();
|
|
@@ -949,9 +1190,9 @@ export class RampsController extends BaseController {
|
|
|
949
1190
|
...options,
|
|
950
1191
|
resourceType: 'paymentMethods',
|
|
951
1192
|
isResultCurrent: () => {
|
|
952
|
-
const regionMatch =
|
|
953
|
-
const tokenMatch =
|
|
954
|
-
const providerMatch =
|
|
1193
|
+
const regionMatch = this.#isRegionCurrent(normalizedRegion);
|
|
1194
|
+
const tokenMatch = this.#isTokenCurrent(assetIdToUse);
|
|
1195
|
+
const providerMatch = this.#isProviderCurrent(providerToUse);
|
|
955
1196
|
return regionMatch && tokenMatch && providerMatch;
|
|
956
1197
|
},
|
|
957
1198
|
});
|
|
@@ -1014,7 +1255,7 @@ export class RampsController extends BaseController {
|
|
|
1014
1255
|
* provider ids that contributed.
|
|
1015
1256
|
*/
|
|
1016
1257
|
async getPaymentMethodsForContext(options) {
|
|
1017
|
-
const regionToUse = options.region ??
|
|
1258
|
+
const regionToUse = options.region ?? this.#requireRegion();
|
|
1018
1259
|
const normalizedRegion = regionToUse.toLowerCase().trim();
|
|
1019
1260
|
const assetId = options.assetId.trim();
|
|
1020
1261
|
if (assetId === '') {
|
|
@@ -1025,7 +1266,7 @@ export class RampsController extends BaseController {
|
|
|
1025
1266
|
const providerIdForState = options.providers?.length === 1
|
|
1026
1267
|
? options.providers[0].trim()
|
|
1027
1268
|
: (this.state.providers.selected?.id.trim() ?? '');
|
|
1028
|
-
const providerIds = await
|
|
1269
|
+
const providerIds = await this.#resolveProviderIdsForPaymentMethods({
|
|
1029
1270
|
assetId,
|
|
1030
1271
|
region: normalizedRegion,
|
|
1031
1272
|
providers: options.providers,
|
|
@@ -1177,7 +1418,7 @@ export class RampsController extends BaseController {
|
|
|
1177
1418
|
* @returns The quotes response containing success, sorted, error, and customActions.
|
|
1178
1419
|
*/
|
|
1179
1420
|
async getQuotes(options) {
|
|
1180
|
-
const regionToUse = options.region ??
|
|
1421
|
+
const regionToUse = options.region ?? this.#requireRegion();
|
|
1181
1422
|
const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
|
|
1182
1423
|
const paymentMethodsToUse = options.paymentMethods ??
|
|
1183
1424
|
this.state.paymentMethods.data.map((pm) => pm.id);
|
|
@@ -1202,14 +1443,14 @@ export class RampsController extends BaseController {
|
|
|
1202
1443
|
// the same `RemoteFeatureFlagController` state snapshot, so a flag edit
|
|
1203
1444
|
// during the awaited quote fetch cannot produce a mixed read.
|
|
1204
1445
|
const { enabled: allProvidersEnabled, allowlist: providerAllowlist } = wantsAutoSelection
|
|
1205
|
-
?
|
|
1446
|
+
? this.#resolveAllProvidersFlag()
|
|
1206
1447
|
: { enabled: false, allowlist: undefined };
|
|
1207
1448
|
const widenToAllProviders = wantsAutoSelection && allProvidersEnabled;
|
|
1208
1449
|
let providersToUse;
|
|
1209
1450
|
let widenedProviderCatalog = this.state.providers.data;
|
|
1210
1451
|
if (options.providers) {
|
|
1211
1452
|
providersToUse = options.restrictToKnownOrNativeProviders
|
|
1212
|
-
? await
|
|
1453
|
+
? await this.#filterProviderIdsBySupport({
|
|
1213
1454
|
providerIds: options.providers,
|
|
1214
1455
|
assetId: normalizedAssetIdForValidation,
|
|
1215
1456
|
region: regionToUse,
|
|
@@ -1220,7 +1461,7 @@ export class RampsController extends BaseController {
|
|
|
1220
1461
|
// `#getSupportingProvidersForRegion` also hydrates the provider catalog
|
|
1221
1462
|
// when controller state is empty, so all-provider quoting cannot silently
|
|
1222
1463
|
// return zero providers here.
|
|
1223
|
-
const { supporting } = await
|
|
1464
|
+
const { supporting } = await this.#getSupportingProvidersForRegion({
|
|
1224
1465
|
assetId: normalizedAssetIdForValidation,
|
|
1225
1466
|
region: regionToUse,
|
|
1226
1467
|
});
|
|
@@ -1232,7 +1473,7 @@ export class RampsController extends BaseController {
|
|
|
1232
1473
|
// The restriction flag implies resolution: it must narrow the provider
|
|
1233
1474
|
// set even when `autoSelectProvider` was not explicitly passed, otherwise
|
|
1234
1475
|
// it would be silently ignored and every provider quoted.
|
|
1235
|
-
providersToUse = await
|
|
1476
|
+
providersToUse = await this.#resolveProviderIdsForQuote({
|
|
1236
1477
|
assetId: normalizedAssetIdForValidation,
|
|
1237
1478
|
region: regionToUse,
|
|
1238
1479
|
preferredProviderIds: options.preferredProviderIds,
|
|
@@ -1315,7 +1556,7 @@ export class RampsController extends BaseController {
|
|
|
1315
1556
|
// and place it at `success[0]`, since single-pick consumers
|
|
1316
1557
|
// (`getRampsQuote` -> `success?.[0]`) rely on index 0 while `success[]`
|
|
1317
1558
|
// order is server-defined rather than ranked.
|
|
1318
|
-
const selectedQuote =
|
|
1559
|
+
const selectedQuote = this.#pickWidenedQuote(response, {
|
|
1319
1560
|
amount: options.amount,
|
|
1320
1561
|
fiat: normalizedFiat,
|
|
1321
1562
|
providers: widenedProviderCatalog,
|
|
@@ -1339,6 +1580,261 @@ export class RampsController extends BaseController {
|
|
|
1339
1580
|
],
|
|
1340
1581
|
};
|
|
1341
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Selects the best quote from a widened multi-provider response.
|
|
1585
|
+
*
|
|
1586
|
+
* Every provider class is eligible (native, in-app WebView aggregator, and
|
|
1587
|
+
* external-browser / custom-action). When the feature flag payload carries a
|
|
1588
|
+
* provider allowlist, candidates from unlisted providers are dropped first.
|
|
1589
|
+
* Enforces per-provider fiat limits up front, then orders by reliability and
|
|
1590
|
+
* falls back to price using the server-provided `sorted` order. Returns
|
|
1591
|
+
* `undefined` when no quote survives.
|
|
1592
|
+
*
|
|
1593
|
+
* @param response - The multi-provider quotes response.
|
|
1594
|
+
* @param options - Selection inputs.
|
|
1595
|
+
* @param options.amount - Fiat amount, for the limit-fit check.
|
|
1596
|
+
* @param options.fiat - Lowercased fiat short code, for the limit lookup.
|
|
1597
|
+
* @param options.providers - Provider catalog for the limit lookup.
|
|
1598
|
+
* @param options.allowlist - Optional provider ids (either `/providers/x`
|
|
1599
|
+
* or bare form) the pick is restricted to.
|
|
1600
|
+
* @returns The selected quote, or `undefined` when none is usable.
|
|
1601
|
+
*/
|
|
1602
|
+
#pickWidenedQuote(response, { amount, fiat, providers, allowlist, }) {
|
|
1603
|
+
const providerByCode = new Map(providers.map((provider) => [provider.id, provider]));
|
|
1604
|
+
const allowedProviderIds = allowlist && allowlist.length > 0
|
|
1605
|
+
? new Set(allowlist.map(normalizeHeadlessProviderId))
|
|
1606
|
+
: undefined;
|
|
1607
|
+
const isAllowedProvider = (quote) => !allowedProviderIds ||
|
|
1608
|
+
allowedProviderIds.has(normalizeHeadlessProviderId(quote.provider));
|
|
1609
|
+
const fitsProviderLimits = (quote) => {
|
|
1610
|
+
const provider = providerByCode.get(quote.provider);
|
|
1611
|
+
const limit = provider?.limits?.fiat?.[fiat]?.[quote.quote.paymentMethod];
|
|
1612
|
+
if (!limit) {
|
|
1613
|
+
// No published limits for this provider/payment method: treat as
|
|
1614
|
+
// eligible and let the provider enforce limits at checkout.
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
return amount >= limit.minAmount && amount <= limit.maxAmount;
|
|
1618
|
+
};
|
|
1619
|
+
const candidates = response.success.filter((quote) => isAllowedProvider(quote) && fitsProviderLimits(quote));
|
|
1620
|
+
if (candidates.length === 0) {
|
|
1621
|
+
return undefined;
|
|
1622
|
+
}
|
|
1623
|
+
const candidateByCode = new Map(candidates.map((quote) => [quote.provider, quote]));
|
|
1624
|
+
const pickBySortOrder = (sortBy) => {
|
|
1625
|
+
const order = response.sorted.find((entry) => entry.sortBy === sortBy)?.ids;
|
|
1626
|
+
if (!order) {
|
|
1627
|
+
return undefined;
|
|
1628
|
+
}
|
|
1629
|
+
for (const providerId of order) {
|
|
1630
|
+
const match = candidateByCode.get(providerId);
|
|
1631
|
+
if (match) {
|
|
1632
|
+
return match;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return undefined;
|
|
1636
|
+
};
|
|
1637
|
+
// Reliability first, then price, then the first surviving candidate.
|
|
1638
|
+
return (pickBySortOrder('reliability') ??
|
|
1639
|
+
pickBySortOrder('price') ??
|
|
1640
|
+
candidates[0]);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Returns the region's providers that support the given asset, plus the full
|
|
1644
|
+
* region provider list. Uses cached providers only when the request targets
|
|
1645
|
+
* the current region; otherwise fetches for the requested region, since
|
|
1646
|
+
* `getProviders` does not persist results for a non-current region. Does not
|
|
1647
|
+
* mutate state.
|
|
1648
|
+
*
|
|
1649
|
+
* @param options - The options.
|
|
1650
|
+
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
1651
|
+
* @param options.region - Region to resolve providers for.
|
|
1652
|
+
* @returns The supporting providers and the full region provider list.
|
|
1653
|
+
*/
|
|
1654
|
+
async #getSupportingProvidersForRegion({ assetId, region, }) {
|
|
1655
|
+
const normalizedRegion = region.toLowerCase().trim();
|
|
1656
|
+
let providers;
|
|
1657
|
+
if (this.#isRegionCurrent(normalizedRegion) &&
|
|
1658
|
+
this.state.providers.data.length > 0) {
|
|
1659
|
+
providers = this.state.providers.data;
|
|
1660
|
+
}
|
|
1661
|
+
else {
|
|
1662
|
+
({ providers } = await this.getProviders(normalizedRegion));
|
|
1663
|
+
}
|
|
1664
|
+
// Case-insensitive CAIP-19 matching is shared with headless-buy consumers
|
|
1665
|
+
// via `getProvidersServingAsset`, so the controller and the UI region gate
|
|
1666
|
+
// cannot disagree about which providers serve the asset.
|
|
1667
|
+
const supporting = getProvidersServingAsset(providers, assetId);
|
|
1668
|
+
return { supporting, all: providers };
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Filters an explicitly-requested provider ID list down to those that support
|
|
1672
|
+
* the asset in the region. Used for headless-buy gating so an explicitly
|
|
1673
|
+
* passed provider that cannot serve the region/asset yields no providers
|
|
1674
|
+
* rather than being trusted blindly.
|
|
1675
|
+
*
|
|
1676
|
+
* @param options - The options.
|
|
1677
|
+
* @param options.providerIds - Explicitly requested provider IDs.
|
|
1678
|
+
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
1679
|
+
* @param options.region - Region to resolve providers for.
|
|
1680
|
+
* @returns The subset of `providerIds` supporting the asset in the region.
|
|
1681
|
+
*/
|
|
1682
|
+
async #filterProviderIdsBySupport({ providerIds, assetId, region, }) {
|
|
1683
|
+
const { supporting } = await this.#getSupportingProvidersForRegion({
|
|
1684
|
+
assetId,
|
|
1685
|
+
region,
|
|
1686
|
+
});
|
|
1687
|
+
const supportingIds = new Set(supporting.map((provider) => provider.id));
|
|
1688
|
+
return providerIds.filter((id) => supportingIds.has(id));
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Resolves the provider IDs to use for a single quote request, scoped to the
|
|
1692
|
+
* given asset and region. Does not mutate `providers.selected` or any other
|
|
1693
|
+
* state.
|
|
1694
|
+
*
|
|
1695
|
+
* Resolves against the region's supporting providers using this precedence:
|
|
1696
|
+
* 1. The currently selected provider, if it is in the supporting set.
|
|
1697
|
+
* 2. The first preferred provider that supports the asset, where the
|
|
1698
|
+
* preference is taken from `preferredProviderIds` when supplied, otherwise
|
|
1699
|
+
* derived from the user's completed-order history (most recent first).
|
|
1700
|
+
* This takes priority over Transak Native to preserve an existing KYC
|
|
1701
|
+
* relationship.
|
|
1702
|
+
* 3. A native provider (e.g. Transak Native).
|
|
1703
|
+
* 4. The first supporting provider — unless `restrictToKnownOrNative` is set, in
|
|
1704
|
+
* which case no further provider is introduced and nothing is returned.
|
|
1705
|
+
*
|
|
1706
|
+
* When `restrictToKnownOrNative` is unset and no provider supports the asset, falls
|
|
1707
|
+
* back to all known provider IDs so the request behaves as if no
|
|
1708
|
+
* auto-selection occurred.
|
|
1709
|
+
*
|
|
1710
|
+
* @param options - The options.
|
|
1711
|
+
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
1712
|
+
* @param options.region - Region to resolve providers for.
|
|
1713
|
+
* @param options.preferredProviderIds - Provider IDs to prefer, in order.
|
|
1714
|
+
* @param options.restrictToKnownOrNative - When true, resolve only a native provider
|
|
1715
|
+
* (or no providers when none supports the asset).
|
|
1716
|
+
* @returns Provider IDs for this request only.
|
|
1717
|
+
*/
|
|
1718
|
+
async #resolveProviderIdsForQuote({ assetId, region, preferredProviderIds, restrictToKnownOrNative, }) {
|
|
1719
|
+
const { supporting, all } = await this.#getSupportingProvidersForRegion({
|
|
1720
|
+
assetId,
|
|
1721
|
+
region,
|
|
1722
|
+
});
|
|
1723
|
+
// When not restricted and nothing supports the asset, behave as if no
|
|
1724
|
+
// auto-selection occurred (quote against all known providers).
|
|
1725
|
+
if (!restrictToKnownOrNative && supporting.length === 0) {
|
|
1726
|
+
return all.map((provider) => provider.id);
|
|
1727
|
+
}
|
|
1728
|
+
// 1. The currently selected provider, if it supports the asset.
|
|
1729
|
+
const { selected } = this.state.providers;
|
|
1730
|
+
if (selected &&
|
|
1731
|
+
supporting.some((provider) => provider.id === selected.id)) {
|
|
1732
|
+
return [selected.id];
|
|
1733
|
+
}
|
|
1734
|
+
// 2. A provider the user has transacted with before — from
|
|
1735
|
+
// `preferredProviderIds` when supplied, otherwise completed-order
|
|
1736
|
+
// history. Takes priority over Transak Native to preserve an existing
|
|
1737
|
+
// KYC relationship and avoid churn.
|
|
1738
|
+
const preferred = preferredProviderIds ?? this.#getPreferredProviderIdsFromOrders();
|
|
1739
|
+
for (const preferredId of preferred) {
|
|
1740
|
+
const match = supporting.find((provider) => provider.id === preferredId);
|
|
1741
|
+
if (match) {
|
|
1742
|
+
return [match.id];
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// 3. A native provider (e.g. Transak Native).
|
|
1746
|
+
const nativeProvider = supporting.find((provider) => provider.type === 'native');
|
|
1747
|
+
if (nativeProvider) {
|
|
1748
|
+
return [nativeProvider.id];
|
|
1749
|
+
}
|
|
1750
|
+
// 4. Fallback. Under headless gating, introduce no other provider — return
|
|
1751
|
+
// nothing so the caller surfaces an "unavailable" state. Otherwise the
|
|
1752
|
+
// aggregator and all other providers are treated equally: first wins.
|
|
1753
|
+
if (restrictToKnownOrNative) {
|
|
1754
|
+
return [];
|
|
1755
|
+
}
|
|
1756
|
+
return [supporting[0].id];
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Resolves provider IDs that should contribute payment methods for a
|
|
1760
|
+
* quoting context. Mirrors {@link getQuotes} provider-set selection, with
|
|
1761
|
+
* one intentional difference on the widened path: when the all-providers
|
|
1762
|
+
* flag allowlist is non-empty, returns supporting providers intersected
|
|
1763
|
+
* with that allowlist (pick survivors for the picker). Does not mutate
|
|
1764
|
+
* state.
|
|
1765
|
+
*
|
|
1766
|
+
* @param options - Resolution inputs aligned with `getQuotes`.
|
|
1767
|
+
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
1768
|
+
* @param options.region - Region to resolve providers for.
|
|
1769
|
+
* @param options.providers - Explicit provider IDs, when provided.
|
|
1770
|
+
* @param options.autoSelectProvider - Resolve providers like `getQuotes`.
|
|
1771
|
+
* @param options.preferredProviderIds - Preferred provider IDs in order.
|
|
1772
|
+
* @param options.restrictToKnownOrNativeProviders - Headless gating.
|
|
1773
|
+
* @returns Provider IDs for this request only.
|
|
1774
|
+
*/
|
|
1775
|
+
async #resolveProviderIdsForPaymentMethods({ assetId, region, providers, autoSelectProvider, preferredProviderIds, restrictToKnownOrNativeProviders, }) {
|
|
1776
|
+
const wantsAutoSelection = !providers &&
|
|
1777
|
+
(autoSelectProvider === true ||
|
|
1778
|
+
restrictToKnownOrNativeProviders === true);
|
|
1779
|
+
const { enabled: allProvidersEnabled, allowlist: providerAllowlist } = wantsAutoSelection
|
|
1780
|
+
? this.#resolveAllProvidersFlag()
|
|
1781
|
+
: { enabled: false, allowlist: undefined };
|
|
1782
|
+
const widenToAllProviders = wantsAutoSelection && allProvidersEnabled;
|
|
1783
|
+
if (providers) {
|
|
1784
|
+
return restrictToKnownOrNativeProviders
|
|
1785
|
+
? this.#filterProviderIdsBySupport({
|
|
1786
|
+
providerIds: providers,
|
|
1787
|
+
assetId,
|
|
1788
|
+
region,
|
|
1789
|
+
})
|
|
1790
|
+
: providers;
|
|
1791
|
+
}
|
|
1792
|
+
if (widenToAllProviders) {
|
|
1793
|
+
const { supporting } = await this.#getSupportingProvidersForRegion({
|
|
1794
|
+
assetId,
|
|
1795
|
+
region,
|
|
1796
|
+
});
|
|
1797
|
+
if (providerAllowlist && providerAllowlist.length > 0) {
|
|
1798
|
+
const allowedProviderIds = new Set(providerAllowlist.map(normalizeHeadlessProviderId));
|
|
1799
|
+
return supporting
|
|
1800
|
+
.filter((provider) => allowedProviderIds.has(normalizeHeadlessProviderId(provider.id)))
|
|
1801
|
+
.map((provider) => provider.id);
|
|
1802
|
+
}
|
|
1803
|
+
return supporting.map((provider) => provider.id);
|
|
1804
|
+
}
|
|
1805
|
+
if (autoSelectProvider || restrictToKnownOrNativeProviders) {
|
|
1806
|
+
return this.#resolveProviderIdsForQuote({
|
|
1807
|
+
assetId,
|
|
1808
|
+
region,
|
|
1809
|
+
preferredProviderIds,
|
|
1810
|
+
restrictToKnownOrNative: restrictToKnownOrNativeProviders,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
const selectedId = this.state.providers.selected?.id;
|
|
1814
|
+
return selectedId ? [selectedId] : [];
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Derives an ordered list of provider IDs from the user's completed-order
|
|
1818
|
+
* history, most recently completed first, with duplicates removed.
|
|
1819
|
+
*
|
|
1820
|
+
* Reads only this controller's own normalized order state, so it carries no
|
|
1821
|
+
* dependency on any client-specific order representation.
|
|
1822
|
+
*
|
|
1823
|
+
* @returns Provider IDs ordered by most recent completed order.
|
|
1824
|
+
*/
|
|
1825
|
+
#getPreferredProviderIdsFromOrders() {
|
|
1826
|
+
const orderedIds = [];
|
|
1827
|
+
const completedOrders = this.state.orders
|
|
1828
|
+
.filter((order) => order.status === RampsOrderStatus.Completed && order.provider?.id)
|
|
1829
|
+
.sort((orderA, orderB) => orderB.createdAt - orderA.createdAt);
|
|
1830
|
+
for (const order of completedOrders) {
|
|
1831
|
+
const id = order.provider?.id;
|
|
1832
|
+
if (id && !orderedIds.includes(id)) {
|
|
1833
|
+
orderedIds.push(id);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return orderedIds;
|
|
1837
|
+
}
|
|
1342
1838
|
// === ORDER MANAGEMENT ===
|
|
1343
1839
|
/**
|
|
1344
1840
|
* Adds or updates a V2 order in controller state.
|
|
@@ -1376,7 +1872,387 @@ export class RampsController extends BaseController {
|
|
|
1376
1872
|
this.update((state) => {
|
|
1377
1873
|
state.orders = state.orders.filter((order) => order.providerOrderId !== providerOrderId);
|
|
1378
1874
|
});
|
|
1379
|
-
|
|
1875
|
+
this.#orderPollingMeta.delete(providerOrderId);
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Adds or updates a local autoramp last-seen cursor (e.g. after create).
|
|
1879
|
+
*
|
|
1880
|
+
* @param accountOrInput - Full account or create fields.
|
|
1881
|
+
* @returns The upserted {@link AutorampAccount}.
|
|
1882
|
+
*/
|
|
1883
|
+
addAutoramp(accountOrInput) {
|
|
1884
|
+
const account = isFullAutorampAccount(accountOrInput)
|
|
1885
|
+
? accountOrInput
|
|
1886
|
+
: createAutorampAccount(accountOrInput);
|
|
1887
|
+
this.update((state) => {
|
|
1888
|
+
const idx = state.autoramps.findIndex((existing) => existing.id === account.id);
|
|
1889
|
+
if (idx === -1) {
|
|
1890
|
+
state.autoramps.push(account);
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
state.autoramps[idx] = {
|
|
1894
|
+
...state.autoramps[idx],
|
|
1895
|
+
...account,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
return (this.state.autoramps.find((existing) => existing.id === account.id) ??
|
|
1900
|
+
account);
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Creates an autoramp via the neo-bank proxy and applies the returned
|
|
1904
|
+
* snapshot as the local last-seen cursor.
|
|
1905
|
+
*
|
|
1906
|
+
* The vendor `customer_id` is resolved via
|
|
1907
|
+
* {@link RampsController.resolveAutorampCustomerId} and injected into the
|
|
1908
|
+
* request (any caller-supplied `customer_id` is overwritten).
|
|
1909
|
+
*
|
|
1910
|
+
* @param request - CreateAutoramp payload.
|
|
1911
|
+
* @param options - Optional idempotency key forwarded to the proxy.
|
|
1912
|
+
* @param options.idempotencyKey - Value sent as `Idempotency-Key`.
|
|
1913
|
+
* @returns The created/updated local {@link AutorampAccount}.
|
|
1914
|
+
*/
|
|
1915
|
+
async createAutoramp(request, options = {}) {
|
|
1916
|
+
const customerId = await this.resolveAutorampCustomerId();
|
|
1917
|
+
const body = { ...request, customer_id: customerId };
|
|
1918
|
+
const remote = await this.messenger.call('NeoBankService:createAutoramp', body, options);
|
|
1919
|
+
return this.#applyAutorampRemoteSnapshot(remote);
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Resolves the vendor `customer_id` for autoramp / Money Account operations.
|
|
1923
|
+
*
|
|
1924
|
+
* Maps the wallet's Profile Sync id (the partner `external_id`) to the
|
|
1925
|
+
* vendor customer via `GET /neobank/customers/{external_id}/external`.
|
|
1926
|
+
*
|
|
1927
|
+
* @returns The vendor customer id.
|
|
1928
|
+
*/
|
|
1929
|
+
async resolveAutorampCustomerId() {
|
|
1930
|
+
const profile = await this.messenger.call('AuthenticationController:getSessionProfile');
|
|
1931
|
+
const canonical = profile?.canonicalProfileId;
|
|
1932
|
+
const externalId = typeof canonical === 'string' && canonical.length > 0
|
|
1933
|
+
? canonical
|
|
1934
|
+
: profile?.profileId;
|
|
1935
|
+
if (typeof externalId !== 'string' || externalId.length === 0) {
|
|
1936
|
+
throw new Error('Cannot resolve MoonPay customer id: wallet is not signed in to Profile Sync.');
|
|
1937
|
+
}
|
|
1938
|
+
const customer = await this.messenger.call('NeoBankService:getCustomerByExternalId', externalId);
|
|
1939
|
+
const customerId = customer &&
|
|
1940
|
+
typeof customer === 'object' &&
|
|
1941
|
+
typeof customer.id === 'string'
|
|
1942
|
+
? customer.id
|
|
1943
|
+
: null;
|
|
1944
|
+
if (!customerId) {
|
|
1945
|
+
throw new Error(`Cannot resolve MoonPay customer id: no MoonPay customer is mapped to external id "${externalId}".`);
|
|
1946
|
+
}
|
|
1947
|
+
return customerId;
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Registers a Money Account wallet with MoonPay Iron via neobank-proxy.
|
|
1951
|
+
*
|
|
1952
|
+
* @param params - Money Account wallet registration parameters.
|
|
1953
|
+
* @param params.address - Monad Money Account address.
|
|
1954
|
+
* @returns The registration state, or `{ type: 'lookupUnavailable' }` when
|
|
1955
|
+
* the address-list lookup fails (never treated as unregistered).
|
|
1956
|
+
*/
|
|
1957
|
+
async registerMoneyAccountWallet({ address, }) {
|
|
1958
|
+
let machine = transitionWalletRegistration(createInitialWalletRegistrationState(), { type: 'START' });
|
|
1959
|
+
const toExistingResult = (status) => {
|
|
1960
|
+
if (status.type === 'active') {
|
|
1961
|
+
return { type: 'alreadyRegistered', registration: status.registration };
|
|
1962
|
+
}
|
|
1963
|
+
if (status.type === 'disabled') {
|
|
1964
|
+
return {
|
|
1965
|
+
type: 'registeredDisabled',
|
|
1966
|
+
registration: status.registration,
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
return undefined;
|
|
1970
|
+
};
|
|
1971
|
+
const customerId = await this.resolveAutorampCustomerId();
|
|
1972
|
+
const toLookupUnavailableResult = (error) => {
|
|
1973
|
+
machine = transitionWalletRegistration(machine, {
|
|
1974
|
+
type: 'LOOKUP_FAILED',
|
|
1975
|
+
});
|
|
1976
|
+
return {
|
|
1977
|
+
type: 'lookupUnavailable',
|
|
1978
|
+
error: error instanceof WalletRegistrationError
|
|
1979
|
+
? error
|
|
1980
|
+
: new WalletRegistrationError('lookupUnavailable', {
|
|
1981
|
+
message: 'self-hosted address lookup failed',
|
|
1982
|
+
body: error instanceof Error ? error.message : undefined,
|
|
1983
|
+
}),
|
|
1984
|
+
};
|
|
1985
|
+
};
|
|
1986
|
+
const lookup = async () => {
|
|
1987
|
+
try {
|
|
1988
|
+
return await this.messenger.call('NeoBankService:getWalletRegistrationStatus', { customerId, address });
|
|
1989
|
+
}
|
|
1990
|
+
catch (error) {
|
|
1991
|
+
return toLookupUnavailableResult(error);
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
const applyLookup = (status) => {
|
|
1995
|
+
let eventType = 'LOOKUP_ABSENT';
|
|
1996
|
+
if (status.type === 'active') {
|
|
1997
|
+
eventType = 'LOOKUP_ACTIVE';
|
|
1998
|
+
}
|
|
1999
|
+
else if (status.type === 'disabled') {
|
|
2000
|
+
eventType = 'LOOKUP_DISABLED';
|
|
2001
|
+
}
|
|
2002
|
+
machine = transitionWalletRegistration(machine, {
|
|
2003
|
+
type: eventType,
|
|
2004
|
+
});
|
|
2005
|
+
return toExistingResult(status);
|
|
2006
|
+
};
|
|
2007
|
+
const resolveLookup = async () => {
|
|
2008
|
+
const status = await lookup();
|
|
2009
|
+
if (status.type === 'lookupUnavailable') {
|
|
2010
|
+
return status;
|
|
2011
|
+
}
|
|
2012
|
+
return applyLookup(status);
|
|
2013
|
+
};
|
|
2014
|
+
const existingResult = await resolveLookup();
|
|
2015
|
+
if (existingResult) {
|
|
2016
|
+
return existingResult;
|
|
2017
|
+
}
|
|
2018
|
+
let idempotencyKey = createIdempotencyKey();
|
|
2019
|
+
let lastMessage;
|
|
2020
|
+
while (true) {
|
|
2021
|
+
const message = buildOwnershipMessage({
|
|
2022
|
+
address,
|
|
2023
|
+
customerId,
|
|
2024
|
+
now: new Date(),
|
|
2025
|
+
});
|
|
2026
|
+
if (lastMessage !== undefined && message !== lastMessage) {
|
|
2027
|
+
idempotencyKey = createIdempotencyKey();
|
|
2028
|
+
}
|
|
2029
|
+
lastMessage = message;
|
|
2030
|
+
let signature;
|
|
2031
|
+
try {
|
|
2032
|
+
signature = await this.messenger.call('KeyringController:signPersonalMessage', { data: message, from: address });
|
|
2033
|
+
machine = transitionWalletRegistration(machine, { type: 'SIGN_OK' });
|
|
2034
|
+
}
|
|
2035
|
+
catch (error) {
|
|
2036
|
+
machine = transitionWalletRegistration(machine, {
|
|
2037
|
+
type: 'SIGN_FAILED',
|
|
2038
|
+
retryable: false,
|
|
2039
|
+
});
|
|
2040
|
+
throw error;
|
|
2041
|
+
}
|
|
2042
|
+
try {
|
|
2043
|
+
const result = await this.messenger.call('NeoBankService:registerSelfHostedWallet', {
|
|
2044
|
+
address,
|
|
2045
|
+
customerId,
|
|
2046
|
+
message,
|
|
2047
|
+
signature,
|
|
2048
|
+
idempotencyKey,
|
|
2049
|
+
});
|
|
2050
|
+
machine = transitionWalletRegistration(machine, { type: 'SUBMIT_OK' });
|
|
2051
|
+
return result;
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
if (!(error instanceof WalletRegistrationError)) {
|
|
2055
|
+
machine = transitionWalletRegistration(machine, {
|
|
2056
|
+
type: 'SUBMIT_TERMINAL',
|
|
2057
|
+
});
|
|
2058
|
+
throw error;
|
|
2059
|
+
}
|
|
2060
|
+
if (error.kind === 'conflict') {
|
|
2061
|
+
machine = transitionWalletRegistration(machine, {
|
|
2062
|
+
type: 'SUBMIT_CONFLICT',
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
else if (error.kind === 'transient') {
|
|
2066
|
+
machine = transitionWalletRegistration(machine, {
|
|
2067
|
+
type: 'SUBMIT_TRANSIENT',
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
else if (error.kind === 'validation') {
|
|
2071
|
+
machine = transitionWalletRegistration(machine, {
|
|
2072
|
+
type: 'SUBMIT_VALIDATION',
|
|
2073
|
+
utcRollover: buildOwnershipMessage({
|
|
2074
|
+
address,
|
|
2075
|
+
customerId,
|
|
2076
|
+
now: new Date(),
|
|
2077
|
+
}) !== message,
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
else if (error.kind === 'rateLimited') {
|
|
2081
|
+
machine = transitionWalletRegistration(machine, {
|
|
2082
|
+
type: 'SUBMIT_RATE_LIMITED',
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
machine = transitionWalletRegistration(machine, {
|
|
2087
|
+
type: 'SUBMIT_TERMINAL',
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
if (machine.status === 'disambiguate409' ||
|
|
2091
|
+
machine.status === 'checkThenRetry') {
|
|
2092
|
+
const reconciledResult = await resolveLookup();
|
|
2093
|
+
if (reconciledResult) {
|
|
2094
|
+
return reconciledResult;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
if (machine.status !== 'signing') {
|
|
2098
|
+
throw error;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Removes a local autoramp last-seen cursor by id.
|
|
2105
|
+
*
|
|
2106
|
+
* @param autorampId - MoonPay autoramp id.
|
|
2107
|
+
*/
|
|
2108
|
+
removeAutoramp(autorampId) {
|
|
2109
|
+
this.update((state) => {
|
|
2110
|
+
state.autoramps = state.autoramps.filter((autoramp) => autoramp.id !== autorampId);
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Marks that the UI has already notified for the autoramp's current status.
|
|
2115
|
+
*
|
|
2116
|
+
* @param autorampId - MoonPay autoramp id.
|
|
2117
|
+
*/
|
|
2118
|
+
markAutorampAsNotified(autorampId) {
|
|
2119
|
+
const existing = this.state.autoramps.find((autoramp) => autoramp.id === autorampId);
|
|
2120
|
+
if (!existing) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const notified = markAutorampNotified(existing);
|
|
2124
|
+
this.update((state) => {
|
|
2125
|
+
const idx = state.autoramps.findIndex((autoramp) => autoramp.id === autorampId);
|
|
2126
|
+
if (idx !== -1) {
|
|
2127
|
+
state.autoramps[idx] = notified;
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Applies a remote autoramp snapshot from a websocket / webhook push.
|
|
2133
|
+
*
|
|
2134
|
+
* @param remote - Remote autoramp snapshot.
|
|
2135
|
+
* @returns The updated local account.
|
|
2136
|
+
*/
|
|
2137
|
+
applyAutorampStatusFromPush(remote) {
|
|
2138
|
+
return this.#applyAutorampRemoteSnapshot(remote);
|
|
2139
|
+
}
|
|
2140
|
+
/**
|
|
2141
|
+
* Fetches one autoramp from the neo-bank proxy and applies it to the
|
|
2142
|
+
* last-seen cursor. Does not recreate a cursor that was removed while the
|
|
2143
|
+
* request was in flight.
|
|
2144
|
+
*
|
|
2145
|
+
* @param autorampId - MoonPay autoramp id.
|
|
2146
|
+
* @returns The updated local account, or an unpersisted snapshot if the
|
|
2147
|
+
* cursor was removed during the fetch.
|
|
2148
|
+
*/
|
|
2149
|
+
async refreshAutoramp(autorampId) {
|
|
2150
|
+
const remote = await this.messenger.call('NeoBankService:getAutoramp', autorampId);
|
|
2151
|
+
return this.#applyAutorampRemoteSnapshot(remote, { allowInsert: false });
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Refreshes all known local autoramps from MoonPay.
|
|
2155
|
+
* Intended for app resume / unlock catch-up when webhooks were missed.
|
|
2156
|
+
*
|
|
2157
|
+
* @returns Updated autoramp accounts (failed fetches are skipped).
|
|
2158
|
+
*/
|
|
2159
|
+
async refreshAutoramps() {
|
|
2160
|
+
const ids = this.state.autoramps.map((autoramp) => autoramp.id);
|
|
2161
|
+
const updated = [];
|
|
2162
|
+
for (const id of ids) {
|
|
2163
|
+
try {
|
|
2164
|
+
const account = await this.refreshAutoramp(id);
|
|
2165
|
+
if (this.state.autoramps.some((autoramp) => autoramp.id === account.id)) {
|
|
2166
|
+
updated.push(account);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
catch {
|
|
2170
|
+
// Keep local cursor for this id; continue remaining refreshes.
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return updated;
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Applies a remote snapshot to the last-seen cursor.
|
|
2177
|
+
*
|
|
2178
|
+
* @param remote - MoonPay snapshot.
|
|
2179
|
+
* @param options - Apply options.
|
|
2180
|
+
* @param options.allowInsert - When false, a missing local cursor is not
|
|
2181
|
+
* recreated (used by refresh so a concurrent `removeAutoramp` wins).
|
|
2182
|
+
* @returns The applied account. When `allowInsert` is false and no local
|
|
2183
|
+
* cursor exists, the snapshot is returned without writing state.
|
|
2184
|
+
*/
|
|
2185
|
+
#applyAutorampRemoteSnapshot(remote, { allowInsert = true } = {}) {
|
|
2186
|
+
const local = this.state.autoramps.find((autoramp) => autoramp.id === remote.id) ??
|
|
2187
|
+
null;
|
|
2188
|
+
if (local === null && !allowInsert) {
|
|
2189
|
+
return applyAutorampRemoteStatus(null, remote).account;
|
|
2190
|
+
}
|
|
2191
|
+
const result = applyAutorampRemoteStatus(local, remote);
|
|
2192
|
+
this.update((state) => {
|
|
2193
|
+
const idx = state.autoramps.findIndex((autoramp) => autoramp.id === result.account.id);
|
|
2194
|
+
if (idx === -1) {
|
|
2195
|
+
state.autoramps.push(result.account);
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
state.autoramps[idx] = result.account;
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
if (result.statusChanged) {
|
|
2202
|
+
this.messenger.publish('RampsController:autorampStatusChanged', {
|
|
2203
|
+
autoramp: result.account,
|
|
2204
|
+
previousStatus: result.previousStatus,
|
|
2205
|
+
shouldNotify: result.shouldNotify,
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
return (this.state.autoramps.find((autoramp) => autoramp.id === result.account.id) ?? result.account);
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Refreshes a single order via the V2 API and updates it in state.
|
|
2212
|
+
* Publishes orderStatusChanged if the status transitioned.
|
|
2213
|
+
*
|
|
2214
|
+
* @param order - The order to refresh (needs provider and providerOrderId).
|
|
2215
|
+
*/
|
|
2216
|
+
async #refreshOrder(order) {
|
|
2217
|
+
const providerCode = order.provider?.id ?? '';
|
|
2218
|
+
if (!providerCode || !order.providerOrderId || !order.walletAddress) {
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
const previousStatus = order.status;
|
|
2222
|
+
try {
|
|
2223
|
+
const updatedOrder = await this.getOrder(providerCode, order.providerOrderId, order.walletAddress);
|
|
2224
|
+
const meta = this.#orderPollingMeta.get(order.providerOrderId) ?? {
|
|
2225
|
+
lastTimeFetched: 0,
|
|
2226
|
+
errorCount: 0,
|
|
2227
|
+
};
|
|
2228
|
+
if (updatedOrder.status === RampsOrderStatus.Unknown) {
|
|
2229
|
+
meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
|
|
2230
|
+
}
|
|
2231
|
+
else {
|
|
2232
|
+
meta.errorCount = 0;
|
|
2233
|
+
}
|
|
2234
|
+
meta.lastTimeFetched = Date.now();
|
|
2235
|
+
this.#orderPollingMeta.set(order.providerOrderId, meta);
|
|
2236
|
+
if (previousStatus !== updatedOrder.status &&
|
|
2237
|
+
previousStatus !== undefined) {
|
|
2238
|
+
this.messenger.publish('RampsController:orderStatusChanged', {
|
|
2239
|
+
order: updatedOrder,
|
|
2240
|
+
previousStatus,
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (TERMINAL_ORDER_STATUSES.has(updatedOrder.status)) {
|
|
2244
|
+
this.#orderPollingMeta.delete(order.providerOrderId);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
catch {
|
|
2248
|
+
const meta = this.#orderPollingMeta.get(order.providerOrderId) ?? {
|
|
2249
|
+
lastTimeFetched: 0,
|
|
2250
|
+
errorCount: 0,
|
|
2251
|
+
};
|
|
2252
|
+
meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
|
|
2253
|
+
meta.lastTimeFetched = Date.now();
|
|
2254
|
+
this.#orderPollingMeta.set(order.providerOrderId, meta);
|
|
2255
|
+
}
|
|
1380
2256
|
}
|
|
1381
2257
|
/**
|
|
1382
2258
|
* Starts polling all pending V2 orders at a fixed interval.
|
|
@@ -1384,21 +2260,49 @@ export class RampsController extends BaseController {
|
|
|
1384
2260
|
* respects pollingSecondsMinimum and backoff from error count.
|
|
1385
2261
|
*/
|
|
1386
2262
|
startOrderPolling() {
|
|
1387
|
-
if (
|
|
2263
|
+
if (this.#orderPollingTimer) {
|
|
1388
2264
|
return;
|
|
1389
2265
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}, DEFAULT_POLLING_INTERVAL_MS)
|
|
1393
|
-
|
|
2266
|
+
this.#orderPollingTimer = setInterval(() => {
|
|
2267
|
+
this.#pollPendingOrders().catch(() => undefined);
|
|
2268
|
+
}, DEFAULT_POLLING_INTERVAL_MS);
|
|
2269
|
+
this.#pollPendingOrders().catch(() => undefined);
|
|
1394
2270
|
}
|
|
1395
2271
|
/**
|
|
1396
2272
|
* Stops order polling and clears the interval.
|
|
1397
2273
|
*/
|
|
1398
2274
|
stopOrderPolling() {
|
|
1399
|
-
if (
|
|
1400
|
-
clearInterval(
|
|
1401
|
-
|
|
2275
|
+
if (this.#orderPollingTimer) {
|
|
2276
|
+
clearInterval(this.#orderPollingTimer);
|
|
2277
|
+
this.#orderPollingTimer = null;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
async #pollPendingOrders() {
|
|
2281
|
+
if (this.#isPolling) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
this.#isPolling = true;
|
|
2285
|
+
try {
|
|
2286
|
+
const pendingOrders = this.state.orders.filter((order) => PENDING_ORDER_STATUSES.has(order.status));
|
|
2287
|
+
const now = Date.now();
|
|
2288
|
+
await Promise.allSettled(pendingOrders.map(async (order) => {
|
|
2289
|
+
const meta = this.#orderPollingMeta.get(order.providerOrderId);
|
|
2290
|
+
if (meta) {
|
|
2291
|
+
const backoffMs = meta.errorCount > 0
|
|
2292
|
+
? Math.min(DEFAULT_POLLING_INTERVAL_MS *
|
|
2293
|
+
Math.pow(2, meta.errorCount - 1), 5 * 60 * 1000)
|
|
2294
|
+
: 0;
|
|
2295
|
+
const pollingMinMs = (order.pollingSecondsMinimum ?? 0) * 1000;
|
|
2296
|
+
const minWait = Math.max(backoffMs, pollingMinMs);
|
|
2297
|
+
if (now - meta.lastTimeFetched < minWait) {
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
await this.#refreshOrder(order);
|
|
2302
|
+
}));
|
|
2303
|
+
}
|
|
2304
|
+
finally {
|
|
2305
|
+
this.#isPolling = false;
|
|
1402
2306
|
}
|
|
1403
2307
|
}
|
|
1404
2308
|
/**
|
|
@@ -1529,6 +2433,30 @@ export class RampsController extends BaseController {
|
|
|
1529
2433
|
const order = await this.messenger.call('RampsService:getOrderFromCallback', providerCode, callbackUrl, wallet);
|
|
1530
2434
|
return order;
|
|
1531
2435
|
}
|
|
2436
|
+
// === TRANSAK METHODS ===
|
|
2437
|
+
//
|
|
2438
|
+
// Auth state is managed at two levels:
|
|
2439
|
+
// - TransakService stores the access token (needed for API calls)
|
|
2440
|
+
// - RampsController stores isAuthenticated (needed for UI state)
|
|
2441
|
+
// Both are kept in sync by the controller methods below.
|
|
2442
|
+
/**
|
|
2443
|
+
* Checks whether an error is a 401 HTTP error (expired/missing token) and,
|
|
2444
|
+
* if so, marks the Transak session as unauthenticated so the UI stays in
|
|
2445
|
+
* sync with the cleared token inside TransakService.
|
|
2446
|
+
*
|
|
2447
|
+
* @param error - The caught error to inspect.
|
|
2448
|
+
*/
|
|
2449
|
+
#syncTransakAuthOnError(error) {
|
|
2450
|
+
if (hasHttpStatus(error) && error.httpStatus === 401) {
|
|
2451
|
+
this.transakSetAuthenticated(false);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
#getNormalizedTransakError(error, options = {}) {
|
|
2455
|
+
if (options.syncAuth) {
|
|
2456
|
+
this.#syncTransakAuthOnError(error);
|
|
2457
|
+
}
|
|
2458
|
+
return getNormalizedRampsError(error);
|
|
2459
|
+
}
|
|
1532
2460
|
/**
|
|
1533
2461
|
* Sets the Transak API key used for all Transak API requests.
|
|
1534
2462
|
*
|
|
@@ -1584,7 +2512,7 @@ export class RampsController extends BaseController {
|
|
|
1584
2512
|
return await this.messenger.call('TransakService:sendUserOtp', email);
|
|
1585
2513
|
}
|
|
1586
2514
|
catch (error) {
|
|
1587
|
-
throw
|
|
2515
|
+
throw this.#getNormalizedTransakError(error).normalizedError;
|
|
1588
2516
|
}
|
|
1589
2517
|
}
|
|
1590
2518
|
/**
|
|
@@ -1603,7 +2531,7 @@ export class RampsController extends BaseController {
|
|
|
1603
2531
|
return token;
|
|
1604
2532
|
}
|
|
1605
2533
|
catch (error) {
|
|
1606
|
-
throw
|
|
2534
|
+
throw this.#getNormalizedTransakError(error).normalizedError;
|
|
1607
2535
|
}
|
|
1608
2536
|
}
|
|
1609
2537
|
/**
|
|
@@ -1617,7 +2545,7 @@ export class RampsController extends BaseController {
|
|
|
1617
2545
|
return await this.messenger.call('TransakService:logout');
|
|
1618
2546
|
}
|
|
1619
2547
|
catch (error) {
|
|
1620
|
-
throw
|
|
2548
|
+
throw this.#getNormalizedTransakError(error).normalizedError;
|
|
1621
2549
|
}
|
|
1622
2550
|
finally {
|
|
1623
2551
|
this.transakClearAccessToken();
|
|
@@ -1647,7 +2575,7 @@ export class RampsController extends BaseController {
|
|
|
1647
2575
|
return details;
|
|
1648
2576
|
}
|
|
1649
2577
|
catch (error) {
|
|
1650
|
-
const { errorInfo, normalizedError } =
|
|
2578
|
+
const { errorInfo, normalizedError } = this.#getNormalizedTransakError(error, {
|
|
1651
2579
|
syncAuth: true,
|
|
1652
2580
|
});
|
|
1653
2581
|
this.update((state) => {
|
|
@@ -1684,7 +2612,7 @@ export class RampsController extends BaseController {
|
|
|
1684
2612
|
return quote;
|
|
1685
2613
|
}
|
|
1686
2614
|
catch (error) {
|
|
1687
|
-
const { errorInfo, normalizedError } =
|
|
2615
|
+
const { errorInfo, normalizedError } = this.#getNormalizedTransakError(error);
|
|
1688
2616
|
this.update((state) => {
|
|
1689
2617
|
state.nativeProviders.transak.buyQuote.isLoading = false;
|
|
1690
2618
|
state.nativeProviders.transak.buyQuote.error = errorInfo.message;
|
|
@@ -1715,7 +2643,7 @@ export class RampsController extends BaseController {
|
|
|
1715
2643
|
return requirement;
|
|
1716
2644
|
}
|
|
1717
2645
|
catch (error) {
|
|
1718
|
-
const { errorInfo, normalizedError } =
|
|
2646
|
+
const { errorInfo, normalizedError } = this.#getNormalizedTransakError(error, {
|
|
1719
2647
|
syncAuth: true,
|
|
1720
2648
|
});
|
|
1721
2649
|
this.update((state) => {
|
|
@@ -1738,7 +2666,7 @@ export class RampsController extends BaseController {
|
|
|
1738
2666
|
return await this.messenger.call('TransakService:getAdditionalRequirements', quoteId);
|
|
1739
2667
|
}
|
|
1740
2668
|
catch (error) {
|
|
1741
|
-
throw
|
|
2669
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1742
2670
|
.normalizedError;
|
|
1743
2671
|
}
|
|
1744
2672
|
}
|
|
@@ -1756,7 +2684,7 @@ export class RampsController extends BaseController {
|
|
|
1756
2684
|
return await this.messenger.call('TransakService:createOrder', quoteId, walletAddress, paymentMethodId);
|
|
1757
2685
|
}
|
|
1758
2686
|
catch (error) {
|
|
1759
|
-
throw
|
|
2687
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1760
2688
|
.normalizedError;
|
|
1761
2689
|
}
|
|
1762
2690
|
}
|
|
@@ -1773,7 +2701,7 @@ export class RampsController extends BaseController {
|
|
|
1773
2701
|
return await this.messenger.call('TransakService:getOrder', orderId, wallet, paymentDetails);
|
|
1774
2702
|
}
|
|
1775
2703
|
catch (error) {
|
|
1776
|
-
throw
|
|
2704
|
+
throw this.#getNormalizedTransakError(error).normalizedError;
|
|
1777
2705
|
}
|
|
1778
2706
|
}
|
|
1779
2707
|
/**
|
|
@@ -1789,7 +2717,7 @@ export class RampsController extends BaseController {
|
|
|
1789
2717
|
return await this.messenger.call('TransakService:getUserLimits', fiatCurrency, paymentMethod, kycType);
|
|
1790
2718
|
}
|
|
1791
2719
|
catch (error) {
|
|
1792
|
-
throw
|
|
2720
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1793
2721
|
.normalizedError;
|
|
1794
2722
|
}
|
|
1795
2723
|
}
|
|
@@ -1803,7 +2731,7 @@ export class RampsController extends BaseController {
|
|
|
1803
2731
|
return await this.messenger.call('TransakService:requestOtt');
|
|
1804
2732
|
}
|
|
1805
2733
|
catch (error) {
|
|
1806
|
-
throw
|
|
2734
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1807
2735
|
.normalizedError;
|
|
1808
2736
|
}
|
|
1809
2737
|
}
|
|
@@ -1834,7 +2762,7 @@ export class RampsController extends BaseController {
|
|
|
1834
2762
|
return await this.messenger.call('TransakService:createWidgetUrl', quote, walletAddress, extraParams);
|
|
1835
2763
|
}
|
|
1836
2764
|
catch (error) {
|
|
1837
|
-
throw
|
|
2765
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1838
2766
|
.normalizedError;
|
|
1839
2767
|
}
|
|
1840
2768
|
}
|
|
@@ -1849,7 +2777,7 @@ export class RampsController extends BaseController {
|
|
|
1849
2777
|
return await this.messenger.call('TransakService:submitPurposeOfUsageForm', purpose);
|
|
1850
2778
|
}
|
|
1851
2779
|
catch (error) {
|
|
1852
|
-
throw
|
|
2780
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1853
2781
|
.normalizedError;
|
|
1854
2782
|
}
|
|
1855
2783
|
}
|
|
@@ -1864,7 +2792,7 @@ export class RampsController extends BaseController {
|
|
|
1864
2792
|
return await this.messenger.call('TransakService:patchUser', data);
|
|
1865
2793
|
}
|
|
1866
2794
|
catch (error) {
|
|
1867
|
-
throw
|
|
2795
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1868
2796
|
.normalizedError;
|
|
1869
2797
|
}
|
|
1870
2798
|
}
|
|
@@ -1880,7 +2808,7 @@ export class RampsController extends BaseController {
|
|
|
1880
2808
|
return await this.messenger.call('TransakService:submitSsnDetails', ssn, quoteId);
|
|
1881
2809
|
}
|
|
1882
2810
|
catch (error) {
|
|
1883
|
-
throw
|
|
2811
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1884
2812
|
.normalizedError;
|
|
1885
2813
|
}
|
|
1886
2814
|
}
|
|
@@ -1896,7 +2824,7 @@ export class RampsController extends BaseController {
|
|
|
1896
2824
|
return await this.messenger.call('TransakService:confirmPayment', orderId, paymentMethodId);
|
|
1897
2825
|
}
|
|
1898
2826
|
catch (error) {
|
|
1899
|
-
throw
|
|
2827
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1900
2828
|
.normalizedError;
|
|
1901
2829
|
}
|
|
1902
2830
|
}
|
|
@@ -1911,7 +2839,7 @@ export class RampsController extends BaseController {
|
|
|
1911
2839
|
return await this.messenger.call('TransakService:getTranslation', request);
|
|
1912
2840
|
}
|
|
1913
2841
|
catch (error) {
|
|
1914
|
-
throw
|
|
2842
|
+
throw this.#getNormalizedTransakError(error).normalizedError;
|
|
1915
2843
|
}
|
|
1916
2844
|
}
|
|
1917
2845
|
/**
|
|
@@ -1925,7 +2853,7 @@ export class RampsController extends BaseController {
|
|
|
1925
2853
|
return await this.messenger.call('TransakService:getIdProofStatus', workFlowRunId);
|
|
1926
2854
|
}
|
|
1927
2855
|
catch (error) {
|
|
1928
|
-
throw
|
|
2856
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1929
2857
|
.normalizedError;
|
|
1930
2858
|
}
|
|
1931
2859
|
}
|
|
@@ -1940,7 +2868,7 @@ export class RampsController extends BaseController {
|
|
|
1940
2868
|
return await this.messenger.call('TransakService:cancelOrder', depositOrderId);
|
|
1941
2869
|
}
|
|
1942
2870
|
catch (error) {
|
|
1943
|
-
throw
|
|
2871
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1944
2872
|
.normalizedError;
|
|
1945
2873
|
}
|
|
1946
2874
|
}
|
|
@@ -1955,7 +2883,7 @@ export class RampsController extends BaseController {
|
|
|
1955
2883
|
return await this.messenger.call('TransakService:cancelAllActiveOrders');
|
|
1956
2884
|
}
|
|
1957
2885
|
catch (error) {
|
|
1958
|
-
throw
|
|
2886
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1959
2887
|
.normalizedError;
|
|
1960
2888
|
}
|
|
1961
2889
|
}
|
|
@@ -1969,455 +2897,9 @@ export class RampsController extends BaseController {
|
|
|
1969
2897
|
return await this.messenger.call('TransakService:getActiveOrders');
|
|
1970
2898
|
}
|
|
1971
2899
|
catch (error) {
|
|
1972
|
-
throw
|
|
2900
|
+
throw this.#getNormalizedTransakError(error, { syncAuth: true })
|
|
1973
2901
|
.normalizedError;
|
|
1974
2902
|
}
|
|
1975
2903
|
}
|
|
1976
2904
|
}
|
|
1977
|
-
|
|
1978
|
-
for (const resourceType of DEPENDENT_RESOURCE_KEYS) {
|
|
1979
|
-
__classPrivateFieldGet(this, _RampsController_pendingResourceCount, "f").delete(resourceType);
|
|
1980
|
-
const generation = __classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").get(resourceType) ?? 0;
|
|
1981
|
-
__classPrivateFieldGet(this, _RampsController_pendingResourceGeneration, "f").set(resourceType, generation + 1);
|
|
1982
|
-
}
|
|
1983
|
-
}, _RampsController_abortDependentRequests = function _RampsController_abortDependentRequests() {
|
|
1984
|
-
for (const [cacheKey, pending] of __classPrivateFieldGet(this, _RampsController_pendingRequests, "f").entries()) {
|
|
1985
|
-
if (pending.resourceType &&
|
|
1986
|
-
DEPENDENT_RESOURCE_KEYS_SET.has(pending.resourceType)) {
|
|
1987
|
-
pending.abortController.abort();
|
|
1988
|
-
__classPrivateFieldGet(this, _RampsController_pendingRequests, "f").delete(cacheKey);
|
|
1989
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_removeRequestState).call(this, cacheKey);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
}, _RampsController_resolveAllProvidersFlag = function _RampsController_resolveAllProvidersFlag() {
|
|
1993
|
-
try {
|
|
1994
|
-
const remoteFeatureFlagState = this.messenger.call('RemoteFeatureFlagController:getState');
|
|
1995
|
-
return {
|
|
1996
|
-
enabled: isHeadlessAllProvidersEnabled(remoteFeatureFlagState),
|
|
1997
|
-
allowlist: getHeadlessProviderAllowlist(remoteFeatureFlagState),
|
|
1998
|
-
};
|
|
1999
|
-
}
|
|
2000
|
-
catch {
|
|
2001
|
-
return { enabled: false };
|
|
2002
|
-
}
|
|
2003
|
-
}, _RampsController_mutateRequests = function _RampsController_mutateRequests(fn) {
|
|
2004
|
-
this.update((state) => {
|
|
2005
|
-
const requests = state.requests;
|
|
2006
|
-
fn(requests);
|
|
2007
|
-
});
|
|
2008
|
-
}, _RampsController_removeRequestState = function _RampsController_removeRequestState(cacheKey) {
|
|
2009
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_mutateRequests).call(this, (requests) => {
|
|
2010
|
-
delete requests[cacheKey];
|
|
2011
|
-
});
|
|
2012
|
-
}, _RampsController_cleanupState = function _RampsController_cleanupState() {
|
|
2013
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_abortDependentRequests).call(this);
|
|
2014
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_clearPendingResourceCountForDependentResources).call(this);
|
|
2015
|
-
this.update((state) => resetDependentResources(state, { clearUserRegionData: true }));
|
|
2016
|
-
}, _RampsController_requireRegion = function _RampsController_requireRegion() {
|
|
2017
|
-
const regionCode = this.state.userRegion?.regionCode;
|
|
2018
|
-
if (!regionCode) {
|
|
2019
|
-
throw new Error('Region is required. Cannot proceed without valid region information.');
|
|
2020
|
-
}
|
|
2021
|
-
return regionCode;
|
|
2022
|
-
}, _RampsController_isRegionCurrent = function _RampsController_isRegionCurrent(normalizedRegion) {
|
|
2023
|
-
const current = this.state.userRegion?.regionCode;
|
|
2024
|
-
return current === undefined || current === normalizedRegion;
|
|
2025
|
-
}, _RampsController_isTokenCurrent = function _RampsController_isTokenCurrent(normalizedAssetId) {
|
|
2026
|
-
const current = this.state.tokens.selected?.assetId ?? '';
|
|
2027
|
-
return current === normalizedAssetId;
|
|
2028
|
-
}, _RampsController_isProviderCurrent = function _RampsController_isProviderCurrent(normalizedProviderId) {
|
|
2029
|
-
const current = this.state.providers.selected?.id ?? '';
|
|
2030
|
-
return current === normalizedProviderId;
|
|
2031
|
-
}, _RampsController_updateResourceField = function _RampsController_updateResourceField(resourceType, field, value) {
|
|
2032
|
-
this.update((state) => {
|
|
2033
|
-
const resource = getResourceState(state, resourceType);
|
|
2034
|
-
resource[field] = value;
|
|
2035
|
-
});
|
|
2036
|
-
}, _RampsController_setResourceLoading = function _RampsController_setResourceLoading(resourceType, loading) {
|
|
2037
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_updateResourceField).call(this, resourceType, 'isLoading', loading);
|
|
2038
|
-
}, _RampsController_setResourceError = function _RampsController_setResourceError(resourceType, errorInfo) {
|
|
2039
|
-
this.update((state) => {
|
|
2040
|
-
const resource = getResourceState(state, resourceType);
|
|
2041
|
-
resource.error = errorInfo?.message ?? null;
|
|
2042
|
-
resource.errorKey = errorInfo?.errorKey ?? null;
|
|
2043
|
-
});
|
|
2044
|
-
}, _RampsController_updateRequestState = function _RampsController_updateRequestState(cacheKey, requestState) {
|
|
2045
|
-
const maxSize = __classPrivateFieldGet(this, _RampsController_requestCacheMaxSize, "f");
|
|
2046
|
-
const ttl = __classPrivateFieldGet(this, _RampsController_requestCacheTTL, "f");
|
|
2047
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_mutateRequests).call(this, (requests) => {
|
|
2048
|
-
requests[cacheKey] = requestState;
|
|
2049
|
-
const keys = Object.keys(requests);
|
|
2050
|
-
for (const key of keys) {
|
|
2051
|
-
const entry = requests[key];
|
|
2052
|
-
if (entry?.status === RequestStatus.SUCCESS &&
|
|
2053
|
-
isCacheExpired(entry, ttl)) {
|
|
2054
|
-
delete requests[key];
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
const remainingKeys = Object.keys(requests);
|
|
2058
|
-
if (remainingKeys.length > maxSize) {
|
|
2059
|
-
const sortedKeys = remainingKeys.sort((a, b) => {
|
|
2060
|
-
const aTime = requests[a]?.timestamp ?? 0;
|
|
2061
|
-
const bTime = requests[b]?.timestamp ?? 0;
|
|
2062
|
-
return aTime - bTime;
|
|
2063
|
-
});
|
|
2064
|
-
const entriesToRemove = remainingKeys.length - maxSize;
|
|
2065
|
-
for (let i = 0; i < entriesToRemove; i++) {
|
|
2066
|
-
const keyToRemove = sortedKeys[i];
|
|
2067
|
-
if (keyToRemove) {
|
|
2068
|
-
delete requests[keyToRemove];
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
});
|
|
2073
|
-
}, _RampsController_runInit = async function _RampsController_runInit(options) {
|
|
2074
|
-
// Force-refetch the catalog on startup so region preset amounts stay
|
|
2075
|
-
// current, bypassing the in-session request cache. The catalog is not
|
|
2076
|
-
// persisted, so a cold start always re-fetches it regardless.
|
|
2077
|
-
await this.getCountries({ ...options, forceRefresh: true });
|
|
2078
|
-
// Always prefer the user's persisted region. Geolocation is only used to
|
|
2079
|
-
// seed the initial value; once the user (or a prior init) has set a region
|
|
2080
|
-
// we must respect that choice — even on forceRefresh.
|
|
2081
|
-
const persistedRegionCode = this.state.userRegion?.regionCode;
|
|
2082
|
-
const regionCode = persistedRegionCode ??
|
|
2083
|
-
(await this.messenger.call('RampsService:getGeolocation'));
|
|
2084
|
-
if (!regionCode) {
|
|
2085
|
-
throw new Error('Failed to fetch geolocation. Cannot initialize controller without valid region information.');
|
|
2086
|
-
}
|
|
2087
|
-
// For an already-persisted region, getCountries() has already re-synced it
|
|
2088
|
-
// from the fresh catalog (see #syncUserRegionFromCountriesCatalog). Calling
|
|
2089
|
-
// setUserRegion here would re-validate against that catalog and, if it is
|
|
2090
|
-
// momentarily empty or no longer lists the region (e.g. a transient/partial
|
|
2091
|
-
// catalog response or a region with no current provider coverage), throw and
|
|
2092
|
-
// wipe the persisted region via #cleanupState. Preserve the existing region
|
|
2093
|
-
// instead; only resolve a brand-new region (from geolocation) strictly.
|
|
2094
|
-
if (persistedRegionCode) {
|
|
2095
|
-
return;
|
|
2096
|
-
}
|
|
2097
|
-
await this.setUserRegion(regionCode, options);
|
|
2098
|
-
}, _RampsController_syncUserRegionFromCountriesCatalog = function _RampsController_syncUserRegionFromCountriesCatalog() {
|
|
2099
|
-
const regionCode = this.state.userRegion?.regionCode;
|
|
2100
|
-
if (!regionCode) {
|
|
2101
|
-
return;
|
|
2102
|
-
}
|
|
2103
|
-
const countriesData = this.state.countries.data;
|
|
2104
|
-
if (!countriesData.length) {
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
const userRegion = findRegionFromCode(regionCode, countriesData);
|
|
2108
|
-
if (!userRegion) {
|
|
2109
|
-
return;
|
|
2110
|
-
}
|
|
2111
|
-
this.update((state) => {
|
|
2112
|
-
state.userRegion = userRegion;
|
|
2113
|
-
});
|
|
2114
|
-
}, _RampsController_pickWidenedQuote = function _RampsController_pickWidenedQuote(response, { amount, fiat, providers, allowlist, }) {
|
|
2115
|
-
const providerByCode = new Map(providers.map((provider) => [provider.id, provider]));
|
|
2116
|
-
const allowedProviderIds = allowlist && allowlist.length > 0
|
|
2117
|
-
? new Set(allowlist.map(normalizeHeadlessProviderId))
|
|
2118
|
-
: undefined;
|
|
2119
|
-
const isAllowedProvider = (quote) => !allowedProviderIds ||
|
|
2120
|
-
allowedProviderIds.has(normalizeHeadlessProviderId(quote.provider));
|
|
2121
|
-
const fitsProviderLimits = (quote) => {
|
|
2122
|
-
const provider = providerByCode.get(quote.provider);
|
|
2123
|
-
const limit = provider?.limits?.fiat?.[fiat]?.[quote.quote.paymentMethod];
|
|
2124
|
-
if (!limit) {
|
|
2125
|
-
// No published limits for this provider/payment method: treat as
|
|
2126
|
-
// eligible and let the provider enforce limits at checkout.
|
|
2127
|
-
return true;
|
|
2128
|
-
}
|
|
2129
|
-
return amount >= limit.minAmount && amount <= limit.maxAmount;
|
|
2130
|
-
};
|
|
2131
|
-
const candidates = response.success.filter((quote) => isAllowedProvider(quote) && fitsProviderLimits(quote));
|
|
2132
|
-
if (candidates.length === 0) {
|
|
2133
|
-
return undefined;
|
|
2134
|
-
}
|
|
2135
|
-
const candidateByCode = new Map(candidates.map((quote) => [quote.provider, quote]));
|
|
2136
|
-
const pickBySortOrder = (sortBy) => {
|
|
2137
|
-
const order = response.sorted.find((entry) => entry.sortBy === sortBy)?.ids;
|
|
2138
|
-
if (!order) {
|
|
2139
|
-
return undefined;
|
|
2140
|
-
}
|
|
2141
|
-
for (const providerId of order) {
|
|
2142
|
-
const match = candidateByCode.get(providerId);
|
|
2143
|
-
if (match) {
|
|
2144
|
-
return match;
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
return undefined;
|
|
2148
|
-
};
|
|
2149
|
-
// Reliability first, then price, then the first surviving candidate.
|
|
2150
|
-
return (pickBySortOrder('reliability') ??
|
|
2151
|
-
pickBySortOrder('price') ??
|
|
2152
|
-
candidates[0]);
|
|
2153
|
-
}, _RampsController_getSupportingProvidersForRegion =
|
|
2154
|
-
/**
|
|
2155
|
-
* Returns the region's providers that support the given asset, plus the full
|
|
2156
|
-
* region provider list. Uses cached providers only when the request targets
|
|
2157
|
-
* the current region; otherwise fetches for the requested region, since
|
|
2158
|
-
* `getProviders` does not persist results for a non-current region. Does not
|
|
2159
|
-
* mutate state.
|
|
2160
|
-
*
|
|
2161
|
-
* @param options - The options.
|
|
2162
|
-
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
2163
|
-
* @param options.region - Region to resolve providers for.
|
|
2164
|
-
* @returns The supporting providers and the full region provider list.
|
|
2165
|
-
*/
|
|
2166
|
-
async function _RampsController_getSupportingProvidersForRegion({ assetId, region, }) {
|
|
2167
|
-
const normalizedRegion = region.toLowerCase().trim();
|
|
2168
|
-
let providers;
|
|
2169
|
-
if (__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_isRegionCurrent).call(this, normalizedRegion) &&
|
|
2170
|
-
this.state.providers.data.length > 0) {
|
|
2171
|
-
providers = this.state.providers.data;
|
|
2172
|
-
}
|
|
2173
|
-
else {
|
|
2174
|
-
({ providers } = await this.getProviders(normalizedRegion));
|
|
2175
|
-
}
|
|
2176
|
-
// Case-insensitive CAIP-19 matching is shared with headless-buy consumers
|
|
2177
|
-
// via `getProvidersServingAsset`, so the controller and the UI region gate
|
|
2178
|
-
// cannot disagree about which providers serve the asset.
|
|
2179
|
-
const supporting = getProvidersServingAsset(providers, assetId);
|
|
2180
|
-
return { supporting, all: providers };
|
|
2181
|
-
}, _RampsController_filterProviderIdsBySupport =
|
|
2182
|
-
/**
|
|
2183
|
-
* Filters an explicitly-requested provider ID list down to those that support
|
|
2184
|
-
* the asset in the region. Used for headless-buy gating so an explicitly
|
|
2185
|
-
* passed provider that cannot serve the region/asset yields no providers
|
|
2186
|
-
* rather than being trusted blindly.
|
|
2187
|
-
*
|
|
2188
|
-
* @param options - The options.
|
|
2189
|
-
* @param options.providerIds - Explicitly requested provider IDs.
|
|
2190
|
-
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
2191
|
-
* @param options.region - Region to resolve providers for.
|
|
2192
|
-
* @returns The subset of `providerIds` supporting the asset in the region.
|
|
2193
|
-
*/
|
|
2194
|
-
async function _RampsController_filterProviderIdsBySupport({ providerIds, assetId, region, }) {
|
|
2195
|
-
const { supporting } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
|
|
2196
|
-
assetId,
|
|
2197
|
-
region,
|
|
2198
|
-
});
|
|
2199
|
-
const supportingIds = new Set(supporting.map((provider) => provider.id));
|
|
2200
|
-
return providerIds.filter((id) => supportingIds.has(id));
|
|
2201
|
-
}, _RampsController_resolveProviderIdsForQuote =
|
|
2202
|
-
/**
|
|
2203
|
-
* Resolves the provider IDs to use for a single quote request, scoped to the
|
|
2204
|
-
* given asset and region. Does not mutate `providers.selected` or any other
|
|
2205
|
-
* state.
|
|
2206
|
-
*
|
|
2207
|
-
* Resolves against the region's supporting providers using this precedence:
|
|
2208
|
-
* 1. The currently selected provider, if it is in the supporting set.
|
|
2209
|
-
* 2. The first preferred provider that supports the asset, where the
|
|
2210
|
-
* preference is taken from `preferredProviderIds` when supplied, otherwise
|
|
2211
|
-
* derived from the user's completed-order history (most recent first).
|
|
2212
|
-
* This takes priority over Transak Native to preserve an existing KYC
|
|
2213
|
-
* relationship.
|
|
2214
|
-
* 3. A native provider (e.g. Transak Native).
|
|
2215
|
-
* 4. The first supporting provider — unless `restrictToKnownOrNative` is set, in
|
|
2216
|
-
* which case no further provider is introduced and nothing is returned.
|
|
2217
|
-
*
|
|
2218
|
-
* When `restrictToKnownOrNative` is unset and no provider supports the asset, falls
|
|
2219
|
-
* back to all known provider IDs so the request behaves as if no
|
|
2220
|
-
* auto-selection occurred.
|
|
2221
|
-
*
|
|
2222
|
-
* @param options - The options.
|
|
2223
|
-
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
2224
|
-
* @param options.region - Region to resolve providers for.
|
|
2225
|
-
* @param options.preferredProviderIds - Provider IDs to prefer, in order.
|
|
2226
|
-
* @param options.restrictToKnownOrNative - When true, resolve only a native provider
|
|
2227
|
-
* (or no providers when none supports the asset).
|
|
2228
|
-
* @returns Provider IDs for this request only.
|
|
2229
|
-
*/
|
|
2230
|
-
async function _RampsController_resolveProviderIdsForQuote({ assetId, region, preferredProviderIds, restrictToKnownOrNative, }) {
|
|
2231
|
-
const { supporting, all } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
|
|
2232
|
-
assetId,
|
|
2233
|
-
region,
|
|
2234
|
-
});
|
|
2235
|
-
// When not restricted and nothing supports the asset, behave as if no
|
|
2236
|
-
// auto-selection occurred (quote against all known providers).
|
|
2237
|
-
if (!restrictToKnownOrNative && supporting.length === 0) {
|
|
2238
|
-
return all.map((provider) => provider.id);
|
|
2239
|
-
}
|
|
2240
|
-
// 1. The currently selected provider, if it supports the asset.
|
|
2241
|
-
const { selected } = this.state.providers;
|
|
2242
|
-
if (selected &&
|
|
2243
|
-
supporting.some((provider) => provider.id === selected.id)) {
|
|
2244
|
-
return [selected.id];
|
|
2245
|
-
}
|
|
2246
|
-
// 2. A provider the user has transacted with before — from
|
|
2247
|
-
// `preferredProviderIds` when supplied, otherwise completed-order
|
|
2248
|
-
// history. Takes priority over Transak Native to preserve an existing
|
|
2249
|
-
// KYC relationship and avoid churn.
|
|
2250
|
-
const preferred = preferredProviderIds ?? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getPreferredProviderIdsFromOrders).call(this);
|
|
2251
|
-
for (const preferredId of preferred) {
|
|
2252
|
-
const match = supporting.find((provider) => provider.id === preferredId);
|
|
2253
|
-
if (match) {
|
|
2254
|
-
return [match.id];
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
// 3. A native provider (e.g. Transak Native).
|
|
2258
|
-
const nativeProvider = supporting.find((provider) => provider.type === 'native');
|
|
2259
|
-
if (nativeProvider) {
|
|
2260
|
-
return [nativeProvider.id];
|
|
2261
|
-
}
|
|
2262
|
-
// 4. Fallback. Under headless gating, introduce no other provider — return
|
|
2263
|
-
// nothing so the caller surfaces an "unavailable" state. Otherwise the
|
|
2264
|
-
// aggregator and all other providers are treated equally: first wins.
|
|
2265
|
-
if (restrictToKnownOrNative) {
|
|
2266
|
-
return [];
|
|
2267
|
-
}
|
|
2268
|
-
return [supporting[0].id];
|
|
2269
|
-
}, _RampsController_resolveProviderIdsForPaymentMethods =
|
|
2270
|
-
/**
|
|
2271
|
-
* Resolves provider IDs that should contribute payment methods for a
|
|
2272
|
-
* quoting context. Mirrors {@link getQuotes} provider-set selection, with
|
|
2273
|
-
* one intentional difference on the widened path: when the all-providers
|
|
2274
|
-
* flag allowlist is non-empty, returns supporting providers intersected
|
|
2275
|
-
* with that allowlist (pick survivors for the picker). Does not mutate
|
|
2276
|
-
* state.
|
|
2277
|
-
*
|
|
2278
|
-
* @param options - Resolution inputs aligned with `getQuotes`.
|
|
2279
|
-
* @param options.assetId - CAIP-19 asset type identifier to resolve for.
|
|
2280
|
-
* @param options.region - Region to resolve providers for.
|
|
2281
|
-
* @param options.providers - Explicit provider IDs, when provided.
|
|
2282
|
-
* @param options.autoSelectProvider - Resolve providers like `getQuotes`.
|
|
2283
|
-
* @param options.preferredProviderIds - Preferred provider IDs in order.
|
|
2284
|
-
* @param options.restrictToKnownOrNativeProviders - Headless gating.
|
|
2285
|
-
* @returns Provider IDs for this request only.
|
|
2286
|
-
*/
|
|
2287
|
-
async function _RampsController_resolveProviderIdsForPaymentMethods({ assetId, region, providers, autoSelectProvider, preferredProviderIds, restrictToKnownOrNativeProviders, }) {
|
|
2288
|
-
const wantsAutoSelection = !providers &&
|
|
2289
|
-
(autoSelectProvider === true ||
|
|
2290
|
-
restrictToKnownOrNativeProviders === true);
|
|
2291
|
-
const { enabled: allProvidersEnabled, allowlist: providerAllowlist } = wantsAutoSelection
|
|
2292
|
-
? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_resolveAllProvidersFlag).call(this)
|
|
2293
|
-
: { enabled: false, allowlist: undefined };
|
|
2294
|
-
const widenToAllProviders = wantsAutoSelection && allProvidersEnabled;
|
|
2295
|
-
if (providers) {
|
|
2296
|
-
return restrictToKnownOrNativeProviders
|
|
2297
|
-
? __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_filterProviderIdsBySupport).call(this, {
|
|
2298
|
-
providerIds: providers,
|
|
2299
|
-
assetId,
|
|
2300
|
-
region,
|
|
2301
|
-
})
|
|
2302
|
-
: providers;
|
|
2303
|
-
}
|
|
2304
|
-
if (widenToAllProviders) {
|
|
2305
|
-
const { supporting } = await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_getSupportingProvidersForRegion).call(this, {
|
|
2306
|
-
assetId,
|
|
2307
|
-
region,
|
|
2308
|
-
});
|
|
2309
|
-
if (providerAllowlist && providerAllowlist.length > 0) {
|
|
2310
|
-
const allowedProviderIds = new Set(providerAllowlist.map(normalizeHeadlessProviderId));
|
|
2311
|
-
return supporting
|
|
2312
|
-
.filter((provider) => allowedProviderIds.has(normalizeHeadlessProviderId(provider.id)))
|
|
2313
|
-
.map((provider) => provider.id);
|
|
2314
|
-
}
|
|
2315
|
-
return supporting.map((provider) => provider.id);
|
|
2316
|
-
}
|
|
2317
|
-
if (autoSelectProvider || restrictToKnownOrNativeProviders) {
|
|
2318
|
-
return __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_resolveProviderIdsForQuote).call(this, {
|
|
2319
|
-
assetId,
|
|
2320
|
-
region,
|
|
2321
|
-
preferredProviderIds,
|
|
2322
|
-
restrictToKnownOrNative: restrictToKnownOrNativeProviders,
|
|
2323
|
-
});
|
|
2324
|
-
}
|
|
2325
|
-
const selectedId = this.state.providers.selected?.id;
|
|
2326
|
-
return selectedId ? [selectedId] : [];
|
|
2327
|
-
}, _RampsController_getPreferredProviderIdsFromOrders = function _RampsController_getPreferredProviderIdsFromOrders() {
|
|
2328
|
-
const orderedIds = [];
|
|
2329
|
-
const completedOrders = this.state.orders
|
|
2330
|
-
.filter((order) => order.status === RampsOrderStatus.Completed && order.provider?.id)
|
|
2331
|
-
.sort((orderA, orderB) => orderB.createdAt - orderA.createdAt);
|
|
2332
|
-
for (const order of completedOrders) {
|
|
2333
|
-
const id = order.provider?.id;
|
|
2334
|
-
if (id && !orderedIds.includes(id)) {
|
|
2335
|
-
orderedIds.push(id);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
return orderedIds;
|
|
2339
|
-
}, _RampsController_refreshOrder =
|
|
2340
|
-
/**
|
|
2341
|
-
* Refreshes a single order via the V2 API and updates it in state.
|
|
2342
|
-
* Publishes orderStatusChanged if the status transitioned.
|
|
2343
|
-
*
|
|
2344
|
-
* @param order - The order to refresh (needs provider and providerOrderId).
|
|
2345
|
-
*/
|
|
2346
|
-
async function _RampsController_refreshOrder(order) {
|
|
2347
|
-
const providerCode = order.provider?.id ?? '';
|
|
2348
|
-
if (!providerCode || !order.providerOrderId || !order.walletAddress) {
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
const previousStatus = order.status;
|
|
2352
|
-
try {
|
|
2353
|
-
const updatedOrder = await this.getOrder(providerCode, order.providerOrderId, order.walletAddress);
|
|
2354
|
-
const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
|
|
2355
|
-
lastTimeFetched: 0,
|
|
2356
|
-
errorCount: 0,
|
|
2357
|
-
};
|
|
2358
|
-
if (updatedOrder.status === RampsOrderStatus.Unknown) {
|
|
2359
|
-
meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
|
|
2360
|
-
}
|
|
2361
|
-
else {
|
|
2362
|
-
meta.errorCount = 0;
|
|
2363
|
-
}
|
|
2364
|
-
meta.lastTimeFetched = Date.now();
|
|
2365
|
-
__classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
|
|
2366
|
-
if (previousStatus !== updatedOrder.status &&
|
|
2367
|
-
previousStatus !== undefined) {
|
|
2368
|
-
this.messenger.publish('RampsController:orderStatusChanged', {
|
|
2369
|
-
order: updatedOrder,
|
|
2370
|
-
previousStatus,
|
|
2371
|
-
});
|
|
2372
|
-
}
|
|
2373
|
-
if (TERMINAL_ORDER_STATUSES.has(updatedOrder.status)) {
|
|
2374
|
-
__classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").delete(order.providerOrderId);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
catch {
|
|
2378
|
-
const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId) ?? {
|
|
2379
|
-
lastTimeFetched: 0,
|
|
2380
|
-
errorCount: 0,
|
|
2381
|
-
};
|
|
2382
|
-
meta.errorCount = Math.min(meta.errorCount + 1, MAX_ERROR_COUNT);
|
|
2383
|
-
meta.lastTimeFetched = Date.now();
|
|
2384
|
-
__classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").set(order.providerOrderId, meta);
|
|
2385
|
-
}
|
|
2386
|
-
}, _RampsController_pollPendingOrders = async function _RampsController_pollPendingOrders() {
|
|
2387
|
-
if (__classPrivateFieldGet(this, _RampsController_isPolling, "f")) {
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
__classPrivateFieldSet(this, _RampsController_isPolling, true, "f");
|
|
2391
|
-
try {
|
|
2392
|
-
const pendingOrders = this.state.orders.filter((order) => PENDING_ORDER_STATUSES.has(order.status));
|
|
2393
|
-
const now = Date.now();
|
|
2394
|
-
await Promise.allSettled(pendingOrders.map(async (order) => {
|
|
2395
|
-
const meta = __classPrivateFieldGet(this, _RampsController_orderPollingMeta, "f").get(order.providerOrderId);
|
|
2396
|
-
if (meta) {
|
|
2397
|
-
const backoffMs = meta.errorCount > 0
|
|
2398
|
-
? Math.min(DEFAULT_POLLING_INTERVAL_MS *
|
|
2399
|
-
Math.pow(2, meta.errorCount - 1), 5 * 60 * 1000)
|
|
2400
|
-
: 0;
|
|
2401
|
-
const pollingMinMs = (order.pollingSecondsMinimum ?? 0) * 1000;
|
|
2402
|
-
const minWait = Math.max(backoffMs, pollingMinMs);
|
|
2403
|
-
if (now - meta.lastTimeFetched < minWait) {
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
await __classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_refreshOrder).call(this, order);
|
|
2408
|
-
}));
|
|
2409
|
-
}
|
|
2410
|
-
finally {
|
|
2411
|
-
__classPrivateFieldSet(this, _RampsController_isPolling, false, "f");
|
|
2412
|
-
}
|
|
2413
|
-
}, _RampsController_syncTransakAuthOnError = function _RampsController_syncTransakAuthOnError(error) {
|
|
2414
|
-
if (hasHttpStatus(error) && error.httpStatus === 401) {
|
|
2415
|
-
this.transakSetAuthenticated(false);
|
|
2416
|
-
}
|
|
2417
|
-
}, _RampsController_getNormalizedTransakError = function _RampsController_getNormalizedTransakError(error, options = {}) {
|
|
2418
|
-
if (options.syncAuth) {
|
|
2419
|
-
__classPrivateFieldGet(this, _RampsController_instances, "m", _RampsController_syncTransakAuthOnError).call(this, error);
|
|
2420
|
-
}
|
|
2421
|
-
return getNormalizedRampsError(error);
|
|
2422
|
-
};
|
|
2423
|
-
//# sourceMappingURL=RampsController.mjs.map
|
|
2905
|
+
//# sourceMappingURL=RampsController.js.map
|