@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,625 @@
1
+ export const EvmWalletStatusTypes = {
2
+ created: 'created',
3
+ verified: 'verified',
4
+ error: 'error',
5
+ error_retry: 'error_retry',
6
+ error_document: 'error_document',
7
+ error_pending: 'error_pending',
8
+ error_suspended: 'error_suspended',
9
+ } as const;
10
+
11
+ export type EvmWalletStatusTypes =
12
+ (typeof EvmWalletStatusTypes)[keyof typeof EvmWalletStatusTypes];
13
+
14
+ export type IEvmWalletBalancesMap = Record<string, {
15
+ id?: number | string;
16
+ offer_id?: number;
17
+ address: string;
18
+ amount: number | string;
19
+ symbol: string;
20
+ asset?: string;
21
+ name?: string;
22
+ icon?: string;
23
+ price_per_usd?: number | string;
24
+ price_per_usd_raw?: string;
25
+ amount_usd?: number;
26
+ chain?: string;
27
+ token_decimals?: number;
28
+ token_standard?: string;
29
+ is_native?: boolean;
30
+ balance_type?: 'rwa_asset' | 'tradable_crypto';
31
+ exchange_eligible?: boolean;
32
+ exchange_ineligible_reason?: string;
33
+ }>;
34
+
35
+ export interface IEvmWalletBalances {
36
+ id?: number;
37
+ offer_id?: number;
38
+ asset?: string;
39
+ address: string;
40
+ amount: number;
41
+ symbol: string;
42
+ name?: string;
43
+ icon?: string;
44
+ price_per_usd?: number | string;
45
+ price_per_usd_raw?: string;
46
+ amount_usd?: number;
47
+ chain?: string;
48
+ token_decimals?: number;
49
+ token_standard?: string;
50
+ is_native?: boolean;
51
+ balance_type?: 'rwa_asset' | 'tradable_crypto';
52
+ exchange_eligible?: boolean;
53
+ exchange_ineligible_reason?: string;
54
+ tokenValue?: string;
55
+ }
56
+
57
+ export type IEvmWalletBalancesInput = IEvmWalletBalancesMap | IEvmWalletBalances[];
58
+
59
+ export interface IEvmWalletChainAccount {
60
+ chain: string;
61
+ wallet_address: string;
62
+ chain_account_status?: string;
63
+ exchange_payout_token_address?: string;
64
+ exchange_payout_token_symbol?: string;
65
+ }
66
+
67
+ export interface IEvmWalletDepositInstructions {
68
+ chain?: string;
69
+ address?: string;
70
+ }
71
+
72
+ export interface IEvmWalletBalanceSummaryResponse {
73
+ amount_usd?: number | string;
74
+ token_count?: number | string;
75
+ }
76
+
77
+ export interface IEvmWalletDataResponse<TTransaction = unknown> {
78
+ id: number;
79
+ wallet_id?: number;
80
+ profile_id?: number;
81
+ status: EvmWalletStatusTypes;
82
+ provider_name?: string;
83
+ turnkey_org_id?: string;
84
+ turnkey_sub_org_id?: string;
85
+ turnkey_user_id?: string;
86
+ turnkey_wallet_id?: string;
87
+ turnkey_account_id?: string;
88
+ balance: string;
89
+ inc_balance: number;
90
+ out_balance: number;
91
+ address: string;
92
+ chain?: string;
93
+ exchange_payout_token_address?: string;
94
+ exchange_payout_token_symbol?: string;
95
+ deposit_instructions?: IEvmWalletDepositInstructions;
96
+ chains?: IEvmWalletChainAccount[];
97
+ balances: IEvmWalletBalancesMap;
98
+ tradable_crypto_balance?: IEvmWalletBalanceSummaryResponse;
99
+ rwa_asset_balance?: IEvmWalletBalanceSummaryResponse;
100
+ transactions: TTransaction[];
101
+ created_at: string;
102
+ updated_at: string;
103
+ }
104
+
105
+ export type IEvmWalletDataForFormatter<TTransaction = unknown> =
106
+ Omit<IEvmWalletDataResponse<TTransaction>, 'balances'> & {
107
+ balances?: IEvmWalletBalancesInput;
108
+ };
109
+
110
+ export const EVM_WALLET_STABLECOIN_SYMBOLS = [
111
+ 'USDC',
112
+ 'USDT',
113
+ 'DAI',
114
+ 'BUSD',
115
+ 'TUSD',
116
+ 'USDP',
117
+ 'FRAX',
118
+ 'LUSD',
119
+ 'SUSD',
120
+ 'GUSD',
121
+ ] as const;
122
+ const EVM_WALLET_STABLECOIN_SYMBOL_SET = new Set<string>(EVM_WALLET_STABLECOIN_SYMBOLS);
123
+
124
+ export interface EvmWalletBalanceTotalsBalance {
125
+ id?: number | string;
126
+ address?: string;
127
+ asset?: string;
128
+ name?: string;
129
+ icon?: string;
130
+ amount?: number | string;
131
+ balance?: number | string;
132
+ symbol?: string;
133
+ price_per_usd?: number | string;
134
+ amount_usd?: number | string;
135
+ chain?: string;
136
+ token_decimals?: number | string;
137
+ token_standard?: string;
138
+ is_native?: boolean;
139
+ }
140
+
141
+ export type EvmWalletBalanceTotalsBalances =
142
+ | Record<string, EvmWalletBalanceTotalsBalance>
143
+ | EvmWalletBalanceTotalsBalance[];
144
+
145
+ export interface EvmWalletBalanceTotalsInput {
146
+ balances?: EvmWalletBalanceTotalsBalances;
147
+ exchange_payout_token_address?: string;
148
+ exchange_payout_token_symbol?: string;
149
+ tradable_crypto_balance?: IEvmWalletBalanceSummaryResponse;
150
+ rwa_asset_balance?: IEvmWalletBalanceSummaryResponse;
151
+ }
152
+
153
+ export interface IEvmWalletChainAccountResponse {
154
+ chain?: string;
155
+ wallet_address?: string;
156
+ chain_account_status?: string;
157
+ exchange_payout_token_address?: string;
158
+ exchange_payout_token_symbol?: string;
159
+ }
160
+
161
+ export interface IEvmWalletInfoStatusResponse {
162
+ profile_id?: number;
163
+ wallet_id?: number;
164
+ wallet_status?: string;
165
+ provider_name?: string;
166
+ turnkey_org_id?: string;
167
+ turnkey_sub_org_id?: string;
168
+ turnkey_user_id?: string;
169
+ turnkey_wallet_id?: string;
170
+ turnkey_account_id?: string;
171
+ chain?: string;
172
+ wallet_address?: string;
173
+ chain_account_status?: string;
174
+ exchange_payout_token_address?: string;
175
+ exchange_payout_token_symbol?: string;
176
+ chains?: IEvmWalletChainAccountResponse[];
177
+ deposit_instructions?: {
178
+ chain?: string;
179
+ address?: string;
180
+ };
181
+ balances?: IEvmWalletInfoBalanceResponse[] | Record<string, IEvmWalletInfoBalanceResponse>;
182
+ tradable_crypto_balance?: IEvmWalletBalanceSummaryResponse;
183
+ rwa_asset_balance?: IEvmWalletBalanceSummaryResponse;
184
+ created_at?: string;
185
+ updated_at?: string;
186
+ }
187
+
188
+ export type IEvmWalletInfoApiResponse<TTransaction = unknown> =
189
+ | IEvmWalletDataResponse<TTransaction>
190
+ | IEvmWalletInfoStatusResponse;
191
+
192
+ export interface NormalizeEvmWalletInfoOptions {
193
+ stableCoinAddress?: string;
194
+ stableCoinSymbol?: string;
195
+ }
196
+
197
+ export interface IEvmWalletInfoBalanceResponse {
198
+ id?: number | string;
199
+ offer_id?: number | string;
200
+ asset?: string;
201
+ asset_name?: string;
202
+ asset_ticker?: string;
203
+ asset_image?: string;
204
+ symbol?: string;
205
+ address?: string;
206
+ amount?: number | string;
207
+ balance?: number | string;
208
+ name?: string;
209
+ icon?: string;
210
+ price_per_usd?: number | string;
211
+ price_usd?: number | string;
212
+ amount_usd?: number | string;
213
+ token_address?: string;
214
+ token_name?: string;
215
+ token_symbol?: string;
216
+ token_logo?: string;
217
+ token_decimals?: number | string;
218
+ token_standard?: string;
219
+ chain?: string;
220
+ is_native?: boolean;
221
+ balance_type?: 'rwa_asset' | 'tradable_crypto';
222
+ exchange_eligible?: boolean;
223
+ exchange_ineligible_reason?: string;
224
+ }
225
+
226
+ const KNOWN_WALLET_STATUSES = new Set<string>(Object.values(EvmWalletStatusTypes));
227
+
228
+ const firstString = (...values: unknown[]): string =>
229
+ values
230
+ .map((value) => String(value ?? '').trim())
231
+ .find(Boolean)
232
+ ?? '';
233
+
234
+ const toOptionalNumber = (value: unknown): number | undefined => {
235
+ if (value == null || value === '') return undefined;
236
+ const next = Number(value);
237
+ return Number.isFinite(next) ? next : undefined;
238
+ };
239
+
240
+ const toNumberOrNull = (value: unknown): number | null => {
241
+ if (value == null || value === '') return null;
242
+ const next = Number(value);
243
+ return Number.isFinite(next) ? next : null;
244
+ };
245
+
246
+ const getWalletBalanceSummaryAmount = (
247
+ summary?: IEvmWalletBalanceSummaryResponse,
248
+ ): number | null => toNumberOrNull(summary?.amount_usd);
249
+
250
+ const getWalletBalancesArray = (
251
+ balances?: EvmWalletBalanceTotalsBalances,
252
+ ): EvmWalletBalanceTotalsBalance[] => {
253
+ if (!balances) return [];
254
+ return Array.isArray(balances) ? balances : Object.values(balances);
255
+ };
256
+
257
+ const isStablecoinSymbol = (symbol: unknown): boolean => (
258
+ EVM_WALLET_STABLECOIN_SYMBOL_SET.has(String(symbol || '').toUpperCase())
259
+ );
260
+
261
+ export const calculateEvmWalletFundingBalance = (
262
+ data: EvmWalletBalanceTotalsInput,
263
+ ): number => {
264
+ const balances = getWalletBalancesArray(data.balances);
265
+ const payoutTokenAddress = String(data.exchange_payout_token_address ?? '').trim().toLowerCase();
266
+ if (payoutTokenAddress) {
267
+ const payoutToken = balances.find(
268
+ balance => String(balance.address ?? '').trim().toLowerCase() === payoutTokenAddress,
269
+ );
270
+ const amount = toNumberOrNull(payoutToken?.amount);
271
+ return amount ?? 0;
272
+ }
273
+
274
+ const payoutTokenSymbol = String(data.exchange_payout_token_symbol ?? '').trim().toUpperCase();
275
+ if (payoutTokenSymbol) {
276
+ const payoutToken = balances.find(
277
+ balance => String(balance.symbol ?? '').trim().toUpperCase() === payoutTokenSymbol,
278
+ );
279
+ const amount = toNumberOrNull(payoutToken?.amount);
280
+ return amount ?? 0;
281
+ }
282
+
283
+ const summaryAmount = getWalletBalanceSummaryAmount(data.tradable_crypto_balance);
284
+ if (summaryAmount !== null) return summaryAmount;
285
+
286
+ return balances.reduce((sum, balance) => {
287
+ const isStablecoin = isStablecoinSymbol(balance.symbol);
288
+ const amount = Number(balance.amount ?? 0);
289
+ return isStablecoin ? sum + amount : sum;
290
+ }, 0);
291
+ };
292
+
293
+ export const calculateEvmWalletRwaValue = (
294
+ data: EvmWalletBalanceTotalsInput,
295
+ ): number => {
296
+ const summaryAmount = getWalletBalanceSummaryAmount(data.rwa_asset_balance);
297
+ if (summaryAmount !== null) return summaryAmount;
298
+
299
+ return getWalletBalancesArray(data.balances).reduce((sum, balance) => {
300
+ const isStablecoin = isStablecoinSymbol(balance.symbol);
301
+ const amount = Number(balance.amount ?? 0);
302
+ const pricePerUsd = Number(balance.price_per_usd ?? 0);
303
+ const valueUsd = amount * pricePerUsd;
304
+ return !isStablecoin ? sum + valueUsd : sum;
305
+ }, 0);
306
+ };
307
+
308
+ const normalizeWalletInfoBalances = (
309
+ balances?: IEvmWalletInfoStatusResponse['balances'],
310
+ ): IEvmWalletDataResponse['balances'] => {
311
+ const balancesArray = Array.isArray(balances)
312
+ ? balances
313
+ : Object.values(balances ?? {});
314
+
315
+ return balancesArray.reduce<IEvmWalletDataResponse['balances']>((acc, balance, index) => {
316
+ const address = firstString(balance?.address, balance?.token_address);
317
+ const name = firstString(
318
+ balance?.name,
319
+ balance?.token_name,
320
+ balance?.asset_name,
321
+ balance?.asset,
322
+ balance?.symbol,
323
+ balance?.token_symbol,
324
+ balance?.asset_ticker,
325
+ );
326
+ const symbol = firstString(
327
+ balance?.symbol,
328
+ balance?.token_symbol,
329
+ balance?.asset_ticker,
330
+ balance?.asset,
331
+ name,
332
+ );
333
+ const asset = firstString(balance?.asset, balance?.asset_ticker, balance?.token_symbol, symbol);
334
+ const icon = firstString(balance?.icon, balance?.token_logo, balance?.asset_image);
335
+ const pricePerUsdRaw = firstString(balance?.price_per_usd, balance?.price_usd);
336
+ const pricePerUsd = toOptionalNumber(pricePerUsdRaw);
337
+ const amountUsd = toOptionalNumber(balance?.amount_usd);
338
+ const id = toOptionalNumber(balance?.id);
339
+ const offerId = toOptionalNumber(balance?.offer_id);
340
+ const tokenDecimals = toOptionalNumber(balance?.token_decimals);
341
+ const tokenStandard = firstString(balance?.token_standard);
342
+ const chain = firstString(balance?.chain);
343
+ const amount = balance?.amount ?? balance?.balance ?? 0;
344
+
345
+ if (!address && !symbol) {
346
+ return acc;
347
+ }
348
+
349
+ const key = address || symbol || String(index);
350
+ acc[key] = {
351
+ ...(id !== undefined ? { id } : {}),
352
+ ...(offerId !== undefined ? { offer_id: offerId } : {}),
353
+ asset: asset || undefined,
354
+ address,
355
+ amount,
356
+ symbol,
357
+ name: name || symbol || undefined,
358
+ ...(icon ? { icon } : {}),
359
+ ...(pricePerUsd !== undefined ? { price_per_usd: pricePerUsd } : {}),
360
+ ...(pricePerUsdRaw ? { price_per_usd_raw: pricePerUsdRaw } : {}),
361
+ ...(amountUsd !== undefined ? { amount_usd: amountUsd } : {}),
362
+ ...(chain ? { chain } : {}),
363
+ ...(tokenDecimals !== undefined ? { token_decimals: tokenDecimals } : {}),
364
+ ...(tokenStandard ? { token_standard: tokenStandard } : {}),
365
+ ...(typeof balance?.is_native === 'boolean' ? { is_native: balance.is_native } : {}),
366
+ ...(balance?.balance_type ? { balance_type: balance.balance_type } : {}),
367
+ ...(typeof balance?.exchange_eligible === 'boolean'
368
+ ? { exchange_eligible: balance.exchange_eligible }
369
+ : {}),
370
+ ...(firstString(balance?.exchange_ineligible_reason)
371
+ ? { exchange_ineligible_reason: firstString(balance.exchange_ineligible_reason) }
372
+ : {}),
373
+ };
374
+ return acc;
375
+ }, {});
376
+ };
377
+
378
+ const reconcileConfiguredStableCoinBalance = (
379
+ balances: IEvmWalletDataResponse['balances'],
380
+ options: NormalizeEvmWalletInfoOptions,
381
+ ): {
382
+ balances: IEvmWalletDataResponse['balances'];
383
+ amountUsdAdjustment: number;
384
+ } => {
385
+ const stableCoinAddress = firstString(options.stableCoinAddress).toLowerCase();
386
+ const stableCoinSymbol = firstString(options.stableCoinSymbol, 'USDC').toUpperCase();
387
+ if (!stableCoinAddress || !stableCoinSymbol) {
388
+ return { balances, amountUsdAdjustment: 0 };
389
+ }
390
+
391
+ let amountUsdAdjustment = 0;
392
+ let matched = false;
393
+ const reconciledBalances = Object.entries(balances).reduce<IEvmWalletDataResponse['balances']>(
394
+ (result, [key, balance]) => {
395
+ if (String(balance.address ?? '').trim().toLowerCase() !== stableCoinAddress) {
396
+ result[key] = balance;
397
+ return result;
398
+ }
399
+
400
+ matched = true;
401
+ const amount = toNumberOrNull(balance.amount) ?? 0;
402
+ const previousAmountUsd = toNumberOrNull(balance.amount_usd) ?? 0;
403
+ amountUsdAdjustment += amount - previousAmountUsd;
404
+
405
+ result[key] = {
406
+ ...balance,
407
+ asset: stableCoinSymbol,
408
+ symbol: stableCoinSymbol,
409
+ name: stableCoinSymbol,
410
+ price_per_usd: 1,
411
+ price_per_usd_raw: '1',
412
+ amount_usd: amount,
413
+ balance_type: 'tradable_crypto',
414
+ };
415
+ return result;
416
+ },
417
+ {},
418
+ );
419
+
420
+ return {
421
+ balances: matched ? reconciledBalances : balances,
422
+ amountUsdAdjustment,
423
+ };
424
+ };
425
+
426
+ const normalizeWalletBalanceSummary = (
427
+ summary?: IEvmWalletBalanceSummaryResponse | null,
428
+ ): IEvmWalletBalanceSummaryResponse | undefined => {
429
+ if (!summary) return undefined;
430
+
431
+ const tokenCount = toOptionalNumber(summary.token_count);
432
+ return {
433
+ amount_usd: String(summary.amount_usd ?? '0'),
434
+ token_count: tokenCount ?? 0,
435
+ };
436
+ };
437
+
438
+ const adjustWalletBalanceSummary = (
439
+ summary: IEvmWalletBalanceSummaryResponse | undefined,
440
+ amountUsdAdjustment: number,
441
+ ): IEvmWalletBalanceSummaryResponse | undefined => {
442
+ if (!summary || !Number.isFinite(amountUsdAdjustment) || amountUsdAdjustment === 0) {
443
+ return summary;
444
+ }
445
+
446
+ const currentAmount = toNumberOrNull(summary.amount_usd) ?? 0;
447
+ return {
448
+ ...summary,
449
+ amount_usd: String(currentAmount + amountUsdAdjustment),
450
+ };
451
+ };
452
+
453
+ const normalizeStatus = (
454
+ walletStatus?: string | null,
455
+ chainStatuses: string[] = [],
456
+ ): EvmWalletStatusTypes => {
457
+ const normalizedWalletStatus = String(walletStatus ?? '').trim().toLowerCase();
458
+ if (KNOWN_WALLET_STATUSES.has(normalizedWalletStatus)) {
459
+ return normalizedWalletStatus as EvmWalletStatusTypes;
460
+ }
461
+
462
+ const knownChainStatus = chainStatuses.find((status) => KNOWN_WALLET_STATUSES.has(status));
463
+ if (knownChainStatus) {
464
+ return knownChainStatus as EvmWalletStatusTypes;
465
+ }
466
+
467
+ return EvmWalletStatusTypes.created;
468
+ };
469
+
470
+ export const isEvmWalletLegacyResponse = <TTransaction = unknown>(
471
+ data: IEvmWalletInfoApiResponse<TTransaction>,
472
+ ): data is IEvmWalletDataResponse<TTransaction> => (
473
+ 'status' in data
474
+ && 'balance' in data
475
+ && 'transactions' in data
476
+ );
477
+
478
+ export const normalizeEvmWalletInfoResponse = <TTransaction = unknown>(
479
+ data: IEvmWalletInfoApiResponse<TTransaction>,
480
+ options: NormalizeEvmWalletInfoOptions = {},
481
+ ): IEvmWalletDataResponse<TTransaction> => {
482
+ if (isEvmWalletLegacyResponse(data)) {
483
+ const reconciled = reconcileConfiguredStableCoinBalance(data.balances, options);
484
+ return reconciled.balances === data.balances
485
+ ? data
486
+ : {
487
+ ...data,
488
+ balances: reconciled.balances,
489
+ tradable_crypto_balance: adjustWalletBalanceSummary(
490
+ data.tradable_crypto_balance,
491
+ reconciled.amountUsdAdjustment,
492
+ ),
493
+ };
494
+ }
495
+
496
+ const chains = Array.isArray(data.chains) ? data.chains : [];
497
+ const rootChain = String(data.chain ?? '').trim();
498
+ const rootWalletAddress = String(data.wallet_address ?? '').trim();
499
+ const rootChainAccountStatus = String(data.chain_account_status ?? '').trim().toLowerCase();
500
+ const normalizedInputChains = [
501
+ ...chains,
502
+ ...((rootChain || rootWalletAddress || rootChainAccountStatus) && !chains.some((chain) => (
503
+ String(chain.chain ?? '').trim().toLowerCase() === rootChain.toLowerCase()
504
+ ))
505
+ ? [{
506
+ chain: rootChain,
507
+ wallet_address: rootWalletAddress,
508
+ chain_account_status: rootChainAccountStatus,
509
+ exchange_payout_token_address: data.exchange_payout_token_address,
510
+ exchange_payout_token_symbol: data.exchange_payout_token_symbol,
511
+ }]
512
+ : []),
513
+ ];
514
+ const chainStatuses = normalizedInputChains
515
+ .map((chain) => String(chain.chain_account_status ?? '').trim().toLowerCase())
516
+ .filter(Boolean);
517
+ const normalizedChains: IEvmWalletChainAccount[] = normalizedInputChains
518
+ .map((chain) => {
519
+ const chainAccountStatus = String(chain.chain_account_status ?? '').trim().toLowerCase();
520
+ const payoutTokenAddress = firstString(chain.exchange_payout_token_address);
521
+ const payoutTokenSymbol = firstString(chain.exchange_payout_token_symbol);
522
+ return {
523
+ chain: String(chain.chain ?? '').trim(),
524
+ wallet_address: String(chain.wallet_address ?? '').trim(),
525
+ ...(chainAccountStatus ? { chain_account_status: chainAccountStatus } : {}),
526
+ ...(payoutTokenAddress ? { exchange_payout_token_address: payoutTokenAddress } : {}),
527
+ ...(payoutTokenSymbol ? { exchange_payout_token_symbol: payoutTokenSymbol } : {}),
528
+ };
529
+ })
530
+ .filter((chain) => Boolean(chain.chain));
531
+ const firstWalletAddress = normalizedChains
532
+ .map((chain) => String(chain.wallet_address ?? '').trim())
533
+ .find(Boolean)
534
+ ?? '';
535
+ const depositInstructionAddress = String(data.deposit_instructions?.address ?? '').trim();
536
+ const address = firstWalletAddress
537
+ || rootWalletAddress
538
+ || depositInstructionAddress
539
+ || '';
540
+ const depositInstructions = data.deposit_instructions
541
+ ? {
542
+ chain: String(data.deposit_instructions.chain ?? '').trim() || undefined,
543
+ address: String(data.deposit_instructions.address ?? '').trim() || undefined,
544
+ }
545
+ : undefined;
546
+ const updatedAt = data.updated_at ?? data.created_at ?? new Date().toISOString();
547
+ const normalizedBalances = normalizeWalletInfoBalances(data.balances);
548
+ const reconciled = reconcileConfiguredStableCoinBalance(normalizedBalances, options);
549
+ const tradableCryptoBalance = adjustWalletBalanceSummary(
550
+ normalizeWalletBalanceSummary(data.tradable_crypto_balance),
551
+ reconciled.amountUsdAdjustment,
552
+ );
553
+ const rwaAssetBalance = normalizeWalletBalanceSummary(data.rwa_asset_balance);
554
+ const providerName = String(data.provider_name ?? '').trim();
555
+ const turnkeyOrgID = firstString(data.turnkey_org_id);
556
+ const turnkeySubOrgID = firstString(data.turnkey_sub_org_id);
557
+ const turnkeyUserID = firstString(data.turnkey_user_id);
558
+ const turnkeyWalletID = firstString(data.turnkey_wallet_id);
559
+ const turnkeyAccountID = firstString(data.turnkey_account_id);
560
+
561
+ return {
562
+ id: Number(data.profile_id ?? data.wallet_id ?? 0),
563
+ ...(data.wallet_id != null ? { wallet_id: Number(data.wallet_id) } : {}),
564
+ status: normalizeStatus(data.wallet_status, chainStatuses),
565
+ ...(providerName ? { provider_name: providerName } : {}),
566
+ ...(turnkeyOrgID ? { turnkey_org_id: turnkeyOrgID } : {}),
567
+ ...(turnkeySubOrgID ? { turnkey_sub_org_id: turnkeySubOrgID } : {}),
568
+ ...(turnkeyUserID ? { turnkey_user_id: turnkeyUserID } : {}),
569
+ ...(turnkeyWalletID ? { turnkey_wallet_id: turnkeyWalletID } : {}),
570
+ ...(turnkeyAccountID ? { turnkey_account_id: turnkeyAccountID } : {}),
571
+ balance: '0',
572
+ inc_balance: 0,
573
+ out_balance: 0,
574
+ address,
575
+ ...(rootChain ? { chain: rootChain } : {}),
576
+ ...(firstString(data.exchange_payout_token_address)
577
+ ? { exchange_payout_token_address: firstString(data.exchange_payout_token_address) }
578
+ : {}),
579
+ ...(firstString(data.exchange_payout_token_symbol)
580
+ ? { exchange_payout_token_symbol: firstString(data.exchange_payout_token_symbol) }
581
+ : {}),
582
+ deposit_instructions: depositInstructions,
583
+ chains: normalizedChains,
584
+ balances: reconciled.balances,
585
+ ...(tradableCryptoBalance ? { tradable_crypto_balance: tradableCryptoBalance } : {}),
586
+ ...(rwaAssetBalance ? { rwa_asset_balance: rwaAssetBalance } : {}),
587
+ transactions: [],
588
+ created_at: data.created_at ?? updatedAt,
589
+ updated_at: updatedAt,
590
+ };
591
+ };
592
+
593
+ export const extractDepositAddressFromWalletInfo = (
594
+ data: IEvmWalletInfoApiResponse,
595
+ ): string => {
596
+ if (isEvmWalletLegacyResponse(data)) {
597
+ return String(data.address ?? '').trim();
598
+ }
599
+
600
+ const depositInstructionAddress = String(data.deposit_instructions?.address ?? '').trim();
601
+ if (depositInstructionAddress) {
602
+ return depositInstructionAddress;
603
+ }
604
+
605
+ const balances = Array.isArray(data.balances)
606
+ ? data.balances
607
+ : Object.values(data.balances ?? {});
608
+ const usdcBalanceAddress = balances
609
+ .find((balance) => {
610
+ const asset = firstString(
611
+ balance?.asset,
612
+ balance?.symbol,
613
+ balance?.token_symbol,
614
+ balance?.asset_ticker,
615
+ ).toUpperCase();
616
+ return asset === 'USDC';
617
+ });
618
+ const usdcAddress = firstString(usdcBalanceAddress?.address, usdcBalanceAddress?.token_address);
619
+ if (usdcAddress) {
620
+ return usdcAddress;
621
+ }
622
+
623
+ const normalizedWallet = normalizeEvmWalletInfoResponse(data);
624
+ return String(normalizedWallet.address ?? '').trim();
625
+ };