@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.
- package/README.md +117 -0
- package/dist/_types/components/AddressDisplay/index.d.ts +43 -0
- package/dist/_types/components/AddressDisplay/index.d.ts.map +1 -0
- package/dist/_types/components/ErrorRetryCard/index.d.ts +21 -0
- package/dist/_types/components/ErrorRetryCard/index.d.ts.map +1 -0
- package/dist/_types/components/FeeBreakdown/feeInfo.d.ts +24 -0
- package/dist/_types/components/FeeBreakdown/feeInfo.d.ts.map +1 -0
- package/dist/_types/components/FeeBreakdown/index.d.ts +28 -0
- package/dist/_types/components/FeeBreakdown/index.d.ts.map +1 -0
- package/dist/_types/components/LoadingCard/index.d.ts +6 -0
- package/dist/_types/components/LoadingCard/index.d.ts.map +1 -0
- package/dist/_types/components/PendingDeposits/index.d.ts +14 -0
- package/dist/_types/components/PendingDeposits/index.d.ts.map +1 -0
- package/dist/_types/components/QrSheet/QrCode.d.ts +28 -0
- package/dist/_types/components/QrSheet/QrCode.d.ts.map +1 -0
- package/dist/_types/components/QrSheet/index.d.ts +16 -0
- package/dist/_types/components/QrSheet/index.d.ts.map +1 -0
- package/dist/_types/components/TxnItem/ExplorerLink.d.ts +16 -0
- package/dist/_types/components/TxnItem/ExplorerLink.d.ts.map +1 -0
- package/dist/_types/components/TxnItem/index.d.ts +19 -0
- package/dist/_types/components/TxnItem/index.d.ts.map +1 -0
- package/dist/_types/constants.d.ts +20 -0
- package/dist/_types/constants.d.ts.map +1 -0
- package/dist/_types/context/SmartRoutingAddressContext.d.ts +32 -0
- package/dist/_types/context/SmartRoutingAddressContext.d.ts.map +1 -0
- package/dist/_types/context/SmartRoutingAddressProvider.d.ts +13 -0
- package/dist/_types/context/SmartRoutingAddressProvider.d.ts.map +1 -0
- package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts +23 -0
- package/dist/_types/hooks/useCreateSmartRoutingAddress.d.ts.map +1 -0
- package/dist/_types/hooks/useDepositStatus.d.ts +25 -0
- package/dist/_types/hooks/useDepositStatus.d.ts.map +1 -0
- package/dist/_types/hooks/useNewDeposits.d.ts +8 -0
- package/dist/_types/hooks/useNewDeposits.d.ts.map +1 -0
- package/dist/_types/hooks/useProviderFees.d.ts +22 -0
- package/dist/_types/hooks/useProviderFees.d.ts.map +1 -0
- package/dist/_types/hooks/useSmartRoutingAddress.d.ts +20 -0
- package/dist/_types/hooks/useSmartRoutingAddress.d.ts.map +1 -0
- package/dist/_types/iconAssets.d.ts +7 -0
- package/dist/_types/iconAssets.d.ts.map +1 -0
- package/dist/_types/index.d.ts +14 -0
- package/dist/_types/index.d.ts.map +1 -0
- package/dist/_types/pages/Deposit.d.ts +12 -0
- package/dist/_types/pages/Deposit.d.ts.map +1 -0
- package/dist/_types/pages/PastDeposits.d.ts +14 -0
- package/dist/_types/pages/PastDeposits.d.ts.map +1 -0
- package/dist/_types/pages/TransactionDetails.d.ts +11 -0
- package/dist/_types/pages/TransactionDetails.d.ts.map +1 -0
- package/dist/_types/pages/index.d.ts +25 -0
- package/dist/_types/pages/index.d.ts.map +1 -0
- package/dist/_types/types.d.ts +75 -0
- package/dist/_types/types.d.ts.map +1 -0
- package/dist/_types/utils/chains.d.ts +8 -0
- package/dist/_types/utils/chains.d.ts.map +1 -0
- package/dist/_types/utils/config.d.ts +35 -0
- package/dist/_types/utils/config.d.ts.map +1 -0
- package/dist/_types/utils/deposits.d.ts +29 -0
- package/dist/_types/utils/deposits.d.ts.map +1 -0
- package/dist/_types/utils/fees.d.ts +14 -0
- package/dist/_types/utils/fees.d.ts.map +1 -0
- package/dist/_types/utils/format.d.ts +27 -0
- package/dist/_types/utils/format.d.ts.map +1 -0
- package/dist/_types/utils/providerFees.d.ts +103 -0
- package/dist/_types/utils/providerFees.d.ts.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +2180 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +2 -0
- package/package.json +91 -0
- package/src/components/AddressDisplay/index.tsx +108 -0
- package/src/components/ErrorRetryCard/index.tsx +68 -0
- package/src/components/FeeBreakdown/feeInfo.ts +25 -0
- package/src/components/FeeBreakdown/index.tsx +218 -0
- package/src/components/LoadingCard/index.tsx +27 -0
- package/src/components/PendingDeposits/index.tsx +152 -0
- package/src/components/QrSheet/QrCode.tsx +163 -0
- package/src/components/QrSheet/index.tsx +80 -0
- package/src/components/TxnItem/ExplorerLink.tsx +37 -0
- package/src/components/TxnItem/index.tsx +178 -0
- package/src/constants.ts +102 -0
- package/src/context/SmartRoutingAddressContext.ts +48 -0
- package/src/context/SmartRoutingAddressProvider.tsx +145 -0
- package/src/hooks/useCreateSmartRoutingAddress.ts +28 -0
- package/src/hooks/useDepositStatus.ts +110 -0
- package/src/hooks/useNewDeposits.ts +25 -0
- package/src/hooks/useProviderFees.ts +235 -0
- package/src/hooks/useSmartRoutingAddress.ts +25 -0
- package/src/iconAssets.ts +130 -0
- package/src/index.ts +27 -0
- package/src/pages/Deposit.tsx +605 -0
- package/src/pages/PastDeposits.tsx +232 -0
- package/src/pages/TransactionDetails.tsx +466 -0
- package/src/pages/index.tsx +165 -0
- package/src/types.ts +84 -0
- package/src/utils/chains.ts +26 -0
- package/src/utils/config.ts +142 -0
- package/src/utils/deposits.ts +106 -0
- package/src/utils/fees.ts +51 -0
- package/src/utils/format.ts +108 -0
- 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
|
+
}
|