diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useCheckpoint.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useCheckpoint.ts new file mode 100644 index 000000000..761cb5311 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useCheckpoint.ts @@ -0,0 +1,31 @@ +import { Checkpoint } from "@hyperdrive/sdk"; +import { QueryStatus, useQuery } from "@tanstack/react-query"; +import { makeQueryKey } from "src/base/makeQueryKey"; +import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; +import { Address } from "wagmi"; + +export function useCheckpoint({ + checkpointId, + hyperdriveAddress, +}: { + hyperdriveAddress: Address; + checkpointId: bigint; +}): { + checkpoint: Checkpoint | undefined; + checkpointStatus: QueryStatus; +} { + const readHyperdrive = useReadHyperdrive(hyperdriveAddress); + const queryEnabled = !!readHyperdrive; + const { data, status } = useQuery({ + queryKey: makeQueryKey("getCheckpoint", { + hyperdriveAddress, + checkpointId: checkpointId.toString(), + }), + queryFn: queryEnabled + ? () => readHyperdrive.getCheckpoint({ checkpointId }) + : undefined, + enabled: queryEnabled, + }); + + return { checkpoint: data, checkpointStatus: status }; +} diff --git a/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/OpenShortsTable.tsx b/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/OpenShortsTable.tsx index 44fef89d2..7084fd957 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/OpenShortsTable.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/OpenShortsTable/OpenShortsTable.tsx @@ -9,12 +9,15 @@ import { useReactTable, } from "@tanstack/react-table"; import classNames from "classnames"; +import * as dnum from "dnum"; import { ReactElement } from "react"; import { Hyperdrive } from "src/appconfig/types"; import { makeQueryKey } from "src/base/makeQueryKey"; import { parseUnits } from "src/base/parseUnits"; import { NonIdealState } from "src/ui/base/components/NonIdealState"; import { formatBalance } from "src/ui/base/formatting/formatBalance"; +import { useCheckpoint } from "src/ui/hyperdrive/hooks/useCheckpoint"; +import { usePoolInfo } from "src/ui/hyperdrive/hooks/usePoolInfo"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; import { getProfitLossText } from "src/ui/hyperdrive/shorts/CloseShortForm/getProfitLossText"; import { CloseShortModalButton } from "src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton"; @@ -57,13 +60,62 @@ const getColumns = (hyperdrive: Hyperdrive) => [ }, }), columnHelper.display({ - header: `Current value (WETH)`, + header: `Accrued yield (${hyperdrive.baseToken.symbol})`, + cell: ({ row }) => { + return ; + }, + }), + columnHelper.display({ + header: `Current value (${hyperdrive.baseToken.symbol})`, cell: ({ row }) => { return ; }, }), ]; +function AccruedYieldCell({ + row, + hyperdrive, +}: { + row: Row; + hyperdrive: Hyperdrive; +}) { + const { poolInfo } = usePoolInfo(hyperdrive.address); + const { checkpoint } = useCheckpoint({ + checkpointId: row.original.checkpointId, + hyperdriveAddress: hyperdrive.address, + }); + + // Accrued yield = (current share price - checkpoint share price) x number of bonds + const accruedYield = dnum.mul( + dnum.sub( + [poolInfo?.sharePrice || 0n, hyperdrive.baseToken.decimals], + [checkpoint?.sharePrice || 0n, 18], + ), + [row.original.bondAmount, hyperdrive.baseToken.decimals], + hyperdrive.baseToken.decimals, + ); + + const currentValue = + accruedYield && + formatBalance({ + balance: accruedYield[0], + decimals: hyperdrive.baseToken.decimals, + places: 6, + }); + + return ( +
+ + {currentValue?.toString()} + +
+ ); +} + function CurrentValueCell({ row, hyperdrive, diff --git a/packages/hyperdrive-sdk/src/hyperdrive/ReadHyperdrive.ts b/packages/hyperdrive-sdk/src/hyperdrive/ReadHyperdrive.ts index 959e610db..205847980 100644 --- a/packages/hyperdrive-sdk/src/hyperdrive/ReadHyperdrive.ts +++ b/packages/hyperdrive-sdk/src/hyperdrive/ReadHyperdrive.ts @@ -25,6 +25,7 @@ import { INetwork } from "src/network/Network"; import { calculateEffectiveShareReserves } from "src/pool/calculateEffectiveShares"; import { getCheckpointId } from "src/pool/getCheckpointId"; import { WITHDRAW_SHARES_ASSET_ID } from "src/withdrawalShares/assetId"; +import { Checkpoint } from "src/pool/Checkpoint"; export interface ReadHyperdriveOptions { contract: IReadHyperdriveContract; @@ -56,6 +57,11 @@ export interface IReadHyperdrive { */ getLiquidity(options?: ContractReadOptions): Promise; + getCheckpoint(args: { + checkpointId: bigint; + options?: ContractReadOptions; + }): Promise; + /** * Calculates the total trading volume in bonds given a block window. * @param options.fromBlock - The start block, defaults to "earliest" @@ -75,10 +81,7 @@ export interface IReadHyperdrive { /** * Gets the active longs opened by a specific user. */ - getOpenLongs({ - account, - options, - }: { + getOpenLongs(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -86,10 +89,7 @@ export interface IReadHyperdrive { /** * Gets the active shorts opened by a specific user. */ - getOpenShorts({ - account, - options, - }: { + getOpenShorts(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -97,10 +97,7 @@ export interface IReadHyperdrive { /** * Gets the closed longs by a specific user. */ - getClosedLongs({ - account, - options, - }: { + getClosedLongs(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -108,10 +105,7 @@ export interface IReadHyperdrive { /** * Gets the inactive shorts opened by a specific user. */ - getClosedShorts({ - account, - options, - }: { + getClosedShorts(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -132,10 +126,7 @@ export interface IReadHyperdrive { /** * Gets the amount of LP shares a user has. */ - getLpShares({ - account, - options, - }: { + getLpShares(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -143,10 +134,7 @@ export interface IReadHyperdrive { /** * Gets the amount of closed LP shares a user has. */ - getClosedLpShares({ - account, - options, - }: { + getClosedLpShares(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -154,10 +142,7 @@ export interface IReadHyperdrive { /** * Gets the amount of withdrawal shares a user has. */ - getWithdrawalShares({ - account, - options, - }: { + getWithdrawalShares(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -165,10 +150,7 @@ export interface IReadHyperdrive { /** * Gets the amount of redeemed withdrawal shares a user has. */ - getRedeemedWithdrawalShares({ - account, - options, - }: { + getRedeemedWithdrawalShares(args: { account: Address; options?: ContractReadOptions; }): Promise; @@ -176,14 +158,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of base asset a user will receive when closing a long. */ - previewCloseLong({ - maturityTime, - bondAmountIn, - minBaseAmountOut, - destination, - asUnderlying, - options, - }: { + previewCloseLong(args: { maturityTime: bigint; bondAmountIn: bigint; minBaseAmountOut: bigint; @@ -195,14 +170,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of base asset a user will receive when closing a short. */ - previewCloseShort({ - maturityTime, - shortAmountIn, - minBaseAmountOut, - destination, - asUnderlying, - options, - }: { + previewCloseShort(args: { maturityTime: bigint; shortAmountIn: bigint; minBaseAmountOut: bigint; @@ -214,14 +182,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of bonds a user will receive when opening a long. */ - previewOpenLong({ - baseAmount, - minBondAmountOut, - minSharePrice, - destination, - asUnderlying, - options, - }: { + previewOpenLong(args: { baseAmount: bigint; minBondAmountOut: bigint; minSharePrice: bigint; @@ -233,14 +194,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of base asset it will cost to open a short. */ - previewOpenShort({ - amountOfBondsToShort, - maxBaseAmountIn, - minSharePrice, - destination, - asUnderlying, - options, - }: { + previewOpenShort(args: { amountOfBondsToShort: bigint; maxBaseAmountIn: bigint; minSharePrice: bigint; @@ -252,14 +206,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of LP shares a user will receive when adding liquidity. */ - previewAddLiquidity({ - contribution, - minAPR, - maxAPR, - destination, - asUnderlying, - options, - }: { + previewAddLiquidity(args: { contribution: bigint; minAPR: bigint; maxAPR: bigint; @@ -271,13 +218,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of base asset and withdrawlshares a user will receive when removing liquidity. */ - previewRemoveLiquidity({ - lpSharesIn, - minBaseAmountOut, - destination, - asUnderlying, - options, - }: { + previewRemoveLiquidity(args: { lpSharesIn: bigint; minBaseAmountOut: bigint; destination: Address; @@ -288,13 +229,7 @@ export interface IReadHyperdrive { /** * Predicts the amount of base asset and redeemed shares a user will receive when redeeming withdrawal shares. */ - previewRedeemWithdrawalShares({ - withdrawalSharesIn, - minBaseAmountOutPerShare, - destination, - asUnderlying, - options, - }: { + previewRedeemWithdrawalShares(args: { withdrawalSharesIn: bigint; minBaseAmountOutPerShare: bigint; destination: Address; @@ -358,6 +293,20 @@ export class ReadHyperdrive implements IReadHyperdrive { this.mathContract = mathContract; this.network = network; } + async getCheckpoint({ + checkpointId, + options, + }: { + checkpointId: bigint; + options?: ContractReadOptions | undefined; + }): Promise { + const [checkpoint] = await this.contract.read( + "getCheckpoint", + [checkpointId], + options, + ); + return checkpoint; + } async getPoolConfig(options?: ContractReadOptions): Promise { const [poolConfig] = await this.contract.read("getPoolConfig", [], options); @@ -709,6 +658,7 @@ export class ReadHyperdrive implements IReadHyperdrive { const fromBlock = "earliest"; const toBlock = options?.blockNumber || options?.blockTag || "latest"; + const { checkpointDuration } = await this.getPoolConfig(options); const closeShortEvents = await this.contract.getEvents("CloseShort", { filter: { trader: account }, fromBlock, @@ -734,7 +684,11 @@ export class ReadHyperdrive implements IReadHyperdrive { const closedShortsById: Record = {}; - for (const { args: eventData, data: eventLog } of transferOutEvents) { + for (const { + args: eventData, + data: eventLog, + blockNumber, + } of transferOutEvents) { const assetId = eventData.id.toString(); if (closedShortsById[assetId]) { @@ -742,10 +696,12 @@ export class ReadHyperdrive implements IReadHyperdrive { continue; } + const { timestamp } = await this.network.getBlock({ blockNumber }); closedShortsById[assetId] = { hyperdriveAddress: this.contract.address, assetId: eventData.id, bondAmount: eventData.value ?? 0n, + checkpointId: getCheckpointId(timestamp, checkpointDuration), baseAmountReceived: amountReceivedByAssetId[assetId] ?? 0n, maturity: decodeAssetFromTransferSingleEventData( eventLog as `0x${string}`, @@ -782,8 +738,13 @@ export class ReadHyperdrive implements IReadHyperdrive { const openShortsById: Record = {}; - for (const { data: eventLog, args: eventData } of transferInEvents) { + for (const { + data: eventLog, + args: eventData, + blockNumber, + } of transferInEvents) { const assetId = eventData.id.toString(); + const { timestamp } = await this.network.getBlock({ blockNumber }); if (openShortsById[assetId]) { openShortsById[assetId].bondAmount += eventData.value; @@ -797,6 +758,7 @@ export class ReadHyperdrive implements IReadHyperdrive { openShortsById[assetId] = { hyperdriveAddress: this.contract.address, assetId: eventData.id, + checkpointId: getCheckpointId(timestamp, checkpointDuration), bondAmount: eventData.value - (closedShortsById[assetId]?.bondAmount ?? 0n), baseAmountPaid: netAmountPaid > 0n ? netAmountPaid : 0n, @@ -867,23 +829,24 @@ export class ReadHyperdrive implements IReadHyperdrive { toBlock, }); + const { checkpointDuration } = await this.getPoolConfig(options); const closedShortsList: ClosedShort[] = await Promise.all( closedShorts.map(async (event) => { - const assetId = event.args.assetId; + const { assetId } = event.args; const decoded = decodeAssetFromTransferSingleEventData( event.data as `0x${string}`, ); + const { timestamp } = await this.network.getBlock({ + blockNumber: event.blockNumber, + }); return { hyperdriveAddress: this.contract.address, assetId, bondAmount: event.args.bondAmount, baseAmountReceived: event.args.baseAmount, maturity: decoded.timestamp, - closedTimestamp: ( - await this.network.getBlock({ - blockNumber: event.blockNumber, - }) - ).timestamp, + closedTimestamp: timestamp, + checkpointId: getCheckpointId(timestamp, checkpointDuration), }; }), ); @@ -909,10 +872,9 @@ export class ReadHyperdrive implements IReadHyperdrive { const { timestamp: blockTimestamp } = await this.network.getBlock(options); const checkpointId = getCheckpointId(blockTimestamp, checkpointDuration); - const [{ longExposure: checkpointLongExposure }] = await this.contract.read( - "getCheckpoint", - [checkpointId], - ); + const { longExposure: checkpointLongExposure } = await this.getCheckpoint({ + checkpointId, + }); const [maxBondsOut] = await this.mathContract.read( "calculateMaxShort", @@ -961,10 +923,10 @@ export class ReadHyperdrive implements IReadHyperdrive { const { timestamp: blockTimestamp } = await this.network.getBlock(options); const checkpointId = getCheckpointId(blockTimestamp, checkpointDuration); - const [{ longExposure: checkpointLongExposure }] = await this.contract.read( - "getCheckpoint", - [checkpointId], - ); + const { longExposure: checkpointLongExposure } = await this.getCheckpoint({ + checkpointId, + options, + }); const [maxBaseIn, maxBondsOut] = await this.mathContract.read( "calculateMaxLong", diff --git a/packages/hyperdrive-sdk/src/index.ts b/packages/hyperdrive-sdk/src/index.ts index 26e0e65e3..73d2e6981 100644 --- a/packages/hyperdrive-sdk/src/index.ts +++ b/packages/hyperdrive-sdk/src/index.ts @@ -44,6 +44,8 @@ export type { // Pool export type { PoolConfig } from "src/pool/PoolConfig"; export type { PoolInfo } from "src/pool/PoolInfo"; +export type { Checkpoint } from "src/pool/Checkpoint"; +export { getCheckpointId } from "src/pool/getCheckpointId"; // Network export type { INetwork, GetBlockParameters } from "src/network/Network"; diff --git a/packages/hyperdrive-sdk/src/pool/Checkpoint.ts b/packages/hyperdrive-sdk/src/pool/Checkpoint.ts new file mode 100644 index 000000000..6557d3443 --- /dev/null +++ b/packages/hyperdrive-sdk/src/pool/Checkpoint.ts @@ -0,0 +1,6 @@ +import { HyperdriveABI } from "src/abis/Hyperdrive"; +import { FunctionReturnType } from "src/base/abitype"; +export type Checkpoint = FunctionReturnType< + typeof HyperdriveABI, + "getCheckpoint" +>[0]; diff --git a/packages/hyperdrive-sdk/src/shorts/types.ts b/packages/hyperdrive-sdk/src/shorts/types.ts index 20918a7f7..448528c72 100644 --- a/packages/hyperdrive-sdk/src/shorts/types.ts +++ b/packages/hyperdrive-sdk/src/shorts/types.ts @@ -4,6 +4,7 @@ export interface Short { hyperdriveAddress: Address; assetId: bigint; bondAmount: bigint; + checkpointId: bigint; /** * Time in seconds when this short will mature */