@metamask/ramps-controller 26.3.0 → 27.0.1
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 +33 -1
- package/dist/RampsController-method-action-types.d.ts +29 -8
- package/dist/RampsController-method-action-types.d.ts.map +1 -1
- package/dist/RampsController-method-action-types.js.map +1 -1
- package/dist/RampsController.d.ts +78 -30
- package/dist/RampsController.d.ts.map +1 -1
- package/dist/RampsController.js +323 -110
- package/dist/RampsController.js.map +1 -1
- package/dist/autorampAccount.d.ts +12 -0
- package/dist/autorampAccount.d.ts.map +1 -1
- package/dist/autorampAccount.js +17 -3
- package/dist/autorampAccount.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/RampsController.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseController } from '@metamask/base-controller';
|
|
2
|
-
import { BrokenCircuitError } from '@metamask/controller-utils';
|
|
2
|
+
import { BrokenCircuitError, HttpError } from '@metamask/controller-utils';
|
|
3
3
|
import { BigNumber } from 'bignumber.js';
|
|
4
|
-
import { applyAutorampRemoteStatus, AutorampStatus,
|
|
4
|
+
import { applyAutorampRemoteStatus, AutorampStatus, createAutorampAccount, markAutorampNotified, } from './autorampAccount.js';
|
|
5
5
|
import { getHeadlessProviderAllowlist, isHeadlessAllProvidersEnabled, normalizeHeadlessProviderId, } from './featureFlags.js';
|
|
6
6
|
import { areOrdersEqual, deleteOrderInUserStorage, syncOrdersWithUserStorage as syncOrdersWithUserStorageInternal, updateOrderInUserStorage, } from './order-syncing/index.js';
|
|
7
7
|
import { PENDING_ORDER_STATUSES, TERMINAL_ORDER_STATUSES, } from './orderStatus.js';
|
|
@@ -81,9 +81,9 @@ export const RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS = [
|
|
|
81
81
|
'AuthenticationController:getSessionProfile',
|
|
82
82
|
'AuthenticationController:isSignedIn',
|
|
83
83
|
'KeyringController:signPersonalMessage',
|
|
84
|
-
'KycController:getSessionStatusForVendor',
|
|
85
84
|
'KycController:getProviderFlowStatus',
|
|
86
|
-
'KycController:
|
|
85
|
+
'KycController:readSessionStatus',
|
|
86
|
+
'KycController:fetchSessionStatusOnce',
|
|
87
87
|
'KycController:hasCompletedVendorDisclaimers',
|
|
88
88
|
'KycController:hasCompletedSessionDisclaimers',
|
|
89
89
|
'KycController:clearState',
|
|
@@ -92,6 +92,15 @@ export const RAMPS_CONTROLLER_REQUIRED_CONTROLLER_ACTIONS = [
|
|
|
92
92
|
'UserStorageController:performGetStorageAllFeatureEntries',
|
|
93
93
|
'UserStorageController:performBatchSetStorage',
|
|
94
94
|
];
|
|
95
|
+
/**
|
|
96
|
+
* `finalStatus` values that end a KYC session. Mirrors
|
|
97
|
+
* `TERMINAL_SESSION_STATUSES` in `@metamask/kyc-controller`.
|
|
98
|
+
*/
|
|
99
|
+
const TERMINAL_VBA_KYC_FINAL_STATUSES = new Set([
|
|
100
|
+
'approved',
|
|
101
|
+
'rejected',
|
|
102
|
+
'retry',
|
|
103
|
+
]);
|
|
95
104
|
/**
|
|
96
105
|
* KYC vocabulary used on the VBA onboarding snapshot. `'none'` means no
|
|
97
106
|
* session exists yet (or the vendor returned an unrecognized status).
|
|
@@ -112,13 +121,21 @@ export const VBA_PROVIDER_FLOW_STATUSES = [
|
|
|
112
121
|
];
|
|
113
122
|
/**
|
|
114
123
|
* Autoramp setup progress after KYC has been approved.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
124
|
+
*
|
|
125
|
+
* `'needs_wallet_registration'` means KYC is approved, this wallet has no
|
|
126
|
+
* persisted registration row, and there is no usable autoramp.
|
|
127
|
+
* `'needs_source_currency'` means KYC is approved, the registration row is
|
|
128
|
+
* `active` or `disabled`, and there is no usable autoramp.
|
|
129
|
+
* `'in_progress'` is reserved for hosts that observe an in-flight hydrate.
|
|
130
|
+
* {@link RampsController.hydrateVbaOnboarding} returns `'ready'`,
|
|
131
|
+
* `'needs_wallet_registration'`, `'needs_source_currency'`, or
|
|
132
|
+
* `'retryable_failure'` once an approved read settles.
|
|
118
133
|
*/
|
|
119
134
|
export const VBA_AUTORAMP_STATUSES = [
|
|
120
135
|
'not_ready',
|
|
121
136
|
'in_progress',
|
|
137
|
+
'needs_wallet_registration',
|
|
138
|
+
'needs_source_currency',
|
|
122
139
|
'ready',
|
|
123
140
|
'retryable_failure',
|
|
124
141
|
];
|
|
@@ -131,6 +148,25 @@ const EMPTY_VBA_ONBOARDING_SNAPSHOT = {
|
|
|
131
148
|
autorampStatus: 'not_ready',
|
|
132
149
|
};
|
|
133
150
|
const VBA_KYC_STATUS_SET = new Set(VBA_KYC_STATUSES);
|
|
151
|
+
/**
|
|
152
|
+
* Reads the MoonPay customer id and lifecycle status from a customer payload.
|
|
153
|
+
*
|
|
154
|
+
* @param customer - `GET /neobank/customers/{external_id}/external` body.
|
|
155
|
+
* @returns The id and status, or `null` when the id is missing.
|
|
156
|
+
*/
|
|
157
|
+
function readMoonpayCustomerRecord(customer) {
|
|
158
|
+
if (!customer || typeof customer !== 'object') {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const { id, status } = customer;
|
|
162
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
status: typeof status === 'string' && status.length > 0 ? status : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
134
170
|
/**
|
|
135
171
|
* Maps a vendor status string onto the snapshot vocabulary.
|
|
136
172
|
*
|
|
@@ -144,24 +180,97 @@ function toVbaKycStatus(value) {
|
|
|
144
180
|
return 'none';
|
|
145
181
|
}
|
|
146
182
|
/**
|
|
147
|
-
*
|
|
183
|
+
* Maps a KYC session onto the snapshot vocabulary.
|
|
184
|
+
*
|
|
185
|
+
* Relay approval (`kycStatus: 'approved'`) counts even when `finalStatus` is
|
|
186
|
+
* still `pending`. A non-approved session whose provider flow was submitted,
|
|
187
|
+
* or whose status is pending, is `pending`.
|
|
188
|
+
*
|
|
189
|
+
* @param session - Persisted or freshly fetched KYC session.
|
|
190
|
+
* @param providerFlowStatus - Durable provider-flow outcome.
|
|
191
|
+
* @returns The snapshot KYC status.
|
|
192
|
+
*/
|
|
193
|
+
function resolveVbaKycStatus(session, providerFlowStatus) {
|
|
194
|
+
if (session.finalStatus === 'approved' || session.kycStatus === 'approved') {
|
|
195
|
+
return 'approved';
|
|
196
|
+
}
|
|
197
|
+
const mapped = toVbaKycStatus(session.finalStatus);
|
|
198
|
+
if (mapped === 'rejected' || mapped === 'retry') {
|
|
199
|
+
return mapped;
|
|
200
|
+
}
|
|
201
|
+
if (session.finalStatus === 'pending' ||
|
|
202
|
+
session.kycStatus === 'pending' ||
|
|
203
|
+
providerFlowStatus === 'submitted') {
|
|
204
|
+
return 'pending';
|
|
205
|
+
}
|
|
206
|
+
return mapped;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Idempotency key for a standing fiat to mUSD autoramp on Monad.
|
|
148
210
|
*
|
|
149
|
-
* The customer and wallet keep retries of one create on the
|
|
150
|
-
*
|
|
151
|
-
*
|
|
211
|
+
* The source fiat, customer, and wallet keep retries of one create on the
|
|
212
|
+
* same key (`brl-musd-monad` versus `usd-musd-monad`). Rejected or cancelled
|
|
213
|
+
* route ids are appended, sorted, so a replacement create is a new key and
|
|
214
|
+
* MoonPay does not replay the terminal route.
|
|
152
215
|
*
|
|
216
|
+
* @param sourceCurrencyCode - Fiat code from the create body. Trimmed and lowercased here.
|
|
153
217
|
* @param customerId - MoonPay customer id.
|
|
154
218
|
* @param walletAddress - Money Account address that receives mUSD.
|
|
155
219
|
* @param terminalAutorampIds - Ids of rejected or cancelled routes for this wallet.
|
|
156
220
|
* @returns Idempotency-Key value.
|
|
157
221
|
*/
|
|
158
|
-
function
|
|
159
|
-
const
|
|
222
|
+
function buildMusdAutorampIdempotencyKey(sourceCurrencyCode, customerId, walletAddress, terminalAutorampIds) {
|
|
223
|
+
const fiatPrefix = sourceCurrencyCode.trim().toLowerCase();
|
|
224
|
+
const base = `${fiatPrefix}-musd-monad:${customerId}:${walletAddress.trim().toLowerCase()}`;
|
|
160
225
|
if (terminalAutorampIds.length === 0) {
|
|
161
226
|
return base;
|
|
162
227
|
}
|
|
163
228
|
return `${base}:${[...terminalAutorampIds].sort().join(':')}`;
|
|
164
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Reads the first fiat `source_currencies` code from an autoramp create body.
|
|
232
|
+
*
|
|
233
|
+
* @param request - Caller-supplied create payload.
|
|
234
|
+
* @returns Lowercased fiat code, or `null` when the body has none.
|
|
235
|
+
*/
|
|
236
|
+
function readCreateAutorampFiatCode(request) {
|
|
237
|
+
const sourceCurrencies = request.source_currencies;
|
|
238
|
+
if (!Array.isArray(sourceCurrencies)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
for (const currency of sourceCurrencies) {
|
|
242
|
+
if (!currency || typeof currency !== 'object') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const record = currency;
|
|
246
|
+
if (record.type !== 'Fiat' || typeof record.code !== 'string') {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const code = record.code.trim().toLowerCase();
|
|
250
|
+
if (code.length > 0) {
|
|
251
|
+
return code;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Reads the destination wallet from an autoramp create body.
|
|
258
|
+
*
|
|
259
|
+
* @param request - Caller-supplied create payload.
|
|
260
|
+
* @returns Trimmed recipient address, or `null` when it is missing.
|
|
261
|
+
*/
|
|
262
|
+
function readCreateAutorampWalletAddress(request) {
|
|
263
|
+
const recipient = request.recipient_account;
|
|
264
|
+
if (!recipient || typeof recipient !== 'object') {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const { address } = recipient;
|
|
268
|
+
if (typeof address !== 'string') {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const trimmed = address.trim();
|
|
272
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
273
|
+
}
|
|
165
274
|
/**
|
|
166
275
|
* Distinguishes an already-materialized {@link AutorampAccount} from the
|
|
167
276
|
* create-fields shape accepted by {@link RampsController.addAutoramp}.
|
|
@@ -290,6 +399,18 @@ const rampsControllerMetadata = {
|
|
|
290
399
|
includeInStateLogs: true,
|
|
291
400
|
usedInUi: true,
|
|
292
401
|
},
|
|
402
|
+
moneyAccountWalletRegistrations: {
|
|
403
|
+
persist: true,
|
|
404
|
+
includeInDebugSnapshot: true,
|
|
405
|
+
includeInStateLogs: true,
|
|
406
|
+
usedInUi: true,
|
|
407
|
+
},
|
|
408
|
+
vbaAutorampListFetchedAt: {
|
|
409
|
+
persist: true,
|
|
410
|
+
includeInDebugSnapshot: true,
|
|
411
|
+
includeInStateLogs: false,
|
|
412
|
+
usedInUi: true,
|
|
413
|
+
},
|
|
293
414
|
providerAutoSelected: {
|
|
294
415
|
persist: true,
|
|
295
416
|
includeInDebugSnapshot: true,
|
|
@@ -340,6 +461,8 @@ export function getDefaultRampsControllerState() {
|
|
|
340
461
|
},
|
|
341
462
|
orders: [],
|
|
342
463
|
autoramps: [],
|
|
464
|
+
moneyAccountWalletRegistrations: [],
|
|
465
|
+
vbaAutorampListFetchedAt: null,
|
|
343
466
|
providerAutoSelected: false,
|
|
344
467
|
};
|
|
345
468
|
}
|
|
@@ -2390,17 +2513,31 @@ export class RampsController extends BaseController {
|
|
|
2390
2513
|
*
|
|
2391
2514
|
* The vendor `customer_id` is resolved via
|
|
2392
2515
|
* {@link RampsController.resolveAutorampCustomerId} and injected into the
|
|
2393
|
-
* request (any caller-supplied `customer_id` is overwritten).
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2516
|
+
* request (any caller-supplied `customer_id` is overwritten). The caller
|
|
2517
|
+
* supplies the fiat body. When that body names a source fiat and a
|
|
2518
|
+
* recipient wallet, the idempotency key is derived from that fiat
|
|
2519
|
+
* (`brl-musd-monad` or `usd-musd-monad`), the customer, the wallet, and
|
|
2520
|
+
* rejected or cancelled autoramp ids. A caller-supplied key is used only
|
|
2521
|
+
* when the body has no source fiat or recipient address.
|
|
2522
|
+
*
|
|
2523
|
+
* @param request - CreateAutoramp payload, including the source fiat.
|
|
2524
|
+
* @param options - Optional idempotency key forwarded when the body does
|
|
2525
|
+
* not identify a fiat route.
|
|
2397
2526
|
* @param options.idempotencyKey - Value sent as `Idempotency-Key`.
|
|
2398
2527
|
* @returns The created/updated local {@link AutorampAccount}.
|
|
2399
2528
|
*/
|
|
2400
2529
|
async createAutoramp(request, options = {}) {
|
|
2401
2530
|
const customerId = await this.resolveAutorampCustomerId();
|
|
2531
|
+
const fiatCode = readCreateAutorampFiatCode(request);
|
|
2532
|
+
const walletAddress = readCreateAutorampWalletAddress(request);
|
|
2533
|
+
const requestOptions = fiatCode && walletAddress
|
|
2534
|
+
? {
|
|
2535
|
+
...options,
|
|
2536
|
+
idempotencyKey: buildMusdAutorampIdempotencyKey(fiatCode, customerId, walletAddress, this.#terminalAutorampIdsForWallet(walletAddress)),
|
|
2537
|
+
}
|
|
2538
|
+
: options;
|
|
2402
2539
|
const body = { ...request, customer_id: customerId };
|
|
2403
|
-
const remote = await this.messenger.call('NeoBankService:createAutoramp', body,
|
|
2540
|
+
const remote = await this.messenger.call('NeoBankService:createAutoramp', body, requestOptions);
|
|
2404
2541
|
return this.#applyAutorampRemoteSnapshot(remote);
|
|
2405
2542
|
}
|
|
2406
2543
|
/**
|
|
@@ -2412,6 +2549,15 @@ export class RampsController extends BaseController {
|
|
|
2412
2549
|
* @returns The vendor customer id.
|
|
2413
2550
|
*/
|
|
2414
2551
|
async resolveAutorampCustomerId() {
|
|
2552
|
+
return (await this.#resolveMoonpayCustomer()).id;
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Loads the MoonPay customer for the signed-in profile, including lifecycle
|
|
2556
|
+
* `status`. Wallet registration is allowed only when that status is `Active`.
|
|
2557
|
+
*
|
|
2558
|
+
* @returns The customer id and status.
|
|
2559
|
+
*/
|
|
2560
|
+
async #resolveMoonpayCustomer() {
|
|
2415
2561
|
const profile = await this.messenger.call('AuthenticationController:getSessionProfile');
|
|
2416
2562
|
const canonical = profile?.canonicalProfileId;
|
|
2417
2563
|
const externalId = typeof canonical === 'string' && canonical.length > 0
|
|
@@ -2420,16 +2566,11 @@ export class RampsController extends BaseController {
|
|
|
2420
2566
|
if (typeof externalId !== 'string' || externalId.length === 0) {
|
|
2421
2567
|
throw new Error('Cannot resolve MoonPay customer id: wallet is not signed in to Profile Sync.');
|
|
2422
2568
|
}
|
|
2423
|
-
const customer = await this.messenger.call('NeoBankService:getCustomerByExternalId', externalId);
|
|
2424
|
-
|
|
2425
|
-
typeof customer === 'object' &&
|
|
2426
|
-
typeof customer.id === 'string'
|
|
2427
|
-
? customer.id
|
|
2428
|
-
: null;
|
|
2429
|
-
if (!customerId) {
|
|
2569
|
+
const customer = readMoonpayCustomerRecord(await this.messenger.call('NeoBankService:getCustomerByExternalId', externalId));
|
|
2570
|
+
if (!customer) {
|
|
2430
2571
|
throw new Error(`Cannot resolve MoonPay customer id: no MoonPay customer is mapped to external id "${externalId}".`);
|
|
2431
2572
|
}
|
|
2432
|
-
return
|
|
2573
|
+
return customer;
|
|
2433
2574
|
}
|
|
2434
2575
|
/**
|
|
2435
2576
|
* Registers a Money Account wallet with MoonPay Iron via neobank-proxy.
|
|
@@ -2453,7 +2594,8 @@ export class RampsController extends BaseController {
|
|
|
2453
2594
|
}
|
|
2454
2595
|
return undefined;
|
|
2455
2596
|
};
|
|
2456
|
-
const
|
|
2597
|
+
const customer = await this.#resolveMoonpayCustomer();
|
|
2598
|
+
const { id: customerId } = customer;
|
|
2457
2599
|
const toLookupUnavailableResult = (error) => {
|
|
2458
2600
|
machine = transitionWalletRegistration(machine, {
|
|
2459
2601
|
type: 'LOOKUP_FAILED',
|
|
@@ -2498,7 +2640,12 @@ export class RampsController extends BaseController {
|
|
|
2498
2640
|
};
|
|
2499
2641
|
const existingResult = await resolveLookup();
|
|
2500
2642
|
if (existingResult) {
|
|
2501
|
-
return existingResult;
|
|
2643
|
+
return this.#finishMoneyAccountWalletRegistration(address, existingResult);
|
|
2644
|
+
}
|
|
2645
|
+
if (customer.status !== 'Active') {
|
|
2646
|
+
throw new WalletRegistrationError('forbidden', {
|
|
2647
|
+
message: 'MoonPay customer must be Active before a self-hosted wallet can be registered',
|
|
2648
|
+
});
|
|
2502
2649
|
}
|
|
2503
2650
|
let idempotencyKey = createIdempotencyKey();
|
|
2504
2651
|
let lastMessage;
|
|
@@ -2533,7 +2680,7 @@ export class RampsController extends BaseController {
|
|
|
2533
2680
|
idempotencyKey,
|
|
2534
2681
|
});
|
|
2535
2682
|
machine = transitionWalletRegistration(machine, { type: 'SUBMIT_OK' });
|
|
2536
|
-
return result;
|
|
2683
|
+
return this.#finishMoneyAccountWalletRegistration(address, result);
|
|
2537
2684
|
}
|
|
2538
2685
|
catch (error) {
|
|
2539
2686
|
if (!(error instanceof WalletRegistrationError)) {
|
|
@@ -2576,7 +2723,7 @@ export class RampsController extends BaseController {
|
|
|
2576
2723
|
machine.status === 'checkThenRetry') {
|
|
2577
2724
|
const reconciledResult = await resolveLookup();
|
|
2578
2725
|
if (reconciledResult) {
|
|
2579
|
-
return reconciledResult;
|
|
2726
|
+
return this.#finishMoneyAccountWalletRegistration(address, reconciledResult);
|
|
2580
2727
|
}
|
|
2581
2728
|
}
|
|
2582
2729
|
if (machine.status !== 'signing') {
|
|
@@ -2586,23 +2733,42 @@ export class RampsController extends BaseController {
|
|
|
2586
2733
|
}
|
|
2587
2734
|
}
|
|
2588
2735
|
/**
|
|
2589
|
-
*
|
|
2590
|
-
* Money Account (wallet registration + autoramp). Hosts map the returned
|
|
2736
|
+
* Reads VBA onboarding facts. Hosts map the returned
|
|
2591
2737
|
* {@link VbaOnboardingSnapshot} onto their own funnel; this method does not
|
|
2592
2738
|
* name screens.
|
|
2593
2739
|
*
|
|
2594
|
-
*
|
|
2595
|
-
*
|
|
2740
|
+
* The read uses the persisted KYC session. `refreshKyc` performs one
|
|
2741
|
+
* session-status GET when a session id is already stored and `finalStatus`
|
|
2742
|
+
* is not terminal. `refreshAutoramps` lists autoramps when KYC is approved,
|
|
2743
|
+
* unless this wallet already has a usable route. An approved session with no
|
|
2744
|
+
* usable autoramp returns `needs_wallet_registration` or
|
|
2745
|
+
* `needs_source_currency` only when
|
|
2746
|
+
* `GET /neobank/customers/{external_id}/external` reports
|
|
2747
|
+
* `status: 'Active'`. Any other customer status returns `not_ready`.
|
|
2748
|
+
* It does not sign, post a self-hosted address, create an autoramp, or start
|
|
2749
|
+
* KYC polling.
|
|
2750
|
+
*
|
|
2751
|
+
* Overlapping calls share one run.
|
|
2596
2752
|
*
|
|
2597
2753
|
* @param params - VBA onboarding parameters.
|
|
2598
2754
|
* @param params.walletAddress - Monad Money Account wallet address.
|
|
2755
|
+
* @param params.refreshKyc - When true, fetch session status once for a
|
|
2756
|
+
* non-terminal persisted session. A failed GET keeps that session. Defaults
|
|
2757
|
+
* to false.
|
|
2758
|
+
* @param params.refreshAutoramps - When true, replace the autoramp cursor
|
|
2759
|
+
* from `GET /neobank/autoramps` unless a usable route is already stored.
|
|
2760
|
+
* Defaults to false.
|
|
2599
2761
|
* @returns Independent KYC and autoramp facts for the current customer.
|
|
2600
2762
|
*/
|
|
2601
|
-
async hydrateVbaOnboarding({ walletAddress, }) {
|
|
2763
|
+
async hydrateVbaOnboarding({ walletAddress, refreshKyc = false, refreshAutoramps = false, }) {
|
|
2602
2764
|
if (this.#vbaOnboardingHydrationPromise) {
|
|
2603
2765
|
return await this.#vbaOnboardingHydrationPromise;
|
|
2604
2766
|
}
|
|
2605
|
-
const hydrationPromise = this.#hydrateVbaOnboarding(
|
|
2767
|
+
const hydrationPromise = this.#hydrateVbaOnboarding({
|
|
2768
|
+
walletAddress,
|
|
2769
|
+
refreshKyc,
|
|
2770
|
+
refreshAutoramps,
|
|
2771
|
+
});
|
|
2606
2772
|
this.#vbaOnboardingHydrationPromise = hydrationPromise;
|
|
2607
2773
|
try {
|
|
2608
2774
|
return await hydrationPromise;
|
|
@@ -2613,46 +2779,39 @@ export class RampsController extends BaseController {
|
|
|
2613
2779
|
}
|
|
2614
2780
|
}
|
|
2615
2781
|
}
|
|
2616
|
-
async #hydrateVbaOnboarding(walletAddress) {
|
|
2617
|
-
|
|
2618
|
-
//
|
|
2619
|
-
//
|
|
2620
|
-
//
|
|
2621
|
-
//
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
session = this.messenger.call('KycController:refreshSessionStatus');
|
|
2631
|
-
sessionFromCache = true;
|
|
2782
|
+
async #hydrateVbaOnboarding({ walletAddress, refreshKyc, refreshAutoramps, }) {
|
|
2783
|
+
let session = this.messenger.call('KycController:readSessionStatus');
|
|
2784
|
+
// A persisted session can outlive the identity that created it. Reusing it
|
|
2785
|
+
// makes session-scoped calls fail closed. Discard it before any fetch, with
|
|
2786
|
+
// the registration rows and autoramp cursor it left behind, since neither
|
|
2787
|
+
// is keyed by profile.
|
|
2788
|
+
if (session && !(await this.#isVbaSessionOwnedByCurrentProfile(session))) {
|
|
2789
|
+
this.messenger.call('KycController:clearState');
|
|
2790
|
+
this.update((state) => {
|
|
2791
|
+
state.moneyAccountWalletRegistrations = [];
|
|
2792
|
+
state.autoramps = [];
|
|
2793
|
+
state.vbaAutorampListFetchedAt = null;
|
|
2794
|
+
});
|
|
2795
|
+
return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
|
|
2632
2796
|
}
|
|
2633
|
-
|
|
2797
|
+
if (refreshKyc &&
|
|
2798
|
+
session?.id &&
|
|
2799
|
+
!TERMINAL_VBA_KYC_FINAL_STATUSES.has(session.finalStatus)) {
|
|
2634
2800
|
try {
|
|
2635
|
-
session = await this.messenger.call('KycController:
|
|
2801
|
+
session = await this.messenger.call('KycController:fetchSessionStatusOnce', session.id);
|
|
2636
2802
|
}
|
|
2637
|
-
catch {
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2803
|
+
catch (error) {
|
|
2804
|
+
if (error instanceof HttpError && error.httpStatus === 404) {
|
|
2805
|
+
this.messenger.call('KycController:clearState');
|
|
2806
|
+
return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
|
|
2807
|
+
}
|
|
2808
|
+
// The persisted session is already in hand. A transient status GET
|
|
2809
|
+
// must not reject the read.
|
|
2641
2810
|
}
|
|
2642
2811
|
}
|
|
2643
2812
|
if (!session) {
|
|
2644
2813
|
return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
|
|
2645
2814
|
}
|
|
2646
|
-
// A persisted session can outlive the identity that created it — e.g. a new
|
|
2647
|
-
// wallet created over an install that still holds a previous customer's
|
|
2648
|
-
// session. Reusing it makes the backend reject every session-scoped call
|
|
2649
|
-
// (owner mismatch), dead-ending the user. Verify ownership up front against
|
|
2650
|
-
// the signed-in profile and, on a mismatch, discard the stale session.
|
|
2651
|
-
if (sessionFromCache &&
|
|
2652
|
-
!(await this.#isVbaSessionOwnedByCurrentProfile(session))) {
|
|
2653
|
-
this.messenger.call('KycController:clearState');
|
|
2654
|
-
return { ...EMPTY_VBA_ONBOARDING_SNAPSHOT };
|
|
2655
|
-
}
|
|
2656
2815
|
const vendorDisclaimersComplete = await this.messenger.call('KycController:hasCompletedVendorDisclaimers');
|
|
2657
2816
|
const sessionDisclaimersComplete = await this.messenger.call('KycController:hasCompletedSessionDisclaimers');
|
|
2658
2817
|
const providerFlowStatus = this.messenger.call('KycController:getProviderFlowStatus');
|
|
@@ -2661,15 +2820,7 @@ export class RampsController extends BaseController {
|
|
|
2661
2820
|
vendorDisclaimersComplete,
|
|
2662
2821
|
sessionDisclaimersComplete,
|
|
2663
2822
|
providerFlowStatus,
|
|
2664
|
-
|
|
2665
|
-
// is made, while the vendor-side `finalStatus` can lag at `'pending'` until
|
|
2666
|
-
// the vendor record is finalized (and in sandbox may never advance). Treat
|
|
2667
|
-
// either signal as approval so a relay-approved user is not stranded on the
|
|
2668
|
-
// pending screen. This mirrors the UKYC backend, whose own "already
|
|
2669
|
-
// approved" check is `finalStatus === 'approved' || kycStatus === 'approved'`.
|
|
2670
|
-
kycStatus: session.finalStatus === 'approved' || session.kycStatus === 'approved'
|
|
2671
|
-
? 'approved'
|
|
2672
|
-
: toVbaKycStatus(session.finalStatus),
|
|
2823
|
+
kycStatus: resolveVbaKycStatus(session, providerFlowStatus),
|
|
2673
2824
|
autorampStatus: 'not_ready',
|
|
2674
2825
|
};
|
|
2675
2826
|
if (snapshot.kycStatus !== 'approved') {
|
|
@@ -2678,46 +2829,108 @@ export class RampsController extends BaseController {
|
|
|
2678
2829
|
if (!walletAddress.trim()) {
|
|
2679
2830
|
throw new Error('walletAddress is required after KYC acceptance.');
|
|
2680
2831
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
const registration = await this.registerMoneyAccountWallet({
|
|
2688
|
-
address: walletAddress,
|
|
2689
|
-
});
|
|
2690
|
-
if (registration.type === 'lookupUnavailable') {
|
|
2691
|
-
throw registration.error;
|
|
2832
|
+
if (this.#hasUsableAutorampForWallet(walletAddress)) {
|
|
2833
|
+
return { ...snapshot, autorampStatus: 'ready' };
|
|
2834
|
+
}
|
|
2835
|
+
if (refreshAutoramps) {
|
|
2836
|
+
try {
|
|
2837
|
+
await this.#replaceAutorampCursorFromList();
|
|
2692
2838
|
}
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
const remoteAutorampIds = new Set(remoteAutoramps.map((autoramp) => autoramp.id));
|
|
2696
|
-
for (const autoramp of remoteAutoramps) {
|
|
2697
|
-
this.#applyAutorampRemoteSnapshot(autoramp);
|
|
2839
|
+
catch {
|
|
2840
|
+
return { ...snapshot, autorampStatus: 'retryable_failure' };
|
|
2698
2841
|
}
|
|
2699
|
-
this
|
|
2700
|
-
|
|
2701
|
-
});
|
|
2702
|
-
const trimmedWalletAddress = walletAddress.trim();
|
|
2703
|
-
const normalizedWalletAddress = trimmedWalletAddress.toLowerCase();
|
|
2704
|
-
const autorampsForWallet = this.state.autoramps.filter((autoramp) => autoramp.walletAddress.toLowerCase() === normalizedWalletAddress);
|
|
2705
|
-
const hasUsableAutoramp = autorampsForWallet.some((autoramp) => autoramp.status !== AutorampStatus.Rejected &&
|
|
2706
|
-
autoramp.status !== AutorampStatus.Cancelled);
|
|
2707
|
-
if (!hasUsableAutoramp) {
|
|
2708
|
-
const terminalAutorampIds = autorampsForWallet
|
|
2709
|
-
.filter((autoramp) => autoramp.status === AutorampStatus.Rejected ||
|
|
2710
|
-
autoramp.status === AutorampStatus.Cancelled)
|
|
2711
|
-
.map((autoramp) => autoramp.id);
|
|
2712
|
-
await this.createAutoramp(buildBrazilMusdAutorampRequest(trimmedWalletAddress), {
|
|
2713
|
-
idempotencyKey: buildBrazilMusdAutorampIdempotencyKey(customerId, trimmedWalletAddress, terminalAutorampIds),
|
|
2714
|
-
});
|
|
2842
|
+
if (this.#hasUsableAutorampForWallet(walletAddress)) {
|
|
2843
|
+
return { ...snapshot, autorampStatus: 'ready' };
|
|
2715
2844
|
}
|
|
2716
2845
|
}
|
|
2846
|
+
let customer;
|
|
2847
|
+
try {
|
|
2848
|
+
customer = await this.#resolveMoonpayCustomer();
|
|
2849
|
+
}
|
|
2717
2850
|
catch {
|
|
2718
2851
|
return { ...snapshot, autorampStatus: 'retryable_failure' };
|
|
2719
2852
|
}
|
|
2720
|
-
|
|
2853
|
+
if (customer.status !== 'Active') {
|
|
2854
|
+
return { ...snapshot, autorampStatus: 'not_ready' };
|
|
2855
|
+
}
|
|
2856
|
+
const registration = this.state.moneyAccountWalletRegistrations.find((row) => row.walletAddress.trim().toLowerCase() ===
|
|
2857
|
+
walletAddress.trim().toLowerCase());
|
|
2858
|
+
if (!registration) {
|
|
2859
|
+
return { ...snapshot, autorampStatus: 'needs_wallet_registration' };
|
|
2860
|
+
}
|
|
2861
|
+
return { ...snapshot, autorampStatus: 'needs_source_currency' };
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Writes the registration row after a successful register outcome.
|
|
2865
|
+
* `lookupUnavailable` writes nothing.
|
|
2866
|
+
*
|
|
2867
|
+
* @param address - Money Account wallet address.
|
|
2868
|
+
* @param result - Registration outcome.
|
|
2869
|
+
* @returns The same outcome.
|
|
2870
|
+
*/
|
|
2871
|
+
#finishMoneyAccountWalletRegistration(address, result) {
|
|
2872
|
+
if (result.type === 'lookupUnavailable') {
|
|
2873
|
+
return result;
|
|
2874
|
+
}
|
|
2875
|
+
const normalized = address.trim().toLowerCase();
|
|
2876
|
+
const status = result.type === 'registeredDisabled' ? 'disabled' : 'active';
|
|
2877
|
+
const row = {
|
|
2878
|
+
walletAddress: normalized,
|
|
2879
|
+
status,
|
|
2880
|
+
updatedAt: Date.now(),
|
|
2881
|
+
};
|
|
2882
|
+
this.update((state) => {
|
|
2883
|
+
const index = state.moneyAccountWalletRegistrations.findIndex((existing) => existing.walletAddress.trim().toLowerCase() === normalized);
|
|
2884
|
+
if (index === -1) {
|
|
2885
|
+
state.moneyAccountWalletRegistrations.push(row);
|
|
2886
|
+
}
|
|
2887
|
+
else {
|
|
2888
|
+
state.moneyAccountWalletRegistrations[index] = row;
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
return result;
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Ids of rejected or cancelled autoramps for a wallet, used in create keys.
|
|
2895
|
+
*
|
|
2896
|
+
* @param walletAddress - Destination wallet address.
|
|
2897
|
+
* @returns Terminal autoramp ids for that wallet.
|
|
2898
|
+
*/
|
|
2899
|
+
#terminalAutorampIdsForWallet(walletAddress) {
|
|
2900
|
+
const normalized = walletAddress.trim().toLowerCase();
|
|
2901
|
+
return this.state.autoramps
|
|
2902
|
+
.filter((autoramp) => autoramp.walletAddress.toLowerCase() === normalized &&
|
|
2903
|
+
(autoramp.status === AutorampStatus.Rejected ||
|
|
2904
|
+
autoramp.status === AutorampStatus.Cancelled))
|
|
2905
|
+
.map((autoramp) => autoramp.id);
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Whether this wallet has an autoramp that is not Rejected or Cancelled.
|
|
2909
|
+
*
|
|
2910
|
+
* @param walletAddress - Destination wallet address.
|
|
2911
|
+
* @returns Whether a usable route is already on the cursor.
|
|
2912
|
+
*/
|
|
2913
|
+
#hasUsableAutorampForWallet(walletAddress) {
|
|
2914
|
+
const normalized = walletAddress.trim().toLowerCase();
|
|
2915
|
+
return this.state.autoramps.some((autoramp) => autoramp.walletAddress.toLowerCase() === normalized &&
|
|
2916
|
+
autoramp.status !== AutorampStatus.Rejected &&
|
|
2917
|
+
autoramp.status !== AutorampStatus.Cancelled);
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Replaces the autoramp cursor from `GET /neobank/autoramps` and records
|
|
2921
|
+
* `vbaAutorampListFetchedAt` only after that GET succeeds.
|
|
2922
|
+
*/
|
|
2923
|
+
async #replaceAutorampCursorFromList() {
|
|
2924
|
+
const customerId = await this.resolveAutorampCustomerId();
|
|
2925
|
+
const remoteAutoramps = await this.messenger.call('NeoBankService:getAutoramps', { customerId });
|
|
2926
|
+
const remoteAutorampIds = new Set(remoteAutoramps.map((autoramp) => autoramp.id));
|
|
2927
|
+
for (const autoramp of remoteAutoramps) {
|
|
2928
|
+
this.#applyAutorampRemoteSnapshot(autoramp);
|
|
2929
|
+
}
|
|
2930
|
+
this.update((state) => {
|
|
2931
|
+
state.autoramps = state.autoramps.filter((autoramp) => remoteAutorampIds.has(autoramp.id));
|
|
2932
|
+
state.vbaAutorampListFetchedAt = Date.now();
|
|
2933
|
+
});
|
|
2721
2934
|
}
|
|
2722
2935
|
/**
|
|
2723
2936
|
* Whether a persisted KYC session belongs to the currently signed-in profile.
|