0xtrails 0.12.0 → 0.12.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 (205) hide show
  1. package/dist/analytics.d.ts +65 -50
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/{ccip-DtfgR432.js → ccip-62W6LwH2.js} +28 -28
  4. package/dist/chains.d.ts.map +1 -1
  5. package/dist/error.d.ts +2 -0
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/estimate.d.ts.map +1 -1
  8. package/dist/fees.d.ts.map +1 -1
  9. package/dist/{index-CHiCSmCD.js → index-C0QTNYIA.js} +43750 -41806
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +199 -171
  13. package/dist/localeUtils.d.ts.map +1 -1
  14. package/dist/meld/components/MeldCountriesList.d.ts +0 -2
  15. package/dist/meld/components/MeldCountriesList.d.ts.map +1 -1
  16. package/dist/meld/components/MeldFundMethods.d.ts.map +1 -1
  17. package/dist/meld/components/MeldTokensList.d.ts.map +1 -1
  18. package/dist/meld/utils/meld.d.ts +2 -52
  19. package/dist/meld/utils/meld.d.ts.map +1 -1
  20. package/dist/poolUtils.d.ts.map +1 -1
  21. package/dist/prepareSend.d.ts.map +1 -1
  22. package/dist/prices.d.ts +1 -2
  23. package/dist/prices.d.ts.map +1 -1
  24. package/dist/query/balance.fetchers.d.ts +2 -2
  25. package/dist/query/balance.fetchers.d.ts.map +1 -1
  26. package/dist/query/fiat.fetchers.d.ts +11 -0
  27. package/dist/query/fiat.fetchers.d.ts.map +1 -0
  28. package/dist/query/fiat.hooks.d.ts +18 -0
  29. package/dist/query/fiat.hooks.d.ts.map +1 -0
  30. package/dist/query/fiat.queries.d.ts +24 -0
  31. package/dist/query/fiat.queries.d.ts.map +1 -0
  32. package/dist/query/meld.fetchers.d.ts +19 -0
  33. package/dist/query/meld.fetchers.d.ts.map +1 -0
  34. package/dist/query/meld.hooks.d.ts +4 -0
  35. package/dist/query/meld.hooks.d.ts.map +1 -0
  36. package/dist/query/meld.queries.d.ts +61 -0
  37. package/dist/query/meld.queries.d.ts.map +1 -0
  38. package/dist/recover.d.ts.map +1 -1
  39. package/dist/tokens.d.ts.map +1 -1
  40. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  41. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts.map +1 -1
  42. package/dist/transactionIntent/deposits/standardDeposit.d.ts +7 -1
  43. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  44. package/dist/transactionIntent/handlers/intentHandler.d.ts +2 -0
  45. package/dist/transactionIntent/handlers/intentHandler.d.ts.map +1 -1
  46. package/dist/transactionIntent/quote/normalizeQuote.d.ts +2 -2
  47. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  48. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  49. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  50. package/dist/transactionIntent/types.d.ts +2 -0
  51. package/dist/transactionIntent/types.d.ts.map +1 -1
  52. package/dist/transactionIntent/utils/balanceChecker.d.ts +3 -1
  53. package/dist/transactionIntent/utils/balanceChecker.d.ts.map +1 -1
  54. package/dist/transactions.d.ts +2 -9
  55. package/dist/transactions.d.ts.map +1 -1
  56. package/dist/umd/trails.min.js +206 -152
  57. package/dist/utils/fiat.d.ts +8 -0
  58. package/dist/utils/fiat.d.ts.map +1 -0
  59. package/dist/utils/format.d.ts.map +1 -1
  60. package/dist/utils/passthrough.d.ts +5 -2
  61. package/dist/utils/passthrough.d.ts.map +1 -1
  62. package/dist/utils/validation.d.ts +33 -0
  63. package/dist/utils/validation.d.ts.map +1 -1
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/walletUtils.d.ts +1 -1
  66. package/dist/walletUtils.d.ts.map +1 -1
  67. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  68. package/dist/widget/components/DepositTracker.d.ts.map +1 -1
  69. package/dist/widget/components/Earn.d.ts +2 -0
  70. package/dist/widget/components/Earn.d.ts.map +1 -1
  71. package/dist/widget/components/FeeOption.d.ts.map +1 -1
  72. package/dist/widget/components/Fund.d.ts.map +1 -1
  73. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  74. package/dist/widget/components/HookModalContent.d.ts.map +1 -1
  75. package/dist/widget/components/MeldForm.d.ts.map +1 -1
  76. package/dist/widget/components/MeldHistory.d.ts.map +1 -1
  77. package/dist/widget/components/MeldStepsFlow.d.ts.map +1 -1
  78. package/dist/widget/components/OFTProgressBar.d.ts +2 -0
  79. package/dist/widget/components/OFTProgressBar.d.ts.map +1 -1
  80. package/dist/widget/components/OnRampProviderSelector.d.ts.map +1 -1
  81. package/dist/widget/components/OnrampHistoryRow.d.ts.map +1 -1
  82. package/dist/widget/components/Pay.d.ts.map +1 -1
  83. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -1
  84. package/dist/widget/components/PoolDeposit.d.ts +2 -0
  85. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  86. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  87. package/dist/widget/components/Receipt.d.ts.map +1 -1
  88. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  89. package/dist/widget/components/Swap.d.ts +2 -0
  90. package/dist/widget/components/Swap.d.ts.map +1 -1
  91. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  92. package/dist/widget/components/Withdraw.d.ts.map +1 -1
  93. package/dist/widget/hooks/useAmountUsd.d.ts.map +1 -1
  94. package/dist/widget/hooks/useCustomTokenSearch.d.ts.map +1 -1
  95. package/dist/widget/hooks/useDisplayCurrencyPreference.d.ts.map +1 -1
  96. package/dist/widget/hooks/useFiatOnRampCurrencies.d.ts +3 -21
  97. package/dist/widget/hooks/useFiatOnRampCurrencies.d.ts.map +1 -1
  98. package/dist/widget/hooks/useMeldTransactionHistory.d.ts.map +1 -1
  99. package/dist/widget/hooks/useOnRampCountryDefaults.d.ts +0 -18
  100. package/dist/widget/hooks/useOnRampCountryDefaults.d.ts.map +1 -1
  101. package/dist/widget/hooks/useOnRampPaymentMethods.d.ts +2 -18
  102. package/dist/widget/hooks/useOnRampPaymentMethods.d.ts.map +1 -1
  103. package/dist/widget/hooks/useOnRampQuote.d.ts.map +1 -1
  104. package/dist/widget/hooks/useQuote.d.ts +5 -1
  105. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  106. package/dist/widget/hooks/useSendForm.d.ts +3 -1
  107. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  108. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  109. package/dist/widget/hooks/useTokenWithFreshBalance.d.ts +3 -2
  110. package/dist/widget/hooks/useTokenWithFreshBalance.d.ts.map +1 -1
  111. package/dist/widget/index.js +1 -1
  112. package/dist/widget/types/commonProps.d.ts +2 -0
  113. package/dist/widget/types/commonProps.d.ts.map +1 -1
  114. package/dist/widget/utils/transactionFailure.d.ts +20 -0
  115. package/dist/widget/utils/transactionFailure.d.ts.map +1 -0
  116. package/dist/widget/widget.d.ts +44 -3
  117. package/dist/widget/widget.d.ts.map +1 -1
  118. package/dist/widget/workers/intentExecutionWorker.d.ts.map +1 -1
  119. package/package.json +22 -22
  120. package/src/analytics.ts +115 -79
  121. package/src/chains.ts +0 -1
  122. package/src/error.ts +11 -0
  123. package/src/estimate.ts +12 -7
  124. package/src/fees.ts +0 -1
  125. package/src/index.ts +11 -0
  126. package/src/localeUtils.ts +3 -1
  127. package/src/meld/components/MeldCountriesList.tsx +30 -15
  128. package/src/meld/components/MeldFundMethods.tsx +8 -4
  129. package/src/meld/components/MeldTokensList.tsx +90 -2
  130. package/src/meld/utils/meld.ts +3 -400
  131. package/src/poolUtils.ts +5 -19
  132. package/src/prepareSend.ts +32 -5
  133. package/src/prices.ts +7 -33
  134. package/src/query/balance.fetchers.ts +128 -168
  135. package/src/query/fiat.fetchers.ts +33 -0
  136. package/src/query/fiat.hooks.ts +71 -0
  137. package/src/query/fiat.queries.ts +67 -0
  138. package/src/query/meld.fetchers.ts +97 -0
  139. package/src/query/meld.hooks.ts +18 -0
  140. package/src/query/meld.queries.ts +184 -0
  141. package/src/recover.ts +6 -1
  142. package/src/tokens.ts +31 -6
  143. package/src/transactionIntent/deposits/depositOrchestrator.ts +2 -0
  144. package/src/transactionIntent/deposits/gaslessDeposit.ts +9 -2
  145. package/src/transactionIntent/deposits/standardDeposit.ts +35 -14
  146. package/src/transactionIntent/handlers/intentHandler.ts +134 -138
  147. package/src/transactionIntent/quote/normalizeQuote.ts +31 -22
  148. package/src/transactionIntent/quote/quoteHelpers.ts +24 -7
  149. package/src/transactionIntent/types.ts +2 -0
  150. package/src/transactionIntent/utils/balanceChecker.ts +10 -4
  151. package/src/transactions.ts +22 -13
  152. package/src/umd.tsx +1 -1
  153. package/src/utils/fiat.ts +32 -0
  154. package/src/utils/format.ts +1 -3
  155. package/src/utils/passthrough.ts +19 -3
  156. package/src/utils/validation.ts +88 -0
  157. package/src/utils.ts +2 -1
  158. package/src/walletUtils.ts +2 -2
  159. package/src/widget/components/AccountIntentTransactionHistory.tsx +2 -2
  160. package/src/widget/components/ClassicSwap.tsx +10 -4
  161. package/src/widget/components/DepositTracker.tsx +2 -5
  162. package/src/widget/components/Earn.tsx +6 -0
  163. package/src/widget/components/FeeOption.tsx +15 -8
  164. package/src/widget/components/Fund.tsx +16 -11
  165. package/src/widget/components/FundMethods.tsx +255 -192
  166. package/src/widget/components/HookModalContent.tsx +4 -0
  167. package/src/widget/components/MeldForm.tsx +44 -42
  168. package/src/widget/components/MeldHistory.tsx +4 -3
  169. package/src/widget/components/MeldStepsFlow.tsx +33 -71
  170. package/src/widget/components/OFTProgressBar.tsx +32 -12
  171. package/src/widget/components/OnRampProviderSelector.tsx +2 -1
  172. package/src/widget/components/OnrampHistoryRow.tsx +2 -1
  173. package/src/widget/components/Pay.tsx +8 -2
  174. package/src/widget/components/PercentageMaxButtons.tsx +5 -3
  175. package/src/widget/components/PoolDeposit.tsx +6 -0
  176. package/src/widget/components/PoolWithdraw.tsx +1 -1
  177. package/src/widget/components/QuoteDetails.tsx +5 -4
  178. package/src/widget/components/Receipt.tsx +4 -3
  179. package/src/widget/components/SlippageToleranceSettings.tsx +3 -2
  180. package/src/widget/components/Swap.tsx +2 -0
  181. package/src/widget/components/TransferPendingVertical.tsx +21 -28
  182. package/src/widget/components/UserPreferences.tsx +1 -1
  183. package/src/widget/components/Withdraw.tsx +20 -14
  184. package/src/widget/hooks/useAmountUsd.ts +3 -15
  185. package/src/widget/hooks/useCustomTokenSearch.tsx +2 -6
  186. package/src/widget/hooks/useDisplayCurrencyPreference.tsx +1 -2
  187. package/src/widget/hooks/useFiatOnRampCurrencies.ts +11 -76
  188. package/src/widget/hooks/useMeldTransactionHistory.ts +24 -89
  189. package/src/widget/hooks/useOnRampCountryDefaults.ts +3 -49
  190. package/src/widget/hooks/useOnRampPaymentMethods.ts +21 -100
  191. package/src/widget/hooks/useOnRampQuote.ts +2 -5
  192. package/src/widget/hooks/useQuote.ts +10 -12
  193. package/src/widget/hooks/useSendForm.ts +6 -0
  194. package/src/widget/hooks/useTokenList.ts +3 -6
  195. package/src/widget/hooks/useTokenWithFreshBalance.ts +141 -11
  196. package/src/widget/types/commonProps.ts +2 -0
  197. package/src/widget/utils/transactionFailure.ts +52 -0
  198. package/src/widget/widget.tsx +137 -59
  199. package/src/widget/workers/intentExecutionWorker.ts +3 -1
  200. package/dist/widget/hooks/useExchangeRate.d.ts +0 -31
  201. package/dist/widget/hooks/useExchangeRate.d.ts.map +0 -1
  202. package/dist/widget/hooks/useFiatCurrencyList.d.ts +0 -3
  203. package/dist/widget/hooks/useFiatCurrencyList.d.ts.map +0 -1
  204. package/src/widget/hooks/useExchangeRate.ts +0 -257
  205. package/src/widget/hooks/useFiatCurrencyList.ts +0 -66
package/src/poolUtils.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { encodeFunctionData, type WalletClient } from "viem"
2
2
  import { logger } from "./logger.js"
3
3
  import type { Pool } from "./pools.js"
4
+ import { isValidStringAmount, isPositiveBigInt } from "./utils/validation.js"
4
5
 
5
6
  /**
6
7
  * Generate Aave deposit calldata
@@ -47,25 +48,15 @@ export function generateAaveDepositCalldata(
47
48
  }
48
49
 
49
50
  // Validate amount
50
- if (
51
- !amount ||
52
- amount === "" ||
53
- Number.isNaN(Number(amount)) ||
54
- Number(amount) <= 0
55
- ) {
51
+ if (!isValidStringAmount(amount)) {
56
52
  throw new Error(`Invalid amount: ${amount}`)
57
53
  }
58
54
 
59
55
  // Validate amount can be converted to BigInt
60
- let amountBigInt: bigint
61
- try {
62
- amountBigInt = BigInt(amount)
63
- if (amountBigInt <= 0n) {
64
- throw new Error("Amount must be greater than 0")
65
- }
66
- } catch {
56
+ if (!isPositiveBigInt(amount)) {
67
57
  throw new Error(`Invalid amount format: ${amount}`)
68
58
  }
59
+ const amountBigInt = BigInt(amount)
69
60
 
70
61
  // Aave V3 Pool contract deposit function
71
62
  // function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
@@ -115,12 +106,7 @@ export function generateMorphoDepositCalldata(
115
106
  }
116
107
 
117
108
  // Validate amount
118
- if (
119
- !amount ||
120
- amount === "" ||
121
- Number.isNaN(Number(amount)) ||
122
- Number(amount) <= 0
123
- ) {
109
+ if (!isValidStringAmount(amount)) {
124
110
  throw new Error("Invalid amount")
125
111
  }
126
112
 
@@ -1,7 +1,7 @@
1
1
  import { abortControllerRegistry } from "./abortController.js"
2
2
  import { trackPaymentError, trackPaymentStarted } from "./analytics.js"
3
3
  import { getSlippageToleranceValue } from "./widget/components/SlippageToleranceSettings.js"
4
- import { isNativeToken, isZeroAccount } from "./utils/address.js"
4
+ import { addressEqual, isNativeToken, isZeroAccount } from "./utils/address.js"
5
5
  import { getERC20TransferData } from "./utils.js"
6
6
  import { getTokenPrice } from "./prices.js"
7
7
  import {
@@ -9,7 +9,7 @@ import {
9
9
  replacePlaceholderInCalldata,
10
10
  TRAILS_ROUTER_PLACEHOLDER_AMOUNT,
11
11
  } from "./placeholder.js"
12
- import { IntentProtocolVersion } from "@0xtrails/api"
12
+ import { IntentProtocolVersion, RouteProvider } from "@0xtrails/api"
13
13
  import { wrapCalldataWithTrailsRouterIfNeeded } from "./abis/trailsRouter.js"
14
14
  import { logger } from "./logger.js"
15
15
  import { getIsCustomCalldata } from "./contractUtils.js"
@@ -24,6 +24,7 @@ import {
24
24
  isSameToken,
25
25
  } from "./transactionIntent/index.js"
26
26
  import { Address, Bytes, Hex } from "ox"
27
+ import { getWethAddress } from "./tokens.js"
27
28
  import {
28
29
  createHydratePayload,
29
30
  encodeHydrateExecuteCalldata,
@@ -76,6 +77,8 @@ export async function prepareSend(
76
77
  originNativeTokenPriceUsd,
77
78
  swapProvider,
78
79
  bridgeProvider,
80
+ swapProviderFallback,
81
+ bridgeProviderFallback,
79
82
  fundMethod: fundMethodFromOptions,
80
83
  mode,
81
84
  checkoutOnHandlers,
@@ -151,11 +154,32 @@ export async function prepareSend(
151
154
  let effectiveDestinationAddress = recipient
152
155
  let effectiveDestinationApprovalAddress: Address.Address | undefined
153
156
  let effectiveDestinationCalldata = destinationCalldata
157
+ let effectiveSwapProvider = swapProvider
154
158
 
155
159
  // Check if this is a same-chain same-token transfer (before modifying effectiveDestinationAddress)
156
160
  const isToSameChain = isSameChain(originChainId, destinationChainId)
157
161
  const isToSameToken = isSameToken(originTokenAddress, destinationTokenAddress)
158
162
  const isSameChainSameToken = isToSameChain && isToSameToken
163
+ const chainWethAddress = getWethAddress(originChainId)
164
+ const isSameChainNativeWrappedPair =
165
+ isToSameChain &&
166
+ !!chainWethAddress &&
167
+ ((isNativeToken(originTokenAddress) &&
168
+ addressEqual(destinationTokenAddress, chainWethAddress)) ||
169
+ (addressEqual(originTokenAddress, chainWethAddress) &&
170
+ isNativeToken(destinationTokenAddress)))
171
+ const isSameChainNativeWrappedToSelf =
172
+ isSameChainNativeWrappedPair && addressEqual(recipient, account.address)
173
+
174
+ if (isSameChainNativeWrappedToSelf) {
175
+ effectiveDestinationAddress = account.address
176
+ effectiveSwapProvider = RouteProvider.WETH
177
+ // Only force empty calldata when caller did not provide custom calldata.
178
+ if (!hasCustomCalldata) {
179
+ effectiveDestinationCalldata = undefined
180
+ hasCustomCalldata = false
181
+ }
182
+ }
159
183
 
160
184
  // For same-chain same-token with custom calldata:
161
185
  // Replace any placeholder in the calldata with the actual swapAmount
@@ -179,7 +203,8 @@ export async function prepareSend(
179
203
  !hasCustomCalldata &&
180
204
  tradeType === TradeType.EXACT_INPUT &&
181
205
  !isNativeToken(destinationTokenAddress) &&
182
- !isSameChainSameToken // Don't override recipient for same-chain same-token transfers
206
+ !isSameChainSameToken &&
207
+ !isSameChainNativeWrappedToSelf // Keep backend-owned passthrough handling for native<->WETH to-self
183
208
  ) {
184
209
  effectiveDestinationCalldata = getERC20TransferData(
185
210
  recipient,
@@ -401,7 +426,7 @@ export async function prepareSend(
401
426
  sourceTokenPriceUsd: sourceTokenPriceUsd?.toString(),
402
427
  destinationTokenPriceUsd: destinationTokenPriceUsd?.toString(),
403
428
  tradeType: tradeType.toString(),
404
- swapProvider: swapProvider || undefined,
429
+ swapProvider: effectiveSwapProvider || undefined,
405
430
  bridgeProvider: bridgeProvider || undefined,
406
431
  onramp,
407
432
  walletId,
@@ -476,8 +501,10 @@ export async function prepareSend(
476
501
  originNativeTokenPriceUsd,
477
502
  slippageTolerance,
478
503
  tradeType,
479
- swapProvider,
504
+ swapProvider: effectiveSwapProvider,
480
505
  bridgeProvider,
506
+ swapProviderFallback,
507
+ bridgeProviderFallback,
481
508
  fundMethod,
482
509
  mode,
483
510
  checkoutOnHandlers,
package/src/prices.ts CHANGED
@@ -7,6 +7,7 @@ import { queryClient as defaultQueryClient } from "./query/client.js"
7
7
  import { useTrailsClient, type TrailsClient } from "./trailsClient.js"
8
8
  import { MINUTE_MS, SECOND_MS, retryDelay } from "./utils/time.js"
9
9
  import { useUserActivityContext } from "./widget/providers/UserActivityProvider.js"
10
+ import { isValidNumber, isNonNegativeNumber } from "./utils/validation.js"
10
11
 
11
12
  import { normalizeAddress } from "./utils/address.js"
12
13
 
@@ -459,25 +460,17 @@ export function calcAmountUsdPrice({
459
460
  const sanitizedPrice = normalizeNumber(usdPrice)
460
461
 
461
462
  // Validate inputs
462
- if (
463
- !Number.isFinite(sanitizedAmount) ||
464
- Number.isNaN(sanitizedAmount) ||
465
- sanitizedAmount < 0
466
- ) {
463
+ if (!isNonNegativeNumber(sanitizedAmount)) {
467
464
  return 0
468
465
  }
469
- if (
470
- !Number.isFinite(sanitizedPrice) ||
471
- Number.isNaN(sanitizedPrice) ||
472
- sanitizedPrice < 0
473
- ) {
466
+ if (!isNonNegativeNumber(sanitizedPrice)) {
474
467
  return 0
475
468
  }
476
469
 
477
470
  const result = sanitizedAmount * sanitizedPrice
478
471
 
479
472
  // Validate result
480
- if (!Number.isFinite(result) || Number.isNaN(result)) {
473
+ if (!isValidNumber(result)) {
481
474
  logger.console.error("[trails-sdk] Error calculating amount USD:", {
482
475
  sanitizedAmount,
483
476
  sanitizedPrice,
@@ -501,7 +494,7 @@ export function normalizeNumber(
501
494
  // If already a number, return it directly
502
495
  if (typeof number === "number") {
503
496
  // Validate the number
504
- if (!Number.isFinite(number) || Number.isNaN(number)) {
497
+ if (!isValidNumber(number)) {
505
498
  logger.console.error("[trails-sdk] Invalid number value:", number)
506
499
  return 0
507
500
  }
@@ -513,7 +506,7 @@ export function normalizeNumber(
513
506
  const normalized = parseLocaleNumber(number)
514
507
 
515
508
  // Return 0 for invalid numbers
516
- if (!Number.isFinite(normalized) || Number.isNaN(normalized)) {
509
+ if (!isValidNumber(normalized)) {
517
510
  logger.console.error("[trails-sdk] Error normalizing number string:", {
518
511
  number,
519
512
  normalized,
@@ -532,23 +525,4 @@ export function formatTvl(tvl: number): string {
532
525
  return `$${tvl.toFixed(0)}`
533
526
  }
534
527
 
535
- // Helper function to validate numeric values
536
- export const isValidNumeric = (
537
- value: number | string | null | undefined,
538
- ): boolean => {
539
- if (value === null || value === undefined || value === "") {
540
- return true // Empty values are considered valid
541
- }
542
- const strValue = String(value).trim()
543
- return /^\d+(\.\d+)?$/.test(strValue)
544
- }
545
-
546
- export const isValidInteger = (
547
- value: number | string | null | undefined,
548
- ): boolean => {
549
- if (value === null || value === undefined || value === "") {
550
- return true // Empty values are considered valid
551
- }
552
- const strValue = String(value).trim()
553
- return /^\d+$/.test(strValue)
554
- }
528
+ export { isValidNumeric, isValidInteger } from "./utils/validation.js"
@@ -3,9 +3,7 @@
3
3
  * This is a leaf module — NO React imports, NO queryClient, NO balanceKeys.
4
4
  */
5
5
  import type {
6
- GatewayNativeTokenBalances,
7
- GatewayTokenBalance,
8
- GetTokenBalancesSummaryReturn,
6
+ IndexerGateway as IndexerGatewayTypes,
9
7
  NativeTokenBalance,
10
8
  Page,
11
9
  SequenceIndexerGateway,
@@ -90,8 +88,14 @@ function addPriceAndUsdFields(
90
88
  priceData?: TokenPrice,
91
89
  ): void {
92
90
  // Get prices from both sources
93
- const apiPriceUSD = (token as { priceUSD?: string }).priceUSD
94
- const apiBalanceUSD = (token as { balanceUSD?: string }).balanceUSD
91
+ const apiPriceUSD =
92
+ "priceUSD" in token && typeof token.priceUSD === "string"
93
+ ? token.priceUSD
94
+ : undefined
95
+ const apiBalanceUSD =
96
+ "balanceUSD" in token && typeof token.balanceUSD === "string"
97
+ ? token.balanceUSD
98
+ : undefined
95
99
  const coinApiPrice = priceData?.priceUsd
96
100
  const parsedIndexerPrice = apiPriceUSD ? parseFloat(apiPriceUSD) : Number.NaN
97
101
  const indexerPrice = Number.isNaN(parsedIndexerPrice)
@@ -233,6 +237,87 @@ export function convertNativeTokenBalanceToToken(
233
237
  // Default empty page info for query fallback
234
238
  export const defaultPage = { page: 1, pageSize: 10, more: false }
235
239
 
240
+ function getPageRequest(page?: number, pageSize?: number): Page | undefined {
241
+ if (page === undefined && pageSize === undefined) return undefined
242
+ return {
243
+ page,
244
+ pageSize,
245
+ }
246
+ }
247
+
248
+ function createEmptyMultiAccountResult(
249
+ accounts: string[],
250
+ ): GetTokenBalancesForMultipleAccountsReturn {
251
+ const result: GetTokenBalancesForMultipleAccountsReturn = {}
252
+ for (const account of accounts) {
253
+ result[account.toLowerCase()] = {
254
+ tokens: [],
255
+ nativeTokens: [],
256
+ erc20Tokens: [],
257
+ page: defaultPage,
258
+ }
259
+ }
260
+ return result
261
+ }
262
+
263
+ function mapChainEntryResults<T extends { accountAddress?: string }>(
264
+ entries: Array<{ chainId: number; results?: T[] }>,
265
+ result: GetTokenBalancesForMultipleAccountsReturn,
266
+ toToken: (entry: T) => Token,
267
+ bucket: "erc20Tokens" | "nativeTokens",
268
+ ): void {
269
+ for (const chainEntry of entries) {
270
+ if (!getChainInfo(chainEntry.chainId)) {
271
+ logger.console.warn(
272
+ `[tokenBalances] Skipping unsupported chain ID: ${chainEntry.chainId}`,
273
+ )
274
+ continue
275
+ }
276
+
277
+ for (const entry of chainEntry.results || []) {
278
+ const accountAddr = entry.accountAddress?.toLowerCase()
279
+ const accountEntry = accountAddr ? result[accountAddr] : undefined
280
+ if (!accountEntry) continue
281
+
282
+ const token = toToken(entry)
283
+ accountEntry[bucket].push(token)
284
+ accountEntry.tokens.push(token)
285
+ }
286
+ }
287
+ }
288
+
289
+ function toTokenPriceKey(chainId: number, tokenAddress?: string): string {
290
+ return `${chainId}:${tokenAddress || zeroAddress}`
291
+ }
292
+
293
+ function createTokenMapFromGatewaySummary(
294
+ summary: IndexerGatewayTypes.GetTokenBalancesSummaryResponse,
295
+ ): Map<string, TokenBalance | NativeTokenBalance> {
296
+ const tokenMap = new Map<string, TokenBalance | NativeTokenBalance>()
297
+
298
+ for (const chainEntry of summary.balances) {
299
+ for (const tokenBalance of chainEntry.results) {
300
+ if (!("contractAddress" in tokenBalance)) continue
301
+ if (!("contractInfo" in tokenBalance)) continue
302
+ tokenMap.set(
303
+ `${tokenBalance.contractAddress}-${tokenBalance.chainId}-${tokenBalance.contractInfo?.symbol}`,
304
+ tokenBalance,
305
+ )
306
+ }
307
+ }
308
+
309
+ for (const chainEntry of summary.nativeBalances) {
310
+ for (const nativeBalance of chainEntry.results) {
311
+ tokenMap.set(
312
+ `${zeroAddress}-${nativeBalance.chainId}-native`,
313
+ nativeBalance,
314
+ )
315
+ }
316
+ }
317
+
318
+ return tokenMap
319
+ }
320
+
236
321
  // Type guard for native token balance
237
322
  export function isNativeToken(
238
323
  token: TokenBalance | NativeTokenBalance,
@@ -304,7 +389,7 @@ export async function fetchTokenBalancesSummary({
304
389
  indexerGatewayClient,
305
390
  page,
306
391
  pageSize,
307
- }: GetTokenBalancesParams): Promise<GetTokenBalancesSummaryReturn> {
392
+ }: GetTokenBalancesParams): Promise<IndexerGatewayTypes.GetTokenBalancesSummaryResponse> {
308
393
  if (!account || !indexerGatewayClient) {
309
394
  throw new Error("Account address and indexer client are required")
310
395
  }
@@ -319,16 +404,10 @@ export async function fetchTokenBalancesSummary({
319
404
  contractWhitelist: tokenAddress ? [tokenAddress] : undefined,
320
405
  omitNativeBalances: false,
321
406
  },
322
- page:
323
- page !== undefined || pageSize !== undefined
324
- ? {
325
- page: page,
326
- pageSize: pageSize,
327
- }
328
- : undefined,
407
+ page: getPageRequest(page, pageSize),
329
408
  })
330
409
 
331
- return summaryFromGateway as unknown as GetTokenBalancesSummaryReturn
410
+ return summaryFromGateway
332
411
  } catch (error) {
333
412
  if (isNetworkError(error)) {
334
413
  logger.console.warn(
@@ -380,84 +459,24 @@ export async function fetchMultiAccountBalances({
380
459
  contractTypes: ["ERC20"],
381
460
  omitNativeBalances: false,
382
461
  },
383
- page:
384
- page !== undefined || pageSize !== undefined
385
- ? {
386
- page: page,
387
- pageSize: pageSize,
388
- }
389
- : undefined,
462
+ page: getPageRequest(page, pageSize),
390
463
  })
391
464
 
392
- // The gateway returns balances grouped by account
393
- // Each entry in balances/nativeBalances arrays corresponds to an account
394
- const result: GetTokenBalancesForMultipleAccountsReturn = {}
395
-
396
- // Initialize result with empty arrays for each account
397
- for (const account of validAccounts) {
398
- result[account.toLowerCase()] = {
399
- tokens: [],
400
- nativeTokens: [],
401
- erc20Tokens: [],
402
- page: defaultPage,
403
- }
404
- }
405
-
406
- // Map balances to their respective accounts and convert to Token type
407
- // Gateway returns data grouped by chainId: { chainId, results: [...] }
408
- // Each result has accountAddress, so we need to iterate through results
409
- const gatewayBalances =
410
- summaryFromGateway.balances as unknown as GatewayTokenBalance[]
411
- const gatewayNativeBalances =
412
- summaryFromGateway.nativeBalances as unknown as GatewayNativeTokenBalances[]
413
-
414
- // For ERC20 balances: iterate through chain entries, then through results
415
- for (const chainEntry of gatewayBalances) {
416
- const chainId = (chainEntry as any).chainId
417
- // Skip unsupported chain IDs
418
- if (!getChainInfo(chainId)) {
419
- logger.console.warn(
420
- `[tokenBalances] Skipping unsupported chain ID: ${chainId}`,
421
- )
422
- continue
423
- }
424
-
425
- const results = (chainEntry as any).results || []
426
- for (const tokenResult of results) {
427
- const accountAddr = tokenResult.accountAddress?.toLowerCase()
428
- if (accountAddr && result[accountAddr]) {
429
- // Convert to Token type and add to arrays
430
- const token = convertTokenBalanceToToken(tokenResult as TokenBalance)
431
- result[accountAddr].erc20Tokens.push(token)
432
- result[accountAddr].tokens.push(token)
433
- }
434
- }
435
- }
465
+ const result = createEmptyMultiAccountResult(validAccounts)
436
466
 
437
- // For native balances: iterate through chain entries, then through results
438
- for (const chainEntry of gatewayNativeBalances) {
439
- const chainId = (chainEntry as any).chainId
440
- // Skip unsupported chain IDs
441
- if (!getChainInfo(chainId)) {
442
- logger.console.warn(
443
- `[tokenBalances] Skipping unsupported chain ID: ${chainId}`,
444
- )
445
- continue
446
- }
467
+ mapChainEntryResults(
468
+ summaryFromGateway.balances,
469
+ result,
470
+ convertTokenBalanceToToken,
471
+ "erc20Tokens",
472
+ )
447
473
 
448
- const results = (chainEntry as any).results || []
449
- for (const nativeResult of results) {
450
- const accountAddr = nativeResult.accountAddress?.toLowerCase()
451
- if (accountAddr && result[accountAddr]) {
452
- // Convert to Token type and add to arrays
453
- const token = convertNativeTokenBalanceToToken(
454
- nativeResult as NativeTokenBalance,
455
- )
456
- result[accountAddr].nativeTokens.push(token)
457
- result[accountAddr].tokens.push(token)
458
- }
459
- }
460
- }
474
+ mapChainEntryResults(
475
+ summaryFromGateway.nativeBalances,
476
+ result,
477
+ convertNativeTokenBalanceToToken,
478
+ "nativeTokens",
479
+ )
461
480
 
462
481
  // Set page info from gateway response
463
482
  for (const account of validAccounts) {
@@ -474,17 +493,7 @@ export async function fetchMultiAccountBalances({
474
493
  "[trails-sdk] Network error fetching token balances, returning empty result:",
475
494
  { error, accounts },
476
495
  )
477
- // Return empty balances for all accounts
478
- const result: GetTokenBalancesForMultipleAccountsReturn = {}
479
- for (const account of validAccounts) {
480
- result[account.toLowerCase()] = {
481
- tokens: [],
482
- nativeTokens: [],
483
- erc20Tokens: [],
484
- page: defaultPage,
485
- }
486
- }
487
- return result
496
+ return createEmptyMultiAccountResult(validAccounts)
488
497
  }
489
498
 
490
499
  logger.console.error(
@@ -510,68 +519,21 @@ export async function fetchBalancesWithPrices({
510
519
  indexerGatewayClient,
511
520
  trailsClient,
512
521
  }: GetTokenBalancesWithPricesParams): Promise<GetTokenBalancesWithPriceReturn> {
513
- // Step 1: Fetch raw balance summary
514
522
  const summaryFromGateway = await fetchTokenBalancesSummary({
515
523
  account,
516
524
  indexerGatewayClient,
517
525
  })
518
-
519
- // Step 2: Flatten into a TokenBalance array (combining ERC20 + native)
520
- const tokenMap = new Map<string, TokenBalance>()
521
-
522
- for (const balance of summaryFromGateway.balances) {
523
- ;(balance as any).results.forEach((b: any) => {
524
- tokenMap.set(
525
- `${b.contractAddress}-${b.contractInfo?.chainId}-${b.contractInfo?.symbol}`,
526
- {
527
- ...b,
528
- contractAddress: b.contractAddress ?? zeroAddress,
529
- tokenId: b.contractInfo?.symbol,
530
- symbol: b.contractInfo?.symbol,
531
- name: b.contractInfo?.name,
532
- decimals: b.contractInfo?.decimals,
533
- imageUrl: getTokenImageUrl({
534
- chainId: b.contractInfo?.chainId || 0,
535
- contractAddress: b.contractAddress ?? zeroAddress,
536
- symbol: b.contractInfo?.symbol,
537
- fallbackImageUrl: b.contractInfo?.logoURI,
538
- }),
539
- },
540
- )
541
- })
542
- }
543
-
544
- for (const balance of summaryFromGateway.nativeBalances) {
545
- ;(balance as any).results.forEach((b: any) => {
546
- const contractAddr = b.contractAddress ?? zeroAddress
547
- tokenMap.set(`${contractAddr}-${b.chainId}-${b.symbol}`, {
548
- ...b,
549
- contractAddress: contractAddr,
550
- tokenId: b.symbol,
551
- symbol: b.symbol,
552
- name: b.name,
553
- decimals: b.decimals ?? b.contractInfo?.decimals ?? 18,
554
- imageUrl: getTokenImageUrl({
555
- chainId: b.chainId,
556
- contractAddress: zeroAddress,
557
- symbol: b.symbol,
558
- fallbackImageUrl: b.contractInfo?.logoURI,
559
- }),
560
- })
561
- })
562
- }
563
-
526
+ const tokenMap = createTokenMapFromGatewaySummary(summaryFromGateway)
564
527
  const tokens = Array.from(tokenMap.values())
565
528
 
566
- // Step 3: Fetch prices for all tokens (graceful — balances without prices beats no data)
567
529
  let tokenPrices: Awaited<ReturnType<typeof getTokenPrices>> = []
568
530
  try {
569
531
  tokenPrices = await getTokenPrices(
570
532
  trailsClient,
571
533
  tokens.map((t) => ({
572
534
  chainId: t.chainId,
573
- tokenAddress: t.contractAddress || zeroAddress,
574
- tokenSymbol: t.contractInfo?.symbol || "",
535
+ tokenAddress: isNativeToken(t) ? zeroAddress : t.contractAddress,
536
+ tokenSymbol: isNativeToken(t) ? "" : t.contractInfo?.symbol || "",
575
537
  })),
576
538
  )
577
539
  } catch (err) {
@@ -581,27 +543,25 @@ export async function fetchBalancesWithPrices({
581
543
  )
582
544
  }
583
545
 
584
- // Step 4: Convert to Token[] with price data
585
- const balancesWithPrices: Token[] = tokens.map((b) => {
586
- const priceData: TokenPrice | undefined = tokenPrices.find((p) => {
587
- const tokenChainId = b.chainId ?? (b as any).contractInfo?.chainId
588
- const isSameChain = p.token.chainId === tokenChainId
589
- let isSameToken = p.token.tokenAddress === b.contractAddress
590
- if (!b.contractAddress) {
591
- isSameToken =
592
- p.token.tokenAddress === zeroAddress || !p.token.tokenAddress
593
- }
594
- return isSameChain && isSameToken
595
- })
546
+ const tokenPriceMap = new Map<string, TokenPrice>()
547
+ for (const tokenPrice of tokenPrices) {
548
+ tokenPriceMap.set(
549
+ toTokenPriceKey(tokenPrice.token.chainId, tokenPrice.token.tokenAddress),
550
+ tokenPrice,
551
+ )
552
+ }
596
553
 
597
- const native = isNativeToken(b)
598
- if (native) {
599
- return convertNativeTokenBalanceToToken(
600
- b as NativeTokenBalance,
601
- priceData,
602
- )
554
+ const balancesWithPrices: Token[] = tokens.map((b) => {
555
+ const priceData = tokenPriceMap.get(
556
+ toTokenPriceKey(
557
+ b.chainId,
558
+ isNativeToken(b) ? zeroAddress : b.contractAddress,
559
+ ),
560
+ )
561
+ if (isNativeToken(b)) {
562
+ return convertNativeTokenBalanceToToken(b, priceData)
603
563
  }
604
- return convertTokenBalanceToToken(b as TokenBalance, priceData)
564
+ return convertTokenBalanceToToken(b, priceData)
605
565
  })
606
566
 
607
567
  return {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure async fetcher functions and helpers for fiat/currency queries.
3
+ * This is a leaf module — NO React imports, NO queryClient.
4
+ */
5
+ import type { ExchangeRate, FiatCurrency } from "@0xtrails/api"
6
+ import type { TrailsClient } from "../trailsClient.js"
7
+
8
+ // ============================================================================
9
+ // Fetchers
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Fetch the list of supported fiat currencies from the API.
14
+ */
15
+ export async function getFiatCurrencyList(
16
+ trailsClient: TrailsClient,
17
+ ): Promise<FiatCurrency[]> {
18
+ const response = await trailsClient.getFiatCurrencyList()
19
+ return response.currencies || []
20
+ }
21
+
22
+ /**
23
+ * Fetch the exchange rate from USD to a target currency.
24
+ */
25
+ export async function getExchangeRate(
26
+ trailsClient: TrailsClient,
27
+ targetCurrency: string,
28
+ ): Promise<ExchangeRate> {
29
+ const response = await trailsClient.getExchangeRate({
30
+ toCurrency: targetCurrency,
31
+ })
32
+ return response.exchangeRate
33
+ }