0xtrails 0.8.2 → 0.8.4

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 (94) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/{ccip-ru_Yzdas.js → ccip-BKavX04a.js} +13 -13
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/fees.d.ts +11 -17
  6. package/dist/fees.d.ts.map +1 -1
  7. package/dist/gasless.d.ts.map +1 -1
  8. package/dist/{index-Si7cO9V7.js → index-D5kULpIU.js} +20430 -20133
  9. package/dist/index.js +425 -847
  10. package/dist/intents.d.ts +1 -2
  11. package/dist/intents.d.ts.map +1 -1
  12. package/dist/prepareSend.d.ts.map +1 -1
  13. package/dist/recover.d.ts +8 -9
  14. package/dist/recover.d.ts.map +1 -1
  15. package/dist/tokenBalances.d.ts +51 -0
  16. package/dist/tokenBalances.d.ts.map +1 -1
  17. package/dist/trailsRouter.d.ts +15 -0
  18. package/dist/trailsRouter.d.ts.map +1 -1
  19. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +1 -3
  20. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  21. package/dist/transactionIntent/deposits/standardDeposit.d.ts +1 -3
  22. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  23. package/dist/transactionIntent/handlers/crossChain.d.ts +2 -4
  24. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  25. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +5 -4
  26. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  27. package/dist/transactionIntent/quote/normalizeQuote.d.ts +1 -1
  28. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  29. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  30. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  31. package/dist/transactionIntent/types.d.ts +11 -18
  32. package/dist/transactionIntent/types.d.ts.map +1 -1
  33. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  34. package/dist/widget/components/ClassicSwap.d.ts +1 -0
  35. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  36. package/dist/widget/components/DynamicSizeInputField.d.ts.map +1 -1
  37. package/dist/widget/components/Fund.d.ts.map +1 -1
  38. package/dist/widget/components/FundSwap.d.ts +1 -0
  39. package/dist/widget/components/FundSwap.d.ts.map +1 -1
  40. package/dist/widget/components/Pay.d.ts.map +1 -1
  41. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  42. package/dist/widget/components/SlippageToleranceSettings.d.ts +2 -1
  43. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  44. package/dist/widget/components/Swap.d.ts +1 -0
  45. package/dist/widget/components/Swap.d.ts.map +1 -1
  46. package/dist/widget/components/WidgetProviders.d.ts.map +1 -1
  47. package/dist/widget/css/compiled.css +1 -1
  48. package/dist/widget/hooks/useDefaultDestinationToken.d.ts +20 -0
  49. package/dist/widget/hooks/useDefaultDestinationToken.d.ts.map +1 -0
  50. package/dist/widget/hooks/{useDefaultTokenSelection.d.ts → useDefaultOriginToken.d.ts} +4 -16
  51. package/dist/widget/hooks/useDefaultOriginToken.d.ts.map +1 -0
  52. package/dist/widget/hooks/useQuote.d.ts +94 -35
  53. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  54. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  55. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  56. package/dist/widget/hooks/useTrailsSendTransaction.d.ts.map +1 -1
  57. package/dist/widget/index.js +1 -1
  58. package/dist/widget/widget.d.ts.map +1 -1
  59. package/package.json +2 -2
  60. package/src/aave.ts +4 -0
  61. package/src/constants.ts +4 -0
  62. package/src/fees.ts +47 -72
  63. package/src/gasless.ts +62 -32
  64. package/src/intents.ts +1 -3
  65. package/src/morpho.ts +1 -1
  66. package/src/prepareSend.ts +42 -6
  67. package/src/recover.ts +116 -172
  68. package/src/tokenBalances.ts +301 -1
  69. package/src/trailsRouter.ts +77 -0
  70. package/src/transactionIntent/deposits/depositOrchestrator.ts +0 -6
  71. package/src/transactionIntent/deposits/standardDeposit.ts +167 -184
  72. package/src/transactionIntent/handlers/crossChain.ts +8 -11
  73. package/src/transactionIntent/handlers/sameChainSameToken.ts +619 -608
  74. package/src/transactionIntent/quote/normalizeQuote.ts +32 -46
  75. package/src/transactionIntent/quote/quoteHelpers.ts +4 -2
  76. package/src/transactionIntent/types.ts +11 -18
  77. package/src/widget/compiled.css +1 -1
  78. package/src/widget/components/AccountIntentTransactionHistory.tsx +50 -18
  79. package/src/widget/components/ClassicSwap.tsx +158 -63
  80. package/src/widget/components/DynamicSizeInputField.tsx +2 -0
  81. package/src/widget/components/Fund.tsx +12 -11
  82. package/src/widget/components/FundSwap.tsx +1 -0
  83. package/src/widget/components/Pay.tsx +15 -14
  84. package/src/widget/components/QuoteDetails.tsx +18 -27
  85. package/src/widget/components/SlippageToleranceSettings.tsx +55 -25
  86. package/src/widget/components/Swap.tsx +1 -0
  87. package/src/widget/components/WidgetProviders.tsx +1 -6
  88. package/src/widget/hooks/useDefaultDestinationToken.tsx +173 -0
  89. package/src/widget/hooks/{useDefaultTokenSelection.tsx → useDefaultOriginToken.tsx} +58 -191
  90. package/src/widget/hooks/useQuote.ts +317 -79
  91. package/src/widget/hooks/useSendForm.ts +123 -764
  92. package/src/widget/hooks/useTrailsSendTransaction.ts +0 -2
  93. package/src/widget/widget.tsx +2 -0
  94. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +0 -1
@@ -11,7 +11,7 @@ import { SearchInputField } from "./SearchInputField.js"
11
11
  import { Identicon } from "./Identicon.js"
12
12
  import { truncateAddress } from "../../utils.js"
13
13
  import { useConnections, useAccount, useWalletClient } from "wagmi"
14
- import { ChevronDown } from "lucide-react"
14
+ import { ChevronDown, Info } from "lucide-react"
15
15
  import { getExplorerUrl } from "../../explorer.js"
16
16
  import { ErrorDisplay } from "./ErrorDisplay.js"
17
17
  import { logger } from "../../logger.js"
@@ -79,6 +79,7 @@ const TransactionItem: React.FC<{
79
79
  intentError,
80
80
  balancesError,
81
81
  hasIntentBalance,
82
+ recoverToken,
82
83
  refetchIntent,
83
84
  } = useIntentRecover({
84
85
  intentId: transaction.intentId && !refundTxHash ? transaction.intentId : "",
@@ -341,23 +342,54 @@ const TransactionItem: React.FC<{
341
342
  </svg>
342
343
  </a>
343
344
  ) : canRecover ? (
344
- <button
345
- type="button"
346
- onClick={handleRecover}
347
- disabled={
348
- isRecovering ||
349
- isLoadingIntent ||
350
- isLoadingBalances ||
351
- !!intentError ||
352
- !!balancesError
353
- }
354
- className="flex items-center justify-center gap-1 py-1 px-3 rounded-md transition-colors cursor-pointer text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
355
- aria-label="Recover intent"
356
- >
357
- {isRecovering || isLoadingIntent || isLoadingBalances
358
- ? "Recovering..."
359
- : "Recover"}
360
- </button>
345
+ <div className="flex items-center gap-1">
346
+ <button
347
+ type="button"
348
+ onClick={handleRecover}
349
+ disabled={
350
+ isRecovering ||
351
+ isLoadingIntent ||
352
+ isLoadingBalances ||
353
+ !!intentError ||
354
+ !!balancesError
355
+ }
356
+ className="flex items-center justify-center gap-1 py-1 px-3 rounded-md transition-colors cursor-pointer text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
357
+ aria-label="Recover intent"
358
+ >
359
+ {isRecovering || isLoadingIntent || isLoadingBalances
360
+ ? "Recovering..."
361
+ : "Recover"}
362
+ </button>
363
+ {/* Info tooltip showing recoverable token details */}
364
+ {recoverToken &&
365
+ !isRecovering &&
366
+ !isLoadingIntent &&
367
+ !isLoadingBalances && (
368
+ <div className="relative group">
369
+ <Info
370
+ className="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help"
371
+ aria-label="Token balance info"
372
+ />
373
+ <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg shadow-lg opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 whitespace-nowrap z-50 pointer-events-none">
374
+ <div className="flex flex-col gap-0.5">
375
+ <span className="font-medium">
376
+ Recover {recoverToken.symbol}
377
+ </span>
378
+ <span className="text-gray-300">
379
+ {recoverToken.balanceFormatted} {recoverToken.symbol}
380
+ </span>
381
+ {recoverToken.balanceUsdDisplay && (
382
+ <span className="text-gray-400">
383
+ ≈ {recoverToken.balanceUsdDisplay}
384
+ </span>
385
+ )}
386
+ </div>
387
+ {/* Tooltip arrow */}
388
+ <div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-gray-900 dark:border-t-gray-700" />
389
+ </div>
390
+ </div>
391
+ )}
392
+ </div>
361
393
  ) : null}
362
394
  {recoverError && (
363
395
  <div className="w-full">
@@ -18,7 +18,8 @@ import { useSelectedRecipient } from "../hooks/useSelectedRecipient.js"
18
18
  import { useOriginSelectedToken } from "../hooks/useOriginSelectedToken.js"
19
19
  import { useDestinationSelectedToken } from "../hooks/useDestinationSelectedToken.js"
20
20
  import { useBalanceVisible } from "../hooks/useBalanceVisible.js"
21
- import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js"
21
+ import { useDefaultOriginToken } from "../hooks/useDefaultOriginToken.js"
22
+ import { useDefaultDestinationToken } from "../hooks/useDefaultDestinationToken.js"
22
23
  import { ChainList } from "./ChainList.js"
23
24
  import { PercentageMaxButtons } from "./PercentageMaxButtons.js"
24
25
  import { TokenSelectorButton } from "./TokenSelectorButton.js"
@@ -71,6 +72,7 @@ interface ClassicSwapProps {
71
72
  onTrackToken?: (token: any) => void
72
73
  title?: string
73
74
  isSequenceWallet?: boolean
75
+ exactInputOnly?: boolean // When true, disables destination amount editing (exact input only)
74
76
  }
75
77
 
76
78
  export const ClassicSwap: React.FC<ClassicSwapProps> = ({
@@ -101,6 +103,7 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
101
103
  onTrackToken,
102
104
  title = "Swap",
103
105
  isSequenceWallet = false,
106
+ exactInputOnly = false,
104
107
  }) => {
105
108
  const { mode } = useMode()
106
109
  const { isBalanceVisible } = useBalanceVisible()
@@ -113,12 +116,26 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
113
116
  const { selectedRecipient, setSelectedRecipient } = useSelectedRecipient()
114
117
  const [isFlipped, setIsFlipped] = useState(false)
115
118
 
116
- // Use new default token selection hook
117
- const {
118
- defaultOriginToken,
119
- defaultDestinationToken,
120
- isLoading: isLoadingDefaults,
121
- } = useDefaultTokenSelection()
119
+ // Track manual selections to prevent auto-selection override
120
+ const [hasManualOriginSelection, setHasManualOriginSelection] =
121
+ useState(false)
122
+ const [hasManualDestinationSelection, setHasManualDestinationSelection] =
123
+ useState(false)
124
+
125
+ // Use a ref to track user actions to prevent race conditions with async state updates
126
+ const userActionRef = useRef({
127
+ hasManualOriginSelection: false,
128
+ hasManualDestinationSelection: false,
129
+ lastOriginSelectionTime: 0,
130
+ lastDestinationSelectionTime: 0,
131
+ })
132
+
133
+ // Use separated default token selection hooks
134
+ const { defaultOriginToken, isLoading: isLoadingOriginDefaults } =
135
+ useDefaultOriginToken()
136
+
137
+ const { defaultDestinationToken, isLoading: isLoadingDestinationDefaults } =
138
+ useDefaultDestinationToken(originToken)
122
139
 
123
140
  const [originChainId, setOriginChainId] = useState<number | null | undefined>(
124
141
  initialSelectedToken?.chainId || originToken?.chainId,
@@ -242,22 +259,33 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
242
259
  }, [])
243
260
 
244
261
  // Handle buy amount input changes
245
- const handleBuyAmountChange = useCallback((value: string) => {
246
- setTradeType(TradeType.EXACT_OUTPUT)
247
- setBuyAmount(value)
248
- setSellAmount("")
249
- setLastInputType("buy")
250
- }, [])
262
+ const handleBuyAmountChange = useCallback(
263
+ (value: string) => {
264
+ // Prevent switching to EXACT_OUTPUT if exactInputOnly mode is enabled
265
+ if (exactInputOnly) return
251
266
 
252
- // Handle buy input focus - clear field when user wants to type
253
- const handleBuyInputFocus = useCallback(() => {
254
- // When focusing on buy field and it's in EXACT_INPUT mode (showing calculated value), switch to EXACT_OUTPUT mode
255
- if (tradeType === TradeType.EXACT_INPUT && !buyAmount) {
256
- setBuyAmount("")
257
267
  setTradeType(TradeType.EXACT_OUTPUT)
268
+ setBuyAmount(value)
269
+ setSellAmount("")
258
270
  setLastInputType("buy")
271
+ },
272
+ [exactInputOnly],
273
+ )
274
+
275
+ // Handle buy input focus - only copy displayed value to state for editing
276
+ // Don't switch trade type or clear sellAmount here - that happens in handleBuyAmountChange
277
+ // This prevents unnecessary requotes when user just focuses without editing
278
+ const handleBuyInputFocus = useCallback(() => {
279
+ // Skip if exactInputOnly mode is enabled
280
+ if (exactInputOnly) return
281
+
282
+ // When focusing on buy field in EXACT_INPUT mode, copy the calculated value
283
+ // to buyAmount state so user can edit it. The actual mode switch happens
284
+ // when the user starts typing (in handleBuyAmountChange)
285
+ if (tradeType === TradeType.EXACT_INPUT && toAmountDisplay && !buyAmount) {
286
+ setBuyAmount(toAmountDisplay)
259
287
  }
260
- }, [tradeType, buyAmount])
288
+ }, [tradeType, toAmountDisplay, buyAmount, exactInputOnly])
261
289
 
262
290
  // Update amounts when quote is received
263
291
  useEffect(() => {
@@ -283,9 +311,9 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
283
311
  }
284
312
  }, [amountRaw, onAmountUpdate])
285
313
 
286
- // Auto-select origin token using new hook
314
+ // Auto-select origin token using separated hook
287
315
  useEffect(() => {
288
- if (!originToken && !isLoadingDefaults && defaultOriginToken) {
316
+ if (!originToken && !isLoadingOriginDefaults && defaultOriginToken) {
289
317
  logger.console.log(
290
318
  "[trails-sdk] Auto-selecting origin token:",
291
319
  defaultOriginToken,
@@ -293,13 +321,33 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
293
321
  setOriginToken(defaultOriginToken as any)
294
322
  setOriginChainId(defaultOriginToken.chainId)
295
323
  }
296
- }, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
324
+ }, [originToken, isLoadingOriginDefaults, defaultOriginToken, setOriginToken])
297
325
 
298
- // Auto-select destination token using new hook
326
+ // Auto-select destination token using separated hook
299
327
  // Note: In swap mode, we still auto-select a destination even if toToken is provided,
300
328
  // since the user needs a starting point to modify
329
+ // IMPORTANT: Only auto-select if user hasn't made manual selections to prevent override
301
330
  useEffect(() => {
302
- if (!destinationToken && !isLoadingDefaults && defaultDestinationToken) {
331
+ // Prevent race conditions by checking both state and ref
332
+ const hasRecentOriginSelection =
333
+ userActionRef.current.hasManualOriginSelection ||
334
+ hasManualOriginSelection ||
335
+ (userActionRef.current.lastOriginSelectionTime > 0 &&
336
+ Date.now() - userActionRef.current.lastOriginSelectionTime < 1000) // 1 second cooldown
337
+
338
+ const hasRecentDestSelection =
339
+ userActionRef.current.hasManualDestinationSelection ||
340
+ hasManualDestinationSelection ||
341
+ (userActionRef.current.lastDestinationSelectionTime > 0 &&
342
+ Date.now() - userActionRef.current.lastDestinationSelectionTime < 1000) // 1 second cooldown
343
+
344
+ if (
345
+ !destinationToken &&
346
+ !isLoadingDestinationDefaults &&
347
+ defaultDestinationToken &&
348
+ !hasRecentDestSelection &&
349
+ !hasRecentOriginSelection // Don't auto-select destination if user recently selected origin
350
+ ) {
303
351
  logger.console.log(
304
352
  "[trails-sdk] Auto-selecting destination token:",
305
353
  defaultDestinationToken,
@@ -315,8 +363,10 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
315
363
  }
316
364
  }, [
317
365
  destinationToken,
318
- isLoadingDefaults,
366
+ isLoadingDestinationDefaults,
319
367
  defaultDestinationToken,
368
+ hasManualDestinationSelection,
369
+ hasManualOriginSelection,
320
370
  setDestinationToken,
321
371
  setSelectedDestToken,
322
372
  setSelectedDestinationChain,
@@ -382,18 +432,35 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
382
432
  setSelectedDestinationChain(newChain)
383
433
  }
384
434
 
385
- if (tradeType === TradeType.EXACT_INPUT) {
386
- // Move the user-entered sell amount into the buy field and treat it as exact output
387
- setBuyAmount(tempSellAmount)
388
- setSellAmount("")
389
- setTradeType(TradeType.EXACT_OUTPUT)
390
- setLastInputType("buy")
391
- } else {
392
- // If we were in EXACT_OUTPUT, use the current buy amount as the new sell amount
393
- setSellAmount(tempBuyAmount)
394
- setBuyAmount("")
435
+ // Flipping is an intentional user action, so maintain manual selection state
436
+ // Both tokens are still manually selected, just swapped
437
+ if (hasManualOriginSelection || hasManualDestinationSelection) {
438
+ setHasManualOriginSelection(true)
439
+ setHasManualDestinationSelection(true)
440
+ }
441
+
442
+ if (exactInputOnly) {
443
+ // When exactInputOnly is enabled, always maintain EXACT_INPUT mode
444
+ // Move the sell amount to the new origin (what was destination)
445
+ setSellAmount(tempSellAmount)
446
+ setBuyAmount("") // Clear buy amount to get fresh quote
395
447
  setTradeType(TradeType.EXACT_INPUT)
396
448
  setLastInputType("sell")
449
+ } else {
450
+ // Normal flip behavior - switch trade types
451
+ if (tradeType === TradeType.EXACT_INPUT) {
452
+ // Move the user-entered sell amount into the buy field and treat it as exact output
453
+ setBuyAmount(tempSellAmount)
454
+ setSellAmount("")
455
+ setTradeType(TradeType.EXACT_OUTPUT)
456
+ setLastInputType("buy")
457
+ } else {
458
+ // If we were in EXACT_OUTPUT, use the current buy amount as the new sell amount
459
+ setSellAmount(tempBuyAmount)
460
+ setBuyAmount("")
461
+ setTradeType(TradeType.EXACT_INPUT)
462
+ setLastInputType("sell")
463
+ }
397
464
  }
398
465
  }, [
399
466
  originToken,
@@ -410,12 +477,19 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
410
477
  buyAmount,
411
478
  toAmountDisplay,
412
479
  amount,
480
+ hasManualOriginSelection,
481
+ hasManualDestinationSelection,
482
+ exactInputOnly,
413
483
  ])
414
484
 
415
485
  // Handle source token selection from full-screen selector
416
486
  const handleSourceTokenSelectorSelect = useCallback(
417
487
  (token: Token) => {
418
- if (token.contractAddress === destinationToken?.contractAddress) {
488
+ // Only flip if it's the exact same token (same contract address AND chain ID)
489
+ if (
490
+ token.contractAddress === destinationToken?.contractAddress &&
491
+ token.chainId === destinationToken?.chainId
492
+ ) {
419
493
  handleFlip()
420
494
  }
421
495
 
@@ -433,6 +507,11 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
433
507
  setOriginChainId(token.chainId)
434
508
  setShowSourceTokenSelector(false)
435
509
 
510
+ // Mark as manual selection to prevent auto-selection override
511
+ setHasManualOriginSelection(true)
512
+ userActionRef.current.hasManualOriginSelection = true
513
+ userActionRef.current.lastOriginSelectionTime = Date.now()
514
+
436
515
  onTrackToken?.(token)
437
516
  logger.console.log("[trails-sdk] selected origin token", token)
438
517
  },
@@ -443,6 +522,7 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
443
522
  originToken,
444
523
  handleFlip,
445
524
  destinationToken?.contractAddress,
525
+ destinationToken?.chainId,
446
526
  destinationToken,
447
527
  ],
448
528
  )
@@ -450,7 +530,11 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
450
530
  // Handle destination token selection from full-screen selector
451
531
  const handleDestinationTokenSelectorSelect = useCallback(
452
532
  (token: Token) => {
453
- if (token.contractAddress === originToken?.contractAddress) {
533
+ // Only flip if it's the exact same token (same contract address AND chain ID)
534
+ if (
535
+ token.contractAddress === originToken?.contractAddress &&
536
+ token.chainId === originToken?.chainId
537
+ ) {
454
538
  handleFlip()
455
539
  }
456
540
 
@@ -464,6 +548,12 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
464
548
  }
465
549
 
466
550
  setShowDestinationTokenSelector(false)
551
+
552
+ // Mark as manual selection to prevent auto-selection override
553
+ setHasManualDestinationSelection(true)
554
+ userActionRef.current.hasManualDestinationSelection = true
555
+ userActionRef.current.lastDestinationSelectionTime = Date.now()
556
+
467
557
  onTrackToken?.(token)
468
558
  logger.console.log("[trails-sdk] selected destination token", token)
469
559
  },
@@ -473,6 +563,7 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
473
563
  setSelectedDestinationChain,
474
564
  onTrackToken,
475
565
  originToken?.contractAddress,
566
+ originToken?.chainId,
476
567
  handleFlip,
477
568
  ],
478
569
  )
@@ -480,7 +571,11 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
480
571
  // Handle recent token selection
481
572
  const handleRecentTokenSelect = useCallback(
482
573
  (token: any) => {
483
- if (token.contractAddress === originToken?.contractAddress) {
574
+ // Only flip if it's the exact same token (same contract address AND chain ID)
575
+ if (
576
+ token.contractAddress === originToken?.contractAddress &&
577
+ token.chainId === originToken?.chainId
578
+ ) {
484
579
  handleFlip()
485
580
  }
486
581
 
@@ -496,6 +591,7 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
496
591
  handleSourceTokenSelectorSelect,
497
592
  handleDestinationTokenSelectorSelect,
498
593
  originToken?.contractAddress,
594
+ originToken?.chainId,
499
595
  handleFlip,
500
596
  ],
501
597
  )
@@ -748,17 +844,23 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
748
844
  value={
749
845
  tradeType === TradeType.EXACT_OUTPUT
750
846
  ? buyAmount
751
- : toAmountDisplay || ""
847
+ : buyAmount || toAmountDisplay || ""
848
+ }
849
+ onChange={
850
+ exactInputOnly
851
+ ? undefined
852
+ : (e) => handleBuyAmountChange(e.target.value)
752
853
  }
753
- onChange={(e) => handleBuyAmountChange(e.target.value)}
754
- onFocus={handleBuyInputFocus}
854
+ onFocus={exactInputOnly ? undefined : handleBuyInputFocus}
755
855
  isLoading={
756
856
  isLoadingQuote && tradeType === TradeType.EXACT_INPUT
757
857
  }
758
858
  readOnly={
859
+ exactInputOnly ||
759
860
  (tradeType === TradeType.EXACT_INPUT && isLoadingQuote) ||
760
861
  !!toAmount
761
862
  }
863
+ disabled={exactInputOnly}
762
864
  showPlaceholderStyle={!amount}
763
865
  variant="default"
764
866
  />
@@ -780,31 +882,24 @@ export const ClassicSwap: React.FC<ClassicSwapProps> = ({
780
882
  </div>
781
883
 
782
884
  {/* Bottom Info Row */}
783
- <div
784
- className={
785
- !prepareSendQuote?.destinationAmountUsdDisplay && !isLoadingQuote
786
- ? "flex justify-end mt-4"
787
- : "mt-4 flex items-center justify-between"
788
- }
789
- >
885
+ <div className="mt-4 flex items-center justify-between">
790
886
  {/* Destination Amount USD from Quote */}
791
- {(prepareSendQuote?.destinationAmountUsdDisplay ||
792
- (isLoadingQuote && tradeType === TradeType.EXACT_INPUT)) && (
793
- <div className="text-xs text-gray-500 dark:text-gray-400">
794
- ≈{" "}
795
- {isLoadingQuote && tradeType === TradeType.EXACT_INPUT
796
- ? "$0.00"
797
- : prepareSendQuote?.destinationAmountUsdDisplay}
798
- </div>
799
- )}
887
+ <div className="text-xs text-gray-500 dark:text-gray-400">
888
+ {(prepareSendQuote?.destinationAmountUsdDisplay ||
889
+ (isLoadingQuote && tradeType === TradeType.EXACT_INPUT)) && (
890
+ <>
891
+ {" "}
892
+ {isLoadingQuote && tradeType === TradeType.EXACT_INPUT
893
+ ? "$0.00"
894
+ : prepareSendQuote?.destinationAmountUsdDisplay}
895
+ </>
896
+ )}
897
+ </div>
800
898
 
801
- {/* Dest Token Balance */}
899
+ {/* Dest Token Balance - always on right */}
802
900
  {destinationToken && (
803
- <div className="flex justify-end">
804
- <div className="text-xs text-gray-500 dark:text-gray-400">
805
- Balance:{" "}
806
- {isBalanceVisible ? destinationBalanceDisplay : "0.00"}
807
- </div>
901
+ <div className="text-xs text-gray-500 dark:text-gray-400">
902
+ Balance: {isBalanceVisible ? destinationBalanceDisplay : "0.00"}
808
903
  </div>
809
904
  )}
810
905
  </div>
@@ -34,6 +34,7 @@ export const DynamicSizeInputField = forwardRef<
34
34
  showPlaceholderStyle = false,
35
35
  onFocus,
36
36
  onBlur,
37
+ disabled,
37
38
  ...restProps
38
39
  },
39
40
  ref,
@@ -88,6 +89,7 @@ export const DynamicSizeInputField = forwardRef<
88
89
  onChange={handleChange}
89
90
  onFocus={onFocus}
90
91
  onBlur={onBlur}
92
+ disabled={disabled}
91
93
  placeholder={placeholder}
92
94
  autoComplete="off"
93
95
  className={
@@ -14,7 +14,8 @@ import { useSwapAmount } from "../hooks/useSwapAmount.js"
14
14
  import { useMode } from "../hooks/useMode.js"
15
15
  import { useBalanceVisible } from "../hooks/useBalanceVisible.js"
16
16
  import { useCurrentScreen } from "../hooks/useCurrentScreen.js"
17
- import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js" // Context version
17
+ import { useDefaultOriginToken } from "../hooks/useDefaultOriginToken.js"
18
+ import { useDefaultDestinationToken } from "../hooks/useDefaultDestinationToken.js"
18
19
  import { useTargetAmount } from "../hooks/useTargetAmount.js"
19
20
  import { TokenImage } from "./TokenImage.js"
20
21
  import { Identicon } from "./Identicon.js"
@@ -109,12 +110,12 @@ export const Fund: React.FC<FundProps> = ({
109
110
  const { setCurrentScreen } = useCurrentScreen()
110
111
  const { targetAmountUsd } = useTargetAmount()
111
112
 
112
- // Use new default token selection hook
113
- const {
114
- defaultOriginToken,
115
- defaultDestinationToken,
116
- isLoading: isLoadingDefaults,
117
- } = useDefaultTokenSelection()
113
+ // Use separated default token selection hooks
114
+ const { defaultOriginToken, isLoading: isLoadingOriginDefaults } =
115
+ useDefaultOriginToken()
116
+
117
+ const { defaultDestinationToken, isLoading: isLoadingDestinationDefaults } =
118
+ useDefaultDestinationToken(originToken)
118
119
 
119
120
  // Local state for fund-specific functionality
120
121
  const [isInputTypeUsd, setIsInputTypeUsd] = useState(false)
@@ -176,14 +177,14 @@ export const Fund: React.FC<FundProps> = ({
176
177
 
177
178
  // Auto-select origin and destination tokens using new hook
178
179
  useEffect(() => {
179
- if (!originToken && !isLoadingDefaults && defaultOriginToken) {
180
+ if (!originToken && !isLoadingOriginDefaults && defaultOriginToken) {
180
181
  logger.console.log(
181
182
  "[trails-sdk] Auto-selecting origin token:",
182
183
  defaultOriginToken,
183
184
  )
184
185
  setOriginToken(defaultOriginToken as any)
185
186
  }
186
- }, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
187
+ }, [originToken, isLoadingOriginDefaults, defaultOriginToken, setOriginToken])
187
188
 
188
189
  // Auto-select destination token using new hook
189
190
  useEffect(() => {
@@ -191,7 +192,7 @@ export const Fund: React.FC<FundProps> = ({
191
192
  !globalDestinationToken &&
192
193
  !toToken &&
193
194
  originToken &&
194
- !isLoadingDefaults &&
195
+ !isLoadingDestinationDefaults &&
195
196
  defaultDestinationToken
196
197
  ) {
197
198
  logger.console.log(
@@ -214,7 +215,7 @@ export const Fund: React.FC<FundProps> = ({
214
215
  originToken,
215
216
  globalDestinationToken,
216
217
  toToken,
217
- isLoadingDefaults,
218
+ isLoadingDestinationDefaults,
218
219
  defaultDestinationToken,
219
220
  setDestinationToken,
220
221
  setSelectedDestToken,
@@ -44,6 +44,7 @@ interface FundProps {
44
44
  onRecentTokenSelect?: (token: Token) => void
45
45
  onTrackToken?: (token: any) => void
46
46
  isSequenceWallet?: boolean
47
+ exactInputOnly?: boolean
47
48
  }
48
49
 
49
50
  export const Fund: React.FC<FundProps> = (props) => {
@@ -14,7 +14,8 @@ import { useSwapAmount } from "../hooks/useSwapAmount.js"
14
14
  import { useTokenList } from "../hooks/useTokenList.js"
15
15
  import { useMode } from "../hooks/useMode.js"
16
16
  import { useCurrentScreen } from "../hooks/useCurrentScreen.js"
17
- import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js" // Context version
17
+ import { useDefaultOriginToken } from "../hooks/useDefaultOriginToken.js"
18
+ import { useDefaultDestinationToken } from "../hooks/useDefaultDestinationToken.js"
18
19
  import { usePayMessage } from "../hooks/usePayMessage.js"
19
20
  import { useTargetAmount } from "../hooks/useTargetAmount.js"
20
21
  import { TokenImage } from "./TokenImage.js"
@@ -120,19 +121,19 @@ export const Pay: React.FC<PayProps> = ({
120
121
  const { setCurrentScreen } = useCurrentScreen()
121
122
  const { targetAmountUsd } = useTargetAmount()
122
123
 
123
- // Use new default token selection hook
124
- const {
125
- defaultOriginToken,
126
- defaultDestinationToken,
127
- isLoading: isLoadingDefaults,
128
- } = useDefaultTokenSelection()
124
+ // Use global origin token state or initial prop
125
+ const originToken = globalOriginToken || initialOriginToken
126
+
127
+ // Use separated default token selection hooks
128
+ const { defaultOriginToken, isLoading: isLoadingOriginDefaults } =
129
+ useDefaultOriginToken()
130
+
131
+ const { defaultDestinationToken, isLoading: isLoadingDestinationDefaults } =
132
+ useDefaultDestinationToken(originToken)
129
133
 
130
134
  // Use pay message hook for payment request display
131
135
  const { message: payMessage } = usePayMessage()
132
136
 
133
- // Use global origin token state or initial prop
134
- const originToken = globalOriginToken || initialOriginToken
135
-
136
137
  // Local state for pay-specific functionality
137
138
  const [tokenAmountForBackend, setTokenAmountForBackend] = useState("")
138
139
  const [inputDisplayValue, setInputDisplayValue] = useState("")
@@ -230,14 +231,14 @@ export const Pay: React.FC<PayProps> = ({
230
231
 
231
232
  // Auto-select origin token using new hook
232
233
  useEffect(() => {
233
- if (!originToken && !isLoadingDefaults && defaultOriginToken) {
234
+ if (!originToken && !isLoadingOriginDefaults && defaultOriginToken) {
234
235
  logger.console.log(
235
236
  "[trails-sdk] Auto-selecting origin token:",
236
237
  defaultOriginToken,
237
238
  )
238
239
  setOriginToken(defaultOriginToken as any)
239
240
  }
240
- }, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
241
+ }, [originToken, isLoadingOriginDefaults, defaultOriginToken, setOriginToken])
241
242
 
242
243
  // Initialize destination token from props or auto-select
243
244
  useEffect(() => {
@@ -347,7 +348,7 @@ export const Pay: React.FC<PayProps> = ({
347
348
  // Prefer global destination token if set
348
349
  const destTokenToUse = globalDestinationToken || defaultDestinationToken
349
350
 
350
- if (destTokenToUse && !isLoadingDefaults) {
351
+ if (destTokenToUse && !isLoadingDestinationDefaults) {
351
352
  logger.console.log("[trails-sdk] Initializing destination token:", {
352
353
  symbol: destTokenToUse.symbol,
353
354
  chainId: destTokenToUse.chainId,
@@ -375,7 +376,7 @@ export const Pay: React.FC<PayProps> = ({
375
376
  toToken,
376
377
  globalDestinationToken,
377
378
  defaultDestinationToken,
378
- isLoadingDefaults,
379
+ isLoadingDestinationDefaults,
379
380
  selectedDestToken?.symbol,
380
381
  setDestinationToken,
381
382
  setSelectedDestToken,