0xtrails 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +11 -2
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/apiClient.d.ts +1 -1
  5. package/dist/apiClient.d.ts.map +1 -1
  6. package/dist/{proxyCaller.d.ts → balanceInjector.d.ts} +5 -4
  7. package/dist/balanceInjector.d.ts.map +1 -0
  8. package/dist/{ccip-D3gTQONK.js → ccip-D6ToCrWc.js} +12 -12
  9. package/dist/cctp.d.ts.map +1 -1
  10. package/dist/cctpqueue.d.ts +3 -3
  11. package/dist/cctpqueue.d.ts.map +1 -1
  12. package/dist/chains.d.ts.map +1 -1
  13. package/dist/config.d.ts +17 -3
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/constants.d.ts +5 -4
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/contractUtils.d.ts +2 -0
  18. package/dist/contractUtils.d.ts.map +1 -1
  19. package/dist/customChains.d.ts +24 -0
  20. package/dist/customChains.d.ts.map +1 -0
  21. package/dist/{index-CnUM7lKf.js → index-BqgeTLL8.js} +34072 -30146
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +411 -400
  25. package/dist/intentEntrypoint.d.ts +96 -0
  26. package/dist/intentEntrypoint.d.ts.map +1 -0
  27. package/dist/intents.d.ts +5 -3
  28. package/dist/intents.d.ts.map +1 -1
  29. package/dist/metaTxnMonitor.d.ts.map +1 -1
  30. package/dist/morpho.d.ts.map +1 -1
  31. package/dist/pools.d.ts +3 -1
  32. package/dist/pools.d.ts.map +1 -1
  33. package/dist/prepareSend.d.ts +8 -2
  34. package/dist/prepareSend.d.ts.map +1 -1
  35. package/dist/prices.d.ts +1 -1
  36. package/dist/prices.d.ts.map +1 -1
  37. package/dist/relaySdk.d.ts.map +1 -1
  38. package/dist/relayer.d.ts.map +1 -1
  39. package/dist/toast.d.ts +9 -0
  40. package/dist/toast.d.ts.map +1 -0
  41. package/dist/tokenBalances.d.ts +6 -2
  42. package/dist/tokenBalances.d.ts.map +1 -1
  43. package/dist/tokens.d.ts.map +1 -1
  44. package/dist/trails.d.ts +6 -5
  45. package/dist/trails.d.ts.map +1 -1
  46. package/dist/trailsClient.d.ts +12 -0
  47. package/dist/trailsClient.d.ts.map +1 -0
  48. package/dist/transactions.d.ts +8 -0
  49. package/dist/transactions.d.ts.map +1 -1
  50. package/dist/wallets.d.ts.map +1 -1
  51. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  52. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  53. package/dist/widget/components/AccountSettings.d.ts +7 -0
  54. package/dist/widget/components/AccountSettings.d.ts.map +1 -0
  55. package/dist/widget/components/ChainList.d.ts +0 -1
  56. package/dist/widget/components/ChainList.d.ts.map +1 -1
  57. package/dist/widget/components/ClassicSwap.d.ts +46 -0
  58. package/dist/widget/components/ClassicSwap.d.ts.map +1 -0
  59. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  60. package/dist/widget/components/ConnectedWallets.d.ts +9 -0
  61. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -0
  62. package/dist/widget/components/DebugMenu.d.ts.map +1 -1
  63. package/dist/widget/components/DebugScreensList.d.ts.map +1 -1
  64. package/dist/widget/components/DebugToast.d.ts +3 -0
  65. package/dist/widget/components/DebugToast.d.ts.map +1 -0
  66. package/dist/widget/components/Earn.d.ts.map +1 -1
  67. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  68. package/dist/widget/components/Fund.d.ts +44 -0
  69. package/dist/widget/components/Fund.d.ts.map +1 -0
  70. package/dist/widget/components/Identicon.d.ts +9 -0
  71. package/dist/widget/components/Identicon.d.ts.map +1 -0
  72. package/dist/widget/components/Pay.d.ts +46 -0
  73. package/dist/widget/components/Pay.d.ts.map +1 -0
  74. package/dist/widget/components/Receive.d.ts.map +1 -1
  75. package/dist/widget/components/RecentTokens.d.ts.map +1 -1
  76. package/dist/widget/components/Recipients.d.ts +9 -0
  77. package/dist/widget/components/Recipients.d.ts.map +1 -0
  78. package/dist/widget/components/RefundWarning.d.ts +9 -0
  79. package/dist/widget/components/RefundWarning.d.ts.map +1 -0
  80. package/dist/widget/components/SimpleSwap.d.ts.map +1 -1
  81. package/dist/widget/components/Swap.d.ts.map +1 -1
  82. package/dist/widget/components/SwapSettings.d.ts +1 -5
  83. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  84. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  85. package/dist/widget/components/ThemeSyncer.d.ts +6 -0
  86. package/dist/widget/components/ThemeSyncer.d.ts.map +1 -0
  87. package/dist/widget/components/Toast.d.ts +24 -0
  88. package/dist/widget/components/Toast.d.ts.map +1 -0
  89. package/dist/widget/components/TokenList.d.ts.map +1 -1
  90. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  91. package/dist/widget/components/TruncatedAddress.d.ts +2 -0
  92. package/dist/widget/components/TruncatedAddress.d.ts.map +1 -1
  93. package/dist/widget/components/UserPreferences.d.ts +7 -0
  94. package/dist/widget/components/UserPreferences.d.ts.map +1 -0
  95. package/dist/widget/hooks/useBalanceVisible.d.ts +1 -0
  96. package/dist/widget/hooks/useBalanceVisible.d.ts.map +1 -1
  97. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  98. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  99. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  100. package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
  101. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
  102. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +54 -0
  103. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -0
  104. package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
  105. package/dist/widget/hooks/usePayMessage.d.ts +34 -0
  106. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -0
  107. package/dist/widget/hooks/useRecipients.d.ts +17 -0
  108. package/dist/widget/hooks/useRecipients.d.ts.map +1 -0
  109. package/dist/widget/hooks/useSelectedRecipient.d.ts +12 -0
  110. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -0
  111. package/dist/widget/hooks/useSendForm.d.ts +2 -0
  112. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  113. package/dist/widget/hooks/useSwapAmount.d.ts +13 -0
  114. package/dist/widget/hooks/useSwapAmount.d.ts.map +1 -0
  115. package/dist/widget/hooks/useSwapSettings.d.ts +16 -0
  116. package/dist/widget/hooks/useSwapSettings.d.ts.map +1 -0
  117. package/dist/widget/hooks/useTargetAmount.d.ts +5 -0
  118. package/dist/widget/hooks/useTargetAmount.d.ts.map +1 -0
  119. package/dist/widget/hooks/useTheme.d.ts +14 -0
  120. package/dist/widget/hooks/useTheme.d.ts.map +1 -0
  121. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  122. package/dist/widget/index.js +2 -2
  123. package/dist/widget/widget.d.ts +9 -0
  124. package/dist/widget/widget.d.ts.map +1 -1
  125. package/package.json +29 -28
  126. package/src/aave.ts +6 -1
  127. package/src/analytics.ts +103 -53
  128. package/src/apiClient.ts +1 -1
  129. package/src/{proxyCaller.ts → balanceInjector.ts} +22 -17
  130. package/src/cctp.ts +6 -2
  131. package/src/cctpqueue.ts +7 -7
  132. package/src/chains.ts +8 -0
  133. package/src/config.ts +40 -9
  134. package/src/constants.ts +11 -8
  135. package/src/contractUtils.ts +33 -2
  136. package/src/customChains.ts +24 -0
  137. package/src/index.ts +11 -1
  138. package/src/intentEntrypoint.ts +253 -0
  139. package/src/intents.ts +87 -54
  140. package/src/metaTxnMonitor.ts +1 -0
  141. package/src/morpho.ts +13 -2
  142. package/src/pools.ts +68 -86
  143. package/src/prepareSend.ts +437 -207
  144. package/src/prices.ts +51 -7
  145. package/src/relaySdk.ts +6 -4
  146. package/src/relayer.ts +2 -0
  147. package/src/toast.ts +110 -0
  148. package/src/tokenBalances.ts +112 -20
  149. package/src/tokens.ts +70 -7
  150. package/src/trails.ts +80 -77
  151. package/src/trailsClient.ts +45 -0
  152. package/src/transactions.ts +27 -35
  153. package/src/umd.tsx +1 -1
  154. package/src/wallets.ts +2 -1
  155. package/src/widget/assets/sequence-logo.svg +15 -0
  156. package/src/widget/compiled.css +2 -2
  157. package/src/widget/components/AccountActionsDropdown.tsx +18 -159
  158. package/src/widget/components/AccountIntentTransactionHistory.tsx +346 -63
  159. package/src/widget/components/AccountSettings.tsx +96 -0
  160. package/src/widget/components/ChainFilterDropdown.tsx +1 -1
  161. package/src/widget/components/ChainList.tsx +10 -20
  162. package/src/widget/components/ClassicSwap.tsx +923 -0
  163. package/src/widget/components/ConfigDisplay.tsx +8 -5
  164. package/src/widget/components/ConnectedWallets.tsx +260 -0
  165. package/src/widget/components/DebugMenu.tsx +2 -0
  166. package/src/widget/components/DebugScreensList.tsx +3 -0
  167. package/src/widget/components/DebugToast.tsx +63 -0
  168. package/src/widget/components/Earn.tsx +108 -116
  169. package/src/widget/components/EarnPools.tsx +2 -4
  170. package/src/widget/components/EarnPoolsFilters.tsx +6 -6
  171. package/src/widget/components/Fund.tsx +1245 -0
  172. package/src/widget/components/FundMethods.tsx +1 -1
  173. package/src/widget/components/FundSendForm.tsx +1 -1
  174. package/src/widget/components/Identicon.tsx +158 -0
  175. package/src/widget/components/Pay.tsx +1088 -0
  176. package/src/widget/components/PaySendForm.tsx +1 -1
  177. package/src/widget/components/QuoteDetails.tsx +1 -1
  178. package/src/widget/components/Receipt.tsx +1 -1
  179. package/src/widget/components/Receive.tsx +4 -2
  180. package/src/widget/components/RecentTokens.tsx +2 -1
  181. package/src/widget/components/Recipients.tsx +448 -0
  182. package/src/widget/components/RefundWarning.tsx +61 -0
  183. package/src/widget/components/ScreenHeader.tsx +1 -1
  184. package/src/widget/components/SimpleSwap.tsx +74 -58
  185. package/src/widget/components/Swap.tsx +35 -853
  186. package/src/widget/components/SwapSettings.tsx +5 -11
  187. package/src/widget/components/ThemeProvider.tsx +32 -0
  188. package/src/widget/components/ThemeSyncer.tsx +47 -0
  189. package/src/widget/components/Toast.tsx +315 -0
  190. package/src/widget/components/TokenList.tsx +2 -34
  191. package/src/widget/components/TokenSelector.tsx +3 -3
  192. package/src/widget/components/TransactionDetails.tsx +153 -13
  193. package/src/widget/components/TruncatedAddress.tsx +5 -1
  194. package/src/widget/components/UserPreferences.tsx +156 -0
  195. package/src/widget/components/WalletList.tsx +1 -1
  196. package/src/widget/hooks/useBalanceVisible.tsx +40 -2
  197. package/src/widget/hooks/useCheckout.ts +13 -0
  198. package/src/widget/hooks/useCurrentScreen.tsx +3 -0
  199. package/src/widget/hooks/useDebugScreens.ts +12 -2
  200. package/src/widget/hooks/useDefaultTokenSelection.tsx +475 -0
  201. package/src/widget/hooks/useIntentTransactionHistory.ts +212 -0
  202. package/src/widget/hooks/usePayMessage.tsx +370 -0
  203. package/src/widget/hooks/useRecipients.ts +168 -0
  204. package/src/widget/hooks/useSelectedRecipient.tsx +48 -0
  205. package/src/widget/hooks/useSendForm.ts +179 -26
  206. package/src/widget/hooks/useSwapAmount.tsx +50 -0
  207. package/src/widget/hooks/useSwapSettings.tsx +100 -0
  208. package/src/widget/hooks/useTargetAmount.ts +23 -0
  209. package/src/widget/hooks/useTheme.tsx +80 -0
  210. package/src/widget/hooks/useTokenList.ts +20 -11
  211. package/src/widget/index.css +45 -21
  212. package/src/widget/widget.tsx +164 -68
  213. package/dist/address.d.ts +0 -2
  214. package/dist/address.d.ts.map +0 -1
  215. package/dist/proxyCaller.d.ts.map +0 -1
  216. package/src/address.ts +0 -6
@@ -39,6 +39,7 @@ import { useResolveEnsAddress } from "../../ens.js"
39
39
  import { etherlink } from "viem/chains"
40
40
  import { logger } from "../../logger.js"
41
41
  import { getIsContract } from "../../contractUtils.js"
42
+ import { useTrailsClient } from "../../trailsClient.js"
42
43
 
43
44
  export interface Token {
44
45
  id: number
@@ -178,6 +179,8 @@ export type UseSendReturn = {
178
179
  quoteErrorPrettified: string | null
179
180
  isSameTokenWithoutCustomCalldata: boolean
180
181
  isRecipientContract: boolean
182
+ isSenderContractOnOrigin: boolean
183
+ isSenderContractOnDestination: boolean
181
184
  }
182
185
 
183
186
  export function useSendForm({
@@ -277,6 +280,7 @@ export function useSendForm({
277
280
  recipient as `0x${string}`,
278
281
  selectedDestinationChain.id,
279
282
  )
283
+ logger.console.log("[trails-sdk] isRecipientContract:", isContract)
280
284
  setIsRecipientContract(isContract)
281
285
  } catch (error) {
282
286
  logger.console.error(
@@ -293,6 +297,64 @@ export function useSendForm({
293
297
  checkRecipientContract()
294
298
  }, [recipient, selectedDestinationChain?.id])
295
299
 
300
+ // Check if sender is a contract address on origin chain
301
+ useEffect(() => {
302
+ const checkSenderContractOnOrigin = async () => {
303
+ if (account?.address && selectedToken?.chainId) {
304
+ try {
305
+ const isContract = await getIsContract(
306
+ account.address as `0x${string}`,
307
+ selectedToken.chainId,
308
+ )
309
+ logger.console.log(
310
+ "[trails-sdk] isSenderContractOnOrigin:",
311
+ isContract,
312
+ )
313
+ setIsSenderContractOnOrigin(isContract)
314
+ } catch (error) {
315
+ logger.console.error(
316
+ "[trails-sdk] Error checking if sender is contract on origin:",
317
+ error,
318
+ )
319
+ setIsSenderContractOnOrigin(false)
320
+ }
321
+ } else {
322
+ setIsSenderContractOnOrigin(false)
323
+ }
324
+ }
325
+
326
+ checkSenderContractOnOrigin()
327
+ }, [account?.address, selectedToken?.chainId])
328
+
329
+ // Check if sender is a contract address on destination chain
330
+ useEffect(() => {
331
+ const checkSenderContractOnDestination = async () => {
332
+ if (account?.address && selectedDestinationChain?.id) {
333
+ try {
334
+ const isContract = await getIsContract(
335
+ account.address as `0x${string}`,
336
+ selectedDestinationChain.id,
337
+ )
338
+ logger.console.log(
339
+ "[trails-sdk] isSenderContractOnDestination:",
340
+ isContract,
341
+ )
342
+ setIsSenderContractOnDestination(isContract)
343
+ } catch (error) {
344
+ logger.console.error(
345
+ "[trails-sdk] Error checking if sender is contract on destination:",
346
+ error,
347
+ )
348
+ setIsSenderContractOnDestination(false)
349
+ }
350
+ } else {
351
+ setIsSenderContractOnDestination(false)
352
+ }
353
+ }
354
+
355
+ checkSenderContractOnDestination()
356
+ }, [account?.address, selectedDestinationChain?.id])
357
+
296
358
  const isCustomToken = useMemo(() => toToken?.startsWith("0x"), [toToken])
297
359
 
298
360
  const {
@@ -354,7 +416,7 @@ export function useSendForm({
354
416
  token = supportedTokens.find(
355
417
  (token) =>
356
418
  (isToTokenAddress // Match by specified destination token address or symbol
357
- ? token.contractAddress === toToken
419
+ ? token.contractAddress.toLowerCase() === toToken.toLowerCase()
358
420
  : token.symbol === toToken) &&
359
421
  (toChainId // Match by specified destination chain id
360
422
  ? token.chainId === toChainId
@@ -372,22 +434,30 @@ export function useSendForm({
372
434
  }, [selectedDestToken, defaultDestToken])
373
435
 
374
436
  const apiClient = useAPIClient()
437
+ const trailsClient = useTrailsClient()
375
438
 
376
439
  const destTokenAddress = useTokenAddress({
377
440
  chainId: selectedDestinationChain?.id,
378
441
  tokenSymbol: selectedDestToken?.symbol,
379
442
  })
380
443
 
444
+ const destTokenPricesInput = useMemo(() => {
445
+ const input =
446
+ selectedDestToken && destTokenAddress && selectedDestinationChain?.id
447
+ ? [
448
+ {
449
+ tokenId: selectedDestToken.symbol,
450
+ contractAddress: destTokenAddress,
451
+ chainId: selectedDestinationChain.id,
452
+ },
453
+ ]
454
+ : []
455
+
456
+ return input
457
+ }, [selectedDestToken, destTokenAddress, selectedDestinationChain?.id])
458
+
381
459
  const { tokenPrices: destTokenPrices } = useTokenPrices(
382
- selectedDestToken && destTokenAddress
383
- ? [
384
- {
385
- tokenId: selectedDestToken.symbol,
386
- contractAddress: destTokenAddress,
387
- chainId: selectedDestinationChain.id,
388
- },
389
- ]
390
- : [],
460
+ destTokenPricesInput,
391
461
  apiClient,
392
462
  )
393
463
 
@@ -421,7 +491,7 @@ export function useSendForm({
421
491
  const newToken = supportedTokens.find(
422
492
  (token) =>
423
493
  (isToTokenAddress // Match by specified destination token address or symbol
424
- ? token.contractAddress === toToken
494
+ ? token.contractAddress.toLowerCase() === toToken.toLowerCase()
425
495
  : token.symbol === toToken) &&
426
496
  (toChainId // Match by specified destination chain id
427
497
  ? token.chainId === toChainId
@@ -479,12 +549,13 @@ export function useSendForm({
479
549
  [onTransactionStateChange],
480
550
  )
481
551
 
482
- const balanceFormatted = selectedToken
483
- ? formatRawAmount(
484
- selectedToken.balance,
485
- selectedToken.contractInfo?.decimals,
486
- )
487
- : "0"
552
+ const balanceFormatted =
553
+ selectedToken?.balance && selectedToken?.contractInfo?.decimals
554
+ ? formatRawAmount(
555
+ selectedToken.balance,
556
+ selectedToken.contractInfo?.decimals!,
557
+ )
558
+ : "0"
488
559
  const balanceUsdDisplay = selectedToken?.balanceUsdFormatted ?? ""
489
560
  const isValidRecipient = Boolean(recipient && isAddress(recipient))
490
561
 
@@ -502,6 +573,10 @@ export function useSendForm({
502
573
  null,
503
574
  )
504
575
  const [isRecipientContract, setIsRecipientContract] = useState(false)
576
+ const [isSenderContractOnOrigin, setIsSenderContractOnOrigin] =
577
+ useState(false)
578
+ const [isSenderContractOnDestination, setIsSenderContractOnDestination] =
579
+ useState(false)
505
580
 
506
581
  const { hasParam } = useQueryParams()
507
582
  const isDryMode = hasParam("dryMode", "true")
@@ -520,7 +595,39 @@ export function useSendForm({
520
595
 
521
596
  // Calculate raw amount (in wei/smallest unit)
522
597
  const amountRaw = useMemo(() => {
523
- if (!amount || !selectedToken?.contractInfo?.decimals) {
598
+ if (
599
+ !amount &&
600
+ !(selectedToken?.contractInfo?.decimals || selectedDestToken?.decimals)
601
+ ) {
602
+ logger.console.warn("[trails-sdk] Missing token decimals for quote", {
603
+ amount,
604
+ selectedToken,
605
+ selectedDestToken,
606
+ tradeType,
607
+ })
608
+ return "0"
609
+ }
610
+
611
+ if (
612
+ tradeType === TradeType.EXACT_INPUT &&
613
+ !selectedToken?.contractInfo?.decimals
614
+ ) {
615
+ logger.console.warn(
616
+ "[trails-sdk] Missing source token decimals for quote",
617
+ {
618
+ selectedToken,
619
+ tradeType,
620
+ },
621
+ )
622
+ return "0"
623
+ } else if (!selectedDestToken?.decimals) {
624
+ logger.console.warn(
625
+ "[trails-sdk] Missing destination token decimals for quote",
626
+ {
627
+ selectedDestToken,
628
+ tradeType,
629
+ },
630
+ )
524
631
  return "0"
525
632
  }
526
633
 
@@ -528,7 +635,7 @@ export function useSendForm({
528
635
  // For EXACT_OUTPUT: use destination token decimals (user enters destination amount)
529
636
  const decimals =
530
637
  tradeType === TradeType.EXACT_INPUT
531
- ? selectedToken.contractInfo?.decimals
638
+ ? selectedToken?.contractInfo?.decimals
532
639
  : selectedDestToken?.decimals
533
640
 
534
641
  if (!decimals) {
@@ -569,6 +676,18 @@ export function useSendForm({
569
676
  amountRaw === "0" ||
570
677
  !selectedToken
571
678
  ) {
679
+ logger.console.log(
680
+ "[trails-sdk] Skipping quote because of missing inputs",
681
+ {
682
+ amount,
683
+ destinationTokenAddress,
684
+ isValidRecipient,
685
+ selectedDestToken,
686
+ selectedDestinationChain,
687
+ amountRaw,
688
+ selectedToken,
689
+ },
690
+ )
572
691
  setQuoteError(null)
573
692
  setPrepareSendResult(null)
574
693
  return
@@ -589,7 +708,13 @@ export function useSendForm({
589
708
  const destinationTokenDecimals = selectedDestToken.decimals
590
709
 
591
710
  if (!sourceTokenDecimals || !destinationTokenDecimals) {
592
- logger.console.warn("[trails-sdk] Missing token decimals for quote")
711
+ logger.console.warn("[trails-sdk] Missing token decimals for quote", {
712
+ sourceTokenDecimals,
713
+ destinationTokenDecimals,
714
+ selectedToken,
715
+ selectedDestToken,
716
+ tradeType,
717
+ })
593
718
  setPrepareSendResult(null)
594
719
  setIsLoadingQuote(false)
595
720
  return
@@ -681,6 +806,7 @@ export function useSendForm({
681
806
  fee: "0",
682
807
  client: walletClient,
683
808
  apiClient,
809
+ trailsClient,
684
810
  originRelayer,
685
811
  destinationRelayer,
686
812
  destinationCalldata: toCalldata,
@@ -721,6 +847,7 @@ export function useSendForm({
721
847
  account,
722
848
  walletClient,
723
849
  apiClient,
850
+ trailsClient,
724
851
  selectedDestToken?.decimals,
725
852
  recipient,
726
853
  destinationTokenAddress,
@@ -776,6 +903,10 @@ export function useSendForm({
776
903
  selectedDestinationChain?.id,
777
904
  toCalldata,
778
905
  refetchTrigger,
906
+ selectedToken?.contractAddress,
907
+ selectedToken?.chainId,
908
+ selectedToken?.balance,
909
+ selectedToken?.tokenPriceUsd,
779
910
  ])
780
911
 
781
912
  // Calculate destination amount from quote if available
@@ -991,9 +1122,13 @@ export function useSendForm({
991
1122
  const checksummedRecipient = getAddress(recipient)
992
1123
  const checksummedAccount = getAddress(account.address)
993
1124
 
994
- if (mode === "swap") {
995
- return `Swap ${amountDisplay} ${tokenSymbol}`
996
- }
1125
+ logger.console.log("[trails-sdk] buttonText:", {
1126
+ mode,
1127
+ fundMethod,
1128
+ isSameChain,
1129
+ isSameToken,
1130
+ checksummedRecipient,
1131
+ })
997
1132
 
998
1133
  if (fundMethod === "exchange") {
999
1134
  return `Continue to Exchange`
@@ -1003,6 +1138,14 @@ export function useSendForm({
1003
1138
  return `Continue to QR Code`
1004
1139
  }
1005
1140
 
1141
+ if (!fundMethod || fundMethod === "wallet") {
1142
+ return `Continue on wallet`
1143
+ }
1144
+
1145
+ if (mode === "swap") {
1146
+ return `Swap ${amountDisplay} ${tokenSymbol}`
1147
+ }
1148
+
1006
1149
  if (mode === "earn") {
1007
1150
  return `Deposit ${destinationAmountDisplay} ${destinationTokenSymbol}`
1008
1151
  }
@@ -1054,7 +1197,7 @@ export function useSendForm({
1054
1197
  }, [quoteError])
1055
1198
 
1056
1199
  // Check if origin and destination tokens are the same (same contract address on same chain)
1057
- // Only block same-token transactions when there's no custom calldata
1200
+ // Only block same-token transactions when there's no custom calldata AND recipient is the same as sender
1058
1201
  const isSameTokenWithoutCustomCalldata = useMemo(() => {
1059
1202
  if (
1060
1203
  !selectedToken ||
@@ -1069,14 +1212,22 @@ export function useSendForm({
1069
1212
  selectedToken.contractAddress.toLowerCase() ===
1070
1213
  destinationTokenAddress.toLowerCase() &&
1071
1214
  selectedToken.chainId === selectedDestinationChain?.id
1072
- // Allow same-token transactions if there's custom calldata (e.g., NFT minting)
1073
- return isSameChainAndToken && !toCalldata
1215
+
1216
+ // Allow same-token transactions if:
1217
+ // 1. There's custom calldata (e.g., NFT minting)
1218
+ // 2. Recipient is different from sender (simple transfer to another address)
1219
+ const recipientIsSameAsSender =
1220
+ recipient?.toLowerCase() === account?.address?.toLowerCase()
1221
+
1222
+ return isSameChainAndToken && !toCalldata && recipientIsSameAsSender
1074
1223
  }, [
1075
1224
  selectedToken,
1076
1225
  destinationTokenAddress,
1077
1226
  selectedDestinationChain,
1078
1227
  toCalldata,
1079
1228
  selectedToken?.chainId,
1229
+ recipient,
1230
+ account?.address,
1080
1231
  ])
1081
1232
 
1082
1233
  return {
@@ -1126,5 +1277,7 @@ export function useSendForm({
1126
1277
  quoteErrorPrettified,
1127
1278
  isSameTokenWithoutCustomCalldata,
1128
1279
  isRecipientContract,
1280
+ isSenderContractOnOrigin,
1281
+ isSenderContractOnDestination,
1129
1282
  }
1130
1283
  }
@@ -0,0 +1,50 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ type ReactNode,
6
+ } from "react"
7
+
8
+ interface SwapAmountContextType {
9
+ amount: string
10
+ setAmount: (amount: string) => void
11
+ clearAmount: () => void
12
+ }
13
+
14
+ const SwapAmountContext = createContext<SwapAmountContextType | undefined>(
15
+ undefined,
16
+ )
17
+
18
+ interface SwapAmountProviderProps {
19
+ children: ReactNode
20
+ }
21
+
22
+ export const SwapAmountProvider: React.FC<SwapAmountProviderProps> = ({
23
+ children,
24
+ }) => {
25
+ const [amount, setAmount] = useState<string>("")
26
+
27
+ const clearAmount = () => {
28
+ setAmount("")
29
+ }
30
+
31
+ return (
32
+ <SwapAmountContext.Provider
33
+ value={{
34
+ amount,
35
+ setAmount,
36
+ clearAmount,
37
+ }}
38
+ >
39
+ {children}
40
+ </SwapAmountContext.Provider>
41
+ )
42
+ }
43
+
44
+ export const useSwapAmount = () => {
45
+ const context = useContext(SwapAmountContext)
46
+ if (context === undefined) {
47
+ throw new Error("useSwapAmount must be used within a SwapAmountProvider")
48
+ }
49
+ return context
50
+ }
@@ -0,0 +1,100 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useCallback,
6
+ type ReactNode,
7
+ } from "react"
8
+ import { logger } from "../../logger.js"
9
+
10
+ interface SwapSettingsContextType {
11
+ isSimpleSwapMode: boolean
12
+ setIsSimpleSwapMode: (simple: boolean) => void
13
+ setIsSimpleSwapModeWithStorage: (simple: boolean) => void
14
+ toggleSimpleSwapMode: () => void
15
+ resetSwapSettings: () => void
16
+ }
17
+
18
+ const SwapSettingsContext = createContext<SwapSettingsContextType | undefined>(
19
+ undefined,
20
+ )
21
+
22
+ interface SwapSettingsProviderProps {
23
+ children: ReactNode
24
+ initialSimpleMode?: boolean
25
+ }
26
+
27
+ const SIMPLE_SWAP_MODE_KEY = "trails-simple-swap-mode"
28
+
29
+ export const SwapSettingsProvider: React.FC<SwapSettingsProviderProps> = ({
30
+ children,
31
+ initialSimpleMode = false,
32
+ }) => {
33
+ // Initialize state from localStorage or use initialSimpleMode as fallback
34
+ const [isSimpleSwapMode, setIsSimpleSwapMode] = useState<boolean>(() => {
35
+ try {
36
+ const stored = localStorage.getItem(SIMPLE_SWAP_MODE_KEY)
37
+ return stored !== null ? JSON.parse(stored) : initialSimpleMode
38
+ } catch (error) {
39
+ logger.console.warn(
40
+ "Failed to read simple swap mode from localStorage:",
41
+ error,
42
+ )
43
+ return initialSimpleMode
44
+ }
45
+ })
46
+
47
+ // Function that updates state and saves to localStorage (for user preferences)
48
+ const setIsSimpleSwapModeWithStorage = useCallback((simple: boolean) => {
49
+ setIsSimpleSwapMode(simple)
50
+ try {
51
+ localStorage.setItem(SIMPLE_SWAP_MODE_KEY, JSON.stringify(simple))
52
+ } catch (error) {
53
+ logger.console.warn(
54
+ "Failed to save simple swap mode to localStorage:",
55
+ error,
56
+ )
57
+ }
58
+ }, [])
59
+
60
+ const toggleSimpleSwapMode = useCallback(() => {
61
+ const newMode = !isSimpleSwapMode
62
+ setIsSimpleSwapModeWithStorage(newMode)
63
+ }, [isSimpleSwapMode, setIsSimpleSwapModeWithStorage])
64
+
65
+ const resetSwapSettings = useCallback(() => {
66
+ try {
67
+ localStorage.removeItem(SIMPLE_SWAP_MODE_KEY)
68
+ setIsSimpleSwapMode(false) // Default: advanced swap mode
69
+ } catch (error) {
70
+ logger.console.warn("Failed to reset swap settings:", error)
71
+ setIsSimpleSwapMode(false) // Still reset state even if localStorage fails
72
+ }
73
+ }, [])
74
+
75
+ const value: SwapSettingsContextType = {
76
+ isSimpleSwapMode,
77
+ setIsSimpleSwapMode, // This one doesn't save to localStorage
78
+ setIsSimpleSwapModeWithStorage, // This one saves to localStorage
79
+ toggleSimpleSwapMode,
80
+ resetSwapSettings,
81
+ }
82
+
83
+ return (
84
+ <SwapSettingsContext.Provider value={value}>
85
+ {children}
86
+ </SwapSettingsContext.Provider>
87
+ )
88
+ }
89
+
90
+ export const useSwapSettings = (): SwapSettingsContextType => {
91
+ const context = useContext(SwapSettingsContext)
92
+
93
+ if (context === undefined) {
94
+ throw new Error(
95
+ "useSwapSettings must be used within a SwapSettingsProvider",
96
+ )
97
+ }
98
+
99
+ return context
100
+ }
@@ -0,0 +1,23 @@
1
+ import { useAmountUsd } from "./useAmountUsd.js"
2
+ import { useWidgetProps } from "./useWidgetProps.js"
3
+
4
+ export function useTargetAmount(): {
5
+ targetAmountUsd: number | null
6
+ targetAmountUsdFormatted: string
7
+ } {
8
+ const { toAmount, toToken, toChainId } = useWidgetProps()
9
+
10
+ const {
11
+ amountUsd: targetAmountUsd,
12
+ amountUsdFormatted: targetAmountUsdFormatted,
13
+ } = useAmountUsd({
14
+ amount: toAmount,
15
+ token: toToken,
16
+ chainId: Number(toChainId),
17
+ })
18
+
19
+ return {
20
+ targetAmountUsd,
21
+ targetAmountUsdFormatted,
22
+ }
23
+ }
@@ -0,0 +1,80 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useState,
5
+ useCallback,
6
+ type ReactNode,
7
+ } from "react"
8
+ import { logger } from "../../logger.js"
9
+
10
+ type Theme = "auto" | "light" | "dark"
11
+
12
+ interface ThemeContextType {
13
+ selectedTheme: Theme | null
14
+ setSelectedTheme: (theme: Theme) => void
15
+ resetThemePreference: () => void
16
+ }
17
+
18
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
19
+
20
+ interface ThemeProviderProps {
21
+ children: ReactNode
22
+ }
23
+
24
+ const THEME_KEY = "trails-theme"
25
+
26
+ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
27
+ // Initialize state from localStorage or return null if no preference is stored
28
+ const [selectedTheme, setSelectedThemeState] = useState<Theme | null>(() => {
29
+ try {
30
+ const stored = localStorage.getItem(THEME_KEY)
31
+ if (stored && ["auto", "light", "dark"].includes(stored)) {
32
+ return stored as Theme
33
+ }
34
+ return null // No user preference stored
35
+ } catch (error) {
36
+ logger.console.warn("Failed to read theme from localStorage:", error)
37
+ return null
38
+ }
39
+ })
40
+
41
+ // Function that updates state and saves to localStorage
42
+ const setSelectedTheme = useCallback((theme: Theme) => {
43
+ setSelectedThemeState(theme)
44
+ try {
45
+ localStorage.setItem(THEME_KEY, theme)
46
+ } catch (error) {
47
+ logger.console.warn("Failed to save theme to localStorage:", error)
48
+ }
49
+ }, [])
50
+
51
+ const resetThemePreference = useCallback(() => {
52
+ try {
53
+ localStorage.removeItem(THEME_KEY)
54
+ setSelectedThemeState(null) // Clear user preference
55
+ } catch (error) {
56
+ logger.console.warn("Failed to reset theme preference:", error)
57
+ setSelectedThemeState(null) // Still reset state even if localStorage fails
58
+ }
59
+ }, [])
60
+
61
+ const value: ThemeContextType = {
62
+ selectedTheme,
63
+ setSelectedTheme,
64
+ resetThemePreference,
65
+ }
66
+
67
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
68
+ }
69
+
70
+ export const useThemePreference = (): ThemeContextType => {
71
+ const context = useContext(ThemeContext)
72
+
73
+ if (context === undefined) {
74
+ throw new Error(
75
+ "useThemePreference must be used within a ThemePreferenceProvider",
76
+ )
77
+ }
78
+
79
+ return context
80
+ }
@@ -310,9 +310,14 @@ export function useTokenList({
310
310
  }, [balanceError, onError])
311
311
 
312
312
  const handleTokenSelect = (token: TokenFormatted) => {
313
- const isNative = !("contractAddress" in token)
313
+ const isNative =
314
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
314
315
  const chainInfo = getChainInfo(token.chainId)
315
316
  const imageUrl = token.imageUrl
317
+ const decimals = isNative ? 18 : token.contractInfo?.decimals
318
+ if (!decimals) {
319
+ throw new Error("Decimals not found")
320
+ }
316
321
 
317
322
  let formattedToken: Token
318
323
  if (isNative) {
@@ -328,7 +333,7 @@ export function useTokenList({
328
333
  (token as TokenBalanceExtended).balanceUsdFormatted ?? "",
329
334
  tokenPriceUsd: (token as TokenBalanceExtended).price?.value ?? 0,
330
335
  contractInfo: {
331
- decimals: 18,
336
+ decimals,
332
337
  symbol: chainInfo?.nativeCurrency.symbol || "ETH",
333
338
  name: chainInfo?.nativeCurrency.name || "Native Token",
334
339
  },
@@ -346,7 +351,7 @@ export function useTokenList({
346
351
  ...token.contractInfo,
347
352
  name: token.contractInfo?.name ?? "Unknown Token",
348
353
  symbol: token.contractInfo?.symbol ?? "???",
349
- decimals: token.contractInfo?.decimals ?? 18,
354
+ decimals,
350
355
  },
351
356
  balanceUsdFormatted: token.balanceUsdFormatted ?? "",
352
357
  tokenPriceUsd: token.price?.value ?? 0,
@@ -373,7 +378,8 @@ export function useTokenList({
373
378
  const isTokenSelected = (token: TokenBalanceExtended): boolean => {
374
379
  if (!selectedToken) return false
375
380
 
376
- const isNative = !("contractAddress" in token)
381
+ const isNative =
382
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
377
383
  return (
378
384
  selectedToken.chainId === token.chainId &&
379
385
  (isNative
@@ -395,7 +401,8 @@ export function useTokenList({
395
401
 
396
402
  const matchingTokens = sortedTokens.filter(
397
403
  (token: TokenBalanceExtended) => {
398
- const isNative = !("contractAddress" in token)
404
+ const isNative =
405
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
399
406
  const chainInfo = getChainInfo(token.chainId)
400
407
  const chainName = chainInfo?.name || ""
401
408
  const chainNameLower = chainName.toLowerCase()
@@ -506,7 +513,8 @@ export function useTokenList({
506
513
  // Get base formatted tokens
507
514
  const baseFormattedTokens = filteredTokens.map(
508
515
  (token: TokenBalanceExtended): TokenFormatted => {
509
- const isNative = !("contractAddress" in token)
516
+ const isNative =
517
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
510
518
  const chainInfo = getChainInfo(token.chainId)
511
519
  const nativeSymbol = chainInfo?.nativeCurrency.symbol || "ETH"
512
520
  const tokenSymbol = isNative
@@ -525,13 +533,14 @@ export function useTokenList({
525
533
  tokenSymbol,
526
534
  token.chainId,
527
535
  )
528
- const formattedBalance = formatRawAmount(
529
- token.balance,
530
- isNative ? 18 : token.contractInfo?.decimals,
531
- )
536
+ const decimals = isNative ? 18 : token.contractInfo?.decimals
537
+ if (!decimals) {
538
+ throw new Error("Decimals not found")
539
+ }
540
+ const formattedBalance = formatRawAmount(token.balance, decimals)
532
541
  const priceUsd = Number(token.price?.value) ?? 0
533
542
  const balanceUsdFormatted = token.balanceUsdFormatted ?? ""
534
- const decimals = isNative ? 18 : (token.contractInfo?.decimals ?? 18)
543
+
535
544
  let isSufficientBalance = true
536
545
  if (targetAmountUsd) {
537
546
  isSufficientBalance = (token.balanceUsd ?? 0) >= targetAmountUsd