diff --git a/apps/hyperdrive-trading/src/base/calculateAnnualizedPercentageChange.ts b/apps/hyperdrive-trading/src/base/calculateAnnualizedPercentageChange.ts new file mode 100644 index 000000000..de7392f0f --- /dev/null +++ b/apps/hyperdrive-trading/src/base/calculateAnnualizedPercentageChange.ts @@ -0,0 +1,25 @@ +/** + * Calculates the annualized profit or loss percentage. + * @param amountBefore The amount paid. + * @param amountAfter The amount received. + * @param days The number of days the investment was held. + * @returns The calculated annualized profit or loss percentage. + */ +export function calculateAnnualizedPercentageChange({ + amountBefore, + amountAfter, + days, +}: { + amountBefore: bigint; + amountAfter: bigint; + days: number; +}): string { + if (amountBefore === 0n) { + return "0"; // Prevent division by zero + } + + const profitOrLoss = + Number(amountAfter - amountBefore) / Number(amountBefore); + const annualizedProfitOrLoss = ((1 + profitOrLoss) ** (365 / days) - 1) * 100; + return annualizedProfitOrLoss.toFixed(2); +} diff --git a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx index 6e7b72ef0..8d788faf8 100644 --- a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx @@ -7,9 +7,9 @@ export function Navbar(): ReactElement {
-
-
-
+
+
+
Hyperdrive
diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx index 43ca22fc7..c012a64e7 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx @@ -19,9 +19,6 @@ export function CloseLongModalButton({ (window as any)[modalId].close(); } - const maturity = new Date(Number(long.maturity * 1000n)); - const hasMatured = maturity < new Date(); - return ( - +

Close position @@ -51,18 +45,6 @@ export function CloseLongModalButton({ />

} - > - {({ showModal }) => ( - - )} - + /> ); } diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongModalButton/OpenLongModalButton.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongModalButton/OpenLongModalButton.tsx index f3d0c0bc2..5b67fbc3d 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongModalButton/OpenLongModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongModalButton/OpenLongModalButton.tsx @@ -28,7 +28,7 @@ export function OpenLongModalButton({ onClick={() => { setSearchParams({ ...searchParams, - position: "Longs", + position: "Buys", openOrClosed: "Open", }); showModal(); @@ -36,15 +36,17 @@ export function OpenLongModalButton({ >
-

Buy hy{hyperdrive.baseToken.symbol}

+

+ Buy hy{hyperdrive.baseToken.symbol} +

Earn {fixedAPR?.formatted || "-"}% APR on{" "} {hyperdrive.baseToken.symbol}

- 1 hy{hyperdrive.baseToken.symbol} is always worth one{" "} - {hyperdrive.baseToken.symbol} at maturity. It{"'"}s a predictable - fixed rate. + 1 hy{hyperdrive.baseToken.symbol} is worth 1{" "} + {hyperdrive.baseToken.symbol} at maturity, giving you a + predictable fixed rate yield.

diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityModalButton/AddLiquidityModalButton.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityModalButton/AddLiquidityModalButton.tsx index 731ee9e66..fd3f74fab 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityModalButton/AddLiquidityModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityModalButton/AddLiquidityModalButton.tsx @@ -34,7 +34,7 @@ export function AddLiquidityModalButton({ >
-

Add Liquidity

+

Add Liquidity

Earn trading fees and interest

@@ -47,10 +47,10 @@ export function AddLiquidityModalButton({ Single-sided deposit with {hyperdrive.baseToken.symbol} - Idle capital earns yield source rate + Idle liquidity earns yield source rate - No terms or rollovers, easy-to-use + No terms or manual LP rollovers
{/* Using a div styled as a button here just as a visual cue. Don't diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx index 0ba537aae..17a70e7d6 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx @@ -28,10 +28,7 @@ export function CloseShortModalButton({ className="daisy-btn-ghost daisy-btn-sm daisy-btn-circle daisy-btn absolute right-4 top-4" onClick={closeModal} > - +

Close position diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortModalButton/OpenShortModalButton.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortModalButton/OpenShortModalButton.tsx index c3c396e08..33a3d9715 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortModalButton/OpenShortModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortModalButton/OpenShortModalButton.tsx @@ -38,16 +38,18 @@ export function OpenShortModalButton({ >
-

Short hy{hyperdrive.baseToken.symbol}

+

+ Short hy{hyperdrive.baseToken.symbol} +

Earn {vaultRate?.formatted}% APY on{" "} {hyperdrive.baseToken.symbol}

- Profit when the price of hy{hyperdrive.baseToken.symbol} drops, - and also earn the yield source rate. + Profit when hy{hyperdrive.baseToken.symbol} price drops, while + maximizing exposure to the yield source.

-
+
Fixed rate up, hy{hyperdrive.baseToken.symbol} price down diff --git a/apps/hyperdrive-trading/src/ui/portfolio/OpenLongsTable/OpenLongsTable.tsx b/apps/hyperdrive-trading/src/ui/portfolio/OpenLongsTable/OpenLongsTable.tsx index a3938d659..577b80f6b 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/OpenLongsTable/OpenLongsTable.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/OpenLongsTable/OpenLongsTable.tsx @@ -1,69 +1,221 @@ /* eslint-disable react/jsx-key */ +import { Long } from "@hyperdrive/sdk"; +import { useQuery } from "@tanstack/react-query"; +import { + Row, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import classNames from "classnames"; import { ReactElement } from "react"; import { Hyperdrive } from "src/appconfig/types"; -import { - CellWithTooltip, - SortableGridTable, -} from "src/ui/base/components/tables/SortableGridTable"; -import { useOpenLongRows } from "src/ui/portfolio/OpenLongsTable/useOpenLongRows"; +import { calculateAnnualizedPercentageChange } from "src/base/calculateAnnualizedPercentageChange"; +import { convertMillisecondsToDays } from "src/base/convertMillisecondsToDays"; +import { makeQueryKey } from "src/base/makeQueryKey"; +import { formatBalance } from "src/ui/base/formatting/formatBalance"; +import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; +import { CloseLongModalButton } from "src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton"; +import { usePreviewCloseLong } from "src/ui/hyperdrive/longs/hooks/usePreviewCloseLong"; +import { parseUnits } from "viem"; import { useAccount } from "wagmi"; -interface OpenOrdersTableProps { +interface OpenLongsTableProps { hyperdrive: Hyperdrive; } +const columnHelper = createColumnHelper(); + +function getColumns(hyperdrive: Hyperdrive) { + return [ + columnHelper.display({ + header: `ID`, + cell: ({ row }) => {Number(row.original.maturity)}, + }), + columnHelper.display({ + header: `Matures on`, + cell: ({ row }) => { + const maturity = new Date(Number(row.original.maturity * 1000n)); + return {maturity.toLocaleDateString()}; + }, + }), + columnHelper.display({ + id: "size", + header: `Size (hy${hyperdrive.baseToken.symbol})`, + cell: ({ row }) => { + return ( + + {formatBalance({ + balance: row.original.bondAmount, + decimals: hyperdrive.baseToken.decimals, + places: 2, + })} + + ); + }, + }), + columnHelper.accessor("baseAmountPaid", { + id: "amountPaid", + header: `Amount paid (${hyperdrive.baseToken.symbol})`, + cell: (baseAmountPaid) => { + const amountPaid = baseAmountPaid.getValue(); + return formatBalance({ + balance: amountPaid, + decimals: hyperdrive.baseToken.decimals, + places: 2, + }); + }, + }), + columnHelper.display({ + id: "fixedRate", + header: `Fixed rate (APR)`, + cell: ({ row }) => { + return `${calculateAnnualizedPercentageChange({ + amountBefore: row.original.baseAmountPaid, + amountAfter: row.original.bondAmount, + days: convertMillisecondsToDays(hyperdrive.termLengthMS), + })}%`; + }, + }), + columnHelper.display({ + id: "value", + header: `Market value (${hyperdrive.baseToken.symbol})`, + cell: ({ row }) => { + return ; + }, + }), + ]; +} export function OpenLongsTable({ hyperdrive, -}: OpenOrdersTableProps): ReactElement { +}: OpenLongsTableProps): ReactElement { const { address: account } = useAccount(); - const { openLongRows = [], openLongRowsStatus } = useOpenLongRows({ - account, - hyperdrive, + const readHyperdrive = useReadHyperdrive(hyperdrive.address); + const queryEnabled = !!readHyperdrive && !!account; + const { data: longs } = useQuery({ + queryKey: makeQueryKey("longPositions", { account }), + queryFn: queryEnabled + ? () => readHyperdrive?.getOpenLongs({ account }) + : undefined, + enabled: queryEnabled, }); + const tableInstance = useReactTable({ + columns: getColumns(hyperdrive), + data: longs || [], + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+ {/* Modal needs to be rendered outside of the table so that dialog can be used. Otherwise react throws a dom nesting error */} + {tableInstance.getRowModel().rows.map((row) => { + const modalId = `${row.original.assetId}`; + return ( + + ); + })} + + + {tableInstance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {tableInstance.getRowModel().rows.map((row) => { + return ( + { + const modalId = `${row.original.assetId}`; + (window as any)[modalId].showModal(); + }} + > + <> + {row.getVisibleCells().map((cell) => { + return ( + + ); + })} + + + ); + })} + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+ ); +} + +function CurrentValueCell({ + row, + hyperdrive, +}: { + row: Row; + hyperdrive: Hyperdrive; +}) { + const { address: account } = useAccount(); + const { baseAmountOut } = usePreviewCloseLong({ + hyperdriveAddress: hyperdrive.address, + maturityTime: row.original.maturity, + bondAmountIn: row.original.bondAmount, + minBaseAmountOut: parseUnits("0", hyperdrive.baseToken.decimals), + destination: account, + }); + const currentValue = + baseAmountOut && + formatBalance({ + balance: baseAmountOut, + decimals: hyperdrive.baseToken.decimals, + places: 4, + }); + + const isPositiveChangeInValue = + baseAmountOut && baseAmountOut > row.original.baseAmountPaid; return ( - - ), - }, - { - cell: ( - - ), - }, - { - cell: ( - - ), - }, - { - cell: ( - - ), - }, - ]} - // cols={["Position", "Bonds", "Amount paid", "Value", "Matures on", ""]} - rows={openLongRows} - showSkeleton={openLongRowsStatus === "loading"} - /> +
+ {currentValue?.toString()} +
+ {isPositiveChangeInValue ? "+" : ""} + {baseAmountOut + ? `${formatBalance({ + balance: baseAmountOut - row.original.baseAmountPaid, + decimals: hyperdrive.baseToken.decimals, + places: 4, + })} ${hyperdrive.baseToken.symbol}` + : undefined} +
+
); } diff --git a/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/useOpenShortRows.tsx b/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/useOpenShortRows.tsx deleted file mode 100644 index 9ec65f8a4..000000000 --- a/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/useOpenShortRows.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { OpenShort } from "@hyperdrive/sdk"; -import { Hyperdrive } from "src/appconfig/types"; -import { Row } from "src/ui/base/components/tables/SortableGridTable"; -import { formatBalance } from "src/ui/base/formatting/formatBalance"; -import { - getProfitLossText, - getStyleClassForProfitLoss, -} from "src/ui/hyperdrive/shorts/CloseShortForm/getProfitLossText"; -import { CloseShortModalButton } from "src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton"; -import { useOpenShorts } from "src/ui/hyperdrive/shorts/hooks/useOpenShorts"; -import { usePreviewCloseShort } from "src/ui/hyperdrive/shorts/hooks/usePreviewCloseShort"; -import { Address, parseUnits } from "viem"; -import { useAccount } from "wagmi"; - -interface UseOpenShortRowsOptions { - account: Address | undefined; - hyperdrive: Hyperdrive | undefined; -} - -export function useOpenShortRows({ - account, - hyperdrive, -}: UseOpenShortRowsOptions): { - openShortRows: Row[] | undefined; - openShortRowsStatus: "error" | "success" | "loading"; -} { - const { openShorts = [], openShortsStatus: openShortRowsStatus } = - useOpenShorts({ - account, - hyperdriveAddress: hyperdrive?.address, - }); - - const openShortRows = hyperdrive - ? openShorts.map((short) => - createOpenShortRow({ - hyperdrive, - short, - }), - ) - : []; - - return { - openShortRows, - openShortRowsStatus, - }; -} - -function createOpenShortRow({ - short, - hyperdrive, -}: { - hyperdrive: Hyperdrive; - short: OpenShort; -}): Row { - const { - baseToken: { decimals: baseDecimals, symbol: baseSymbol }, - } = hyperdrive; - - const modalId = `${short.assetId}`; - - return { - cells: [ - - Short - , - - {formatBalance({ - balance: short.bondAmount, - decimals: baseDecimals, - places: 4, - })} - , - , - - {new Date(Number(short.maturity * 1000n)).toLocaleDateString()} - , - - - , - ], - }; -} - -function ProfitLossCell({ - baseDecimals, - hyperdriveAddress, - baseSymbol, - short, - hyperdrive, -}: { - short: OpenShort; - hyperdriveAddress: Address; - baseDecimals: number; - baseSymbol: string; - hyperdrive: Hyperdrive; -}) { - const { address: account } = useAccount(); - const { baseAmountOut } = usePreviewCloseShort({ - hyperdriveAddress, - maturityTime: short.maturity, - shortAmountIn: short.bondAmount, - minBaseAmountOut: parseUnits("0", 18), - destination: account, - }); - - const profitLossClass = baseAmountOut - ? getStyleClassForProfitLoss(baseAmountOut, short.baseAmountPaid) - : ""; - - return ( - - {baseAmountOut && short.bondAmount !== 0n - ? `${getProfitLossText({ - baseAmountOut, - amountInput: short.baseAmountPaid, - baseDecimals, - baseSymbol, - showPercentage: false, - })}` - : ""} - - ); -} diff --git a/apps/hyperdrive-trading/src/ui/portfolio/PositionTabs/PositionTabs.tsx b/apps/hyperdrive-trading/src/ui/portfolio/PositionTabs/PositionTabs.tsx index df63d4a62..75c3ca0c8 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/PositionTabs/PositionTabs.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/PositionTabs/PositionTabs.tsx @@ -1,7 +1,7 @@ import classNames from "classnames"; import { ReactElement } from "react"; -export type PositionTab = "Longs" | "Shorts" | "LP"; +export type PositionTab = "Buys" | "Shorts" | "LP"; export function PositionTabs({ onTabClick, @@ -13,15 +13,15 @@ export function PositionTabs({ return (