@global-torque/invest-core 0.2.2

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +11 -0
  4. package/README.md +76 -0
  5. package/SECURITY.md +9 -0
  6. package/SUPPORT.md +6 -0
  7. package/dist/node/app/config.js +173 -0
  8. package/dist/node/helpers/text.js +37 -0
  9. package/dist/node/markdown/tableWrap.js +23 -0
  10. package/package.json +113 -0
  11. package/src/accreditation/status.ts +100 -0
  12. package/src/analytics/__tests__/analyticsBody.test.ts +50 -0
  13. package/src/analytics/analyticsBody.ts +270 -0
  14. package/src/app/config.test.ts +79 -0
  15. package/src/app/config.ts +329 -0
  16. package/src/decimal/__tests__/canonicalDecimal.test.ts +58 -0
  17. package/src/decimal/canonicalDecimal.ts +154 -0
  18. package/src/evm/__tests__/walletInfo.test.ts +488 -0
  19. package/src/evm/walletInfo.ts +625 -0
  20. package/src/filer/__tests__/documentFormatter.test.ts +208 -0
  21. package/src/filer/__tests__/publicImage.test.ts +42 -0
  22. package/src/filer/documentFormatter.ts +195 -0
  23. package/src/filer/publicImage.ts +120 -0
  24. package/src/form-validation/__tests__/general.test.ts +78 -0
  25. package/src/form-validation/__tests__/investment.test.ts +100 -0
  26. package/src/form-validation/ajv.ts +109 -0
  27. package/src/form-validation/constants.ts +22 -0
  28. package/src/form-validation/general.ts +114 -0
  29. package/src/form-validation/index.ts +5 -0
  30. package/src/form-validation/investment.ts +65 -0
  31. package/src/form-validation/rules.ts +35 -0
  32. package/src/formatting/__tests__/buildInfo.test.ts +19 -0
  33. package/src/formatting/__tests__/dateTime.test.ts +24 -0
  34. package/src/formatting/__tests__/display.test.ts +30 -0
  35. package/src/formatting/buildInfo.ts +31 -0
  36. package/src/formatting/dateTime.ts +43 -0
  37. package/src/formatting/display.ts +24 -0
  38. package/src/helpers/arrays.ts +11 -0
  39. package/src/helpers/currency.ts +19 -0
  40. package/src/helpers/formatters/formatToDate.ts +47 -0
  41. package/src/helpers/formatters/formatToNumber.ts +39 -0
  42. package/src/helpers/formatters/formatToPhone.ts +13 -0
  43. package/src/helpers/general.ts +164 -0
  44. package/src/helpers/model.ts +87 -0
  45. package/src/helpers/numberFormatter.ts +4 -0
  46. package/src/helpers/text.ts +51 -0
  47. package/src/index.ts +22 -0
  48. package/src/investment/__tests__/status.test.ts +59 -0
  49. package/src/investment/rawAmount.test.ts +23 -0
  50. package/src/investment/rawAmount.ts +81 -0
  51. package/src/investment/status.ts +56 -0
  52. package/src/kyc/__tests__/kycAlert.formatter.test.ts +47 -0
  53. package/src/kyc/__tests__/kycAlert.test.ts +47 -0
  54. package/src/kyc/__tests__/thirdPartyScreen.test.ts +16 -0
  55. package/src/kyc/kycAlert.ts +47 -0
  56. package/src/kyc/status.ts +109 -0
  57. package/src/kyc/thirdPartyScreen.ts +28 -0
  58. package/src/markdown/tableWrap.ts +29 -0
  59. package/src/notifications/shareFields.ts +15 -0
  60. package/src/offer/__tests__/metrics.test.ts +67 -0
  61. package/src/offer/formatter.ts +559 -0
  62. package/src/offer/metrics.ts +83 -0
  63. package/src/onboarding/__tests__/intents.test.ts +83 -0
  64. package/src/onboarding/intents.ts +128 -0
  65. package/src/profiles/__tests__/formatting.test.ts +24 -0
  66. package/src/profiles/avatarInitial.ts +5 -0
  67. package/src/profiles/formatting.ts +38 -0
  68. package/src/repository/__tests__/formatterCache.test.ts +45 -0
  69. package/src/repository/formatterCache.ts +56 -0
  70. package/src/wallet/__tests__/auth.test.ts +102 -0
  71. package/src/wallet/__tests__/operationPresentation.test.ts +94 -0
  72. package/src/wallet/__tests__/setupError.test.ts +32 -0
  73. package/src/wallet/auth.ts +491 -0
  74. package/src/wallet/operationPresentation.ts +106 -0
  75. package/src/wallet/setupError.ts +50 -0
@@ -0,0 +1,491 @@
1
+ const BACKEND_READY_STATUSES = new Set<string>([
2
+ 'created',
3
+ 'verified',
4
+ ]);
5
+
6
+ const BACKEND_ERROR_STATUSES = new Set<string>([
7
+ 'error',
8
+ 'error_retry',
9
+ 'error_document',
10
+ 'error_pending',
11
+ 'error_suspended',
12
+ ]);
13
+
14
+ export type WalletAuthStep =
15
+ | 'intro'
16
+ | 'sending_otp'
17
+ | 'awaiting_otp'
18
+ | 'binding'
19
+ | 'success'
20
+ | 'error';
21
+
22
+ export type WalletAuthOpenPayload = {
23
+ profileId: number;
24
+ profileType?: string | null;
25
+ profileName?: string | null;
26
+ fullAccountName?: string | null;
27
+ userEmail?: string | null;
28
+ walletStatus?: string | null;
29
+ isKycApproved?: boolean | null;
30
+ turnkeyLoginTarget?: WalletAuthTurnkeyLoginTarget | null;
31
+ };
32
+
33
+ export type WalletAuthOperationIntentBase = {
34
+ profileId: number;
35
+ chain: string;
36
+ nonce: string;
37
+ amount: string;
38
+ assetAddress: string;
39
+ assetSymbol: string;
40
+ };
41
+
42
+ export type WalletAuthWithdrawalOperationIntent = WalletAuthOperationIntentBase & {
43
+ source: 'withdrawal';
44
+ destinationAddress: string;
45
+ };
46
+
47
+ export type WalletAuthExchangeOperationIntent = WalletAuthOperationIntentBase & {
48
+ source: 'exchange';
49
+ toAssetAddress: string;
50
+ toAssetSymbol: string;
51
+ destinationAddress: string;
52
+ estimatedReceiveText?: string;
53
+ estimatedRateText?: string;
54
+ };
55
+
56
+ export type WalletAuthOperationIntent =
57
+ | WalletAuthWithdrawalOperationIntent
58
+ | WalletAuthExchangeOperationIntent;
59
+
60
+ export type CompletedPostAuthAction =
61
+ | 'zero_transaction_warmup';
62
+
63
+ export type PendingPostAuthAction = {
64
+ profileId: number;
65
+ run: () => Promise<void>;
66
+ successMarker?: CompletedPostAuthAction;
67
+ operationIntent?: WalletAuthOperationIntent;
68
+ };
69
+
70
+ export type TriggerZeroTransactionWarmupResult =
71
+ | 'completed'
72
+ | 'deferred_to_wallet_auth';
73
+
74
+ export type WalletSignerErrorInfo = {
75
+ message: string;
76
+ };
77
+
78
+ export type WalletAuthTurnkeyChainAccountReference = {
79
+ chain: string;
80
+ account_id: string;
81
+ address?: string;
82
+ };
83
+
84
+ export type WalletAuthTurnkeyDetails = {
85
+ orgId: string;
86
+ subOrgId: string;
87
+ userId: string;
88
+ walletId: string;
89
+ accountId: string;
90
+ otpAttemptId?: string;
91
+ accounts?: WalletAuthTurnkeyChainAccountReference[];
92
+ };
93
+
94
+ export type WalletAuthTurnkeyLoginTarget = {
95
+ orgId?: string | null;
96
+ subOrgId: string;
97
+ userId?: string | null;
98
+ walletId: string;
99
+ accountId?: string | null;
100
+ address?: string | null;
101
+ };
102
+
103
+ export type WalletAuthDetails = {
104
+ providerName?: 'turnkey';
105
+ address?: string;
106
+ walletAddress?: string;
107
+ turnkey?: WalletAuthTurnkeyDetails;
108
+ [key: string]: unknown;
109
+ };
110
+
111
+ export type WalletDirectTransactionChain =
112
+ | 'ethereum'
113
+ | 'ethereum-sepolia'
114
+ | 'polygon'
115
+ | 'base';
116
+
117
+ export type WalletDirectTransactionRequest = {
118
+ chain: WalletDirectTransactionChain;
119
+ fromAddress: string;
120
+ toAddress: string;
121
+ data: `0x${string}`;
122
+ /** Vault operation this transaction belongs to, for durable submission evidence. */
123
+ operationId: number;
124
+ /** Stable per-operation key a wallet adapter uses to resume its own pending call. */
125
+ operationKey: string;
126
+ };
127
+
128
+ export type WalletDirectTransactionOptions = {
129
+ beforeSubmit?: () => Promise<void>;
130
+ };
131
+
132
+ export type WalletDirectTransactionResult = {
133
+ chain: WalletDirectTransactionChain;
134
+ fromAddress: string;
135
+ toAddress: string;
136
+ transactionHash: `0x${string}`;
137
+ submittedAt: string;
138
+ };
139
+
140
+ export type WalletAuthorizationSignatureRequest = {
141
+ type?: string;
142
+ data?: unknown;
143
+ };
144
+
145
+ export type WalletAuthorizationSigningOptions = {
146
+ expectedWalletAddress?: string | null;
147
+ expectedTurnkeyAccountId?: string | null;
148
+ };
149
+
150
+ export type WalletAuthOtpSubmissionResult = 'connected';
151
+
152
+ export type WalletAuthStartEmailOtpResult = 'awaiting_otp' | 'connected';
153
+
154
+ export type WalletAuthStartEmailOtpOptions = {
155
+ profileId?: number;
156
+ turnkeyLoginTarget?: WalletAuthTurnkeyLoginTarget | null;
157
+ };
158
+
159
+ export type WalletAuthorizationTurnkeySmartContractInterface = {
160
+ address: string;
161
+ interface: string;
162
+ type: string;
163
+ label: string;
164
+ notes?: string;
165
+ };
166
+
167
+ export type WalletAuthorizationTurnkeyPolicyRequest = {
168
+ profileId: number;
169
+ organizationId: string;
170
+ policyName: string;
171
+ effect: string;
172
+ condition: string;
173
+ consensus: string;
174
+ notes?: string;
175
+ smartContractInterfaces?: WalletAuthorizationTurnkeySmartContractInterface[];
176
+ };
177
+
178
+ export type WalletAuthorizationTurnkeyPolicyResult = {
179
+ policyId: string;
180
+ policyName: string;
181
+ activityId?: string;
182
+ smartContractInterfaceIds?: string[];
183
+ };
184
+
185
+ export const normalizeWalletProfileSlug = (value?: string | null) =>
186
+ String(value ?? '')
187
+ .trim()
188
+ .toLowerCase()
189
+ .replace(/[\s_./+]+/g, '-')
190
+ .replace(/[^a-z0-9-]/g, '')
191
+ .replace(/-+/g, '-')
192
+ .replace(/^-|-$/g, '');
193
+
194
+ export const deriveWalletAuthEmail = (
195
+ userEmail?: string | null,
196
+ profileName?: string | null,
197
+ isIndividual: boolean = false,
198
+ ) => {
199
+ const email = String(userEmail ?? '').trim().toLowerCase();
200
+
201
+ if (!email) {
202
+ return '';
203
+ }
204
+
205
+ if (isIndividual) {
206
+ return email;
207
+ }
208
+
209
+ const [localPart, domain] = email.split('@');
210
+ const profileSlug = normalizeWalletProfileSlug(profileName);
211
+
212
+ if (!localPart || !domain || !profileSlug) {
213
+ return email;
214
+ }
215
+
216
+ return `${localPart}+${profileSlug}@${domain}`;
217
+ };
218
+
219
+ export const isWalletBackendReady = (status?: string | null) =>
220
+ Boolean(status && BACKEND_READY_STATUSES.has(status));
221
+
222
+ export const isWalletBackendError = (status?: string | null) =>
223
+ Boolean(status && BACKEND_ERROR_STATUSES.has(status));
224
+
225
+ export const shouldPromptWalletAuth = ({
226
+ isKycApproved,
227
+ walletStatus,
228
+ }: Pick<WalletAuthOpenPayload, 'isKycApproved' | 'walletStatus'>) => (
229
+ Boolean(isKycApproved)
230
+ && !isWalletBackendReady(walletStatus)
231
+ && !isWalletBackendError(walletStatus)
232
+ );
233
+
234
+ export const getWalletAuthAddressFromDetails = (authDetails: unknown) => {
235
+ const details = authDetails as WalletAuthDetails | null | undefined;
236
+ const address = details?.address ?? details?.walletAddress;
237
+ return typeof address === 'string' && address.trim() ? address.trim() : '';
238
+ };
239
+
240
+ export const assertTurnkeyWalletAuthDetails = (authDetails: unknown): WalletAuthDetails => {
241
+ const details = authDetails as WalletAuthDetails | null | undefined;
242
+ if (details?.providerName !== 'turnkey') {
243
+ throw new Error('This wallet is not registered with the supported Turnkey provider.');
244
+ }
245
+ return details;
246
+ };
247
+
248
+ export const getWalletAuthTurnkeyDetails = (authDetails: unknown) => {
249
+ const turnkey = (authDetails as WalletAuthDetails | null | undefined)?.turnkey;
250
+
251
+ if (
252
+ turnkey?.orgId
253
+ && turnkey.subOrgId
254
+ && turnkey.userId
255
+ && turnkey.walletId
256
+ && turnkey.accountId
257
+ ) {
258
+ return turnkey;
259
+ }
260
+
261
+ return null;
262
+ };
263
+
264
+ const firstNonEmpty = (...values: unknown[]) =>
265
+ values.map((value) => String(value ?? '').trim()).find(Boolean) ?? '';
266
+
267
+ export const getWalletAuthTurnkeyLoginTarget = (
268
+ source: unknown,
269
+ ): WalletAuthTurnkeyLoginTarget | null => {
270
+ const data = source as Record<string, unknown> | null | undefined;
271
+ const turnkey = data?.turnkey as Record<string, unknown> | null | undefined;
272
+ const directChainStatus = firstNonEmpty(
273
+ data?.chain_account_status,
274
+ data?.chainAccountStatus,
275
+ ).toLowerCase();
276
+ if (directChainStatus && directChainStatus !== 'verified') {
277
+ return null;
278
+ }
279
+ const chains = Array.isArray(data?.chains)
280
+ ? data.chains as Array<Record<string, unknown>>
281
+ : [];
282
+ if (chains.length > 0 && chains.some((chain) => {
283
+ const status = firstNonEmpty(chain.chain_account_status, chain.chainAccountStatus).toLowerCase();
284
+ return status !== 'verified';
285
+ })) {
286
+ return null;
287
+ }
288
+ const subOrgId = firstNonEmpty(
289
+ data?.turnkey_sub_org_id,
290
+ data?.turnkeySubOrgId,
291
+ turnkey?.subOrgId,
292
+ turnkey?.sub_org_id,
293
+ );
294
+ const userId = firstNonEmpty(
295
+ data?.turnkey_user_id,
296
+ data?.turnkeyUserId,
297
+ turnkey?.userId,
298
+ turnkey?.user_id,
299
+ );
300
+ const walletId = firstNonEmpty(
301
+ data?.turnkey_wallet_id,
302
+ data?.turnkeyWalletId,
303
+ turnkey?.walletId,
304
+ turnkey?.wallet_id,
305
+ );
306
+ const accountId = firstNonEmpty(
307
+ data?.turnkey_account_id,
308
+ data?.turnkeyAccountId,
309
+ turnkey?.accountId,
310
+ turnkey?.account_id,
311
+ );
312
+
313
+ if (!subOrgId || !walletId || !accountId) {
314
+ return null;
315
+ }
316
+
317
+ const orgId = firstNonEmpty(
318
+ data?.turnkey_org_id,
319
+ data?.turnkeyOrgId,
320
+ turnkey?.orgId,
321
+ turnkey?.org_id,
322
+ );
323
+ const address = firstNonEmpty(
324
+ data?.wallet_address,
325
+ data?.walletAddress,
326
+ data?.address,
327
+ turnkey?.address,
328
+ );
329
+
330
+ return {
331
+ ...(orgId ? { orgId } : {}),
332
+ subOrgId,
333
+ ...(userId ? { userId } : {}),
334
+ walletId,
335
+ accountId,
336
+ ...(address ? { address } : {}),
337
+ };
338
+ };
339
+
340
+ const normalizeIntentText = (value: string) => value.trim();
341
+
342
+ const normalizeOptionalIntentText = (value?: string) => {
343
+ const normalized = normalizeIntentText(value ?? '');
344
+ return normalized || undefined;
345
+ };
346
+
347
+ export const normalizeWalletAuthOperationIntent = (
348
+ intent: WalletAuthOperationIntent,
349
+ ): WalletAuthOperationIntent => {
350
+ const baseIntent = {
351
+ profileId: intent.profileId,
352
+ chain: normalizeIntentText(intent.chain),
353
+ nonce: normalizeIntentText(intent.nonce),
354
+ amount: normalizeIntentText(intent.amount),
355
+ assetAddress: normalizeIntentText(intent.assetAddress),
356
+ assetSymbol: normalizeIntentText(intent.assetSymbol),
357
+ };
358
+
359
+ if (intent.source === 'withdrawal') {
360
+ return {
361
+ ...baseIntent,
362
+ source: 'withdrawal',
363
+ destinationAddress: normalizeIntentText(intent.destinationAddress),
364
+ };
365
+ }
366
+
367
+ return {
368
+ ...baseIntent,
369
+ source: 'exchange',
370
+ toAssetAddress: normalizeIntentText(intent.toAssetAddress),
371
+ toAssetSymbol: normalizeIntentText(intent.toAssetSymbol),
372
+ destinationAddress: normalizeIntentText(intent.destinationAddress),
373
+ ...(normalizeOptionalIntentText(intent.estimatedReceiveText)
374
+ ? { estimatedReceiveText: normalizeOptionalIntentText(intent.estimatedReceiveText) }
375
+ : {}),
376
+ ...(normalizeOptionalIntentText(intent.estimatedRateText)
377
+ ? { estimatedRateText: normalizeOptionalIntentText(intent.estimatedRateText) }
378
+ : {}),
379
+ };
380
+ };
381
+
382
+ const tryParseJson = (value: string) => {
383
+ try {
384
+ return JSON.parse(value) as unknown;
385
+ } catch {
386
+ return null;
387
+ }
388
+ };
389
+
390
+ const extractWalletAuthErrorMessage = (error: unknown): string => {
391
+ if (!error) {
392
+ return '';
393
+ }
394
+
395
+ if (typeof error === 'string') {
396
+ const parsed = tryParseJson(error);
397
+ if (parsed) {
398
+ return extractWalletAuthErrorMessage(parsed);
399
+ }
400
+
401
+ return error.trim();
402
+ }
403
+
404
+ if (error instanceof Error) {
405
+ return extractWalletAuthErrorMessage({
406
+ message: error.message,
407
+ name: error.name,
408
+ cause: error.cause,
409
+ });
410
+ }
411
+
412
+ if (typeof error !== 'object') {
413
+ return '';
414
+ }
415
+
416
+ const errorRecord = error as Record<string, unknown>;
417
+ const nestedCandidates = [
418
+ errorRecord.message,
419
+ errorRecord.error,
420
+ errorRecord.details,
421
+ errorRecord.cause,
422
+ ];
423
+
424
+ for (const candidate of nestedCandidates) {
425
+ const message = extractWalletAuthErrorMessage(candidate);
426
+ if (message) {
427
+ return message;
428
+ }
429
+ }
430
+
431
+ return '';
432
+ };
433
+
434
+ export const resolveWalletAuthErrorMessage = (
435
+ error: unknown,
436
+ fallbackMessage: string,
437
+ ) => extractWalletAuthErrorMessage(error) || fallbackMessage;
438
+
439
+ const normalizeErrorMessage = (error?: unknown) => {
440
+ const errorLike = error as Error | undefined;
441
+
442
+ return `${String(errorLike?.name ?? '').trim()} ${String(errorLike?.message ?? error ?? '').trim()}`
443
+ .trim()
444
+ .toLowerCase();
445
+ };
446
+
447
+ export const isUserRejectedWalletSignatureError = (error?: unknown) => {
448
+ const message = normalizeErrorMessage(error);
449
+
450
+ return [
451
+ 'user rejected',
452
+ 'user denied',
453
+ 'rejected the request',
454
+ 'signature rejected',
455
+ 'cancelled',
456
+ 'canceled',
457
+ ].some((pattern) => message.includes(pattern));
458
+ };
459
+
460
+ export const isRecoverableWalletAuthError = (error?: unknown) => {
461
+ const message = normalizeErrorMessage(error);
462
+
463
+ if (!message) {
464
+ return false;
465
+ }
466
+
467
+ if (isUserRejectedWalletSignatureError(error)) {
468
+ return false;
469
+ }
470
+
471
+ return [
472
+ 'notauthenticatederror',
473
+ 'not authenticated',
474
+ 'signer not authenticated',
475
+ 'please authenticate',
476
+ 'authentication failed',
477
+ 'wallet auth',
478
+ 'wallet signer',
479
+ 'not connected',
480
+ 'disconnected',
481
+ 'session expired',
482
+ 'expired session',
483
+ 'iframe container cannot be found',
484
+ 'key not initialized',
485
+ 'call init() first',
486
+ 'wallet session does not match requested wallet',
487
+ 'turnkey account does not match requested authorization',
488
+ 'missing signer',
489
+ 'no signer',
490
+ ].some((pattern) => message.includes(pattern));
491
+ };
@@ -0,0 +1,106 @@
1
+ import type { WalletAuthOperationIntent } from './auth.ts';
2
+
3
+ export type WalletAuthOperationLink = {
4
+ href: string;
5
+ text: string;
6
+ title: string;
7
+ ariaLabel: string;
8
+ };
9
+
10
+ export const shortenWalletAuthOperationText = (value: string) => {
11
+ const trimmed = value.trim();
12
+
13
+ if (trimmed.length <= 20) {
14
+ return trimmed;
15
+ }
16
+
17
+ return `${trimmed.slice(0, 8)}...${trimmed.slice(-6)}`;
18
+ };
19
+
20
+ export const formatWalletAuthAddressValue = (value: string) => (
21
+ shortenWalletAuthOperationText(value)
22
+ );
23
+
24
+ export const formatWalletAuthAssetValue = (symbol: string, address: string) => {
25
+ const formattedSymbol = symbol.trim();
26
+ const formattedAddress = address.trim();
27
+
28
+ if (!formattedAddress) {
29
+ return formattedSymbol;
30
+ }
31
+
32
+ if (!formattedSymbol || formattedSymbol.toLowerCase() === formattedAddress.toLowerCase()) {
33
+ return formatWalletAuthAddressValue(formattedAddress);
34
+ }
35
+
36
+ return `${formattedSymbol} (${formatWalletAuthAddressValue(formattedAddress)})`;
37
+ };
38
+
39
+ export const buildWalletAuthExplorerLink = ({
40
+ baseUrl,
41
+ type,
42
+ value,
43
+ text,
44
+ ariaLabel,
45
+ }: {
46
+ baseUrl: string;
47
+ type: 'address' | 'token';
48
+ value: string;
49
+ text: string;
50
+ ariaLabel: string;
51
+ }): WalletAuthOperationLink | null => {
52
+ const trimmedValue = value.trim();
53
+ const trimmedBaseUrl = baseUrl.trim().replace(/\/$/, '');
54
+
55
+ if (!trimmedValue || !trimmedBaseUrl) {
56
+ return null;
57
+ }
58
+
59
+ return {
60
+ href: `${trimmedBaseUrl}/${type}/${trimmedValue}`,
61
+ text,
62
+ title: trimmedValue,
63
+ ariaLabel,
64
+ };
65
+ };
66
+
67
+ const EXPLORER_BASE_BY_CHAIN: Readonly<Record<string, string>> = {
68
+ ethereum: 'https://etherscan.io',
69
+ 'ethereum-mainnet': 'https://etherscan.io',
70
+ mainnet: 'https://etherscan.io',
71
+ homestead: 'https://etherscan.io',
72
+ 'ethereum-sepolia': 'https://sepolia.etherscan.io',
73
+ 'ethereum sepolia': 'https://sepolia.etherscan.io',
74
+ sepolia: 'https://sepolia.etherscan.io',
75
+ polygon: 'https://polygonscan.com',
76
+ base: 'https://basescan.org',
77
+ };
78
+
79
+ const getChainExplorerBaseUrl = (chain: string | null | undefined) => (
80
+ chain ? EXPLORER_BASE_BY_CHAIN[chain.trim().toLowerCase()] : undefined
81
+ );
82
+
83
+ /** Builds a transaction link from the Vault deployment chain, without a network default. */
84
+ export const buildChainExplorerTransactionUrl = (
85
+ chain: string | null | undefined,
86
+ transactionHash: string | null | undefined,
87
+ ): string | null => {
88
+ const base = getChainExplorerBaseUrl(chain);
89
+ return base && transactionHash ? `${base}/tx/${transactionHash}` : null;
90
+ };
91
+
92
+ /** Builds an address link from the Vault deployment chain, without a network default. */
93
+ export const buildChainExplorerAddressUrl = (
94
+ chain: string | null | undefined,
95
+ address: string | null | undefined,
96
+ ): string | null => {
97
+ const base = getChainExplorerBaseUrl(chain);
98
+ const normalizedAddress = address?.trim();
99
+ return base && normalizedAddress && /^0x[0-9a-f]{40}$/i.test(normalizedAddress)
100
+ ? `${base}/address/${normalizedAddress}`
101
+ : null;
102
+ };
103
+
104
+ export const getWalletAuthExchangeTargetAssetAddress = (
105
+ intent: WalletAuthOperationIntent,
106
+ ) => (intent.source === 'exchange' ? intent.toAssetAddress : '');
@@ -0,0 +1,50 @@
1
+ const WALLET_CREATION_REQUIRED_ERROR_FRAGMENTS = [
2
+ 'not found',
3
+ 'create wallet',
4
+ 'wallet registration is incomplete',
5
+ ];
6
+
7
+ const getErrorStatusCode = (error: unknown) => {
8
+ const data = (error as {
9
+ data?: {
10
+ statusCode?: unknown;
11
+ status?: unknown;
12
+ };
13
+ } | null | undefined)?.data;
14
+
15
+ if (typeof data?.statusCode === 'number') {
16
+ return data.statusCode;
17
+ }
18
+
19
+ if (typeof data?.status === 'number') {
20
+ return data.status;
21
+ }
22
+
23
+ return undefined;
24
+ };
25
+
26
+ export const isWalletSetupRequiredError = (
27
+ error: unknown,
28
+ options: {
29
+ isKycApproved?: boolean;
30
+ walletData?: unknown;
31
+ } = {},
32
+ ) => {
33
+ const { isKycApproved = false, walletData } = options;
34
+
35
+ const errorTexts = [
36
+ error instanceof Error && typeof error.message === 'string' ? error.message : '',
37
+ error ? String(error) : '',
38
+ ].map((value) => value.toLowerCase());
39
+ const errorStatusCode = getErrorStatusCode(error);
40
+
41
+ const isMissingApprovedWallet =
42
+ Boolean(error)
43
+ && walletData === undefined
44
+ && isKycApproved
45
+ && errorStatusCode === 404;
46
+
47
+ return isMissingApprovedWallet || WALLET_CREATION_REQUIRED_ERROR_FRAGMENTS.some((fragment) => (
48
+ errorTexts.some((value) => value.includes(fragment))
49
+ ));
50
+ };