0xtrails 0.9.2 → 0.9.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 (79) hide show
  1. package/dist/{ccip-g6lDdnrD.js → ccip-lAtzqne5.js} +1 -1
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/{index-D-QngA_s.js → index-D5AG6huo.js} +22290 -21786
  8. package/dist/index.js +3 -3
  9. package/dist/intents.d.ts +1 -1
  10. package/dist/intents.d.ts.map +1 -1
  11. package/dist/mutations.d.ts +5 -2
  12. package/dist/mutations.d.ts.map +1 -1
  13. package/dist/tokens.d.ts.map +1 -1
  14. package/dist/transactionIntent/constants.d.ts +1 -0
  15. package/dist/transactionIntent/constants.d.ts.map +1 -1
  16. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +3 -1
  17. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  18. package/dist/transactionIntent/deposits/standardDeposit.d.ts +4 -1
  19. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  20. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  21. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  22. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  23. package/dist/transactionIntent/types.d.ts +2 -0
  24. package/dist/transactionIntent/types.d.ts.map +1 -1
  25. package/dist/transactionIntent/utils/resilientDepositTracker.d.ts +25 -0
  26. package/dist/transactionIntent/utils/resilientDepositTracker.d.ts.map +1 -0
  27. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  28. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  29. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  30. package/dist/widget/components/DynamicInputStyles.d.ts +2 -2
  31. package/dist/widget/components/Earn.d.ts.map +1 -1
  32. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  33. package/dist/widget/components/Fund.d.ts.map +1 -1
  34. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  35. package/dist/widget/components/Receipt.d.ts.map +1 -1
  36. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  37. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  38. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  39. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  40. package/dist/widget/css/compiled.css +1 -1
  41. package/dist/widget/hooks/useQuote.d.ts +2 -0
  42. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  43. package/dist/widget/index.js +1 -1
  44. package/dist/widget/providers/TrailsProvider.d.ts +2 -0
  45. package/dist/widget/providers/TrailsProvider.d.ts.map +1 -1
  46. package/dist/widget/widget.d.ts +1 -0
  47. package/dist/widget/widget.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/config.ts +1 -0
  50. package/src/constants.ts +1 -0
  51. package/src/error.ts +6 -1
  52. package/src/intents.ts +22 -1
  53. package/src/prices.ts +1 -1
  54. package/src/tokens.ts +4 -3
  55. package/src/transactionIntent/constants.ts +2 -0
  56. package/src/transactionIntent/deposits/depositOrchestrator.ts +7 -0
  57. package/src/transactionIntent/deposits/standardDeposit.ts +194 -37
  58. package/src/transactionIntent/handlers/crossChain.ts +152 -105
  59. package/src/transactionIntent/handlers/sameChainSameToken.ts +1 -0
  60. package/src/transactionIntent/quote/normalizeQuote.ts +7 -4
  61. package/src/transactionIntent/types.ts +2 -0
  62. package/src/transactionIntent/utils/resilientDepositTracker.ts +281 -0
  63. package/src/widget/compiled.css +1 -1
  64. package/src/widget/components/AccountIntentTransactionHistory.tsx +170 -87
  65. package/src/widget/components/ClassicSwap.tsx +7 -1
  66. package/src/widget/components/ConfigDisplay.tsx +5 -0
  67. package/src/widget/components/Earn.tsx +14 -1
  68. package/src/widget/components/EarnPools.tsx +180 -59
  69. package/src/widget/components/Fund.tsx +3 -1
  70. package/src/widget/components/PoolWithdraw.tsx +1 -1
  71. package/src/widget/components/QuoteDetails.tsx +12 -35
  72. package/src/widget/components/Receipt.tsx +66 -40
  73. package/src/widget/components/SlippageToleranceSettings.tsx +86 -44
  74. package/src/widget/components/TransactionDetails.tsx +138 -218
  75. package/src/widget/components/UserPreferences.tsx +114 -41
  76. package/src/widget/components/WalletConnect.tsx +111 -48
  77. package/src/widget/hooks/useQuote.ts +389 -352
  78. package/src/widget/providers/TrailsProvider.tsx +5 -0
  79. package/src/widget/widget.tsx +2 -0
@@ -1,6 +1,7 @@
1
1
  import { TrendingUp } from "lucide-react"
2
2
  import { motion } from "motion/react"
3
3
  import { useState, useEffect, useMemo } from "react"
4
+ import { useAccount } from "wagmi"
4
5
  import { usePools, type Pool } from "../../pools.js"
5
6
  import { TokenImage } from "./TokenImage.js"
6
7
  import { getChainInfo } from "../../chains.js"
@@ -11,6 +12,10 @@ import { ScreenHeader } from "./ScreenHeader.js"
11
12
  import { EarnPoolsFilters, type SortOption } from "./EarnPoolsFilters.js"
12
13
  import { ChainList } from "./ChainList.js"
13
14
  import { useChainFilter } from "../hooks/useChainFilter.js"
15
+ import {
16
+ useTokenBalances,
17
+ formatUsdAmountDisplay,
18
+ } from "../../tokenBalances.js"
14
19
  import aaveLogo from "../assets/aave.svg"
15
20
  import morphoLogo from "../assets/morpho.svg"
16
21
  import { logger } from "../../logger.js"
@@ -25,6 +30,8 @@ export const EarnPools: React.FC<EarnPoolsProps> = ({
25
30
  onPoolSelect,
26
31
  }) => {
27
32
  const { data: pools, loading, error } = usePools()
33
+ const { address } = useAccount()
34
+ const { sortedTokens: userTokens } = useTokenBalances(address || null)
28
35
  const [selectedProtocol, setSelectedProtocol] = useState<string>("all")
29
36
  const [searchFilter, setSearchFilter] = useState<string>("")
30
37
  const [filterByChainId, setFilterByChainId] = useState<number | null>(null)
@@ -87,6 +94,18 @@ export const EarnPools: React.FC<EarnPoolsProps> = ({
87
94
  }
88
95
  }, [pools])
89
96
 
97
+ // Create a map to quickly lookup user balances by token address and chain
98
+ const userBalanceMap = useMemo(() => {
99
+ const map = new Map<string, (typeof userTokens)[0]>()
100
+ if (userTokens && Array.isArray(userTokens)) {
101
+ userTokens.forEach((token) => {
102
+ const key = `${token.contractAddress.toLowerCase()}-${token.chainId}`
103
+ map.set(key, token)
104
+ })
105
+ }
106
+ return map
107
+ }, [userTokens])
108
+
90
109
  const filteredPools = useMemo(() => {
91
110
  if (!pools || !Array.isArray(pools)) {
92
111
  return []
@@ -130,13 +149,58 @@ export const EarnPools: React.FC<EarnPoolsProps> = ({
130
149
  }
131
150
  })
132
151
 
133
- // Sort the filtered pools
134
- return filtered.sort((a, b) => {
152
+ // Add user balance data to pools and sort
153
+ const poolsWithBalances = filtered.map((pool) => {
154
+ const key = `${pool.depositAddress.toLowerCase()}-${pool.chainId}`
155
+ const userBalance = userBalanceMap.get(key)
156
+
157
+ // Calculate USD value if we have balance and price
158
+ let calculatedUsdValue = userBalance?.balanceUsd || 0
159
+ if (userBalance?.balanceFormatted && userBalance?.priceUsd) {
160
+ calculatedUsdValue =
161
+ parseFloat(userBalance.balanceFormatted) * userBalance.priceUsd
162
+ }
163
+
164
+ return {
165
+ ...pool,
166
+ userBalance: userBalance?.balance,
167
+ userBalanceUsd: calculatedUsdValue,
168
+ userBalanceFormatted: userBalance?.balanceFormatted,
169
+ userBalanceDisplay: userBalance?.balanceDisplay,
170
+ userTokenSymbol: userBalance?.symbol,
171
+ }
172
+ })
173
+
174
+ // Sort the filtered pools - always prioritize by user balance
175
+ return poolsWithBalances.sort((a, b) => {
135
176
  try {
177
+ const aHasBalance =
178
+ a.userBalanceFormatted && parseFloat(a.userBalanceFormatted) > 0
179
+ const bHasBalance =
180
+ b.userBalanceFormatted && parseFloat(b.userBalanceFormatted) > 0
181
+
182
+ // First prioritize pools with user balance
183
+ if (aHasBalance && !bHasBalance) return -1
184
+ if (!aHasBalance && bHasBalance) return 1
185
+
186
+ // If both have balance, sort by USD value first, then by token balance
187
+ if (aHasBalance && bHasBalance) {
188
+ // Sort by USD value if both have it
189
+ if (a.userBalanceUsd > 0 && b.userBalanceUsd > 0) {
190
+ return b.userBalanceUsd - a.userBalanceUsd
191
+ }
192
+
193
+ // Otherwise sort by token balance
194
+ const aBalance = parseFloat(a.userBalanceFormatted || "0")
195
+ const bBalance = parseFloat(b.userBalanceFormatted || "0")
196
+ return bBalance - aBalance
197
+ }
198
+
199
+ // For pools without user balance, use the selected sort
136
200
  if (sortBy === "tvl") {
137
- return (b.tvl || 0) - (a.tvl || 0) // Descending order (highest first)
201
+ return (b.tvl || 0) - (a.tvl || 0)
138
202
  } else if (sortBy === "apy") {
139
- return (b.apy || 0) - (a.apy || 0) // Descending order (highest first)
203
+ return (b.apy || 0) - (a.apy || 0)
140
204
  }
141
205
  return 0
142
206
  } catch (error) {
@@ -144,7 +208,94 @@ export const EarnPools: React.FC<EarnPoolsProps> = ({
144
208
  return 0
145
209
  }
146
210
  })
147
- }, [pools, selectedProtocol, filterByChainId, searchFilter, sortBy])
211
+ }, [
212
+ pools,
213
+ selectedProtocol,
214
+ filterByChainId,
215
+ searchFilter,
216
+ sortBy,
217
+ userBalanceMap,
218
+ ])
219
+
220
+ // Separate pools into user pools and other pools
221
+ const userPools = useMemo(() => {
222
+ return filteredPools.filter(
223
+ (pool) =>
224
+ pool.userBalanceFormatted && parseFloat(pool.userBalanceFormatted) > 0,
225
+ )
226
+ }, [filteredPools])
227
+
228
+ const otherPools = useMemo(() => {
229
+ return filteredPools.filter(
230
+ (pool) =>
231
+ !pool.userBalanceFormatted ||
232
+ parseFloat(pool.userBalanceFormatted) === 0,
233
+ )
234
+ }, [filteredPools])
235
+
236
+ // Function to render a pool item
237
+ const renderPoolItem = (pool: any) => (
238
+ <motion.div
239
+ key={pool.id}
240
+ whileHover={{ scale: 1 }}
241
+ whileTap={{ scale: 0.99 }}
242
+ onClick={() => onPoolSelect(pool)}
243
+ className="p-4 trails-border-radius-container trails-list-item cursor-pointer transition-all overflow-hidden"
244
+ >
245
+ <div className="flex items-center justify-between">
246
+ <div className="flex items-center space-x-3">
247
+ <div style={{ width: "32px", height: "32px" }}>
248
+ <TokenImage
249
+ symbol={pool.token.symbol}
250
+ imageUrl={pool.token.logoUrl}
251
+ chainId={pool.chainId}
252
+ size={32}
253
+ />
254
+ </div>
255
+ <div>
256
+ <h3 className="font-medium text-gray-900 dark:text-white text-sm">
257
+ {pool.name}
258
+ </h3>
259
+ <div className="flex items-center space-x-1">
260
+ <span className="text-xs text-gray-500 dark:text-gray-400 flex items-center">
261
+ {pool.protocol === "Aave" && (
262
+ <img src={aaveLogo} alt="Aave" className="w-3 h-3 mr-1" />
263
+ )}
264
+ {pool.protocol === "Morpho" && (
265
+ <img src={morphoLogo} alt="Morpho" className="w-3 h-3 mr-1" />
266
+ )}
267
+ {pool.protocol}
268
+ </span>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ <div className="text-right">
273
+ <div className="flex items-center justify-end space-x-1 text-green-600 dark:text-green-400 mb-1 whitespace-nowrap">
274
+ <TrendingUp className="w-3 h-3" />
275
+ <span className="font-semibold text-sm">
276
+ {pool.apy.toFixed(1)}% APY
277
+ </span>
278
+ </div>
279
+ <p className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
280
+ TVL: {formatTvl(pool.tvl)}
281
+ </p>
282
+ {/* User balance display below TVL */}
283
+ {pool.userBalanceFormatted &&
284
+ parseFloat(pool.userBalanceFormatted) > 0 && (
285
+ <p
286
+ className="text-xs font-bold mt-1 whitespace-nowrap"
287
+ style={{ color: "#71717A" }}
288
+ >
289
+ Balance:{" "}
290
+ {pool.userBalanceUsd > 0
291
+ ? formatUsdAmountDisplay(pool.userBalanceUsd)
292
+ : pool.userBalanceDisplay || pool.userBalanceFormatted}
293
+ </p>
294
+ )}
295
+ </div>
296
+ </div>
297
+ </motion.div>
298
+ )
148
299
 
149
300
  // Show chain list screen
150
301
  if (showChainList) {
@@ -223,63 +374,33 @@ export const EarnPools: React.FC<EarnPoolsProps> = ({
223
374
  </p>
224
375
  </div>
225
376
  ) : (
226
- filteredPools.map((pool) => (
227
- <motion.div
228
- key={pool.id}
229
- whileHover={{ scale: 1 }}
230
- whileTap={{ scale: 0.99 }}
231
- onClick={() => onPoolSelect(pool)}
232
- className="p-4 trails-border-radius-container trails-list-item cursor-pointer transition-all overflow-hidden"
233
- >
234
- <div className="flex items-center justify-between">
235
- <div className="flex items-center space-x-3">
236
- <div style={{ width: "32px", height: "32px" }}>
237
- <TokenImage
238
- symbol={pool.token.symbol}
239
- imageUrl={pool.token.logoUrl}
240
- chainId={pool.chainId}
241
- size={32}
242
- />
243
- </div>
244
- <div>
245
- <h3 className="font-medium text-gray-900 dark:text-white text-sm">
246
- {pool.name}
247
- </h3>
248
- <div className="flex items-center space-x-1">
249
- <span className="text-xs text-gray-500 dark:text-gray-400 flex items-center">
250
- {pool.protocol === "Aave" && (
251
- <img
252
- src={aaveLogo}
253
- alt="Aave"
254
- className="w-3 h-3 mr-1"
255
- />
256
- )}
257
- {pool.protocol === "Morpho" && (
258
- <img
259
- src={morphoLogo}
260
- alt="Morpho"
261
- className="w-3 h-3 mr-1"
262
- />
263
- )}
264
- {pool.protocol}
265
- </span>
266
- </div>
267
- </div>
377
+ <>
378
+ {/* My Vaults Section */}
379
+ {userPools.length > 0 && (
380
+ <>
381
+ <div className="px-2 py-1">
382
+ <p className="text-xs font-semibold text-gray-600 dark:text-gray-400 tracking-wider">
383
+ My Vaults
384
+ </p>
268
385
  </div>
269
- <div className="text-right">
270
- <div className="flex items-center justify-end space-x-1 text-green-600 dark:text-green-400 mb-1 whitespace-nowrap">
271
- <TrendingUp className="w-3 h-3" />
272
- <span className="font-semibold text-sm">
273
- {pool.apy.toFixed(1)}% APY
274
- </span>
275
- </div>
276
- <p className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap">
277
- TVL: {formatTvl(pool.tvl)}
386
+ {userPools.map(renderPoolItem)}
387
+ </>
388
+ )}
389
+
390
+ {/* Other Vaults Section */}
391
+ {otherPools.length > 0 && (
392
+ <>
393
+ <div
394
+ className={`px-2 py-1 ${userPools.length > 0 ? "mt-4" : ""}`}
395
+ >
396
+ <p className="text-xs font-semibold text-gray-600 dark:text-gray-400 tracking-wider">
397
+ {userPools.length > 0 ? "Other Vaults" : "Available Vaults"}
278
398
  </p>
279
399
  </div>
280
- </div>
281
- </motion.div>
282
- ))
400
+ {otherPools.map(renderPoolItem)}
401
+ </>
402
+ )}
403
+ </>
283
404
  )}
284
405
  </div>
285
406
  </div>
@@ -1,6 +1,7 @@
1
1
  import { Loader2, ArrowLeftRight } from "lucide-react"
2
2
  import type React from "react"
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react"
4
+ import { useBack } from "../hooks/useBack.js"
4
5
 
5
6
  // Helper function to get centered window parameters
6
7
  const getCenteredWindowParams = (width: number, height: number): string => {
@@ -146,6 +147,7 @@ const Fund: React.FC<FundProps> = ({
146
147
  }) => {
147
148
  const { mode } = useMode()
148
149
  const { setCurrentScreen } = useCurrentScreen()
150
+ const { setCurrentScreenWithBack } = useBack()
149
151
  const {
150
152
  selectedToken: originToken,
151
153
  setSelectedToken: setOriginToken,
@@ -1161,7 +1163,7 @@ const Fund: React.FC<FundProps> = ({
1161
1163
  customActions={
1162
1164
  <button
1163
1165
  type="button"
1164
- onClick={() => setCurrentScreen("swap")}
1166
+ onClick={() => setCurrentScreenWithBack("swap", "fund-form")}
1165
1167
  className="flex h-8 px-3 justify-center items-center gap-1.5 rounded-full bg-gray-50 dark:bg-gray-700 cursor-pointer transition-colors text-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600"
1166
1168
  title="Swap"
1167
1169
  >
@@ -705,7 +705,7 @@ export const PoolWithdraw: React.FC<PoolWithdrawProps> = ({
705
705
  }}
706
706
  title="Click to withdraw full balance"
707
707
  >
708
- Pool Balance:{" "}
708
+ Balance:{" "}
709
709
  {isBalanceVisible
710
710
  ? isLoadingBalance
711
711
  ? "Loading..."
@@ -9,14 +9,13 @@ import { truncateAddress } from "../../utils.js"
9
9
  import { PriceImpactWarning } from "./PriceImpactWarning.js"
10
10
  import { usePriceImpactWarning } from "../hooks/usePriceImpactWarning.js"
11
11
  import {
12
- Copy,
13
- Check,
14
12
  Info,
15
13
  ExternalLink,
16
14
  Clock,
17
15
  ChevronDown,
18
16
  TriangleAlert,
19
17
  } from "lucide-react"
18
+ import { useTrails } from "../providers/TrailsProvider.js"
20
19
  import type { MeldQuote } from "../../meld/utils/meld.js"
21
20
  import { logger } from "../../logger.js"
22
21
 
@@ -50,8 +49,8 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
50
49
  const [showCalldata, setShowCalldata] = useState(false)
51
50
  const [showOriginRate, setShowOriginRate] = useState(true)
52
51
  const [isExpanded, setIsExpanded] = useState(initialExpanded)
53
- const [intentIdCopied, setIntentIdCopied] = useState(false)
54
52
  const containerRef = useRef<HTMLDivElement>(null)
53
+ const { trailsAppUrl } = useTrails()
55
54
  const calldataRef = useRef<HTMLDivElement>(null)
56
55
 
57
56
  const priceImpactConfig = usePriceImpactWarning()
@@ -86,22 +85,6 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
86
85
  onExpand?.(isExpanded)
87
86
  }, [isExpanded, onExpand])
88
87
 
89
- const handleCopyIntentId = useCallback(async () => {
90
- if (!quote?.intentId) return
91
- try {
92
- await navigator.clipboard.writeText(quote.intentId)
93
- setIntentIdCopied(true)
94
- } catch (error) {
95
- console.error("[trails-sdk] Failed to copy intentId", error)
96
- }
97
- }, [quote?.intentId])
98
-
99
- useEffect(() => {
100
- if (!intentIdCopied) return
101
- const timeout = setTimeout(() => setIntentIdCopied(false), 1500)
102
- return () => clearTimeout(timeout)
103
- }, [intentIdCopied])
104
-
105
88
  const getFiatToCryptoRate = useCallback(() => {
106
89
  if (!onRampQuote || !quote?.originTokenRate) {
107
90
  return null
@@ -525,24 +508,18 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
525
508
 
526
509
  {/* Intent ID */}
527
510
  {hasIntentId && (
528
- <Row className="gap-2">
511
+ <Row>
529
512
  <RowLabel>Intent ID</RowLabel>
530
- <RowValue className="flex items-center gap-1">
531
- <span title={quote.intentId ?? undefined}>
532
- {truncateAddress(quote?.intentId ?? "", 9, 3)}
533
- </span>
534
- <button
535
- type="button"
536
- onClick={handleCopyIntentId}
537
- className="p-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
538
- aria-label="Copy intent ID"
513
+ <RowValue>
514
+ <a
515
+ href={`${trailsAppUrl}/intent/${quote.intentId}`}
516
+ target="_blank"
517
+ rel="noopener noreferrer"
518
+ className="font-mono text-xs hover:underline flex items-center gap-1 text-blue-500"
539
519
  >
540
- {intentIdCopied ? (
541
- <Check className="size-3 text-emerald-500" />
542
- ) : (
543
- <Copy className="size-3 text-gray-500 dark:text-gray-400" />
544
- )}
545
- </button>
520
+ {truncateAddress(quote?.intentId ?? "", 9, 3)}
521
+ <ExternalLink className="size-3" />
522
+ </a>
546
523
  </RowValue>
547
524
  </Row>
548
525
  )}
@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query"
2
2
  import type React from "react"
3
3
  import type { ReactNode } from "react"
4
4
  import { useEffect, useState, useCallback, useMemo } from "react"
5
- import { ChevronRight } from "lucide-react"
6
5
  import type { TransactionState } from "../../transactions.js"
7
6
  import { GreenCheckAnimation } from "./GreenCheckAnimation.js"
8
7
  import { YellowWarningAnimation } from "./YellowWarningAnimation.js"
@@ -13,11 +12,11 @@ import { truncateAddress } from "../../utils.js"
13
12
  import { formatElapsed } from "../../utils.js"
14
13
  import { ChainImage } from "./ChainImage.js"
15
14
  import { getChainInfo } from "../../chains.js"
16
- import { useMode } from "../hooks/useMode.js"
17
15
  import { ScreenHeader } from "./ScreenHeader.js"
18
16
  import { getExplorerUrl } from "../../explorer.js"
19
17
  import { logger } from "../../logger.js"
20
18
  import type { OnrampQuote } from "../hooks/useOnRampQuote.js"
19
+ import { useTrails } from "../providers/TrailsProvider.js"
21
20
 
22
21
  interface ReceiptProps {
23
22
  onSendAnother?: () => void
@@ -110,7 +109,7 @@ export const Receipt: React.FC<ReceiptProps> = ({
110
109
  actionButtonText,
111
110
  onRampQuote,
112
111
  }) => {
113
- const { mode } = useMode()
112
+ const { trailsAppUrl } = useTrails()
114
113
  const [showContent, setShowContent] = useState(false)
115
114
  const [showRefundInfo, setShowRefundInfo] = useState(false)
116
115
  const [refundMessage, setRefundMessage] = useState<string | ReactNode | null>(
@@ -142,20 +141,12 @@ export const Receipt: React.FC<ReceiptProps> = ({
142
141
  }, [transactionStates])
143
142
 
144
143
  const buttonText = useMemo(() => {
145
- // Use custom actionButtonText if provided, otherwise use mode-based text
144
+ // Use custom actionButtonText if provided, otherwise use "Start new transaction" for all modes
146
145
  if (actionButtonText) {
147
146
  return actionButtonText
148
147
  }
149
- if (mode === "pay") {
150
- return "Pay Again"
151
- } else if (mode === "fund") {
152
- return "Fund Again"
153
- } else if (mode === "swap") {
154
- return "Swap Again"
155
- } else {
156
- return "Send Again"
157
- }
158
- }, [mode, actionButtonText])
148
+ return "Start new transaction"
149
+ }, [actionButtonText])
159
150
 
160
151
  const {
161
152
  finalExplorerUrl,
@@ -163,6 +154,11 @@ export const Receipt: React.FC<ReceiptProps> = ({
163
154
  completionTimeSeconds: calculatedCompletionTime,
164
155
  } = useReceipt(transactionStates)
165
156
 
157
+ const intentReceiptUrl = useMemo(() => {
158
+ if (!trailsAppUrl || !quote?.intentId) return null
159
+ return `${trailsAppUrl}/intent/${quote.intentId}`
160
+ }, [quote?.intentId, trailsAppUrl])
161
+
166
162
  // Use provided totalCompletionSeconds if available, otherwise use calculated time
167
163
  const completionTimeSeconds =
168
164
  totalCompletionSeconds ?? calculatedCompletionTime
@@ -517,7 +513,7 @@ export const Receipt: React.FC<ReceiptProps> = ({
517
513
  </div>
518
514
  </div>
519
515
 
520
- {finalExplorerUrl && (
516
+ {(finalExplorerUrl || intentReceiptUrl) && (
521
517
  <div
522
518
  className={`text-center transition-all duration-500 ease-out delay-100 ${
523
519
  showContent
@@ -525,29 +521,60 @@ export const Receipt: React.FC<ReceiptProps> = ({
525
521
  : "opacity-0 translate-y-4"
526
522
  }`}
527
523
  >
528
- <a
529
- href={finalExplorerUrl}
530
- target="_blank"
531
- rel="noopener noreferrer"
532
- className="inline-flex items-center gap-1 px-3 py-1.5 rounded-md font-medium transition-colors border border-solid text-sm bg-white border-gray-200 text-black hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-900 dark:border-gray-700 dark:text-blue-300 dark:hover:bg-gray-800 dark:hover:text-blue-200"
533
- >
534
- {finalChainId && <ChainImage chainId={finalChainId} size={16} />}
535
- View on Explorer
536
- <svg
537
- className="w-4 h-4 ml-1 text-black dark:text-blue-300"
538
- fill="none"
539
- viewBox="0 0 24 24"
540
- stroke="currentColor"
541
- >
542
- <title>External Link</title>
543
- <path
544
- strokeLinecap="round"
545
- strokeLinejoin="round"
546
- strokeWidth={2}
547
- d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
548
- />
549
- </svg>
550
- </a>
524
+ <div className="flex flex-col items-center gap-2">
525
+ {finalExplorerUrl && (
526
+ <a
527
+ href={finalExplorerUrl}
528
+ target="_blank"
529
+ rel="noopener noreferrer"
530
+ className="inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
531
+ >
532
+ {finalChainId && (
533
+ <ChainImage chainId={finalChainId} size={16} />
534
+ )}
535
+ View on Explorer
536
+ <svg
537
+ className="w-4 h-4 ml-1 text-blue-600 dark:text-blue-400"
538
+ fill="none"
539
+ viewBox="0 0 24 24"
540
+ stroke="currentColor"
541
+ >
542
+ <title>External Link</title>
543
+ <path
544
+ strokeLinecap="round"
545
+ strokeLinejoin="round"
546
+ strokeWidth={2}
547
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
548
+ />
549
+ </svg>
550
+ </a>
551
+ )}
552
+
553
+ {intentReceiptUrl && (
554
+ <a
555
+ href={intentReceiptUrl}
556
+ target="_blank"
557
+ rel="noopener noreferrer"
558
+ className="inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
559
+ >
560
+ View Transaction Receipt
561
+ <svg
562
+ className="w-4 h-4 ml-1 text-blue-600 dark:text-blue-400"
563
+ fill="none"
564
+ viewBox="0 0 24 24"
565
+ stroke="currentColor"
566
+ >
567
+ <title>External Link</title>
568
+ <path
569
+ strokeLinecap="round"
570
+ strokeLinejoin="round"
571
+ strokeWidth={2}
572
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
573
+ />
574
+ </svg>
575
+ </a>
576
+ )}
577
+ </div>
551
578
  </div>
552
579
  )}
553
580
 
@@ -603,10 +630,9 @@ export const Receipt: React.FC<ReceiptProps> = ({
603
630
  <button
604
631
  type="button"
605
632
  onClick={onSendAnother}
606
- className="inline-flex items-center gap-1 px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 trails-border-radius-button transition-colors duration-200 text-gray-600 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 cursor-pointer font-medium"
633
+ className="inline-flex items-center gap-1 px-4 py-2 text-sm bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 trails-border-radius-button transition-colors duration-200 text-white cursor-pointer font-medium"
607
634
  >
608
635
  {buttonText}
609
- <ChevronRight className="w-4 h-4" />
610
636
  </button>
611
637
  </div>
612
638
  )}