diff --git a/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx b/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx index 340be6f44..3153bbb79 100644 --- a/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx @@ -19,6 +19,9 @@ export function DevtoolsMenu(): ReactElement { Bridge Assets + + New Open Long Form + { throw new Error( diff --git a/apps/hyperdrive-trading/src/ui/base/components/PrimaryStat.tsx b/apps/hyperdrive-trading/src/ui/base/components/PrimaryStat.tsx new file mode 100644 index 000000000..20c3da2fa --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/base/components/PrimaryStat.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +export function PrimaryStat({ + label, + value, + valueUnit, + subValue, + valueClassName, +}: { + label: string; + value: ReactNode; + valueUnit: string; + subValue?: ReactNode; + valueClassName?: string; +}): JSX.Element { + return ( +
+

{label}

+
+

{value}

+

{valueUnit}

+
+ {subValue &&

{subValue}

} +
+ ); +} diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/TransactionView.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/TransactionView.tsx index 0160c75b4..4298181e3 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/TransactionView.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/TransactionView.tsx @@ -5,6 +5,7 @@ interface TransactionViewProps { tokenInput: ReactNode; setting?: ReactNode; + primaryStats?: ReactNode; transactionPreview: ReactNode; disclaimer?: ReactNode; @@ -15,6 +16,7 @@ export function TransactionView({ heading, tokenInput, setting, + primaryStats, transactionPreview, disclaimer, actionButton, @@ -27,11 +29,11 @@ export function TransactionView({ {setting}
+ {primaryStats}
Preview transaction
{transactionPreview}
-
{actionButton}
{disclaimer ?
{disclaimer}
: null}
diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx index ff0c76922..8b23a16ca 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx @@ -21,6 +21,7 @@ import { useMaxLong } from "src/ui/hyperdrive/longs/hooks/useMaxLong"; import { useOpenLong } from "src/ui/hyperdrive/longs/hooks/useOpenLong"; import { usePreviewOpenLong } from "src/ui/hyperdrive/longs/hooks/usePreviewOpenLong"; import { OpenLongPreview } from "src/ui/hyperdrive/longs/OpenLongPreview/OpenLongPreview"; +import { OpenLongStats } from "src/ui/hyperdrive/longs/OpenLongPreview/OpenLongStats"; import { TransactionView } from "src/ui/hyperdrive/TransactionView"; import { ApproveTokenChoices } from "src/ui/token/ApproveTokenChoices"; import { useActiveToken } from "src/ui/token/hooks/useActiveToken"; @@ -28,8 +29,11 @@ import { useSlippageSettings } from "src/ui/token/hooks/useSlippageSettings"; import { useTokenAllowance } from "src/ui/token/hooks/useTokenAllowance"; import { useTokenBalance } from "src/ui/token/hooks/useTokenBalance"; import { SlippageSettings } from "src/ui/token/SlippageSettings"; +import { SlippageSettingsTwo } from "src/ui/token/SlippageSettingsTwo"; import { TokenInput } from "src/ui/token/TokenInput"; +import { TokenInputTwo } from "src/ui/token/TokenInputTwo"; import { TokenPicker } from "src/ui/token/TokenPicker"; +import { TokenPickerTwo } from "src/ui/token/TokenPickerTwo"; import { formatUnits } from "viem"; import { useAccount } from "wagmi"; interface OpenLongFormProps { @@ -45,6 +49,8 @@ export function OpenLongForm({ }: OpenLongFormProps): ReactElement { const { address: account } = useAccount(); const { isFlagEnabled: isBridgingEnabled } = useFeatureFlag("bridge"); + const { isFlagEnabled: isNewOpenLongFormEnabled } = + useFeatureFlag("new-open-long-form"); const appConfig = useAppConfig(); const { poolInfo } = usePoolInfo({ hyperdriveAddress: hyperdrive.address }); @@ -210,54 +216,109 @@ export function OpenLongForm({ return ( - } - name={activeToken.symbol} - token={ - { - setActiveToken(tokenAddress); - setAmount("0"); - }} - joined={true} - /> - } - value={depositAmount ?? ""} - maxValue={maxButtonValue} - inputLabel="Amount to spend" - stat={ -
- - {activeTokenBalance - ? `Balance: ${formatBalance({ - balance: activeTokenBalance?.value, - decimals: activeToken.decimals, - places: activeToken.places, - })} ${activeToken.symbol}` - : undefined} - - {`Slippage: ${slippage || "0.5"}%`} -
- } - onChange={(newAmount) => setAmount(newAmount)} - /> + isNewOpenLongFormEnabled ? ( + + } + name={activeToken.symbol} + token={ + { + setActiveToken(tokenAddress); + setAmount("0"); + }} + /> + } + value={depositAmount ?? ""} + maxValue={maxButtonValue} + inputLabel="You spend" + stat={ +
+ + {activeTokenBalance + ? `Balance: ${formatBalance({ + balance: activeTokenBalance?.value, + decimals: activeToken.decimals, + places: activeToken.places, + })}` + : undefined} + +
+ } + onChange={(newAmount) => setAmount(newAmount)} + /> + ) : ( + + } + name={activeToken.symbol} + token={ + { + setActiveToken(tokenAddress); + setAmount("0"); + }} + joined={true} + /> + } + value={depositAmount ?? ""} + maxValue={maxButtonValue} + inputLabel="Amount to spend" + stat={ +
+ + {activeTokenBalance + ? `Balance: ${formatBalance({ + balance: activeTokenBalance?.value, + decimals: activeToken.decimals, + places: activeToken.places, + })} ${activeToken.symbol}` + : undefined} + + {`Slippage: ${slippage || "0.5"}%`} +
+ } + onChange={(newAmount) => setAmount(newAmount)} + /> + ) } setting={ isBridgingEnabled && hasBridgeableBalance ? switchToBridgeUIButton : null } + primaryStats={ + isNewOpenLongFormEnabled ? ( + + ) : null + } transactionPreview={ + ); + } return ( ; + asBase: boolean; + vaultSharePrice: bigint | undefined; +} +export function OpenLongStats({ + hyperdrive, + openLongPreviewStatus, + amountPaid, + bondAmount, + activeToken, + asBase, + vaultSharePrice, +}: OpenLongStatsProps): JSX.Element { + const appConfig = useAppConfig(); + const baseToken = findBaseToken({ + baseTokenAddress: hyperdrive.baseToken, + tokens: appConfig.tokens, + }); + const sharesToken = findYieldSourceToken({ + yieldSourceTokenAddress: hyperdrive.sharesToken, + tokens: appConfig.tokens, + }); + const { fixedApr } = useFixedRate(hyperdrive.address); + + const isBaseAmount = asBase || sharesToken.extensions.isSharesPeggedToBase; + const amountPaidInBase = isBaseAmount + ? amountPaid + : convertSharesToBase({ + sharesAmount: amountPaid, + vaultSharePrice: vaultSharePrice, + decimals: baseToken.decimals, + }); + const yieldAtMaturity = bondAmount - amountPaidInBase; + const termLengthMS = Number(hyperdrive.poolConfig.positionDuration * 1000n); + const numDays = convertMillisecondsToDays(termLengthMS); + return ( +
+ + ) : ( + <> + {bondAmount > 0 + ? `${formatRate( + calculateAprFromPrice({ + positionDuration: + hyperdrive.poolConfig.positionDuration || 0n, + baseAmount: amountPaidInBase, + bondAmount: bondAmount, + }), + baseToken.decimals, + )}%` + : `${fixedApr?.formatted}%`} + + ) + } + valueUnit="APR" + subValue={ + openLongPreviewStatus === "loading" ? ( + + ) : ( + <>{`${formatBalance({ + balance: bondAmount, + decimals: baseToken.decimals, + places: baseToken.places, + })} hy${baseToken.symbol}`} + ) + } + valueClassName="bg-gradient-to-r from-accent to-primary bg-clip-text text-transparent flex items-end" + /> +
+ + ) : ( + + + {`${formatBalance({ + balance: amountPaidInBase + yieldAtMaturity, + decimals: baseToken.decimals, + places: baseToken.places, + })} `} + + ) + } + valueUnit={`${activeToken.symbol}`} + valueClassName="text-base-content flex items-end" + subValue={`Term: ${numDays} days`} + /> +
+ ); +} diff --git a/apps/hyperdrive-trading/src/ui/token/SlippageSettingsTwo.tsx b/apps/hyperdrive-trading/src/ui/token/SlippageSettingsTwo.tsx new file mode 100644 index 000000000..8a89d814d --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/token/SlippageSettingsTwo.tsx @@ -0,0 +1,84 @@ +import { + Cog6ToothIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; +import classNames from "classnames"; +import { ReactElement } from "react"; +import { PercentInput } from "src/ui/base/components/PercentInput"; + +export const DEFAULT_SLIPPAGE_AMOUNT = "0.5"; + +export function SlippageSettingsTwo({ + slippage, + onSlippageChange, + activeOption, + onActiveOptionChange, + tooltip, +}: { + slippage: string; + onSlippageChange: (slippage: string) => void; + activeOption: "auto" | "custom"; + onActiveOptionChange: (activeTab: "auto" | "custom") => void; + tooltip?: string; +}): ReactElement { + return ( +
+ +
+
+ Max. Slippage +
+ +
+
+
+
+ + +
+ { + onActiveOptionChange("custom"); + onSlippageChange(e); + }} + /> +
+
+
+ ); +} diff --git a/apps/hyperdrive-trading/src/ui/token/TokenInputTwo.tsx b/apps/hyperdrive-trading/src/ui/token/TokenInputTwo.tsx new file mode 100644 index 000000000..ae665b9d6 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/token/TokenInputTwo.tsx @@ -0,0 +1,125 @@ +import classNames from "classnames"; +import { ReactElement, ReactNode } from "react"; +import { HIDE_NUMERIC_INPUT_ARROWS_CLASS } from "src/ui/base/numericInput"; + +interface TokenInputProps { + token: ReactNode; + name: string; + value: string; + onChange: (newAmount: string) => void; + /** + * If provided, the MAX button will be shown + */ + maxValue?: string; + /** + * If provided, this text will be used instead for the input's label + */ + inputLabel?: string; + /** + * Optional stat to show, useful for things like wallet balances + */ + stat?: ReactNode; + settings?: ReactNode; + disabled?: boolean; + /** + * If true, this will render the input with error styling + */ + hasError?: boolean; + autoFocus?: boolean; +} + +export function TokenInputTwo({ + value, + token, + name, + onChange, + maxValue, + inputLabel = "Enter amount", + stat, + settings, + hasError = false, + disabled = false, + autoFocus = false, +}: TokenInputProps): ReactElement { + return ( +
+ {settings ? settings : null} +
+ +
+ { + // Prevent typing '-' and 'e' + if (["-", "e", "E"].includes(event.key)) { + event.preventDefault(); + } + }} + onChange={(event) => { + onChange(event.target.value); + }} + /> + {typeof token === "string" ? ( +
+ {token} +
+ ) : ( + token + )} +
+
+ {/* TODO: Implement USD Stat here */} + {stat ? ( + + ) : null} +
+
+
+ ); +} diff --git a/apps/hyperdrive-trading/src/ui/token/TokenPickerTwo.tsx b/apps/hyperdrive-trading/src/ui/token/TokenPickerTwo.tsx new file mode 100644 index 000000000..c9080c52f --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/token/TokenPickerTwo.tsx @@ -0,0 +1,104 @@ +import { ChevronDownIcon } from "@heroicons/react/20/solid"; +import { TokenConfig } from "@hyperdrive/appconfig"; +import classNames from "classnames"; +import { ReactElement } from "react"; +import { formatBalance } from "src/ui/base/formatting/formatBalance"; +import { Address } from "viem"; + +export interface TokenChoice { + tokenConfig: TokenConfig; + disabled?: boolean; + tokenBalance?: bigint; +} + +export function TokenPickerTwo({ + tokens, + activeTokenAddress, + onChange, + label, +}: { + tokens: TokenChoice[]; + activeTokenAddress: Address; + onChange: (tokenAddress: Address) => void; + label?: string; +}): ReactElement { + // A single element doesn't need a dropdown + if (tokens.length === 1) { + return ( +
+ {label ? ( + + ) : undefined} +
+ + + {tokens[0].tokenConfig.symbol} + +
+
+ ); + } + const activeToken = tokens.find( + ({ tokenConfig }) => tokenConfig?.address === activeTokenAddress, + ); + + return ( +
+ {label ? ( + + ) : undefined} +
+ +
    + {[ + tokens.map(({ tokenConfig, tokenBalance }) => ( +
  • + +
  • + )), + ]} +
+
+
+ ); +} diff --git a/docs/CodingStyle.md b/docs/CodingStyle.md index 2945b1c08..bab6aacfa 100644 --- a/docs/CodingStyle.md +++ b/docs/CodingStyle.md @@ -112,9 +112,29 @@ the first word, for instance, 'View all' instead of 'View All'. Note: Proper nouns should still be capitalized, e.g.: "Dai Savings Rate" -### **4. Functions should have verbs** +### **5. Functions should have verbs** Function names should begin with verbs to enhance readability and express the action they perform. This convention aids in conveying the function's purpose, such as `calculateSum()`, `formatLabel()`, `setIsDisabled()`, `convertInchesToFeet`, etc. + +### **6. List required fields first in interfaces/object types** + +When defining interfaces or object types, always list required fields first. This improves readability and ensures that the most critical information is immediately visible. + +```typescript +// ❌ Instead of this +interface User { + optionalField?: string; + id: number; + name: string; +} + +// ✅ Use this +interface User { + id: number; + name: string; + optionalField?: string; +} +``` \ No newline at end of file