Skip to content

Commit 6462aed

Browse files
authored
Scaffold Long form in new pool details page (#1452)
* Add variant to TokenInput2 * Move focus to the outter div a la uniswap * Make OpenLongForm2 * Cleanup markup
1 parent d023ef8 commit 6462aed

File tree

5 files changed

+385
-24
lines changed

5 files changed

+385
-24
lines changed

apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ interface OpenLongFormProps {
3939
onOpenLong?: (e: MouseEvent<HTMLButtonElement>) => void;
4040
}
4141

42+
/**
43+
*
44+
* @deprecated use OpenLongForm2 instead
45+
*/
4246
export function OpenLongForm({
4347
hyperdrive: hyperdrive,
4448
onOpenLong,
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import { fixed } from "@delvtech/fixed-point-wasm";
2+
import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js-core";
3+
import {
4+
findBaseToken,
5+
findToken,
6+
HyperdriveConfig,
7+
} from "@hyperdrive/appconfig";
8+
import { MouseEvent, ReactElement } from "react";
9+
import { isTestnetChain } from "src/chains/isTestnetChain";
10+
import { getIsValidTradeSize } from "src/hyperdrive/getIsValidTradeSize";
11+
import { getHasEnoughAllowance } from "src/token/getHasEnoughAllowance";
12+
import { getHasEnoughBalance } from "src/token/getHasEnoughBalance";
13+
import { useAppConfig } from "src/ui/appconfig/useAppConfig";
14+
import { ConnectWalletButton } from "src/ui/base/components/ConnectWallet";
15+
import { LoadingButton } from "src/ui/base/components/LoadingButton";
16+
import { formatBalance } from "src/ui/base/formatting/formatBalance";
17+
import { useNumericInput } from "src/ui/base/hooks/useNumericInput";
18+
import { usePoolInfo } from "src/ui/hyperdrive/hooks/usePoolInfo";
19+
import { useMaxLong } from "src/ui/hyperdrive/longs/hooks/useMaxLong";
20+
import { useOpenLong } from "src/ui/hyperdrive/longs/hooks/useOpenLong";
21+
import { usePreviewOpenLong } from "src/ui/hyperdrive/longs/hooks/usePreviewOpenLong";
22+
import { OpenLongPreview } from "src/ui/hyperdrive/longs/OpenLongPreview/OpenLongPreview";
23+
import { OpenLongStats } from "src/ui/hyperdrive/longs/OpenLongPreview/OpenLongStats";
24+
import { TransactionView } from "src/ui/hyperdrive/TransactionView";
25+
import { ApproveTokenChoices } from "src/ui/token/ApproveTokenChoices";
26+
import { useActiveToken } from "src/ui/token/hooks/useActiveToken";
27+
import { useSlippageSettings } from "src/ui/token/hooks/useSlippageSettings";
28+
import { useTokenAllowance } from "src/ui/token/hooks/useTokenAllowance";
29+
import { useTokenBalance } from "src/ui/token/hooks/useTokenBalance";
30+
import { useTokenFiatPrice } from "src/ui/token/hooks/useTokenFiatPrice";
31+
import { SlippageSettingsTwo } from "src/ui/token/SlippageSettingsTwo";
32+
import { TokenInputTwo } from "src/ui/token/TokenInputTwo";
33+
import { TokenChoice, TokenPickerTwo } from "src/ui/token/TokenPickerTwo";
34+
import { formatUnits } from "viem";
35+
import { useAccount, useChainId } from "wagmi";
36+
37+
interface OpenLongFormProps {
38+
hyperdrive: HyperdriveConfig;
39+
onOpenLong?: (e: MouseEvent<HTMLButtonElement>) => void;
40+
}
41+
42+
export function OpenLongForm2({
43+
hyperdrive: hyperdrive,
44+
onOpenLong,
45+
}: OpenLongFormProps): ReactElement {
46+
const { address: account } = useAccount();
47+
const chainId = useChainId();
48+
49+
const appConfig = useAppConfig();
50+
const { poolInfo } = usePoolInfo({
51+
hyperdriveAddress: hyperdrive.address,
52+
chainId: hyperdrive.chainId,
53+
});
54+
const baseToken = findBaseToken({
55+
hyperdriveChainId: hyperdrive.chainId,
56+
hyperdriveAddress: hyperdrive.address,
57+
appConfig,
58+
});
59+
60+
const { balance: baseTokenBalance } = useTokenBalance({
61+
account,
62+
tokenAddress: baseToken.address,
63+
decimals: baseToken.decimals,
64+
});
65+
66+
const { balance: sharesTokenBalance } = useTokenBalance({
67+
account,
68+
tokenAddress: hyperdrive.poolConfig.vaultSharesToken,
69+
decimals: hyperdrive.decimals,
70+
});
71+
72+
const tokenChoices: TokenChoice[] = [];
73+
if (baseToken && hyperdrive.depositOptions.isBaseTokenDepositEnabled) {
74+
tokenChoices.push({
75+
tokenConfig: baseToken,
76+
tokenBalance: baseTokenBalance?.value,
77+
});
78+
}
79+
80+
const sharesToken = findToken({
81+
chainId: hyperdrive.chainId,
82+
tokens: appConfig.tokens,
83+
tokenAddress: hyperdrive.poolConfig.vaultSharesToken,
84+
});
85+
86+
if (sharesToken && hyperdrive.depositOptions.isShareTokenDepositsEnabled) {
87+
tokenChoices.push({
88+
tokenConfig: sharesToken,
89+
tokenBalance: sharesTokenBalance?.value,
90+
});
91+
}
92+
93+
const { activeToken, activeTokenBalance, setActiveToken, isActiveTokenEth } =
94+
useActiveToken({
95+
account,
96+
defaultActiveToken: hyperdrive.depositOptions.isBaseTokenDepositEnabled
97+
? baseToken.address
98+
: hyperdrive.poolConfig.vaultSharesToken,
99+
tokens: tokenChoices.map((token) => token.tokenConfig),
100+
});
101+
const { fiatPrice: activeTokenPrice } = useTokenFiatPrice({
102+
tokenAddress: activeToken.address,
103+
chainId: activeToken.chainId,
104+
});
105+
// All tokens besides ETH require an allowance to spend it on hyperdrive
106+
const requiresAllowance = !isActiveTokenEth;
107+
const { tokenAllowance: activeTokenAllowance } = useTokenAllowance({
108+
account,
109+
enabled: requiresAllowance,
110+
spender: hyperdrive.address,
111+
tokenAddress: activeToken.address,
112+
tokenChainId: activeToken.chainId,
113+
});
114+
115+
const {
116+
amount: depositAmount,
117+
amountAsBigInt: depositAmountAsBigInt,
118+
setAmount,
119+
} = useNumericInput({
120+
decimals: activeToken.decimals,
121+
});
122+
123+
const hasEnoughAllowance = getHasEnoughAllowance({
124+
requiresAllowance,
125+
allowance: activeTokenAllowance,
126+
amount: depositAmountAsBigInt,
127+
});
128+
const hasEnoughBalance = getHasEnoughBalance({
129+
balance: activeTokenBalance?.value,
130+
amount: depositAmountAsBigInt,
131+
});
132+
133+
const { maxBaseIn, maxSharesIn, maxBondsOut } = useMaxLong({
134+
hyperdriveAddress: hyperdrive.address,
135+
chainId: hyperdrive.chainId,
136+
});
137+
const activeTokenMaxTradeSize =
138+
activeToken.address === baseToken.address ? maxBaseIn : maxSharesIn;
139+
140+
const hasEnoughLiquidity = getIsValidTradeSize({
141+
tradeAmount: depositAmountAsBigInt,
142+
maxTradeSize: activeTokenMaxTradeSize,
143+
});
144+
145+
const {
146+
bondsReceived,
147+
spotRateAfterOpen,
148+
curveFee,
149+
status: openLongPreviewStatus,
150+
} = usePreviewOpenLong({
151+
chainId: hyperdrive.chainId,
152+
hyperdriveAddress: hyperdrive.address,
153+
amountIn: depositAmountAsBigInt,
154+
asBase: activeToken.address === baseToken.address,
155+
});
156+
157+
const {
158+
setSlippage,
159+
slippage,
160+
slippageAsBigInt,
161+
activeOption: activeSlippageOption,
162+
setActiveOption: setActiveSlippageOption,
163+
} = useSlippageSettings({ decimals: activeToken.decimals });
164+
165+
const bondsReceivedAfterSlippage =
166+
bondsReceived &&
167+
adjustAmountByPercentage({
168+
amount: bondsReceived,
169+
percentage: slippageAsBigInt,
170+
decimals: activeToken.decimals,
171+
direction: "down",
172+
});
173+
174+
const { openLong, openLongStatus } = useOpenLong({
175+
chainId: hyperdrive.chainId,
176+
hyperdriveAddress: hyperdrive.address,
177+
asBase: activeToken.address === baseToken.address,
178+
amount: depositAmountAsBigInt,
179+
ethValue: isActiveTokenEth ? depositAmountAsBigInt : undefined,
180+
minBondsOut: bondsReceivedAfterSlippage,
181+
minSharePrice: poolInfo?.vaultSharePrice,
182+
destination: account,
183+
enabled: openLongPreviewStatus === "success" && hasEnoughAllowance,
184+
onSubmitted: () => {
185+
(document.getElementById("open-long") as HTMLDialogElement).close();
186+
},
187+
onExecuted: () => {
188+
setAmount("");
189+
},
190+
});
191+
192+
// Max button is wired up to the user's balance, or the pool's max long.
193+
// Whichever is smallest.
194+
let maxButtonValue = "0";
195+
if (activeTokenBalance && activeTokenMaxTradeSize) {
196+
maxButtonValue = formatUnits(
197+
activeTokenBalance.value > activeTokenMaxTradeSize
198+
? activeTokenMaxTradeSize
199+
: activeTokenBalance?.value,
200+
activeToken.decimals,
201+
);
202+
}
203+
204+
return (
205+
<TransactionView
206+
tokenInput={
207+
<TokenInputTwo
208+
variant="lighter"
209+
settings={
210+
<SlippageSettingsTwo
211+
onSlippageChange={setSlippage}
212+
slippage={slippage}
213+
activeOption={activeSlippageOption}
214+
onActiveOptionChange={setActiveSlippageOption}
215+
tooltip="Your transaction will revert if the price changes unfavorably by more than this percentage."
216+
/>
217+
}
218+
name={activeToken.symbol}
219+
token={
220+
<TokenPickerTwo
221+
tokens={tokenChoices}
222+
activeTokenAddress={activeToken.address}
223+
onChange={(tokenAddress) => {
224+
setActiveToken(tokenAddress);
225+
setAmount("0");
226+
}}
227+
/>
228+
}
229+
value={depositAmount ?? ""}
230+
maxValue={maxButtonValue}
231+
inputLabel="You spend"
232+
bottomLeftElement={
233+
// Defillama fetches the token price via {chain}:{tokenAddress}. Since the token address differs on testnet, price display is disabled there.
234+
!isTestnetChain(chainId) ? (
235+
<label className="text-sm text-neutral-content">
236+
{`$${formatBalance({
237+
balance:
238+
activeTokenPrice && depositAmountAsBigInt
239+
? fixed(depositAmountAsBigInt, activeToken.decimals).mul(
240+
activeTokenPrice,
241+
18, // prices are always in 18 decimals
242+
).bigint
243+
: 0n,
244+
decimals: activeToken.decimals,
245+
places: 2,
246+
})}`}
247+
</label>
248+
) : null
249+
}
250+
bottomRightElement={
251+
<div className="flex flex-col gap-1 text-xs text-neutral-content">
252+
<span>
253+
{activeTokenBalance
254+
? `Balance: ${formatBalance({
255+
balance: activeTokenBalance?.value,
256+
decimals: activeToken.decimals,
257+
places: activeToken.places,
258+
})}`
259+
: undefined}
260+
</span>
261+
</div>
262+
}
263+
onChange={(newAmount) => setAmount(newAmount)}
264+
/>
265+
}
266+
primaryStats={
267+
<OpenLongStats
268+
hyperdrive={hyperdrive}
269+
activeToken={activeToken}
270+
amountPaid={depositAmountAsBigInt || 0n}
271+
bondAmount={bondsReceived || 0n}
272+
openLongPreviewStatus={openLongPreviewStatus}
273+
asBase={activeToken.address === baseToken.address}
274+
vaultSharePrice={poolInfo?.vaultSharePrice}
275+
/>
276+
}
277+
transactionPreview={
278+
<OpenLongPreview
279+
hyperdrive={hyperdrive}
280+
spotRateAfterOpen={spotRateAfterOpen}
281+
curveFee={curveFee}
282+
activeToken={activeToken}
283+
amountPaid={depositAmountAsBigInt || 0n}
284+
bondAmount={bondsReceived || 0n}
285+
openLongPreviewStatus={openLongPreviewStatus}
286+
asBase={activeToken.address === baseToken.address}
287+
vaultSharePrice={poolInfo?.vaultSharePrice}
288+
/>
289+
}
290+
disclaimer={(() => {
291+
if (!!depositAmountAsBigInt && !hasEnoughLiquidity) {
292+
return (
293+
<p className="text-center text-sm text-error">
294+
Pool limit exceeded. Max long size is{" "}
295+
{formatBalance({
296+
balance: maxBondsOut || 0n,
297+
decimals: baseToken.decimals,
298+
places: baseToken.places,
299+
})}{" "}
300+
hy{baseToken.symbol}
301+
</p>
302+
);
303+
}
304+
if (!!depositAmountAsBigInt && !hasEnoughBalance) {
305+
return (
306+
<p className="text-center text-sm text-error">
307+
Insufficient balance
308+
</p>
309+
);
310+
}
311+
})()}
312+
actionButton={(() => {
313+
if (!account) {
314+
return <ConnectWalletButton />;
315+
}
316+
317+
if (!hasEnoughBalance || !hasEnoughLiquidity) {
318+
return (
319+
<button
320+
disabled
321+
className="daisy-btn daisy-btn-circle daisy-btn-primary w-full disabled:bg-primary disabled:text-base-100 disabled:opacity-30"
322+
>
323+
Open Long
324+
</button>
325+
);
326+
}
327+
328+
if (!hasEnoughAllowance) {
329+
return (
330+
<ApproveTokenChoices
331+
spender={hyperdrive.address}
332+
token={activeToken}
333+
amountAsBigInt={depositAmountAsBigInt}
334+
amount={depositAmount}
335+
/>
336+
);
337+
}
338+
339+
if (openLongStatus === "loading") {
340+
return <LoadingButton label="Opening Long" variant="primary" />;
341+
}
342+
343+
return (
344+
<button
345+
disabled={!openLong}
346+
className="daisy-btn daisy-btn-circle daisy-btn-primary w-full disabled:bg-primary disabled:text-base-100 disabled:opacity-30"
347+
onClick={(e) => {
348+
openLong?.();
349+
onOpenLong?.(e);
350+
}}
351+
>
352+
Open Long
353+
</button>
354+
);
355+
})()}
356+
/>
357+
);
358+
}

0 commit comments

Comments
 (0)