0xtrails 0.1.3 → 0.1.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 (111) hide show
  1. package/dist/address.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +18 -0
  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/{ccip-CWd4g9uZ.js → ccip-dLSEJjCf.js} +55 -55
  7. package/dist/cctpqueue.d.ts +1 -1
  8. package/dist/cctpqueue.d.ts.map +1 -1
  9. package/dist/constants.d.ts +1 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/decoders.d.ts +58 -0
  12. package/dist/decoders.d.ts.map +1 -0
  13. package/dist/ens.d.ts +6 -0
  14. package/dist/ens.d.ts.map +1 -1
  15. package/dist/error.d.ts +7 -0
  16. package/dist/error.d.ts.map +1 -1
  17. package/dist/{index-BTUBzx4R.js → index-BXbaLmtt.js} +25694 -24507
  18. package/dist/index.js +2 -2
  19. package/dist/intents.d.ts +4 -4
  20. package/dist/intents.d.ts.map +1 -1
  21. package/dist/metaTxns.d.ts +1 -1
  22. package/dist/metaTxns.d.ts.map +1 -1
  23. package/dist/mode.d.ts +1 -1
  24. package/dist/mode.d.ts.map +1 -1
  25. package/dist/preconditions.d.ts +1 -1
  26. package/dist/preconditions.d.ts.map +1 -1
  27. package/dist/prepareSend.d.ts +30 -24
  28. package/dist/prepareSend.d.ts.map +1 -1
  29. package/dist/prices.d.ts +1 -1
  30. package/dist/prices.d.ts.map +1 -1
  31. package/dist/proxyCaller.d.ts +0 -1
  32. package/dist/proxyCaller.d.ts.map +1 -1
  33. package/dist/tokenBalances.d.ts +1 -1
  34. package/dist/tokenBalances.d.ts.map +1 -1
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/trails.d.ts +1 -1
  37. package/dist/trails.d.ts.map +1 -1
  38. package/dist/transactions.d.ts +4 -0
  39. package/dist/transactions.d.ts.map +1 -1
  40. package/dist/utils.d.ts +6 -0
  41. package/dist/utils.d.ts.map +1 -1
  42. package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
  43. package/dist/widget/components/ErrorDisplay.d.ts +9 -0
  44. package/dist/widget/components/ErrorDisplay.d.ts.map +1 -0
  45. package/dist/widget/components/FundSendForm.d.ts +1 -1
  46. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  47. package/dist/widget/components/OriginTransferInformation.d.ts +10 -0
  48. package/dist/widget/components/OriginTransferInformation.d.ts.map +1 -0
  49. package/dist/widget/components/PaySendForm.d.ts +1 -1
  50. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  51. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  52. package/dist/widget/components/Receipt.d.ts.map +1 -1
  53. package/dist/widget/components/Receive.d.ts +12 -0
  54. package/dist/widget/components/Receive.d.ts.map +1 -0
  55. package/dist/widget/components/Swap.d.ts +5 -1
  56. package/dist/widget/components/Swap.d.ts.map +1 -1
  57. package/dist/widget/components/SwapDisplay.d.ts +9 -0
  58. package/dist/widget/components/SwapDisplay.d.ts.map +1 -0
  59. package/dist/widget/components/TransferPendingVertical.d.ts +2 -0
  60. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  61. package/dist/widget/components/YellowWarningAnimation.d.ts +2 -0
  62. package/dist/widget/components/YellowWarningAnimation.d.ts.map +1 -0
  63. package/dist/widget/hooks/useDebugScreens.d.ts +22 -0
  64. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -0
  65. package/dist/widget/hooks/useSendForm.d.ts +7 -3
  66. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  67. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  68. package/dist/widget/index.js +1 -1
  69. package/dist/widget/widget.d.ts.map +1 -1
  70. package/package.json +16 -15
  71. package/src/address.ts +3 -0
  72. package/src/analytics.ts +105 -4
  73. package/src/apiClient.ts +1 -1
  74. package/src/cctpqueue.ts +1 -1
  75. package/src/constants.ts +3 -0
  76. package/src/decoders.ts +310 -0
  77. package/src/ens.ts +16 -1
  78. package/src/error.ts +86 -1
  79. package/src/intents.ts +10 -2
  80. package/src/metaTxns.ts +1 -1
  81. package/src/mode.ts +1 -1
  82. package/src/preconditions.ts +1 -1
  83. package/src/prepareSend.ts +432 -114
  84. package/src/prices.ts +5 -1
  85. package/src/proxyCaller.ts +2 -14
  86. package/src/tokenBalances.ts +21 -17
  87. package/src/tokens.ts +62 -3
  88. package/src/trails.ts +2 -2
  89. package/src/transactions.ts +35 -18
  90. package/src/utils.ts +28 -0
  91. package/src/widget/compiled.css +2 -2
  92. package/src/widget/components/DebugScreensDropdown.tsx +3 -0
  93. package/src/widget/components/ErrorDisplay.tsx +150 -0
  94. package/src/widget/components/FundSendForm.tsx +62 -9
  95. package/src/widget/components/OriginTransferInformation.tsx +59 -0
  96. package/src/widget/components/PaySendForm.tsx +65 -12
  97. package/src/widget/components/QRCodeDeposit.tsx +5 -5
  98. package/src/widget/components/QrCode.tsx +1 -1
  99. package/src/widget/components/QuoteDetails.tsx +93 -25
  100. package/src/widget/components/Receipt.tsx +296 -103
  101. package/src/widget/components/Receive.tsx +146 -0
  102. package/src/widget/components/RecentTokens.tsx +1 -1
  103. package/src/widget/components/Swap.tsx +206 -85
  104. package/src/widget/components/SwapDisplay.tsx +68 -0
  105. package/src/widget/components/TokenSelector.tsx +1 -1
  106. package/src/widget/components/TransferPendingVertical.tsx +162 -112
  107. package/src/widget/components/YellowWarningAnimation.tsx +146 -0
  108. package/src/widget/hooks/useDebugScreens.ts +583 -0
  109. package/src/widget/hooks/useSendForm.ts +46 -4
  110. package/src/widget/hooks/useTokenList.ts +8 -27
  111. package/src/widget/widget.tsx +201 -484
@@ -16,6 +16,7 @@ import { formatUsdAmountDisplay } from "../../tokenBalances.js"
16
16
  import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
17
17
  import { ScreenHeader } from "./ScreenHeader.js"
18
18
  import { TokenSelector } from "./TokenSelector.js"
19
+ import { ErrorDisplay } from "./ErrorDisplay.js"
19
20
 
20
21
  interface SwapProps {
21
22
  selectedToken: Token | null
@@ -48,8 +49,11 @@ interface SwapProps {
48
49
  quote?: PrepareSendQuote | null,
49
50
  ) => void
50
51
  onAmountUpdate?: (amount: string) => void
51
- mode?: "pay" | "fund" | "earn" | "swap"
52
+ mode?: "pay" | "fund" | "earn" | "swap" | "receive"
52
53
  checkoutOnHandlers?: CheckoutOnHandlers
54
+ recentTokens?: SupportedToken[]
55
+ onRecentTokenSelect?: (token: SupportedToken) => void
56
+ onTrackToken?: (token: any) => void
53
57
  }
54
58
 
55
59
  export const Swap: React.FC<SwapProps> = ({
@@ -76,6 +80,9 @@ export const Swap: React.FC<SwapProps> = ({
76
80
  onAmountUpdate,
77
81
  mode,
78
82
  checkoutOnHandlers,
83
+ recentTokens,
84
+ onRecentTokenSelect,
85
+ onTrackToken,
79
86
  }) => {
80
87
  const [isFlipped, setIsFlipped] = useState(false)
81
88
  const [originChainId, setOriginChainId] = useState<number | null | undefined>(
@@ -84,6 +91,10 @@ export const Swap: React.FC<SwapProps> = ({
84
91
  const [originToken, setOriginToken] = useState<SupportedToken | null>(
85
92
  initialSelectedToken as SupportedToken | null,
86
93
  )
94
+ const [tradeType, setTradeType] = useState<TradeType>(TradeType.EXACT_INPUT)
95
+ const [sellAmount, setSellAmount] = useState("")
96
+ const [buyAmount, setBuyAmount] = useState("")
97
+ const [lastInputType, setLastInputType] = useState<"sell" | "buy">("sell")
87
98
 
88
99
  const {
89
100
  amount,
@@ -108,9 +119,12 @@ export const Swap: React.FC<SwapProps> = ({
108
119
  destinationTokenAddress,
109
120
  isValidCustomToken,
110
121
  prepareSendQuote,
122
+ quoteError,
123
+ quoteErrorPrettified,
124
+ isSameTokenWithoutCustomCalldata,
111
125
  } = useSendForm({
112
126
  account,
113
- toAmount,
127
+ toAmount: tradeType === TradeType.EXACT_OUTPUT ? buyAmount : toAmount,
114
128
  toRecipient: account.address,
115
129
  toChainId,
116
130
  toToken,
@@ -126,7 +140,7 @@ export const Swap: React.FC<SwapProps> = ({
126
140
  onSend,
127
141
  selectedToken: originToken as any,
128
142
  setWalletConfirmRetryHandler,
129
- tradeType: TradeType.EXACT_INPUT,
143
+ tradeType: tradeType,
130
144
  quoteProvider,
131
145
  fundMethod,
132
146
  mode,
@@ -134,18 +148,57 @@ export const Swap: React.FC<SwapProps> = ({
134
148
  checkoutOnHandlers,
135
149
  })
136
150
 
137
- // Handle amount input changes with decimal validation
138
- const handleAmountChange = useCallback(
139
- (value: string) => {
140
- // Validate decimal places (max 8 decimals)
141
- const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
142
- if (!decimalMatch && value !== "") {
143
- return // Don't update if more than 8 decimals
144
- }
145
- setAmount(value)
146
- },
147
- [setAmount],
148
- )
151
+ // Update amount based on last input type
152
+ useEffect(() => {
153
+ if (lastInputType === "sell") {
154
+ setAmount(sellAmount)
155
+ } else {
156
+ setAmount(buyAmount)
157
+ }
158
+ }, [sellAmount, buyAmount, lastInputType, setAmount])
159
+
160
+ // Handle sell amount input changes with decimal validation
161
+ const handleSellAmountChange = useCallback((value: string) => {
162
+ // Validate decimal places (max 8 decimals)
163
+ const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
164
+ if (!decimalMatch && value !== "") {
165
+ return // Don't update if more than 8 decimals
166
+ }
167
+ setTradeType(TradeType.EXACT_INPUT)
168
+ setSellAmount(value)
169
+ setBuyAmount("")
170
+ setLastInputType("sell")
171
+ }, [])
172
+
173
+ // Handle buy amount input changes with decimal validation
174
+ const handleBuyAmountChange = useCallback((value: string) => {
175
+ // Validate decimal places (max 8 decimals)
176
+ const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
177
+ if (!decimalMatch && value !== "") {
178
+ return // Don't update if more than 8 decimals
179
+ }
180
+ setTradeType(TradeType.EXACT_OUTPUT)
181
+ setBuyAmount(value)
182
+ setSellAmount("")
183
+ setLastInputType("buy")
184
+ }, [])
185
+
186
+ // Update amounts when quote is received
187
+ useEffect(() => {
188
+ if (!prepareSendQuote) return
189
+
190
+ if (
191
+ tradeType === TradeType.EXACT_OUTPUT &&
192
+ prepareSendQuote.originAmountDisplay
193
+ ) {
194
+ setSellAmount(prepareSendQuote.originAmountDisplay)
195
+ } else if (
196
+ tradeType === TradeType.EXACT_INPUT &&
197
+ prepareSendQuote.destinationAmountDisplay
198
+ ) {
199
+ setBuyAmount(prepareSendQuote.destinationAmountDisplay)
200
+ }
201
+ }, [prepareSendQuote, tradeType])
149
202
 
150
203
  // Handle percentage button clicks
151
204
  const handlePercentageClick = useCallback(
@@ -157,9 +210,12 @@ export const Swap: React.FC<SwapProps> = ({
157
210
  if (Number.isNaN(balance)) return
158
211
 
159
212
  const amount = (balance * percentage) / 100
160
- setAmount(amount.toFixed(6))
213
+ setTradeType(TradeType.EXACT_INPUT)
214
+ setSellAmount(amount.toFixed(6))
215
+ setBuyAmount("")
216
+ setLastInputType("sell")
161
217
  },
162
- [originToken, balanceFormatted, setAmount],
218
+ [originToken, balanceFormatted],
163
219
  )
164
220
 
165
221
  // Call onAmountUpdate when amountRaw changes
@@ -222,12 +278,11 @@ export const Swap: React.FC<SwapProps> = ({
222
278
  // Store current values
223
279
  const tempOriginToken = originToken
224
280
  const tempOriginChainId = originChainId
281
+ const tempBuyAmount = buyAmount || toAmountDisplay || ""
225
282
 
226
283
  // Swap origin and destination
227
284
  setOriginToken(selectedDestToken as any)
228
285
  setOriginChainId((selectedDestToken as any)?.chainId as any)
229
-
230
- // Set destination to previous origin (convert to TokenInfo)
231
286
  setSelectedDestToken(tempOriginToken as any)
232
287
 
233
288
  // Update destination chain
@@ -235,6 +290,21 @@ export const Swap: React.FC<SwapProps> = ({
235
290
  if (newChain) {
236
291
  setSelectedDestinationChain(newChain)
237
292
  }
293
+
294
+ // When flipping, always set the current visible amount as the new sell amount
295
+ if (tradeType === TradeType.EXACT_INPUT) {
296
+ // If we were in EXACT_INPUT, use the buy amount (from quote) as the new sell amount
297
+ setSellAmount(tempBuyAmount)
298
+ setBuyAmount("")
299
+ setTradeType(TradeType.EXACT_INPUT)
300
+ setLastInputType("sell")
301
+ } else {
302
+ // If we were in EXACT_OUTPUT, use the current buy amount as the new sell amount
303
+ setSellAmount(tempBuyAmount)
304
+ setBuyAmount("")
305
+ setTradeType(TradeType.EXACT_INPUT)
306
+ setLastInputType("sell")
307
+ }
238
308
  }
239
309
 
240
310
  return (
@@ -250,7 +320,7 @@ export const Swap: React.FC<SwapProps> = ({
250
320
  <div className="trails-bg-secondary trails-border-radius-container p-3 group">
251
321
  {/* Sell Label and Percentage Buttons */}
252
322
  <div className="flex justify-between items-center mb-2">
253
- <div className="text-sm font-medium trails-text-secondary">
323
+ <div className="text-sm font-medium trails-text-secondary text-left">
254
324
  Sell
255
325
  </div>
256
326
 
@@ -292,15 +362,27 @@ export const Swap: React.FC<SwapProps> = ({
292
362
  <div className="flex items-center space-x-2">
293
363
  {/* Amount Input */}
294
364
  <div className="flex-1">
295
- <input
296
- ref={inputRef}
297
- id="amount"
298
- type="text"
299
- value={amount}
300
- onChange={(e) => handleAmountChange(e.target.value)}
301
- placeholder="0.00"
302
- className="w-full bg-transparent text-xl font-medium trails-text-primary placeholder:trails-text-muted border-none outline-none"
303
- />
365
+ <div className="flex items-center space-x-2">
366
+ <input
367
+ ref={inputRef}
368
+ id="amount"
369
+ type="text"
370
+ value={sellAmount}
371
+ onChange={(e) => handleSellAmountChange(e.target.value)}
372
+ placeholder="0.00"
373
+ className={`w-full bg-transparent text-xl font-medium trails-text-primary placeholder:trails-text-muted border-none outline-none ${
374
+ isLoadingQuote && tradeType === TradeType.EXACT_OUTPUT
375
+ ? "animate-pulse"
376
+ : ""
377
+ }`}
378
+ readOnly={
379
+ tradeType === TradeType.EXACT_OUTPUT && isLoadingQuote
380
+ }
381
+ />
382
+ {isLoadingQuote && tradeType === TradeType.EXACT_OUTPUT && (
383
+ <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
384
+ )}
385
+ </div>
304
386
  </div>
305
387
 
306
388
  {/* Token Selection Button */}
@@ -353,14 +435,33 @@ export const Swap: React.FC<SwapProps> = ({
353
435
  name: token.name,
354
436
  },
355
437
  } as any)
438
+ console.log("selected origin token", token)
356
439
  setOriginChainId(token.chainId)
357
440
  setIsOriginTokenDropdownOpen(false)
441
+ // Track the token selection
442
+ onTrackToken?.(token)
358
443
  }}
359
444
  onError={onError}
360
445
  mode="swap"
361
446
  fundMethod={fundMethod}
362
447
  showContinueButton={false}
363
448
  compactMode={false}
449
+ recentTokens={recentTokens}
450
+ onRecentTokenSelect={(token) => {
451
+ setOriginToken({
452
+ ...token,
453
+ decimals: token.decimals,
454
+ contractInfo: {
455
+ decimals: token.decimals,
456
+ contractAddress: token.contractAddress,
457
+ symbol: token.symbol,
458
+ name: token.name,
459
+ },
460
+ } as any)
461
+ setOriginChainId(token.chainId)
462
+ setIsOriginTokenDropdownOpen(false)
463
+ onRecentTokenSelect?.(token)
464
+ }}
364
465
  />
365
466
  </div>
366
467
  </div>
@@ -368,12 +469,17 @@ export const Swap: React.FC<SwapProps> = ({
368
469
  </div>
369
470
  </div>
370
471
 
371
- {/* Bottom Info Row */}
472
+ {/* Bottom Info Row for sell */}
372
473
  <div className="mt-2 flex justify-between items-center">
373
474
  {/* USD Amount */}
374
- {amountUsdDisplay && originToken?.symbol && (
475
+ {originToken?.symbol && (
375
476
  <div className="text-xs trails-text-muted">
376
- {amountUsdDisplay}
477
+ {" "}
478
+ {tradeType === TradeType.EXACT_INPUT
479
+ ? amountUsdDisplay || "$0.00"
480
+ : isLoadingQuote
481
+ ? "$0.00"
482
+ : prepareSendQuote?.originAmountUsdDisplay || "$0.00"}
377
483
  </div>
378
484
  )}
379
485
 
@@ -386,7 +492,10 @@ export const Swap: React.FC<SwapProps> = ({
386
492
  if (balanceFormatted) {
387
493
  const balance = parseFloat(balanceFormatted)
388
494
  if (!Number.isNaN(balance)) {
389
- setAmount(balance.toFixed(6))
495
+ setTradeType(TradeType.EXACT_INPUT)
496
+ setSellAmount(balance.toFixed(6))
497
+ setBuyAmount("")
498
+ setLastInputType("sell")
390
499
  }
391
500
  }
392
501
  }}
@@ -396,7 +505,10 @@ export const Swap: React.FC<SwapProps> = ({
396
505
  if (balanceFormatted) {
397
506
  const balance = parseFloat(balanceFormatted)
398
507
  if (!Number.isNaN(balance)) {
399
- setAmount(balance.toFixed(6))
508
+ setTradeType(TradeType.EXACT_INPUT)
509
+ setSellAmount(balance.toFixed(6))
510
+ setBuyAmount("")
511
+ setLastInputType("sell")
400
512
  }
401
513
  }
402
514
  }
@@ -423,7 +535,7 @@ export const Swap: React.FC<SwapProps> = ({
423
535
  {/* Output Section - Amount + Token Selection */}
424
536
  <div className="trails-bg-secondary trails-border-radius-container p-3">
425
537
  {/* Buy Label */}
426
- <div className="text-sm font-medium trails-text-secondary mb-2">
538
+ <div className="text-sm font-medium trails-text-secondary mb-2 text-left">
427
539
  Buy
428
540
  </div>
429
541
 
@@ -431,12 +543,25 @@ export const Swap: React.FC<SwapProps> = ({
431
543
  {/* Output Amount */}
432
544
  <div className="flex-1">
433
545
  <div className="flex items-center space-x-2">
434
- <div
435
- className={`text-xl font-medium ${!amount ? "text-gray-400 dark:text-gray-500" : "trails-text-primary"} ${isLoadingQuote ? "animate-pulse" : ""}`}
436
- >
437
- {toAmountDisplay || "0.00"}
438
- </div>
439
- {isLoadingQuote && (
546
+ <input
547
+ type="text"
548
+ value={
549
+ tradeType === TradeType.EXACT_OUTPUT
550
+ ? buyAmount
551
+ : toAmountDisplay || ""
552
+ }
553
+ onChange={(e) => handleBuyAmountChange(e.target.value)}
554
+ placeholder="0.00"
555
+ className={`w-full bg-transparent text-xl font-medium placeholder:trails-text-muted border-none outline-none ${
556
+ !amount
557
+ ? "text-gray-400 dark:text-gray-500"
558
+ : "trails-text-primary"
559
+ } ${isLoadingQuote && tradeType === TradeType.EXACT_INPUT ? "animate-pulse" : ""}`}
560
+ readOnly={
561
+ tradeType === TradeType.EXACT_INPUT && isLoadingQuote
562
+ }
563
+ />
564
+ {isLoadingQuote && tradeType === TradeType.EXACT_INPUT && (
440
565
  <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
441
566
  )}
442
567
  </div>
@@ -487,7 +612,11 @@ export const Swap: React.FC<SwapProps> = ({
487
612
  token.contractInfo?.decimals || token.decimals,
488
613
  },
489
614
  } as any)
615
+ console.log("selected destination token", token)
616
+
490
617
  setIsTokenDropdownOpen(false)
618
+ // Track the token selection
619
+ onTrackToken?.(token)
491
620
  }}
492
621
  onError={onError}
493
622
  mode="swap"
@@ -495,6 +624,18 @@ export const Swap: React.FC<SwapProps> = ({
495
624
  allSupportedTokens={true}
496
625
  showContinueButton={false}
497
626
  compactMode={false}
627
+ recentTokens={recentTokens}
628
+ onRecentTokenSelect={(token) => {
629
+ setSelectedDestToken({
630
+ ...token,
631
+ decimals: token.decimals,
632
+ contractInfo: {
633
+ decimals: token.decimals,
634
+ },
635
+ } as any)
636
+ setIsTokenDropdownOpen(false)
637
+ onRecentTokenSelect?.(token)
638
+ }}
498
639
  />
499
640
  </div>
500
641
  </div>
@@ -505,54 +646,18 @@ export const Swap: React.FC<SwapProps> = ({
505
646
  {/* Bottom Info Row */}
506
647
  <div className="mt-2 flex justify-between items-center">
507
648
  {/* Destination Amount USD from Quote */}
508
- {prepareSendQuote?.destinationAmountUsdDisplay && (
649
+ {(prepareSendQuote?.destinationAmountUsdDisplay ||
650
+ (isLoadingQuote && tradeType === TradeType.EXACT_INPUT)) && (
509
651
  <div className="text-xs trails-text-muted">
510
- {prepareSendQuote.destinationAmountUsdDisplay}
652
+ {" "}
653
+ {isLoadingQuote && tradeType === TradeType.EXACT_INPUT
654
+ ? "$0.00"
655
+ : prepareSendQuote?.destinationAmountUsdDisplay}
511
656
  </div>
512
657
  )}
513
658
  </div>
514
659
  </div>
515
660
 
516
- {/* Receive Section - Show what user will receive */}
517
- {(toAmount || toChainId || toToken) && selectedDestinationChain && (
518
- <div className="space-y-1">
519
- <div className="text-lg font-semibold text-left trails-text-primary">
520
- You Will Receive
521
- </div>
522
-
523
- <div className="p-2">
524
- <div className="flex items-center space-x-3">
525
- <TokenImage
526
- symbol={selectedDestToken?.symbol}
527
- imageUrl={selectedDestToken?.imageUrl}
528
- chainId={selectedDestinationChain.id}
529
- size={32}
530
- />
531
- <div>
532
- <div className="flex items-center space-x-2">
533
- <div
534
- className={`text-lg font-semibold trails-text-primary ${isLoadingQuote ? "animate-pulse" : ""}`}
535
- >
536
- {toAmountDisplay} {selectedDestToken?.symbol}
537
- </div>
538
- {isLoadingQuote && (
539
- <div className="animate-spin trails-border-radius-button h-4 w-4 border-solid border-b-2 trails-primary" />
540
- )}
541
- </div>
542
- <div
543
- className={`text-xs trails-text-muted ${isLoadingQuote ? "animate-pulse" : ""}`}
544
- >
545
- ≈ {amountUsdDisplay}{" "}
546
- {selectedDestinationChain
547
- ? `on ${selectedDestinationChain.name}`
548
- : ""}
549
- </div>
550
- </div>
551
- </div>
552
- </div>
553
- </div>
554
- )}
555
-
556
661
  {/* Fee Options */}
557
662
  <FeeOptions
558
663
  options={FEE_TOKENS}
@@ -561,6 +666,18 @@ export const Swap: React.FC<SwapProps> = ({
561
666
  />
562
667
 
563
668
  {/* Warning Messages - Show only one at a time */}
669
+ {isSameTokenWithoutCustomCalldata ? (
670
+ <ErrorDisplay
671
+ errorPrettified="Cannot swap to the same token on the same chain without custom calldata. Please select a different destination token."
672
+ severity="error"
673
+ />
674
+ ) : (
675
+ <ErrorDisplay
676
+ errorPrettified={quoteErrorPrettified}
677
+ error={quoteError}
678
+ severity="warning"
679
+ />
680
+ )}
564
681
  {prepareSendQuote?.noSufficientBalance ? (
565
682
  <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
566
683
  <div className="flex items-center space-x-2">
@@ -579,7 +696,8 @@ export const Swap: React.FC<SwapProps> = ({
579
696
  />
580
697
  </svg>
581
698
  <p className="text-sm text-amber-600 dark:text-amber-400">
582
- Insufficient balance to complete this transaction
699
+ Insufficient balance to complete this transaction. Required{" "}
700
+ {prepareSendQuote?.originAmountDisplay} {originToken?.symbol}
583
701
  </p>
584
702
  </div>
585
703
  </div>
@@ -618,7 +736,8 @@ export const Swap: React.FC<SwapProps> = ({
618
736
  !isValidCustomToken ||
619
737
  isLoadingQuote ||
620
738
  !prepareSendQuote ||
621
- prepareSendQuote?.noSufficientBalance
739
+ prepareSendQuote?.noSufficientBalance ||
740
+ isSameTokenWithoutCustomCalldata
622
741
  }
623
742
  className="w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative"
624
743
  >
@@ -627,6 +746,8 @@ export const Swap: React.FC<SwapProps> = ({
627
746
  <Loader2 className="w-5 h-5 animate-spin mr-2 trails-text-muted" />
628
747
  <span>{buttonText}</span>
629
748
  </div>
749
+ ) : isSameTokenWithoutCustomCalldata ? (
750
+ "Select Different Tokens"
630
751
  ) : prepareSendQuote?.noSufficientBalance ? (
631
752
  "Insufficient Balance"
632
753
  ) : (
@@ -0,0 +1,68 @@
1
+ import type React from "react"
2
+ import { TokenImage } from "./TokenImage.js"
3
+ import type { PrepareSendQuote } from "../../prepareSend.js"
4
+ import { formatElapsed } from "../../utils.js"
5
+
6
+ interface SwapDisplayProps {
7
+ quote: PrepareSendQuote
8
+ elapsedSeconds: number
9
+ }
10
+
11
+ export const SwapDisplay: React.FC<SwapDisplayProps> = ({
12
+ quote,
13
+ elapsedSeconds,
14
+ }) => {
15
+ return (
16
+ <div className="w-full max-w-sm p-3 flex justify-center">
17
+ <div className="flex items-center space-x-4">
18
+ {/* Origin Token */}
19
+ <div className="flex flex-col items-start space-y-1">
20
+ <div style={{ width: "28px", height: "28px" }}>
21
+ <TokenImage
22
+ imageUrl={quote.originToken.imageUrl}
23
+ symbol={quote.originToken.symbol}
24
+ chainId={quote.originChain.id}
25
+ size={28}
26
+ />
27
+ </div>
28
+ <div className="text-left">
29
+ <div className="text-base font-bold text-gray-900 dark:text-white">
30
+ {quote.originToken.symbol}
31
+ </div>
32
+ <div className="text-xs text-gray-600 dark:text-gray-400">
33
+ {quote.originAmountFormatted}
34
+ </div>
35
+ </div>
36
+ </div>
37
+
38
+ {/* Arrow with elapsed time below */}
39
+ <div className="flex flex-col items-center space-y-1">
40
+ <div className="text-xl text-gray-400 dark:text-gray-500">→</div>
41
+ <div className="text-xs text-gray-500 dark:text-gray-400">
42
+ {formatElapsed(elapsedSeconds)}
43
+ </div>
44
+ </div>
45
+
46
+ {/* Destination Token */}
47
+ <div className="flex flex-col items-start space-y-1">
48
+ <div style={{ width: "28px", height: "28px" }}>
49
+ <TokenImage
50
+ imageUrl={quote.destinationToken.imageUrl}
51
+ symbol={quote.destinationToken.symbol}
52
+ chainId={quote.destinationChain.id}
53
+ size={28}
54
+ />
55
+ </div>
56
+ <div className="text-left">
57
+ <div className="text-base font-bold text-gray-900 dark:text-white">
58
+ {quote.destinationToken.symbol}
59
+ </div>
60
+ <div className="text-xs text-gray-600 dark:text-gray-400">
61
+ {quote.destinationAmountFormatted}
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
@@ -211,7 +211,7 @@ export const TokenSelector: React.FC<TokenSelectorProps> = ({
211
211
  {/* Token List */}
212
212
  {chainFilteredTokens.length > 0 && (
213
213
  <div
214
- className={`${compactMode ? "max-h-48" : "max-h-[24vh]"} overflow-y-auto trails-scrollbar space-y-1`}
214
+ className={`${compactMode ? "max-h-48" : filteredRecentTokens.length > 0 ? "max-h-[17vh]" : "max-h-[24vh]"} overflow-y-auto trails-scrollbar space-y-1`}
215
215
  >
216
216
  <AnimatePresence mode="popLayout">
217
217
  {chainFilteredTokens.map((token: TokenFormatted) => {