@zerodev/smart-routing-address-react-ui 0.0.1

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 (100) hide show
  1. package/README.md +117 -0
  2. package/dist/_types/components/AddressDisplay/index.d.ts +43 -0
  3. package/dist/_types/components/AddressDisplay/index.d.ts.map +1 -0
  4. package/dist/_types/components/ErrorRetryCard/index.d.ts +21 -0
  5. package/dist/_types/components/ErrorRetryCard/index.d.ts.map +1 -0
  6. package/dist/_types/components/FeeBreakdown/feeInfo.d.ts +24 -0
  7. package/dist/_types/components/FeeBreakdown/feeInfo.d.ts.map +1 -0
  8. package/dist/_types/components/FeeBreakdown/index.d.ts +28 -0
  9. package/dist/_types/components/FeeBreakdown/index.d.ts.map +1 -0
  10. package/dist/_types/components/LoadingCard/index.d.ts +6 -0
  11. package/dist/_types/components/LoadingCard/index.d.ts.map +1 -0
  12. package/dist/_types/components/PendingDeposits/index.d.ts +14 -0
  13. package/dist/_types/components/PendingDeposits/index.d.ts.map +1 -0
  14. package/dist/_types/components/QrSheet/QrCode.d.ts +28 -0
  15. package/dist/_types/components/QrSheet/QrCode.d.ts.map +1 -0
  16. package/dist/_types/components/QrSheet/index.d.ts +16 -0
  17. package/dist/_types/components/QrSheet/index.d.ts.map +1 -0
  18. package/dist/_types/components/TxnItem/ExplorerLink.d.ts +16 -0
  19. package/dist/_types/components/TxnItem/ExplorerLink.d.ts.map +1 -0
  20. package/dist/_types/components/TxnItem/index.d.ts +19 -0
  21. package/dist/_types/components/TxnItem/index.d.ts.map +1 -0
  22. package/dist/_types/constants.d.ts +20 -0
  23. package/dist/_types/constants.d.ts.map +1 -0
  24. package/dist/_types/context/SmartRoutingAddressContext.d.ts +32 -0
  25. package/dist/_types/context/SmartRoutingAddressContext.d.ts.map +1 -0
  26. package/dist/_types/context/SmartRoutingAddressProvider.d.ts +13 -0
  27. package/dist/_types/context/SmartRoutingAddressProvider.d.ts.map +1 -0
  28. package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts +23 -0
  29. package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts.map +1 -0
  30. package/dist/_types/hooks/useDepositStatus.d.ts +25 -0
  31. package/dist/_types/hooks/useDepositStatus.d.ts.map +1 -0
  32. package/dist/_types/hooks/useNewDeposits.d.ts +8 -0
  33. package/dist/_types/hooks/useNewDeposits.d.ts.map +1 -0
  34. package/dist/_types/hooks/useProviderFees.d.ts +22 -0
  35. package/dist/_types/hooks/useProviderFees.d.ts.map +1 -0
  36. package/dist/_types/hooks/useSmartRoutingAddress.d.ts +20 -0
  37. package/dist/_types/hooks/useSmartRoutingAddress.d.ts.map +1 -0
  38. package/dist/_types/iconAssets.d.ts +7 -0
  39. package/dist/_types/iconAssets.d.ts.map +1 -0
  40. package/dist/_types/index.d.ts +14 -0
  41. package/dist/_types/index.d.ts.map +1 -0
  42. package/dist/_types/pages/Deposit.d.ts +12 -0
  43. package/dist/_types/pages/Deposit.d.ts.map +1 -0
  44. package/dist/_types/pages/PastDeposits.d.ts +14 -0
  45. package/dist/_types/pages/PastDeposits.d.ts.map +1 -0
  46. package/dist/_types/pages/TransactionDetails.d.ts +11 -0
  47. package/dist/_types/pages/TransactionDetails.d.ts.map +1 -0
  48. package/dist/_types/pages/index.d.ts +25 -0
  49. package/dist/_types/pages/index.d.ts.map +1 -0
  50. package/dist/_types/types.d.ts +75 -0
  51. package/dist/_types/types.d.ts.map +1 -0
  52. package/dist/_types/utils/chains.d.ts +8 -0
  53. package/dist/_types/utils/chains.d.ts.map +1 -0
  54. package/dist/_types/utils/config.d.ts +35 -0
  55. package/dist/_types/utils/config.d.ts.map +1 -0
  56. package/dist/_types/utils/deposits.d.ts +29 -0
  57. package/dist/_types/utils/deposits.d.ts.map +1 -0
  58. package/dist/_types/utils/fees.d.ts +14 -0
  59. package/dist/_types/utils/fees.d.ts.map +1 -0
  60. package/dist/_types/utils/format.d.ts +27 -0
  61. package/dist/_types/utils/format.d.ts.map +1 -0
  62. package/dist/_types/utils/providerFees.d.ts +103 -0
  63. package/dist/_types/utils/providerFees.d.ts.map +1 -0
  64. package/dist/index.cjs +2 -0
  65. package/dist/index.cjs.map +1 -0
  66. package/dist/index.mjs +2180 -0
  67. package/dist/index.mjs.map +1 -0
  68. package/dist/styles.css +2 -0
  69. package/package.json +91 -0
  70. package/src/components/AddressDisplay/index.tsx +108 -0
  71. package/src/components/ErrorRetryCard/index.tsx +68 -0
  72. package/src/components/FeeBreakdown/feeInfo.ts +25 -0
  73. package/src/components/FeeBreakdown/index.tsx +218 -0
  74. package/src/components/LoadingCard/index.tsx +27 -0
  75. package/src/components/PendingDeposits/index.tsx +152 -0
  76. package/src/components/QrSheet/QrCode.tsx +163 -0
  77. package/src/components/QrSheet/index.tsx +80 -0
  78. package/src/components/TxnItem/ExplorerLink.tsx +37 -0
  79. package/src/components/TxnItem/index.tsx +178 -0
  80. package/src/constants.ts +102 -0
  81. package/src/context/SmartRoutingAddressContext.ts +48 -0
  82. package/src/context/SmartRoutingAddressProvider.tsx +145 -0
  83. package/src/hooks/useCreateSmartRoutingAddress.ts +28 -0
  84. package/src/hooks/useDepositStatus.ts +110 -0
  85. package/src/hooks/useNewDeposits.ts +25 -0
  86. package/src/hooks/useProviderFees.ts +235 -0
  87. package/src/hooks/useSmartRoutingAddress.ts +25 -0
  88. package/src/iconAssets.ts +130 -0
  89. package/src/index.ts +27 -0
  90. package/src/pages/Deposit.tsx +605 -0
  91. package/src/pages/PastDeposits.tsx +232 -0
  92. package/src/pages/TransactionDetails.tsx +466 -0
  93. package/src/pages/index.tsx +165 -0
  94. package/src/types.ts +84 -0
  95. package/src/utils/chains.ts +26 -0
  96. package/src/utils/config.ts +142 -0
  97. package/src/utils/deposits.ts +106 -0
  98. package/src/utils/fees.ts +51 -0
  99. package/src/utils/format.ts +108 -0
  100. package/src/utils/providerFees.ts +301 -0
@@ -0,0 +1,152 @@
1
+ import { cn, Text } from '@zerodev/react-ui'
2
+ import type { DepositedToken } from '@zerodev/smart-routing-address'
3
+ import { CHAIN_ICONS, TOKEN_ICONS } from '../../iconAssets'
4
+ import type {
5
+ DepositStage,
6
+ DepositWithTimestamp,
7
+ EstimatedFee,
8
+ SmartRoutingAddressConfig,
9
+ } from '../../types'
10
+ import { getTxUrl } from '../../utils/chains'
11
+ import {
12
+ getSourceTokenSymbol,
13
+ resolveDestChain,
14
+ sourceTokensFromFees,
15
+ } from '../../utils/config'
16
+ import { getDepositStage } from '../../utils/deposits'
17
+ import { findFeeDataByToken, tokenAddressMatches } from '../../utils/fees'
18
+ import {
19
+ formatDisplayAmount,
20
+ formatRelativeTime,
21
+ truncateAddress,
22
+ } from '../../utils/format'
23
+ import { TxnItem, type TxnStatus } from '../TxnItem'
24
+
25
+ export interface PendingDepositsProps {
26
+ deposits: DepositedToken[]
27
+ estimatedFees: EstimatedFee[]
28
+ config: SmartRoutingAddressConfig
29
+ /** Fired when a row is tapped — the widget uses this to open the
30
+ * transaction-details view for the selected deposit. When omitted, rows
31
+ * render as static (non-interactive). */
32
+ onSelectDeposit?: (deposit: DepositedToken) => void
33
+ className?: string
34
+ }
35
+
36
+ const STAGE_TO_STATUS: Record<DepositStage, TxnStatus> = {
37
+ pending: 'Detected',
38
+ bridging: 'Routing',
39
+ completed: 'Received',
40
+ failed: 'Failed',
41
+ }
42
+
43
+ export function PendingDeposits({
44
+ deposits,
45
+ estimatedFees,
46
+ config,
47
+ onSelectDeposit,
48
+ className,
49
+ }: PendingDepositsProps) {
50
+ if (deposits.length === 0) return null
51
+
52
+ const destChain = resolveDestChain(config)
53
+ const destChainLogo = CHAIN_ICONS[destChain.id]
54
+
55
+ return (
56
+ <section
57
+ aria-label="Pending deposits"
58
+ className={cn(
59
+ 'zd:relative zd:flex zd:w-full zd:flex-col zd:gap-2 zd:overflow-hidden zd:rounded-2xl zd:p-4',
60
+ 'zd:border-offWhite zd:border-[0.3px] zd:bg-white/20',
61
+ 'zd:shadow-[inset_0_-4px_4px_0_rgba(255,255,255,0.1),inset_0_3px_4px_0_rgba(0,0,0,0.02)]',
62
+ className,
63
+ )}
64
+ >
65
+ <Text className="zd:text-h3">Pending Deposit</Text>
66
+ <ul className="zd:flex zd:w-full zd:flex-col">
67
+ {deposits.map((raw) => {
68
+ const deposit = raw as DepositWithTimestamp
69
+ const { chainId, token, amount, transactionHash } = deposit.deposit
70
+ const feeData = findFeeDataByToken(estimatedFees, chainId, token)
71
+
72
+ // Source pair: reconstruct the SourceToken so we can look up its
73
+ // symbol + chain icon the same way the trigger pill does. Matching
74
+ // on the on-chain address (via `tokenAddressMatches`) — the
75
+ // server's `feeData.name` is a display symbol (e.g. "ETH"), not the
76
+ // TOKEN_TYPE ("NATIVE"), so a direct `t.tokenType === name` compare
77
+ // misses native tokens.
78
+ const source =
79
+ sourceTokensFromFees(estimatedFees).find(
80
+ (t) =>
81
+ t.chain.id === chainId &&
82
+ tokenAddressMatches(t.tokenType, chainId, token),
83
+ ) ?? null
84
+ // Prefer the reconstructed source's symbol; fall back to the
85
+ // server's `feeData.name` so past deposits whose route dropped
86
+ // out of the current fee estimates still get a symbol / icon.
87
+ const sourceSymbol = source
88
+ ? getSourceTokenSymbol(source)
89
+ : (feeData?.name ?? '')
90
+ const sourceTokenLogo = sourceSymbol
91
+ ? TOKEN_ICONS[sourceSymbol.toUpperCase()]
92
+ : undefined
93
+ const sourceChainLogo = CHAIN_ICONS[chainId]
94
+
95
+ // Destination token equals source token — widget's default
96
+ // actions forward the deposited asset unchanged.
97
+ const destTokenLogo = sourceTokenLogo
98
+
99
+ const status = STAGE_TO_STATUS[getDepositStage(deposit)]
100
+ const amountLabel = feeData
101
+ ? `${formatDisplayAmount(amount, feeData.decimal, 'down')} ${sourceSymbol}`
102
+ : String(amount)
103
+ const timestamp = deposit.createdAt
104
+ ? (formatRelativeTime(deposit.createdAt) ?? '')
105
+ : ''
106
+
107
+ // Explorer URL from the chain id alone — independent of whether
108
+ // the deposit's token was matched in the current fee estimates.
109
+ const href = getTxUrl(chainId, transactionHash)
110
+
111
+ const row = (
112
+ <TxnItem
113
+ amount={amountLabel}
114
+ address={truncateAddress(transactionHash)}
115
+ {...(href && { href })}
116
+ timestamp={timestamp}
117
+ status={status}
118
+ {...(sourceTokenLogo && {
119
+ sourceTokenIconUrl: sourceTokenLogo,
120
+ })}
121
+ {...(sourceChainLogo && {
122
+ sourceChainIconUrl: sourceChainLogo,
123
+ })}
124
+ {...(destTokenLogo && { destTokenIconUrl: destTokenLogo })}
125
+ {...(destChainLogo && { destChainIconUrl: destChainLogo })}
126
+ />
127
+ )
128
+
129
+ return (
130
+ <li key={transactionHash}>
131
+ {onSelectDeposit ? (
132
+ // -mx-1 + px-1 + a wider explicit width extend the hover
133
+ // surface 4px past the row content on each side, so the
134
+ // highlight has visible breathing room without shifting the
135
+ // TxnItem's visual position.
136
+ <button
137
+ type="button"
138
+ onClick={() => onSelectDeposit(deposit)}
139
+ className="zd:w-full zd:cursor-pointer zd:rounded-xl zd:px-1 zd:text-left zd:hover:bg-white/30"
140
+ >
141
+ {row}
142
+ </button>
143
+ ) : (
144
+ row
145
+ )}
146
+ </li>
147
+ )
148
+ })}
149
+ </ul>
150
+ </section>
151
+ )
152
+ }
@@ -0,0 +1,163 @@
1
+ import { encode } from 'uqr'
2
+
3
+ interface QrCodeProps {
4
+ /** Data to encode. */
5
+ value: string
6
+ /** Pixel size of the rendered SVG square. */
7
+ size: number
8
+ /** Error correction level. Defaults to `'M'`. */
9
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'
10
+ /** Corner radius (in modules, scaled to px internally) for finder patterns. */
11
+ eyeRadius?: number
12
+ }
13
+
14
+ const FINDER_SIZE = 7 // every QR has three 7×7 finder patterns
15
+ const MODULE_COLOR = '#000'
16
+ const BG_COLOR = '#fff'
17
+ /** Vertical shrink applied to data-module pills so rows read as distinct
18
+ * without introducing white slivers wide enough to look like transitions
19
+ * to a scanner. Kept small (5%) so total black area stays close to spec. */
20
+ const PILL_PAD_RATIO = 0.05
21
+
22
+ /**
23
+ * Custom QR renderer that draws runs of horizontally-adjacent data modules as
24
+ * a single rounded pill. Finder patterns stay as sharp concentric squares
25
+ * (rounding them breaks the 1:1:3:1:1 corner ratio scanners use to locate the
26
+ * code). Isolated data modules render as pill-ish rects — visually close to
27
+ * circles at very small `PILL_PAD_RATIO`, still square-adjacent for scanners.
28
+ *
29
+ * Default `errorCorrectionLevel` is `'H'` (30% recovery) so the decorative
30
+ * rounding + shrinkage has plenty of headroom before scans fail.
31
+ *
32
+ * Quiet zone (spec: ≥4 modules of white around the code) is expected to be
33
+ * provided by the caller — e.g. via a white-background padded wrapper. This
34
+ * component fills the given `size` with the QR data area itself so the
35
+ * consumer controls the visual footprint.
36
+ */
37
+ export function QrCode({
38
+ value,
39
+ size,
40
+ errorCorrectionLevel = 'H',
41
+ eyeRadius = 0,
42
+ }: QrCodeProps) {
43
+ // `uqr` returns a 2D boolean matrix (`data[row][col]`) and the module count
44
+ // per side. Same information as `qrcode`, just shaped differently — the
45
+ // pill/finder rendering below is unchanged.
46
+ const qr = encode(value, { ecc: errorCorrectionLevel, border: 0 })
47
+ const matrix = qr.data
48
+ const moduleCount = qr.size
49
+ const cellSize = size / moduleCount
50
+ const eyeRadiusPx = eyeRadius * cellSize
51
+
52
+ const finders = [
53
+ { row: 0, col: 0 },
54
+ { row: 0, col: moduleCount - FINDER_SIZE },
55
+ { row: moduleCount - FINDER_SIZE, col: 0 },
56
+ ]
57
+
58
+ const inFinder = (row: number, col: number) =>
59
+ finders.some(
60
+ (f) =>
61
+ row >= f.row &&
62
+ row < f.row + FINDER_SIZE &&
63
+ col >= f.col &&
64
+ col < f.col + FINDER_SIZE,
65
+ )
66
+
67
+ // Collect maximal horizontal runs of `on` modules, skipping finder regions.
68
+ const runs: { row: number; col: number; length: number }[] = []
69
+ for (let row = 0; row < moduleCount; row++) {
70
+ let col = 0
71
+ while (col < moduleCount) {
72
+ if (inFinder(row, col)) {
73
+ col++
74
+ continue
75
+ }
76
+ if (matrix[row]?.[col]) {
77
+ let length = 1
78
+ while (
79
+ col + length < moduleCount &&
80
+ !inFinder(row, col + length) &&
81
+ matrix[row]?.[col + length]
82
+ ) {
83
+ length++
84
+ }
85
+ runs.push({ row, col, length })
86
+ col += length
87
+ } else {
88
+ col++
89
+ }
90
+ }
91
+ }
92
+
93
+ return (
94
+ <svg
95
+ width={size}
96
+ height={size}
97
+ viewBox={`0 0 ${size} ${size}`}
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ role="img"
100
+ aria-label={`QR code for ${value}`}
101
+ >
102
+ <rect width={size} height={size} fill={BG_COLOR} />
103
+ {runs.map(({ row, col, length }) => {
104
+ // Shrink each pill vertically so rows read as distinct without
105
+ // creating scanner-confusing white gaps mid-run. Isolated modules
106
+ // (length === 1) also shrink horizontally so they render close to a
107
+ // circle instead of an oval.
108
+ const pad = cellSize * PILL_PAD_RATIO
109
+ const pillHeight = cellSize - 2 * pad
110
+ const isSingle = length === 1
111
+ return (
112
+ <rect
113
+ key={`r${row}-${col}`}
114
+ x={col * cellSize + (isSingle ? pad : 0)}
115
+ y={row * cellSize + pad}
116
+ width={length * cellSize - (isSingle ? 2 * pad : 0)}
117
+ height={pillHeight}
118
+ rx={pillHeight / 2}
119
+ ry={pillHeight / 2}
120
+ fill={MODULE_COLOR}
121
+ />
122
+ )
123
+ })}
124
+ {finders.map(({ row, col }) => {
125
+ const x = col * cellSize
126
+ const y = row * cellSize
127
+ const outerSize = FINDER_SIZE * cellSize
128
+ const innerSize = 3 * cellSize
129
+ return (
130
+ <g key={`f${row}-${col}`}>
131
+ <rect
132
+ x={x}
133
+ y={y}
134
+ width={outerSize}
135
+ height={outerSize}
136
+ rx={eyeRadiusPx}
137
+ ry={eyeRadiusPx}
138
+ fill={MODULE_COLOR}
139
+ />
140
+ <rect
141
+ x={x + cellSize}
142
+ y={y + cellSize}
143
+ width={outerSize - 2 * cellSize}
144
+ height={outerSize - 2 * cellSize}
145
+ rx={Math.max(eyeRadiusPx - cellSize, 0)}
146
+ ry={Math.max(eyeRadiusPx - cellSize, 0)}
147
+ fill={BG_COLOR}
148
+ />
149
+ <rect
150
+ x={x + 2 * cellSize}
151
+ y={y + 2 * cellSize}
152
+ width={innerSize}
153
+ height={innerSize}
154
+ rx={Math.max(eyeRadiusPx - 2 * cellSize, 0)}
155
+ ry={Math.max(eyeRadiusPx - 2 * cellSize, 0)}
156
+ fill={MODULE_COLOR}
157
+ />
158
+ </g>
159
+ )
160
+ })}
161
+ </svg>
162
+ )
163
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ BottomSheet,
3
+ BottomSheetClose,
4
+ BottomSheetContent,
5
+ BottomSheetTitle,
6
+ Button,
7
+ Text,
8
+ } from '@zerodev/react-ui'
9
+ import { QrCode } from './QrCode'
10
+
11
+ export interface QrSheetProps {
12
+ /** Whether the modal is open. Controlled. */
13
+ open: boolean
14
+ /** Called when the modal requests to close (backdrop click, ESC,
15
+ * Cancel button). */
16
+ onOpenChange: (open: boolean) => void
17
+ /** Address to encode in the QR code and display below it. */
18
+ address: string
19
+ /** Called when the user taps "Copy address". */
20
+ onCopy: () => void
21
+ }
22
+
23
+ /** Bottom-anchored modal showing the deposit address as a QR code. Composes
24
+ * `<BottomSheet>` (state) + `<BottomSheetContent>` (chrome); this file owns the
25
+ * QR / address / copy-cancel layout inside it. */
26
+ export function QrSheet({ open, onOpenChange, address, onCopy }: QrSheetProps) {
27
+ return (
28
+ <BottomSheet open={open} onOpenChange={onOpenChange}>
29
+ <BottomSheetContent>
30
+ <BottomSheetTitle>Your deposit address</BottomSheetTitle>
31
+ {/* Bottom-half gradient — light blue at bottom-left fading into
32
+ peach/orange at bottom-right — mirrors the "amorphic" image used
33
+ in the Figma design. Colors echo `MultiRadialBackground`. */}
34
+ <div
35
+ aria-hidden
36
+ className="zd:absolute zd:inset-0 zd:pointer-events-none"
37
+ style={{
38
+ background:
39
+ 'radial-gradient(ellipse 78% 65% at 5% 105%, rgba(69,171,251,0.35) 0%, rgba(69,171,251,0) 72%), radial-gradient(ellipse 82% 65% at 100% 105%, rgba(250,200,172,0.65) 0%, rgba(250,200,172,0) 72%), radial-gradient(ellipse 58% 52% at 100% 100%, rgba(242,123,62,0.35) 0%, rgba(242,123,62,0) 72%)',
40
+ }}
41
+ />
42
+ <div className="zd:relative zd:flex zd:flex-col zd:gap-6 zd:items-center zd:pt-6 zd:px-4 zd:pb-3">
43
+ <div className="zd:flex zd:flex-col zd:gap-3 zd:items-center">
44
+ <Text className="zd:text-h2 zd:text-center">
45
+ Your deposit address
46
+ </Text>
47
+ <Text className="zd:text-body2 zd:text-center zd:max-w-[300px]">
48
+ Send funds to this address to automatically bridge and swap
49
+ desired assets.
50
+ </Text>
51
+ </div>
52
+
53
+ {/* The white bg + `p-5` (20px) padding around the QR doubles as
54
+ the required 4-module quiet zone — QrCode itself no longer
55
+ reserves one internally so the code fills its declared size. */}
56
+ <div className="zd:relative zd:bg-white zd:w-[240px] zd:h-[240px] zd:p-5 zd:flex zd:items-center zd:justify-center">
57
+ <QrCode value={address} size={200} eyeRadius={2} />
58
+ {/* Corner brackets — viewfinder-style framing. Color/thickness
59
+ match the Figma's `rgba(19,14,11,0.1)` 1px outline. */}
60
+ <div className="zd:absolute zd:top-0 zd:left-0 zd:w-5 zd:h-5 zd:border-t zd:border-l zd:border-greyScale/10 zd:rounded-tl-lg zd:pointer-events-none" />
61
+ <div className="zd:absolute zd:top-0 zd:right-0 zd:w-5 zd:h-5 zd:border-t zd:border-r zd:border-greyScale/10 zd:rounded-tr-lg zd:pointer-events-none" />
62
+ <div className="zd:absolute zd:bottom-0 zd:left-0 zd:w-5 zd:h-5 zd:border-b zd:border-l zd:border-greyScale/10 zd:rounded-bl-lg zd:pointer-events-none" />
63
+ <div className="zd:absolute zd:bottom-0 zd:right-0 zd:w-5 zd:h-5 zd:border-b zd:border-r zd:border-greyScale/10 zd:rounded-br-lg zd:pointer-events-none" />
64
+ </div>
65
+
66
+ <Text className="zd:text-body2 zd:text-center zd:break-all zd:max-w-[230px]">
67
+ {address}
68
+ </Text>
69
+
70
+ <div className="zd:flex zd:flex-col zd:gap-1 zd:w-full">
71
+ <Button action="primary" text="Copy address" onClick={onCopy} />
72
+ <BottomSheetClose asChild>
73
+ <Button action="secondary" text="Cancel" />
74
+ </BottomSheetClose>
75
+ </div>
76
+ </div>
77
+ </BottomSheetContent>
78
+ </BottomSheet>
79
+ )
80
+ }
@@ -0,0 +1,37 @@
1
+ import { cn, Icon } from '@zerodev/react-ui'
2
+
3
+ export interface ExplorerLinkProps {
4
+ /** Full block-explorer URL, e.g. `https://etherscan.io/tx/0x…`. */
5
+ href: string
6
+ /** Short address / hash used for the accessible label. */
7
+ address: string
8
+ className?: string
9
+ }
10
+
11
+ /**
12
+ * Small icon-only anchor to a block-explorer entry — the trailing action on a
13
+ * `TxnItem` row. Kept as its own component so both `PendingDeposits` (renders
14
+ * it inline via `TxnItem`'s `trailingAction` slot) and `PastDeposits`
15
+ * (renders it as an absolute sibling of the row-select button) use identical
16
+ * markup and styling.
17
+ */
18
+ export function ExplorerLink({ href, address, className }: ExplorerLinkProps) {
19
+ return (
20
+ <a
21
+ href={href}
22
+ target="_blank"
23
+ rel="noreferrer noopener"
24
+ aria-label={`View ${address} on block explorer`}
25
+ // Stop the click from bubbling to a parent that might otherwise
26
+ // intercept it (e.g. a row-select `<button>` in `PastDeposits` when
27
+ // this anchor is layered over the row via absolute positioning).
28
+ onClick={(e) => e.stopPropagation()}
29
+ className={cn(
30
+ 'zd:inline-flex zd:items-center zd:justify-center zd:rounded-md zd:p-1.5 zd:text-solarOrange zd:hover:bg-white/40',
31
+ className,
32
+ )}
33
+ >
34
+ <Icon name="export" className="zd:size-4" aria-hidden />
35
+ </a>
36
+ )
37
+ }
@@ -0,0 +1,178 @@
1
+ import { cn, Icon, Text } from '@zerodev/react-ui'
2
+
3
+ export type TxnStatus = 'Routing' | 'Detected' | 'Received' | 'Failed'
4
+
5
+ export interface TxnItemProps {
6
+ /** Pre-formatted amount, e.g. `"$248.00 USD"`. */
7
+ amount: string
8
+ /** Short address/hash display, e.g. `"0x4d2a…ba99"`. */
9
+ address: string
10
+ /** Optional block-explorer URL. Address row becomes a link when supplied. */
11
+ href?: string
12
+ /** Pre-formatted relative timestamp, e.g. `"2 mo ago"`. */
13
+ timestamp: string
14
+ status: TxnStatus
15
+ sourceTokenIconUrl?: string
16
+ sourceChainIconUrl?: string
17
+ destTokenIconUrl?: string
18
+ destChainIconUrl?: string
19
+ className?: string
20
+ }
21
+
22
+ const STATUS_COLOR: Record<TxnStatus, string> = {
23
+ Routing: 'zd:text-solarOrange',
24
+ Detected: 'zd:text-greyScale/50',
25
+ Received: 'zd:text-positive',
26
+ Failed: 'zd:text-negative',
27
+ }
28
+
29
+ export function TxnItem({
30
+ amount,
31
+ address,
32
+ href,
33
+ timestamp,
34
+ status,
35
+ sourceTokenIconUrl,
36
+ sourceChainIconUrl,
37
+ destTokenIconUrl,
38
+ destChainIconUrl,
39
+ className,
40
+ }: TxnItemProps) {
41
+ return (
42
+ <div
43
+ className={cn(
44
+ 'zd:relative zd:flex zd:w-full zd:items-center zd:gap-3 zd:py-2',
45
+ className,
46
+ )}
47
+ >
48
+ <PairMarkPair
49
+ {...(sourceTokenIconUrl && { sourceTokenIconUrl })}
50
+ {...(sourceChainIconUrl && { sourceChainIconUrl })}
51
+ {...(destTokenIconUrl && { destTokenIconUrl })}
52
+ {...(destChainIconUrl && { destChainIconUrl })}
53
+ />
54
+
55
+ <div className="zd:flex zd:min-w-0 zd:flex-1 zd:flex-col">
56
+ <Text className="zd:text-body2 zd:truncate">{amount}</Text>
57
+ <AddressLine address={address} {...(href && { href })} />
58
+ </div>
59
+
60
+ <div className="zd:flex zd:shrink-0 zd:flex-col zd:items-end zd:gap-1">
61
+ <Text className="zd:text-body3 zd:text-greyScale/50">{timestamp}</Text>
62
+ <Text className={cn('zd:text-body3', STATUS_COLOR[status])}>
63
+ {status}
64
+ </Text>
65
+ </div>
66
+ </div>
67
+ )
68
+ }
69
+
70
+ function PairMarkPair(props: {
71
+ sourceTokenIconUrl?: string
72
+ sourceChainIconUrl?: string
73
+ destTokenIconUrl?: string
74
+ destChainIconUrl?: string
75
+ }) {
76
+ return (
77
+ <div className="zd:flex zd:shrink-0 zd:items-center">
78
+ <PairMark
79
+ {...(props.sourceTokenIconUrl && {
80
+ tokenIconUrl: props.sourceTokenIconUrl,
81
+ })}
82
+ {...(props.sourceChainIconUrl && {
83
+ chainIconUrl: props.sourceChainIconUrl,
84
+ })}
85
+ badgeSide="left"
86
+ />
87
+ <PairMark
88
+ {...(props.destTokenIconUrl && {
89
+ tokenIconUrl: props.destTokenIconUrl,
90
+ })}
91
+ {...(props.destChainIconUrl && {
92
+ chainIconUrl: props.destChainIconUrl,
93
+ })}
94
+ badgeSide="right"
95
+ className="zd:-ml-4"
96
+ />
97
+ </div>
98
+ )
99
+ }
100
+
101
+ function PairMark({
102
+ tokenIconUrl,
103
+ chainIconUrl,
104
+ badgeSide,
105
+ className,
106
+ }: {
107
+ tokenIconUrl?: string
108
+ chainIconUrl?: string
109
+ badgeSide: 'left' | 'right'
110
+ className?: string
111
+ }) {
112
+ return (
113
+ <div
114
+ className={cn(
115
+ 'zd:relative zd:size-11 zd:shrink-0 zd:rounded-xl zd:isolate zd:bg-white/60 zd:backdrop-blur-[30px]',
116
+ 'zd:shadow-[inset_0_3px_4px_0_rgba(0,0,0,0.02),inset_0_-4px_4px_0_rgba(255,255,255,0.1)]',
117
+ className,
118
+ )}
119
+ >
120
+ <div className="zd:absolute zd:top-1/2 zd:left-1/2 zd:size-8.5 zd:-translate-x-1/2 zd:-translate-y-1/2 zd:overflow-hidden zd:rounded-full zd:bg-greyScale/10">
121
+ {tokenIconUrl && (
122
+ <img
123
+ src={tokenIconUrl}
124
+ alt=""
125
+ className="zd:size-full zd:object-contain"
126
+ />
127
+ )}
128
+ </div>
129
+ {chainIconUrl && (
130
+ <div
131
+ className={cn(
132
+ 'zd:absolute zd:bottom-1 zd:size-3 zd:overflow-hidden zd:rounded-full zd:bg-white zd:p-px',
133
+ badgeSide === 'left' ? 'zd:left-1' : 'zd:right-1',
134
+ )}
135
+ >
136
+ <img
137
+ src={chainIconUrl}
138
+ alt=""
139
+ className="zd:size-full zd:object-contain"
140
+ />
141
+ </div>
142
+ )}
143
+ </div>
144
+ )
145
+ }
146
+
147
+ function AddressLine({ address, href }: { address: string; href?: string }) {
148
+ const inner = (
149
+ <>
150
+ <span className="zd:truncate">{address}</span>
151
+ {href && (
152
+ <Icon
153
+ name="export"
154
+ className="zd:size-3 zd:shrink-0 zd:text-solarOrange"
155
+ aria-hidden
156
+ />
157
+ )}
158
+ </>
159
+ )
160
+
161
+ const className =
162
+ 'zd:flex zd:min-w-0 zd:items-center zd:gap-1 zd:text-body3 zd:text-greyScale/50'
163
+
164
+ if (href) {
165
+ return (
166
+ <a
167
+ href={href}
168
+ target="_blank"
169
+ rel="noreferrer noopener"
170
+ aria-label={`View ${address} on block explorer`}
171
+ className={cn(className, 'zd:hover:text-greyScale')}
172
+ >
173
+ {inner}
174
+ </a>
175
+ )
176
+ }
177
+ return <span className={className}>{inner}</span>
178
+ }