@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,605 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowCardPair,
|
|
3
|
+
cn,
|
|
4
|
+
DataRow,
|
|
5
|
+
Icon,
|
|
6
|
+
Pill,
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectIcon,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
Text,
|
|
13
|
+
TokenListItem,
|
|
14
|
+
Tooltip,
|
|
15
|
+
Wrapper,
|
|
16
|
+
} from '@zerodev/react-ui'
|
|
17
|
+
import type { DepositedToken, TOKEN_TYPE } from '@zerodev/smart-routing-address'
|
|
18
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
19
|
+
import { AddressDisplay } from '../components/AddressDisplay'
|
|
20
|
+
import { ErrorRetryCard } from '../components/ErrorRetryCard'
|
|
21
|
+
import {
|
|
22
|
+
FeeBreakdownRows,
|
|
23
|
+
FeeSummary,
|
|
24
|
+
LiveValue,
|
|
25
|
+
} from '../components/FeeBreakdown'
|
|
26
|
+
import { FEE_INFO } from '../components/FeeBreakdown/feeInfo'
|
|
27
|
+
import { LoadingCard } from '../components/LoadingCard'
|
|
28
|
+
import { PendingDeposits } from '../components/PendingDeposits'
|
|
29
|
+
import { DEFAULT_FILL_TIME_SECONDS } from '../constants'
|
|
30
|
+
import { useSmartRoutingAddressContext } from '../context/SmartRoutingAddressContext'
|
|
31
|
+
import { useDepositStatus } from '../hooks/useDepositStatus'
|
|
32
|
+
import { useNewDeposits } from '../hooks/useNewDeposits'
|
|
33
|
+
import { useProviderFees } from '../hooks/useProviderFees'
|
|
34
|
+
import { CHAIN_ICONS, PROVIDER_ICONS, TOKEN_ICONS } from '../iconAssets'
|
|
35
|
+
import type { SourceToken } from '../types'
|
|
36
|
+
import {
|
|
37
|
+
getSourceTokenSymbol,
|
|
38
|
+
resolveBaseUrl,
|
|
39
|
+
resolveDestChain,
|
|
40
|
+
sourceTokensFromFees,
|
|
41
|
+
} from '../utils/config'
|
|
42
|
+
import { findFeeData, resolveTokenAddress } from '../utils/fees'
|
|
43
|
+
import {
|
|
44
|
+
formatDisplayAmount,
|
|
45
|
+
formatDuration,
|
|
46
|
+
formatSlippage,
|
|
47
|
+
} from '../utils/format'
|
|
48
|
+
import { buildFeeBreakdown } from '../utils/providerFees'
|
|
49
|
+
|
|
50
|
+
export interface DepositProps {
|
|
51
|
+
onQrClick?: () => void
|
|
52
|
+
/** Navigate to the "Past deposits" view. When omitted, the row is hidden. */
|
|
53
|
+
onViewPastDeposits?: () => void
|
|
54
|
+
/** Fired when a pending-deposit row is tapped. Wires through to
|
|
55
|
+
* `PendingDeposits` so an in-flight deposit opens the transaction-details
|
|
56
|
+
* view, matching the past-deposits behaviour. */
|
|
57
|
+
onSelectDeposit?: (deposit: DepositedToken) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const SUBTITLE =
|
|
61
|
+
"Send any supported token from any network. We'll swap & bridge it directly to your account, ready to use."
|
|
62
|
+
|
|
63
|
+
// The two picker panels span the full pill row (Figma "Send" layout), not
|
|
64
|
+
// just the trigger cell each Select lives in. Trigger width = half the row
|
|
65
|
+
// minus half the 4px gap, so full-row width = trigger * 2 + 4px.
|
|
66
|
+
const FULL_ROW_PANEL_STYLE = {
|
|
67
|
+
width: 'calc(var(--radix-select-trigger-width) * 2 + 4px)',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function Deposit({
|
|
71
|
+
onQrClick,
|
|
72
|
+
onViewPastDeposits,
|
|
73
|
+
onSelectDeposit,
|
|
74
|
+
}: DepositProps) {
|
|
75
|
+
const { config, addressState, recipient, retry, setActiveRoute } =
|
|
76
|
+
useSmartRoutingAddressContext()
|
|
77
|
+
const [feeOpen, setFeeOpen] = useState(false)
|
|
78
|
+
const [retrying, setRetrying] = useState(false)
|
|
79
|
+
|
|
80
|
+
const success = addressState.status === 'success' ? addressState : null
|
|
81
|
+
const address = success?.address
|
|
82
|
+
const estimatedFees = success?.estimatedFees ?? []
|
|
83
|
+
|
|
84
|
+
// allowPartialRoutes lets the server drop source tokens it can't route, so
|
|
85
|
+
// the routable set is exactly the tokens the fee estimates came back with.
|
|
86
|
+
const srcTokens = useMemo(
|
|
87
|
+
() => sourceTokensFromFees(estimatedFees),
|
|
88
|
+
[estimatedFees],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Track the user's picker selection. `null` means "no explicit choice yet" —
|
|
92
|
+
// an effect below seeds it to the first routable option once srcTokens land.
|
|
93
|
+
const [selectedTokenType, setSelectedTokenType] = useState<TOKEN_TYPE | null>(
|
|
94
|
+
null,
|
|
95
|
+
)
|
|
96
|
+
const [selectedChainId, setSelectedChainId] = useState<number | null>(null)
|
|
97
|
+
|
|
98
|
+
// Seed picker state from the first routable option as soon as srcTokens
|
|
99
|
+
// arrive. Also re-seeds if a stale selection becomes unroutable.
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (srcTokens.length === 0) return
|
|
102
|
+
const currentValid = srcTokens.some(
|
|
103
|
+
(t) =>
|
|
104
|
+
t.tokenType === selectedTokenType && t.chain.id === selectedChainId,
|
|
105
|
+
)
|
|
106
|
+
if (currentValid) return
|
|
107
|
+
// Prefer to keep the token, swap the chain to one that's available for it.
|
|
108
|
+
const forSameToken = srcTokens.find(
|
|
109
|
+
(t) => t.tokenType === selectedTokenType,
|
|
110
|
+
)
|
|
111
|
+
const fallback = forSameToken ?? srcTokens[0]
|
|
112
|
+
if (!fallback) return
|
|
113
|
+
setSelectedTokenType(fallback.tokenType)
|
|
114
|
+
setSelectedChainId(fallback.chain.id)
|
|
115
|
+
}, [srcTokens, selectedTokenType, selectedChainId])
|
|
116
|
+
|
|
117
|
+
const source: SourceToken | null = useMemo(() => {
|
|
118
|
+
if (!selectedTokenType || selectedChainId === null) {
|
|
119
|
+
return srcTokens[0] ?? null
|
|
120
|
+
}
|
|
121
|
+
return (
|
|
122
|
+
srcTokens.find(
|
|
123
|
+
(t) =>
|
|
124
|
+
t.tokenType === selectedTokenType && t.chain.id === selectedChainId,
|
|
125
|
+
) ??
|
|
126
|
+
srcTokens[0] ??
|
|
127
|
+
null
|
|
128
|
+
)
|
|
129
|
+
}, [srcTokens, selectedTokenType, selectedChainId])
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
deposits,
|
|
133
|
+
hasLoaded,
|
|
134
|
+
error: depositsError,
|
|
135
|
+
refetch: refetchDeposits,
|
|
136
|
+
} = useDepositStatus({
|
|
137
|
+
address,
|
|
138
|
+
baseUrl: resolveBaseUrl(config),
|
|
139
|
+
})
|
|
140
|
+
const newDeposits = useNewDeposits(deposits, hasLoaded)
|
|
141
|
+
const pastDepositsCount = deposits.length - newDeposits.length
|
|
142
|
+
|
|
143
|
+
const destChain = resolveDestChain(config)
|
|
144
|
+
// Widget's default actions forward the deposited token, so the destination
|
|
145
|
+
// token equals the source token — one symbol and logo serve both cards.
|
|
146
|
+
// Only the chain differs, hence the source/dest split below.
|
|
147
|
+
const tokenSymbol = source ? getSourceTokenSymbol(source) : undefined
|
|
148
|
+
const tokenLogo = tokenSymbol
|
|
149
|
+
? TOKEN_ICONS[tokenSymbol.toUpperCase()]
|
|
150
|
+
: undefined
|
|
151
|
+
const sourceChainLogo = source ? CHAIN_ICONS[source.chain.id] : undefined
|
|
152
|
+
const destChainLogo = CHAIN_ICONS[destChain.id]
|
|
153
|
+
const feeData = source
|
|
154
|
+
? findFeeData(estimatedFees, source.chain.id, source.tokenType)
|
|
155
|
+
: null
|
|
156
|
+
const fillTime = formatDuration(DEFAULT_FILL_TIME_SECONDS)
|
|
157
|
+
|
|
158
|
+
// Live bridge quotes from Across / Relay, keyed off the selected route.
|
|
159
|
+
// Enriches the SRA fee estimate with the itemised legs it doesn't expose.
|
|
160
|
+
const providerFees = useProviderFees(source, destChain, feeData, recipient)
|
|
161
|
+
const breakdown =
|
|
162
|
+
feeData && tokenSymbol
|
|
163
|
+
? buildFeeBreakdown(feeData, tokenSymbol, providerFees.fees)
|
|
164
|
+
: null
|
|
165
|
+
|
|
166
|
+
// Publish the current picker selection so hosts (e.g. a demo "send" panel)
|
|
167
|
+
// can mirror the widget's route. Cleared when the picker is empty so
|
|
168
|
+
// downstream mocks show their fallback instead of stale state.
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
if (!source || !feeData || !tokenSymbol) {
|
|
171
|
+
setActiveRoute(null)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
const token = resolveTokenAddress(source.tokenType, source.chain.id)
|
|
175
|
+
if (!token) {
|
|
176
|
+
setActiveRoute(null)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
setActiveRoute({
|
|
180
|
+
sourceChainId: source.chain.id,
|
|
181
|
+
sourceChainName: source.chain.name,
|
|
182
|
+
token,
|
|
183
|
+
symbol: tokenSymbol,
|
|
184
|
+
decimals: feeData.decimal,
|
|
185
|
+
// `feeData.fee` is a `Hex` string from the SDK — normalise to a
|
|
186
|
+
// decimal atomic-units string here so `activeRoute.feeAmount` has
|
|
187
|
+
// one consistent representation across every write path (see
|
|
188
|
+
// `ActiveRoute.feeAmount` — the demo's fallback uses decimals too).
|
|
189
|
+
// Hosts using `BigInt()` work either way, but `Number()` /
|
|
190
|
+
// `parseInt(x, 10)` would silently misread the hex form.
|
|
191
|
+
feeAmount: BigInt(feeData.fee).toString(),
|
|
192
|
+
})
|
|
193
|
+
}, [source, feeData, tokenSymbol, setActiveRoute])
|
|
194
|
+
|
|
195
|
+
// Deduped list of routable token types — the token picker's rows. Chain
|
|
196
|
+
// count per token drives the subtitle. Kept as SourceToken (not a bespoke
|
|
197
|
+
// dropdown-item shape) so the render below can call the same helpers used
|
|
198
|
+
// for the trigger.
|
|
199
|
+
const uniqueTokens = useMemo(() => {
|
|
200
|
+
const seen = new Set<TOKEN_TYPE>()
|
|
201
|
+
const out: SourceToken[] = []
|
|
202
|
+
for (const token of srcTokens) {
|
|
203
|
+
if (seen.has(token.tokenType)) continue
|
|
204
|
+
seen.add(token.tokenType)
|
|
205
|
+
out.push(token)
|
|
206
|
+
}
|
|
207
|
+
return out
|
|
208
|
+
}, [srcTokens])
|
|
209
|
+
|
|
210
|
+
// Chains that carry the currently-selected token — the chain picker's rows.
|
|
211
|
+
const availableChains = useMemo(() => {
|
|
212
|
+
if (!selectedTokenType) return []
|
|
213
|
+
const seen = new Set<number>()
|
|
214
|
+
const out: SourceToken[] = []
|
|
215
|
+
for (const token of srcTokens) {
|
|
216
|
+
if (token.tokenType !== selectedTokenType) continue
|
|
217
|
+
if (seen.has(token.chain.id)) continue
|
|
218
|
+
seen.add(token.chain.id)
|
|
219
|
+
out.push(token)
|
|
220
|
+
}
|
|
221
|
+
return out
|
|
222
|
+
}, [srcTokens, selectedTokenType])
|
|
223
|
+
|
|
224
|
+
// Only formatted when the consumer explicitly sets a slippage — otherwise
|
|
225
|
+
// the server picks its own default and there is no meaningful value to show.
|
|
226
|
+
const slippage =
|
|
227
|
+
typeof config.slippage === 'number' ? formatSlippage(config.slippage) : null
|
|
228
|
+
|
|
229
|
+
const minDepositAmount =
|
|
230
|
+
feeData && tokenSymbol
|
|
231
|
+
? `${formatDisplayAmount(feeData.minDeposit, feeData.decimal, 'up')} ${tokenSymbol}`
|
|
232
|
+
: null
|
|
233
|
+
|
|
234
|
+
// Flash key re-triggers the LiveValue animation on the estimated-fee row
|
|
235
|
+
// when the underlying quote changes; combining every meaningful component
|
|
236
|
+
// catches all updates in one dependency.
|
|
237
|
+
const feeFlashKey = breakdown
|
|
238
|
+
? `${breakdown.totalText ?? ''}|${breakdown.ratePct ?? ''}|${breakdown.flatUsd ?? ''}`
|
|
239
|
+
: ''
|
|
240
|
+
|
|
241
|
+
const pickerDisabled = uniqueTokens.length === 0
|
|
242
|
+
const sourceChainName = source?.chain.name
|
|
243
|
+
|
|
244
|
+
// Error surfaces the retry card handles. Only one is shown at a time,
|
|
245
|
+
// matched top-down in severity order: (1) address creation itself failed,
|
|
246
|
+
// (2) creation succeeded but no routes came back, (3) deposit polling is
|
|
247
|
+
// erroring after we already have an address. `hasLoaded` gates the polling
|
|
248
|
+
// case so first-load latency isn't mistaken for an error.
|
|
249
|
+
const addressError = addressState.status === 'error'
|
|
250
|
+
const noRoutesError = !!success && srcTokens.length === 0
|
|
251
|
+
const pollingError = !!address && !!depositsError && hasLoaded
|
|
252
|
+
const errorMessage = addressError
|
|
253
|
+
? 'Failed to create deposit address...'
|
|
254
|
+
: noRoutesError
|
|
255
|
+
? 'No routes found, try one more time...'
|
|
256
|
+
: pollingError
|
|
257
|
+
? 'Failed to load deposits, try again...'
|
|
258
|
+
: null
|
|
259
|
+
|
|
260
|
+
const handleRetry = useCallback(async () => {
|
|
261
|
+
if (retrying) return
|
|
262
|
+
setRetrying(true)
|
|
263
|
+
try {
|
|
264
|
+
if (pollingError && !addressError && !noRoutesError) {
|
|
265
|
+
refetchDeposits()
|
|
266
|
+
} else {
|
|
267
|
+
await retry()
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
setRetrying(false)
|
|
271
|
+
}
|
|
272
|
+
}, [
|
|
273
|
+
retrying,
|
|
274
|
+
pollingError,
|
|
275
|
+
addressError,
|
|
276
|
+
noRoutesError,
|
|
277
|
+
refetchDeposits,
|
|
278
|
+
retry,
|
|
279
|
+
])
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className="zd:flex zd:h-full zd:w-full zd:flex-col zd:items-center zd:gap-4 zd:pt-4 zd:pb-6">
|
|
283
|
+
<Text className="zd:w-full zd:text-center">{SUBTITLE}</Text>
|
|
284
|
+
|
|
285
|
+
<div className="zd:relative zd:flex zd:w-full zd:flex-1 zd:flex-col zd:gap-2">
|
|
286
|
+
<ArrowCardPair
|
|
287
|
+
topCard={
|
|
288
|
+
<Wrapper
|
|
289
|
+
variant="ghost"
|
|
290
|
+
className="zd:relative zd:flex zd:w-full zd:flex-col zd:rounded-2xl zd:p-1"
|
|
291
|
+
>
|
|
292
|
+
<CardTitle>Send</CardTitle>
|
|
293
|
+
<PillRow
|
|
294
|
+
left={
|
|
295
|
+
<Select
|
|
296
|
+
value={selectedTokenType ?? ''}
|
|
297
|
+
onValueChange={(id) =>
|
|
298
|
+
setSelectedTokenType(id as TOKEN_TYPE)
|
|
299
|
+
}
|
|
300
|
+
disabled={pickerDisabled}
|
|
301
|
+
>
|
|
302
|
+
<SelectTrigger asChild>
|
|
303
|
+
<Pill
|
|
304
|
+
label={tokenSymbol ?? ''}
|
|
305
|
+
{...(tokenLogo && { logoUri: tokenLogo })}
|
|
306
|
+
disabled={pickerDisabled}
|
|
307
|
+
loading={!tokenSymbol}
|
|
308
|
+
trailingIcon={!pickerDisabled && <SelectIcon />}
|
|
309
|
+
/>
|
|
310
|
+
</SelectTrigger>
|
|
311
|
+
<SelectContent align="start" style={FULL_ROW_PANEL_STYLE}>
|
|
312
|
+
{uniqueTokens.map((token, i) => {
|
|
313
|
+
const symbol = getSourceTokenSymbol(token)
|
|
314
|
+
const chainCount = srcTokens.filter(
|
|
315
|
+
(t) => t.tokenType === token.tokenType,
|
|
316
|
+
).length
|
|
317
|
+
const logo = TOKEN_ICONS[symbol.toUpperCase()]
|
|
318
|
+
return (
|
|
319
|
+
<SelectItem
|
|
320
|
+
key={token.tokenType}
|
|
321
|
+
value={token.tokenType}
|
|
322
|
+
textValue={symbol}
|
|
323
|
+
className="zd:p-0"
|
|
324
|
+
>
|
|
325
|
+
<TokenListItem
|
|
326
|
+
symbol={symbol}
|
|
327
|
+
subtitle={`${chainCount} network${chainCount === 1 ? '' : 's'}`}
|
|
328
|
+
{...(logo && { imageSource: logo })}
|
|
329
|
+
/>
|
|
330
|
+
{i === 0 && (
|
|
331
|
+
<span className="zd:absolute zd:top-1/2 zd:right-3 zd:-translate-y-1/2 zd:inline-flex zd:items-center zd:rounded-full zd:bg-positive/15 zd:px-2 zd:py-1 zd:text-body3 zd:text-positive zd:pointer-events-none">
|
|
332
|
+
Recommended
|
|
333
|
+
</span>
|
|
334
|
+
)}
|
|
335
|
+
</SelectItem>
|
|
336
|
+
)
|
|
337
|
+
})}
|
|
338
|
+
</SelectContent>
|
|
339
|
+
</Select>
|
|
340
|
+
}
|
|
341
|
+
right={
|
|
342
|
+
<Select
|
|
343
|
+
value={
|
|
344
|
+
selectedChainId !== null ? String(selectedChainId) : ''
|
|
345
|
+
}
|
|
346
|
+
onValueChange={(id) => setSelectedChainId(Number(id))}
|
|
347
|
+
disabled={pickerDisabled}
|
|
348
|
+
>
|
|
349
|
+
<SelectTrigger asChild>
|
|
350
|
+
<Pill
|
|
351
|
+
label={sourceChainName ?? ''}
|
|
352
|
+
{...(sourceChainLogo && { logoUri: sourceChainLogo })}
|
|
353
|
+
disabled={pickerDisabled}
|
|
354
|
+
loading={!sourceChainName}
|
|
355
|
+
trailingIcon={!pickerDisabled && <SelectIcon />}
|
|
356
|
+
/>
|
|
357
|
+
</SelectTrigger>
|
|
358
|
+
<SelectContent align="end" style={FULL_ROW_PANEL_STYLE}>
|
|
359
|
+
{availableChains.map((token) => {
|
|
360
|
+
const logo = CHAIN_ICONS[token.chain.id]
|
|
361
|
+
return (
|
|
362
|
+
<SelectItem
|
|
363
|
+
key={token.chain.id}
|
|
364
|
+
value={String(token.chain.id)}
|
|
365
|
+
textValue={token.chain.name}
|
|
366
|
+
className="zd:p-0"
|
|
367
|
+
>
|
|
368
|
+
<TokenListItem
|
|
369
|
+
symbol={token.chain.name}
|
|
370
|
+
iconVariant="network"
|
|
371
|
+
{...(logo && { imageSource: logo })}
|
|
372
|
+
/>
|
|
373
|
+
</SelectItem>
|
|
374
|
+
)
|
|
375
|
+
})}
|
|
376
|
+
</SelectContent>
|
|
377
|
+
</Select>
|
|
378
|
+
}
|
|
379
|
+
/>
|
|
380
|
+
<div className="zd:flex zd:w-full zd:flex-col zd:items-start zd:gap-2 zd:px-2 zd:py-4">
|
|
381
|
+
{slippage && (
|
|
382
|
+
<DataRow
|
|
383
|
+
label="Max slippage"
|
|
384
|
+
value={slippage}
|
|
385
|
+
info
|
|
386
|
+
infoTooltip={FEE_INFO.maxSlippage}
|
|
387
|
+
trailing={
|
|
388
|
+
breakdown?.provider &&
|
|
389
|
+
PROVIDER_ICONS[breakdown.provider] ? (
|
|
390
|
+
<LiveValue
|
|
391
|
+
loading={providerFees.loading}
|
|
392
|
+
flashKey={breakdown.provider}
|
|
393
|
+
>
|
|
394
|
+
<Tooltip content={`Quoted via ${breakdown.provider}`}>
|
|
395
|
+
<button
|
|
396
|
+
type="button"
|
|
397
|
+
aria-label={`Quoted via ${breakdown.provider}`}
|
|
398
|
+
className="zd:inline-flex zd:items-center zd:justify-center zd:cursor-help zd:outline-none zd:bg-transparent"
|
|
399
|
+
>
|
|
400
|
+
<img
|
|
401
|
+
src={PROVIDER_ICONS[breakdown.provider]}
|
|
402
|
+
alt=""
|
|
403
|
+
aria-hidden
|
|
404
|
+
className="zd:size-4 zd:shrink-0 zd:rounded-[4px] zd:object-cover"
|
|
405
|
+
/>
|
|
406
|
+
</button>
|
|
407
|
+
</Tooltip>
|
|
408
|
+
</LiveValue>
|
|
409
|
+
) : null
|
|
410
|
+
}
|
|
411
|
+
/>
|
|
412
|
+
)}
|
|
413
|
+
<DataRow
|
|
414
|
+
label="Estimated fee"
|
|
415
|
+
value={
|
|
416
|
+
breakdown ? (
|
|
417
|
+
<LiveValue
|
|
418
|
+
loading={providerFees.loading}
|
|
419
|
+
flashKey={feeFlashKey}
|
|
420
|
+
>
|
|
421
|
+
<FeeSummary breakdown={breakdown} />
|
|
422
|
+
</LiveValue>
|
|
423
|
+
) : (
|
|
424
|
+
// Match Min deposit's skeleton so both loading
|
|
425
|
+
// affordances share one visual language.
|
|
426
|
+
<div className="zd:h-3.5 zd:w-20 zd:rounded-md zd:bg-greyScale/15 zd:animate-skel-pulse" />
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
info
|
|
430
|
+
infoTooltip={FEE_INFO.estimatedFee}
|
|
431
|
+
trailing={
|
|
432
|
+
breakdown ? (
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
onClick={() => setFeeOpen((prev) => !prev)}
|
|
436
|
+
aria-expanded={feeOpen}
|
|
437
|
+
aria-label={
|
|
438
|
+
feeOpen ? 'Hide fee details' : 'Show fee details'
|
|
439
|
+
}
|
|
440
|
+
className="zd:inline-flex zd:items-center zd:justify-center zd:cursor-pointer"
|
|
441
|
+
>
|
|
442
|
+
<Icon
|
|
443
|
+
name={feeOpen ? 'chevronUp' : 'chevronDown'}
|
|
444
|
+
className="zd:w-3.5 zd:h-3.5 zd:text-greyScale"
|
|
445
|
+
/>
|
|
446
|
+
</button>
|
|
447
|
+
) : null
|
|
448
|
+
}
|
|
449
|
+
/>
|
|
450
|
+
{feeOpen && breakdown && (
|
|
451
|
+
<FeeBreakdownRows breakdown={breakdown} />
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</Wrapper>
|
|
455
|
+
}
|
|
456
|
+
bottomCard={
|
|
457
|
+
<Wrapper
|
|
458
|
+
variant="ghost"
|
|
459
|
+
className="zd:relative zd:flex zd:w-full zd:flex-col zd:gap-2 zd:rounded-2xl zd:p-1"
|
|
460
|
+
>
|
|
461
|
+
<CardTitle>Arrives as</CardTitle>
|
|
462
|
+
<PillRow
|
|
463
|
+
left={
|
|
464
|
+
<Pill
|
|
465
|
+
label={tokenSymbol ?? ''}
|
|
466
|
+
{...(tokenLogo && { logoUri: tokenLogo })}
|
|
467
|
+
disabled
|
|
468
|
+
loading={!tokenSymbol}
|
|
469
|
+
/>
|
|
470
|
+
}
|
|
471
|
+
right={
|
|
472
|
+
<Pill
|
|
473
|
+
label={destChain.name}
|
|
474
|
+
{...(destChainLogo && { logoUri: destChainLogo })}
|
|
475
|
+
disabled
|
|
476
|
+
/>
|
|
477
|
+
}
|
|
478
|
+
/>
|
|
479
|
+
<div className="zd:flex zd:w-full zd:flex-col zd:items-start zd:px-2">
|
|
480
|
+
<DataRow
|
|
481
|
+
label="Ready in"
|
|
482
|
+
value={fillTime}
|
|
483
|
+
info
|
|
484
|
+
infoTooltip={FEE_INFO.readyIn}
|
|
485
|
+
/>
|
|
486
|
+
</div>
|
|
487
|
+
<AddressDisplay
|
|
488
|
+
status={addressState.status}
|
|
489
|
+
address={address}
|
|
490
|
+
onQrClick={onQrClick}
|
|
491
|
+
/>
|
|
492
|
+
{errorMessage && (
|
|
493
|
+
<ErrorRetryCard
|
|
494
|
+
message={errorMessage}
|
|
495
|
+
onRetry={handleRetry}
|
|
496
|
+
busy={retrying}
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
<DataRow
|
|
500
|
+
label="Min deposit"
|
|
501
|
+
value={
|
|
502
|
+
minDepositAmount ?? (
|
|
503
|
+
// Match PillSkeleton's greyish pulse so all loading
|
|
504
|
+
// affordances in this card share one visual language.
|
|
505
|
+
// `<output>` gets an implicit `role="status"` — biome's
|
|
506
|
+
// `useSemanticElements` prefers it over `<div role>`.
|
|
507
|
+
// `aria-busy` + `aria-label` so screen readers hear
|
|
508
|
+
// "loading" instead of landing on an empty region.
|
|
509
|
+
<output
|
|
510
|
+
aria-busy="true"
|
|
511
|
+
aria-label="Loading minimum deposit"
|
|
512
|
+
className="zd:block zd:h-3.5 zd:w-20 zd:rounded-md zd:bg-greyScale/15 zd:animate-skel-pulse"
|
|
513
|
+
/>
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
info
|
|
517
|
+
infoTooltip={FEE_INFO.minDeposit}
|
|
518
|
+
variant="warning"
|
|
519
|
+
/>
|
|
520
|
+
</Wrapper>
|
|
521
|
+
}
|
|
522
|
+
/>
|
|
523
|
+
|
|
524
|
+
{newDeposits.length > 0 ? (
|
|
525
|
+
<PendingDeposits
|
|
526
|
+
deposits={newDeposits}
|
|
527
|
+
estimatedFees={estimatedFees}
|
|
528
|
+
config={config}
|
|
529
|
+
{...(onSelectDeposit && { onSelectDeposit })}
|
|
530
|
+
/>
|
|
531
|
+
) : (
|
|
532
|
+
<LoadingCard
|
|
533
|
+
text={
|
|
534
|
+
source
|
|
535
|
+
? `Watching for your deposit on ${source.chain.name}…`
|
|
536
|
+
: 'Watching for your deposit…'
|
|
537
|
+
}
|
|
538
|
+
/>
|
|
539
|
+
)}
|
|
540
|
+
|
|
541
|
+
{pastDepositsCount > 0 &&
|
|
542
|
+
(onViewPastDeposits ? (
|
|
543
|
+
<button
|
|
544
|
+
type="button"
|
|
545
|
+
onClick={onViewPastDeposits}
|
|
546
|
+
className="zd:flex zd:w-full zd:items-center zd:gap-2 zd:px-4 zd:py-4 zd:cursor-pointer"
|
|
547
|
+
>
|
|
548
|
+
<Icon
|
|
549
|
+
name="clock"
|
|
550
|
+
className="zd:size-4 zd:text-greyScale/50"
|
|
551
|
+
aria-hidden
|
|
552
|
+
/>
|
|
553
|
+
<Text className="zd:flex-1 zd:text-left zd:text-body1">
|
|
554
|
+
Past deposits ({pastDepositsCount})
|
|
555
|
+
</Text>
|
|
556
|
+
<Icon
|
|
557
|
+
name="chevronRight"
|
|
558
|
+
className="zd:size-4 zd:text-greyScale/50"
|
|
559
|
+
aria-hidden
|
|
560
|
+
/>
|
|
561
|
+
</button>
|
|
562
|
+
) : (
|
|
563
|
+
<div className="zd:flex zd:w-full zd:items-center zd:gap-2 zd:px-4 zd:py-4">
|
|
564
|
+
<Icon
|
|
565
|
+
name="clock"
|
|
566
|
+
className="zd:size-4 zd:text-greyScale/50"
|
|
567
|
+
aria-hidden
|
|
568
|
+
/>
|
|
569
|
+
<Text className="zd:flex-1 zd:text-left zd:text-body1">
|
|
570
|
+
Past deposits ({pastDepositsCount})
|
|
571
|
+
</Text>
|
|
572
|
+
<Icon
|
|
573
|
+
name="chevronRight"
|
|
574
|
+
className="zd:size-4 zd:text-greyScale/50"
|
|
575
|
+
aria-hidden
|
|
576
|
+
/>
|
|
577
|
+
</div>
|
|
578
|
+
))}
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function CardTitle({ children }: { children: string }) {
|
|
585
|
+
return (
|
|
586
|
+
<div className={cn('zd:flex zd:items-center zd:px-2 zd:py-3')}>
|
|
587
|
+
<Text className="zd:whitespace-nowrap zd:text-h3">{children}</Text>
|
|
588
|
+
</div>
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function PillRow({
|
|
593
|
+
left,
|
|
594
|
+
right,
|
|
595
|
+
}: {
|
|
596
|
+
left: React.ReactNode
|
|
597
|
+
right: React.ReactNode
|
|
598
|
+
}) {
|
|
599
|
+
return (
|
|
600
|
+
<div className="zd:flex zd:w-full zd:items-start zd:gap-1">
|
|
601
|
+
<div className="zd:min-w-0 zd:flex-1">{left}</div>
|
|
602
|
+
<div className="zd:min-w-0 zd:flex-1">{right}</div>
|
|
603
|
+
</div>
|
|
604
|
+
)
|
|
605
|
+
}
|