|
1 | | -import { fixed } from "@delvtech/fixed-point-wasm"; |
2 | | -import { ClockIcon } from "@heroicons/react/24/outline"; |
3 | | -import { HyperdriveConfig, findBaseToken } from "@hyperdrive/appconfig"; |
4 | | -import { Link, useNavigate } from "@tanstack/react-router"; |
5 | | -import classNames from "classnames"; |
6 | | -import { ReactElement, ReactNode } from "react"; |
7 | | -import Skeleton from "react-loading-skeleton"; |
8 | | -import { formatRate } from "src/base/formatRate"; |
9 | | -import { isTestnetChain } from "src/chains/isTestnetChain"; |
10 | | -import { useAppConfig } from "src/ui/appconfig/useAppConfig"; |
11 | | -import { Well } from "src/ui/base/components/Well/Well"; |
12 | | -import { formatCompact } from "src/ui/base/formatting/formatCompact"; |
13 | | -import { useIsNewPool } from "src/ui/hyperdrive/hooks/useIsNewPool"; |
14 | | -import { useLpApy } from "src/ui/hyperdrive/hooks/useLpApy"; |
15 | | -import { usePresentValue } from "src/ui/hyperdrive/hooks/usePresentValue"; |
16 | | -import { useFixedRate } from "src/ui/hyperdrive/longs/hooks/useFixedRate"; |
| 1 | +import { ReactElement } from "react"; |
17 | 2 | import { Hero } from "src/ui/landing/Hero/Hero"; |
18 | | -import { AssetStack } from "src/ui/markets/AssetStack"; |
19 | | -import { formatTermLength2 } from "src/ui/markets/formatTermLength"; |
20 | | -import { MARKET_DETAILS_ROUTE } from "src/ui/markets/routes"; |
21 | | -import { RewardsTooltip } from "src/ui/rewards/RewardsTooltip"; |
22 | | -import { useTokenFiatPrice } from "src/ui/token/hooks/useTokenFiatPrice"; |
23 | | -import { useYieldSourceRate } from "src/ui/vaults/useYieldSourceRate"; |
| 3 | +import { PoolRows } from "src/ui/markets/PoolRow/PoolRows"; |
24 | 4 |
|
25 | 5 | export function Landing(): ReactElement | null { |
26 | 6 | return ( |
@@ -49,306 +29,3 @@ export function Landing(): ReactElement | null { |
49 | 29 | </div> |
50 | 30 | ); |
51 | 31 | } |
52 | | - |
53 | | -function PoolRows() { |
54 | | - const appConfig = useAppConfig(); |
55 | | - return ( |
56 | | - <div className="flex w-full flex-col gap-5"> |
57 | | - { |
58 | | - // Show the newest pools first |
59 | | - [...appConfig.hyperdrives] |
60 | | - .sort( |
61 | | - (a, b) => |
62 | | - Number(b.initializationBlock) - Number(a.initializationBlock), |
63 | | - ) |
64 | | - .map((hyperdrive) => ( |
65 | | - <PoolRow |
66 | | - // Combine address and chainId for a unique key, as addresses may overlap across chains (e.g. cloudchain and mainnet) |
67 | | - key={`${hyperdrive.address}-${hyperdrive.chainId}`} |
68 | | - hyperdrive={hyperdrive} |
69 | | - /> |
70 | | - )) |
71 | | - } |
72 | | - </div> |
73 | | - ); |
74 | | -} |
75 | | -function PoolRow({ hyperdrive }: { hyperdrive: HyperdriveConfig }) { |
76 | | - const navigate = useNavigate(); |
77 | | - const appConfig = useAppConfig(); |
78 | | - const { yieldSources, chains } = appConfig; |
79 | | - |
80 | | - const chainInfo = chains[hyperdrive.chainId]; |
81 | | - const { fixedApr, fixedRateStatus } = useFixedRate({ |
82 | | - chainId: hyperdrive.chainId, |
83 | | - hyperdriveAddress: hyperdrive.address, |
84 | | - }); |
85 | | - const { vaultRate, vaultRateStatus } = useYieldSourceRate({ |
86 | | - chainId: hyperdrive.chainId, |
87 | | - hyperdriveAddress: hyperdrive.address, |
88 | | - }); |
89 | | - const { lpApy, lpApyStatus } = useLpApy({ |
90 | | - hyperdriveAddress: hyperdrive.address, |
91 | | - chainId: hyperdrive.chainId, |
92 | | - }); |
93 | | - |
94 | | - // if the pool was deployed less than one historical period ago, it's new. |
95 | | - const isYoungerThanOneDay = useIsNewPool({ hyperdrive }); |
96 | | - |
97 | | - const isLpApyNew = |
98 | | - isYoungerThanOneDay || (lpApyStatus !== "loading" && lpApy === undefined); |
99 | | - |
100 | | - // Display TVL as base value on testnet due to lack of reliable fiat pricing. |
101 | | - // On mainnet and others, use DeFiLlama's fiat price. |
102 | | - const { presentValue } = usePresentValue({ |
103 | | - chainId: hyperdrive.chainId, |
104 | | - hyperdriveAddress: hyperdrive.address, |
105 | | - }); |
106 | | - const isFiatPriceEnabled = !isTestnetChain(chainInfo.id); |
107 | | - const baseToken = findBaseToken({ |
108 | | - hyperdriveChainId: hyperdrive.chainId, |
109 | | - hyperdriveAddress: hyperdrive.address, |
110 | | - appConfig, |
111 | | - }); |
112 | | - const { fiatPrice } = useTokenFiatPrice({ |
113 | | - chainId: baseToken.chainId, |
114 | | - tokenAddress: isFiatPriceEnabled |
115 | | - ? hyperdrive.poolConfig.baseToken |
116 | | - : undefined, |
117 | | - }); |
118 | | - let tvlLabel = `${formatCompact({ |
119 | | - value: presentValue || 0n, |
120 | | - decimals: hyperdrive.decimals, |
121 | | - })} ${baseToken.symbol}`; |
122 | | - |
123 | | - if (isFiatPriceEnabled) { |
124 | | - const presentValueFiat = |
125 | | - presentValue && fiatPrice && isFiatPriceEnabled |
126 | | - ? fixed(presentValue, hyperdrive.decimals).mul(fiatPrice).bigint |
127 | | - : 0n; |
128 | | - tvlLabel = `$${formatCompact({ |
129 | | - value: presentValueFiat || 0n, |
130 | | - decimals: hyperdrive.decimals, |
131 | | - })}`; |
132 | | - } |
133 | | - |
134 | | - return ( |
135 | | - <Well |
136 | | - block |
137 | | - onClick={() => { |
138 | | - navigate({ |
139 | | - to: MARKET_DETAILS_ROUTE, |
140 | | - resetScroll: true, |
141 | | - params: { |
142 | | - address: hyperdrive.address, |
143 | | - chainId: hyperdrive.chainId.toString(), |
144 | | - }, |
145 | | - }); |
146 | | - }} |
147 | | - > |
148 | | - <div className="flex flex-col justify-between gap-6 lg:flex-row lg:gap-4"> |
149 | | - {/* Left side */} |
150 | | - <div className="flex items-center gap-6 lg:w-[440px]"> |
151 | | - <div |
152 | | - className={ |
153 | | - // Set a fixed width so pools with one or two asset icons still |
154 | | - // line up |
155 | | - "w-16" |
156 | | - } |
157 | | - > |
158 | | - <AssetStack hyperdriveAddress={hyperdrive.address} /> |
159 | | - </div> |
160 | | - <div className="flex flex-col gap-1"> |
161 | | - <h4 className="text-left"> |
162 | | - {yieldSources[hyperdrive.yieldSource].shortName} |
163 | | - </h4> |
164 | | - <div className="flex flex-wrap gap-5 gap-y-2"> |
165 | | - <div className="flex items-center gap-1.5 text-sm"> |
166 | | - <ClockIcon className="size-4 text-gray-500" />{" "} |
167 | | - <span className="text-neutral-content"> |
168 | | - {formatTermLength2( |
169 | | - Number(hyperdrive.poolConfig.positionDuration * 1000n), |
170 | | - )} |
171 | | - </span> |
172 | | - </div> |
173 | | - <div className="flex items-center gap-1.5 text-sm"> |
174 | | - <span className="text-gray-500">TVL</span>{" "} |
175 | | - <span className="font-dmMono text-neutral-content"> |
176 | | - {tvlLabel} |
177 | | - </span> |
178 | | - </div> |
179 | | - <div className="flex items-center gap-1.5 text-sm"> |
180 | | - <img className="size-4 rounded-full" src={chainInfo.iconUrl} /> |
181 | | - <span className="text-neutral-content">{chainInfo.name}</span> |
182 | | - </div> |
183 | | - </div> |
184 | | - </div> |
185 | | - </div> |
186 | | - |
187 | | - {/* Right side */} |
188 | | - <div className="flex shrink-0 justify-between gap-10 lg:items-end lg:justify-start"> |
189 | | - <PoolStat |
190 | | - label={"Fixed APR"} |
191 | | - isLoading={fixedRateStatus === "loading"} |
192 | | - value={ |
193 | | - fixedApr ? ( |
194 | | - <PercentLabel value={formatRate(fixedApr.apr, 18, false)} /> |
195 | | - ) : ( |
196 | | - "-" |
197 | | - ) |
198 | | - } |
199 | | - variant="gradient" |
200 | | - action={ |
201 | | - <Link |
202 | | - to="/market/$chainId/$address" |
203 | | - params={{ |
204 | | - address: hyperdrive.address, |
205 | | - chainId: hyperdrive.chainId.toString(), |
206 | | - }} |
207 | | - search={{ position: "longs" }} |
208 | | - className="daisy-btn daisy-btn-sm rounded-full bg-gray-600" |
209 | | - onClick={(e) => { |
210 | | - e.stopPropagation(); |
211 | | - }} |
212 | | - > |
213 | | - Long |
214 | | - </Link> |
215 | | - } |
216 | | - /> |
217 | | - <PoolStat |
218 | | - label={"Variable APY"} |
219 | | - isNew={isLpApyNew} |
220 | | - isLoading={vaultRateStatus === "loading"} |
221 | | - value={ |
222 | | - vaultRate ? ( |
223 | | - <RewardsTooltip |
224 | | - hyperdriveAddress={hyperdrive.address} |
225 | | - chainId={hyperdrive.chainId} |
226 | | - positionType="short" |
227 | | - > |
228 | | - <PercentLabel |
229 | | - value={formatRate(vaultRate.vaultRate, 18, false)} |
230 | | - /> |
231 | | - </RewardsTooltip> |
232 | | - ) : ( |
233 | | - "-" |
234 | | - ) |
235 | | - } |
236 | | - action={ |
237 | | - <Link |
238 | | - to="/market/$chainId/$address" |
239 | | - params={{ |
240 | | - address: hyperdrive.address, |
241 | | - chainId: hyperdrive.chainId.toString(), |
242 | | - }} |
243 | | - search={{ position: "shorts" }} |
244 | | - className="daisy-btn daisy-btn-sm rounded-full bg-gray-600" |
245 | | - onClick={(e) => { |
246 | | - e.stopPropagation(); |
247 | | - }} |
248 | | - > |
249 | | - Short |
250 | | - </Link> |
251 | | - } |
252 | | - /> |
253 | | - <PoolStat |
254 | | - label={`LP APY (${yieldSources[hyperdrive.yieldSource].historicalRatePeriod}d)`} |
255 | | - isLoading={lpApyStatus === "loading"} |
256 | | - isNew={isLpApyNew} |
257 | | - value={ |
258 | | - // TODO: Fix useLpApy to have the same interface as |
259 | | - // useYieldSourceRate and useFixedRate |
260 | | - lpApy && !isLpApyNew ? ( |
261 | | - <RewardsTooltip |
262 | | - positionType="lp" |
263 | | - chainId={hyperdrive.chainId} |
264 | | - hyperdriveAddress={hyperdrive.address} |
265 | | - > |
266 | | - <PercentLabel value={`${(lpApy * 100).toFixed(2)}`} /> |
267 | | - </RewardsTooltip> |
268 | | - ) : ( |
269 | | - "-" |
270 | | - ) |
271 | | - } |
272 | | - action={ |
273 | | - <Link |
274 | | - to="/market/$chainId/$address" |
275 | | - params={{ |
276 | | - address: hyperdrive.address, |
277 | | - chainId: hyperdrive.chainId.toString(), |
278 | | - }} |
279 | | - search={{ position: "lp" }} |
280 | | - className="daisy-btn daisy-btn-sm rounded-full bg-gray-600" |
281 | | - onClick={(e) => { |
282 | | - e.stopPropagation(); |
283 | | - }} |
284 | | - > |
285 | | - Supply |
286 | | - </Link> |
287 | | - } |
288 | | - /> |
289 | | - </div> |
290 | | - </div> |
291 | | - </Well> |
292 | | - ); |
293 | | -} |
294 | | - |
295 | | -function PoolStat({ |
296 | | - label, |
297 | | - labelTooltip, |
298 | | - value, |
299 | | - isNew = false, |
300 | | - variant = "default", |
301 | | - isLoading = false, |
302 | | - action, |
303 | | -}: { |
304 | | - label: string; |
305 | | - labelTooltip?: string; |
306 | | - value: ReactNode; |
307 | | - isLoading?: boolean; |
308 | | - isNew?: boolean; |
309 | | - variant?: "default" | "gradient"; |
310 | | - action?: ReactNode; |
311 | | -}): ReactElement { |
312 | | - let displayValue; |
313 | | - if (isLoading) { |
314 | | - displayValue = <Skeleton width={70} />; |
315 | | - } else if (isNew) { |
316 | | - displayValue = "✨New✨"; |
317 | | - } else { |
318 | | - displayValue = value; |
319 | | - } |
320 | | - |
321 | | - return ( |
322 | | - <div className="flex w-24 flex-col items-start gap-1.5"> |
323 | | - <p |
324 | | - data-tip={labelTooltip} |
325 | | - className={ |
326 | | - "group daisy-tooltip cursor-help text-sm text-neutral-content before:z-40 before:max-w-56 before:p-2 before:text-start" |
327 | | - } |
328 | | - > |
329 | | - {label} |
330 | | - </p> |
331 | | - <div |
332 | | - className={classNames("font-dmMono text-h4 font-medium", { |
333 | | - "gradient-text": variant === "gradient", |
334 | | - })} |
335 | | - > |
336 | | - {displayValue} |
337 | | - </div> |
338 | | - <div>{action}</div> |
339 | | - </div> |
340 | | - ); |
341 | | -} |
342 | | - |
343 | | -function PercentLabel({ value }: { value: string }) { |
344 | | - return ( |
345 | | - <div |
346 | | - className={classNames( |
347 | | - "font-dmMono text-h4 font-medium", |
348 | | - "after:text-h5 after:content-['%']", |
349 | | - )} |
350 | | - > |
351 | | - {value} |
352 | | - </div> |
353 | | - ); |
354 | | -} |
0 commit comments