diff --git a/.changeset/config.json b/.changeset/config.json index 4da54cf31..07124f10a 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -6,7 +6,7 @@ ["@delvtech/hyperdrive-wasm", "hyperdrive-wasm"], ["@delvtech/fixed-point-wasm", "fixed-point-wasm"] ], - "linked": [["@delvtech/hyperdrive-viem", "@delvtech/hyperdrive-js-core"]], + "linked": [["@delvtech/hyperdrive-viem", "@delvtech/hyperdrive-js"]], "access": "restricted", "baseBranch": "main", "updateInternalDependencies": "patch", diff --git a/.changeset/gentle-llamas-glow.md b/.changeset/gentle-llamas-glow.md new file mode 100644 index 000000000..2fe8fbdd8 --- /dev/null +++ b/.changeset/gentle-llamas-glow.md @@ -0,0 +1,5 @@ +--- +"@delvtech/hyperdrive-js": patch +--- + +Initial release of drift based SDK diff --git a/.changeset/tall-plants-appear.md b/.changeset/tall-plants-appear.md new file mode 100644 index 000000000..43efb59a2 --- /dev/null +++ b/.changeset/tall-plants-appear.md @@ -0,0 +1,5 @@ +--- +"@delvtech/hyperdrive-viem": minor +--- + +Refactored to drift under the hood diff --git a/apps/hyperdrive-trading/package.json b/apps/hyperdrive-trading/package.json index 1a4d31822..759e8aede 100644 --- a/apps/hyperdrive-trading/package.json +++ b/apps/hyperdrive-trading/package.json @@ -28,9 +28,11 @@ "gen:walletconnect": "bash ./scripts/generate-walletconnect.sh" }, "dependencies": { + "@delvtech/drift": "^0.0.1-beta.11", + "@delvtech/drift-viem": "^0.0.1-beta.13", "@delvtech/fixed-point-wasm": "^0.0.6", "@delvtech/hyperdrive-appconfig": "^0.0.1", - "@delvtech/hyperdrive-viem": "^3.0.6", + "@delvtech/hyperdrive-js": "^0.0.0", "@headlessui/react": "^2.1.5", "@heroicons/react": "^2.0.16", "@radix-ui/react-tooltip": "^1.1.2", diff --git a/apps/hyperdrive-trading/src/drift/getDrift.ts b/apps/hyperdrive-trading/src/drift/getDrift.ts new file mode 100644 index 000000000..5f15e70d2 --- /dev/null +++ b/apps/hyperdrive-trading/src/drift/getDrift.ts @@ -0,0 +1,23 @@ +import { createLruSimpleCache, Drift, DriftOptions } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { getPublicClient, GetPublicClientParameters } from "@wagmi/core"; +import { wagmiConfig } from "src/network/wagmiClient"; + +// 1 minute TTL to match the queryClient's staleTime +export const sdkCache = createLruSimpleCache({ max: 500, ttl: 60_000 }); + +export function getDriftOptions({ + chainId, +}: { + chainId?: number; +} = {}): DriftOptions { + return { + cache: sdkCache, + cacheNamespace: chainId, + }; +} + +export function getDrift(params?: GetPublicClientParameters): Drift { + const publicClient = getPublicClient(wagmiConfig as any, params) as any; + return new Drift(viemAdapter({ publicClient }), getDriftOptions(params)); +} diff --git a/apps/hyperdrive-trading/src/hyperdrive/getLpApy.ts b/apps/hyperdrive-trading/src/hyperdrive/getLpApy.ts index 027005f4e..f669cad01 100644 --- a/apps/hyperdrive-trading/src/hyperdrive/getLpApy.ts +++ b/apps/hyperdrive-trading/src/hyperdrive/getLpApy.ts @@ -1,10 +1,11 @@ +import { Block } from "@delvtech/drift"; import { fixed } from "@delvtech/fixed-point-wasm"; import { appConfig, getRewardsFn, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { Block, ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { getPublicClient } from "@wagmi/core"; import { convertMillisecondsToDays } from "src/base/convertMillisecondsToDays"; import { isForkChain } from "src/chains/isForkChain"; @@ -45,7 +46,7 @@ export async function getLpApy({ readHyperdrive: ReadHyperdrive; hyperdrive: HyperdriveConfig; }): Promise { - const currentBlock = (await readHyperdrive.network.getBlock()) as Block; + const currentBlock = (await readHyperdrive.drift.getBlock()) as Block; const currentBlockNumber = currentBlock.blockNumber!; // Appconfig tells us how many days to look back for historical rates const numBlocksForHistoricalRate = isForkChain(hyperdrive.chainId) diff --git a/apps/hyperdrive-trading/src/hyperdrive/getReadHyperdrive.ts b/apps/hyperdrive-trading/src/hyperdrive/getReadHyperdrive.ts deleted file mode 100644 index 88883d4fa..000000000 --- a/apps/hyperdrive-trading/src/hyperdrive/getReadHyperdrive.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - AppConfig, - findHyperdriveConfig, -} from "@delvtech/hyperdrive-appconfig"; -import { - ReadHyperdrive, - ReadHyperdriveOptions, - ReadMetaMorphoHyperdrive, - ReadStEthHyperdrive, -} from "@delvtech/hyperdrive-viem"; -import { - ReadHyperdrive_v1_0_14, - ReadMetaMorphoHyperdrive_v1_0_14, - ReadStEthHyperdrive_v1_0_14, -} from "@delvtech/hyperdrive-viem/v1.0.14"; -import semver from "semver"; -import { sdkCache } from "src/sdk/sdkCache"; -import { failedRequestToast } from "src/ui/base/components/Toaster/failedRequestToast"; -import { Address, PublicClient } from "viem"; - -export async function getReadHyperdrive({ - hyperdriveAddress, - publicClient, - appConfig, -}: { - hyperdriveAddress: Address; - publicClient: PublicClient; - appConfig: AppConfig; -}): Promise { - let hyperdrive: ReadHyperdrive; - - const hyperdriveConfig = findHyperdriveConfig({ - hyperdriveChainId: publicClient.chain?.id as number, - hyperdriveAddress, - hyperdrives: appConfig.hyperdrives, - }); - const options: ReadHyperdriveOptions = { - address: hyperdriveAddress, - publicClient, - cache: sdkCache, - namespace: publicClient.chain?.id.toString(), - earliestBlock: hyperdriveConfig.initializationBlock, - }; - - try { - // steth - if (hyperdriveConfig.kind === "StETHHyperdrive") { - hyperdrive = new ReadStEthHyperdrive(options); - - // <= v1.0.14 - if (await isV1_0_14(hyperdrive)) { - return new ReadStEthHyperdrive_v1_0_14(options); - } - - return hyperdrive; - } - - // morpho - if (hyperdriveConfig.kind === "MorphoBlueHyperdrive") { - hyperdrive = new ReadMetaMorphoHyperdrive(options); - - // <= v1.0.14 - if (await isV1_0_14(hyperdrive)) { - return new ReadMetaMorphoHyperdrive_v1_0_14(options); - } - - return hyperdrive; - } - - // base - - hyperdrive = new ReadHyperdrive(options); - - // <= v1.0.14 - if (await isV1_0_14(hyperdrive)) { - return new ReadHyperdrive_v1_0_14(options); - } - - return hyperdrive; - } catch (error) { - failedRequestToast(); - console.error(error); - return new ReadHyperdrive(options); - } -} - -async function isV1_0_14(hyperdrive: ReadHyperdrive): Promise { - const version = await hyperdrive.getVersion(); - return semver.lte(version.string, "1.0.14"); -} diff --git a/apps/hyperdrive-trading/src/hyperdrive/getReadWriteHyperdrive.ts b/apps/hyperdrive-trading/src/hyperdrive/getReadWriteHyperdrive.ts deleted file mode 100644 index 64cd786fa..000000000 --- a/apps/hyperdrive-trading/src/hyperdrive/getReadWriteHyperdrive.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - AppConfig, - findHyperdriveConfig, -} from "@delvtech/hyperdrive-appconfig"; -import { - ReadWriteHyperdrive, - ReadWriteHyperdriveOptions, - ReadWriteMetaMorphoHyperdrive, - ReadWriteStEthHyperdrive, -} from "@delvtech/hyperdrive-viem"; -import { - ReadWriteHyperdrive_v1_0_14, - ReadWriteStEthHyperdrive_v1_0_14, -} from "@delvtech/hyperdrive-viem/v1.0.14"; -import semver from "semver"; -import { sdkCache } from "src/sdk/sdkCache"; -import { failedRequestToast } from "src/ui/base/components/Toaster/failedRequestToast"; -import { Address, PublicClient, WalletClient } from "viem"; - -export async function getReadWriteHyperdrive({ - hyperdriveAddress, - publicClient, - walletClient, - appConfig, -}: { - hyperdriveAddress: Address; - publicClient: PublicClient; - walletClient: WalletClient; - appConfig: AppConfig; -}): Promise { - let hyperdrive: ReadWriteHyperdrive; - const hyperdriveConfig = findHyperdriveConfig({ - hyperdriveChainId: publicClient.chain?.id as number, - hyperdriveAddress, - hyperdrives: appConfig.hyperdrives, - }); - const options: ReadWriteHyperdriveOptions = { - address: hyperdriveAddress, - publicClient, - walletClient, - cache: sdkCache, - namespace: publicClient.chain?.id.toString(), - earliestBlock: hyperdriveConfig.initializationBlock, - }; - - try { - // steth - if (hyperdriveConfig.kind === "StETHHyperdrive") { - hyperdrive = new ReadWriteStEthHyperdrive(options); - - // <= v1.0.14 - if (await isV1_0_14(hyperdrive)) { - return new ReadWriteStEthHyperdrive_v1_0_14(options); - } - - return hyperdrive; - } - - // morpho - if (hyperdriveConfig.kind === "MorphoBlueHyperdrive") { - hyperdrive = new ReadWriteMetaMorphoHyperdrive(options); - - return hyperdrive; - } - - // base - hyperdrive = new ReadWriteHyperdrive(options); - - // <= v1.0.14 - if (await isV1_0_14(hyperdrive)) { - return new ReadWriteHyperdrive_v1_0_14(options); - } - - return hyperdrive; - } catch (error) { - failedRequestToast(); - console.error(error); - return new ReadWriteHyperdrive(options); - } -} - -async function isV1_0_14(hyperdrive: ReadWriteHyperdrive): Promise { - const version = await hyperdrive.getVersion(); - return semver.lte(version.string, "1.0.14"); -} diff --git a/apps/hyperdrive-trading/src/hyperdrive/getYieldSourceRate.ts b/apps/hyperdrive-trading/src/hyperdrive/getYieldSourceRate.ts index bca577f14..0a8ca5af9 100644 --- a/apps/hyperdrive-trading/src/hyperdrive/getYieldSourceRate.ts +++ b/apps/hyperdrive-trading/src/hyperdrive/getYieldSourceRate.ts @@ -1,3 +1,4 @@ +import { Block } from "@delvtech/drift"; import { fixed } from "@delvtech/fixed-point-wasm"; import { AppConfig, @@ -5,7 +6,7 @@ import { getRewardsFn, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { Block, ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { getPublicClient } from "@wagmi/core"; import { convertMillisecondsToDays } from "src/base/convertMillisecondsToDays"; import { isForkChain } from "src/chains/isForkChain"; @@ -16,7 +17,7 @@ export async function getYieldSourceRate( readHyperdrive: ReadHyperdrive, appConfig: AppConfig, ): Promise<{ rate: bigint; ratePeriodDays: number; netRate: bigint }> { - const hyperdriveChainId = await readHyperdrive.network.getChainId(); + const hyperdriveChainId = await readHyperdrive.drift.getChainId(); const hyperdrive = findHyperdriveConfig({ hyperdriveChainId, hyperdriveAddress: readHyperdrive.address, @@ -28,7 +29,7 @@ export async function getYieldSourceRate( hyperdrive, }); - const currentBlock = (await readHyperdrive.network.getBlock()) as Block; + const currentBlock = (await readHyperdrive.drift.getBlock()) as Block; const initializationBlock = hyperdrive.initializationBlock; const isPoolYoungerThanOneRatePeriod = diff --git a/apps/hyperdrive-trading/src/registry/data.ts b/apps/hyperdrive-trading/src/registry/data.ts index c39daf3af..5943120f6 100644 --- a/apps/hyperdrive-trading/src/registry/data.ts +++ b/apps/hyperdrive-trading/src/registry/data.ts @@ -1,52 +1,19 @@ -import { Hex } from "viem"; - -export const statusById = { - "1": "active", - "2": "sunset", -} as const; - -/** - * An ID used to represent status in the registry. - */ -export type StatusId = keyof typeof statusById; - /** * The status of an instance or factory in the registry. */ -export type Status = (typeof statusById)[StatusId]; - -/** - * The decoded data of an instance in the registry. - */ -export interface InstanceData { - status: Status; -} - -/** - * Decodes the `data` field of an instance in the registry according to the - * ElementDAO registry schema. - */ -export function decodeInstanceData(data: Hex): InstanceData { - const statusId = BigInt(data).toString() as StatusId; - return { - status: statusById[statusId], - }; -} - -/** - * The decoded data of a factory in the registry. - */ -export interface FactoryData { - status: Status; -} +export type Status = "active" | "sunset"; /** - * Decodes the `data` field of a factory in the registry according to the - * ElementDAO registry schema. + * Returns the status of an instance or factory in the registry according to the + * ElementDAO registry data schema. */ -export function decodeFactoryData(data: Hex): FactoryData { - const statusId = BigInt(data).toString() as StatusId; - return { - status: statusById[statusId], - }; +export function getStatus(statusId: bigint): Status { + switch (statusId) { + case 1n: + return "active"; + case 2n: + return "sunset"; + default: + throw new Error(`Unknown status ID: ${statusId}`); + } } diff --git a/apps/hyperdrive-trading/src/sdk/sdkCache.ts b/apps/hyperdrive-trading/src/sdk/sdkCache.ts deleted file mode 100644 index 38ef4bcc6..000000000 --- a/apps/hyperdrive-trading/src/sdk/sdkCache.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createLruSimpleCache } from "@delvtech/hyperdrive-viem"; - -// 1 minute TTL to match the queryClient's staleTime -export const sdkCache = createLruSimpleCache({ max: 500, ttl: 60_000 }); diff --git a/apps/hyperdrive-trading/src/ui/chainlog/FactoriesTable.tsx b/apps/hyperdrive-trading/src/ui/chainlog/FactoriesTable.tsx index e09cfdf46..5680a941d 100644 --- a/apps/hyperdrive-trading/src/ui/chainlog/FactoriesTable.tsx +++ b/apps/hyperdrive-trading/src/ui/chainlog/FactoriesTable.tsx @@ -1,4 +1,4 @@ -import { ReadRegistry } from "@delvtech/hyperdrive-viem"; +import { ReadRegistry } from "@delvtech/hyperdrive-js"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; import { UseQueryResult, useQuery } from "@tanstack/react-query"; import { @@ -8,20 +8,18 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { getPublicClient } from "@wagmi/core"; import classNames from "classnames"; import { ReactElement } from "react"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { wagmiConfig } from "src/network/wagmiClient"; -import { Status, decodeFactoryData } from "src/registry/data"; -import { sdkCache } from "src/sdk/sdkCache"; +import { getDrift } from "src/drift/getDrift"; +import { Status, getStatus } from "src/registry/data"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; import { NonIdealState } from "src/ui/base/components/NonIdealState"; import { TableSkeleton } from "src/ui/base/components/TableSkeleton"; import { AddressCell } from "src/ui/chainlog/AddressCell"; import { ChainCell } from "src/ui/chainlog/ChainCell"; import { StatusCell } from "src/ui/chainlog/StatusCell"; -import { Address, PublicClient } from "viem"; +import { Address } from "viem"; export function FactoriesTable(): ReactElement { const { data = [], isFetching } = useFactoriesQuery(); @@ -163,7 +161,7 @@ interface Factory { status: Status; } -function useFactoriesQuery(): UseQueryResult { +function useFactoriesQuery(): UseQueryResult { const connectedAppConfig = useAppConfigForConnectedChain(); const chainIds = Object.keys(connectedAppConfig.registries).map(Number); @@ -178,23 +176,16 @@ function useFactoriesQuery(): UseQueryResult { await Promise.all( chainIds.map(async (chainId) => { - // TODO: Cleanup type casting - const publicClient = getPublicClient(wagmiConfig as any, { - chainId, - }) as PublicClient; - const registry = new ReadRegistry({ address: connectedAppConfig.registries[chainId], - publicClient, - cache: sdkCache, - namespace: chainId.toString(), + drift: getDrift({ chainId }), }); const factoryAddresses = await registry.getFactoryAddresses(); const metas = await registry.getFactoryInfos(factoryAddresses); for (const [i, { data, name, version }] of metas.entries()) { - const { status } = decodeFactoryData(data); + const status = getStatus(data); factories.push({ name, address: factoryAddresses[i], diff --git a/apps/hyperdrive-trading/src/ui/chainlog/PoolsTable.tsx b/apps/hyperdrive-trading/src/ui/chainlog/PoolsTable.tsx index cb337a561..2a31d0cfe 100644 --- a/apps/hyperdrive-trading/src/ui/chainlog/PoolsTable.tsx +++ b/apps/hyperdrive-trading/src/ui/chainlog/PoolsTable.tsx @@ -1,6 +1,6 @@ -import { ReadRegistry } from "@delvtech/hyperdrive-viem"; +import { ReadRegistry } from "@delvtech/hyperdrive-js"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; -import { UseQueryResult, useQuery } from "@tanstack/react-query"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { createColumnHelper, @@ -9,13 +9,11 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { getPublicClient } from "@wagmi/core"; import classNames from "classnames"; import { ReactElement } from "react"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { wagmiConfig } from "src/network/wagmiClient"; -import { Status, decodeInstanceData } from "src/registry/data"; -import { sdkCache } from "src/sdk/sdkCache"; +import { getDrift } from "src/drift/getDrift"; +import { getStatus, Status } from "src/registry/data"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; import { NonIdealState } from "src/ui/base/components/NonIdealState"; import { TableSkeleton } from "src/ui/base/components/TableSkeleton"; @@ -23,7 +21,7 @@ import { AddressCell } from "src/ui/chainlog/AddressCell"; import { ChainCell } from "src/ui/chainlog/ChainCell"; import { PausedCell } from "src/ui/chainlog/PausedCell"; import { StatusCell } from "src/ui/chainlog/StatusCell"; -import { Address, PublicClient } from "viem"; +import { Address } from "viem"; export function PoolsTable(): ReactElement { const { data = [], isFetching } = usePoolsQuery(); @@ -209,7 +207,7 @@ interface Pool { vaultToken: Address; } -function usePoolsQuery(): UseQueryResult { +function usePoolsQuery(): UseQueryResult { const connectedAppConfig = useAppConfigForConnectedChain(); const chainIds = Object.keys(connectedAppConfig.registries).map(Number); @@ -225,15 +223,9 @@ function usePoolsQuery(): UseQueryResult { // requests by 80% and data transfer by 65%. Promise.all( chainIds.map(async (chainId) => { - const publicClient = getPublicClient(wagmiConfig as any, { - chainId, - }) as PublicClient; - const registry = new ReadRegistry({ address: connectedAppConfig.registries[chainId], - publicClient, - cache: sdkCache, - namespace: chainId.toString(), + drift: getDrift({ chainId }), }); return registry.getInstances().then((instances) => { @@ -242,7 +234,7 @@ function usePoolsQuery(): UseQueryResult { return registry .getInstanceInfo(pool.address) .then(({ data, factory, name, version }) => { - const { status } = decodeInstanceData(data); + const status = getStatus(data); return Promise.all([ pool.getPoolConfig(), diff --git a/apps/hyperdrive-trading/src/ui/drift/useDrift.ts b/apps/hyperdrive-trading/src/ui/drift/useDrift.ts new file mode 100644 index 000000000..2b9c6dd49 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/drift/useDrift.ts @@ -0,0 +1,46 @@ +import { Drift, ReadWriteAdapter } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { useMemo } from "react"; +import { getDriftOptions } from "src/drift/getDrift"; +import { + usePublicClient, + UsePublicClientParameters, + useWalletClient, + UseWalletClientParameters, +} from "wagmi"; + +export function useDrift( + params?: UsePublicClientParameters, +): Drift | undefined { + const publicClient = usePublicClient(params); + const { data: walletClient } = useWalletClient(params); + + return useMemo( + () => + publicClient + ? new Drift( + viemAdapter({ publicClient, walletClient }), + getDriftOptions({ chainId: publicClient.chain.id }), + ) + : undefined, + [publicClient, walletClient], + ); +} + +export function useReadWriteDrift( + params?: UseWalletClientParameters, +): Drift | undefined { + const publicClient = usePublicClient(params); + const { data: walletClient } = useWalletClient(params); + + return useMemo( + () => + publicClient && walletClient + ? new Drift( + viemAdapter({ publicClient, walletClient }), + getDriftOptions({ chainId: publicClient.chain.id }), + ) + : undefined, + [publicClient, walletClient], + ); +} diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useMarketState.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useMarketState.ts index 44378a896..3252fd120 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useMarketState.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useMarketState.ts @@ -1,4 +1,4 @@ -import { ReadHyperdrive } from "@delvtech/hyperdrive-js-core"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { QueryStatus, useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePoolInfo.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePoolInfo.ts index 0bc328044..291f8ec12 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePoolInfo.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePoolInfo.ts @@ -1,4 +1,4 @@ -import { PoolInfo } from "@delvtech/hyperdrive-viem"; +import { PoolInfo } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesIn.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesIn.ts index cc1711151..1d3b30d1c 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesIn.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesIn.ts @@ -3,7 +3,7 @@ import { appConfig, findHyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { QueryStatusWithIdle, getStatus } from "src/base/queryStatus"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesOut.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesOut.ts index 92f0cc27c..b5352d5d6 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesOut.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/usePrepareSharesOut.ts @@ -3,7 +3,7 @@ import { appConfig, findHyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { QueryStatusWithIdle, getStatus } from "src/base/queryStatus"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadHyperdrive.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadHyperdrive.ts index 58565fdf0..ef11be6d4 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadHyperdrive.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadHyperdrive.ts @@ -1,10 +1,12 @@ -import { appConfig } from "@delvtech/hyperdrive-appconfig"; -import { ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { + appConfig, + findHyperdriveConfig, +} from "@delvtech/hyperdrive-appconfig"; +import { getHyperdrive, ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { getReadHyperdrive } from "src/hyperdrive/getReadHyperdrive"; +import { useDrift } from "src/ui/drift/useDrift"; import { Address } from "viem"; -import { usePublicClient } from "wagmi"; export function useReadHyperdrive({ chainId, @@ -13,9 +15,8 @@ export function useReadHyperdrive({ chainId: number; address: Address | undefined; }): ReadHyperdrive | undefined { - const publicClient = usePublicClient({ chainId }); - - const enabled = !!address && !!publicClient; + const drift = useDrift({ chainId }); + const enabled = !!address && !!drift; const { data } = useQuery({ queryKey: makeQueryKey("getReadHyperdrive", { @@ -24,12 +25,18 @@ export function useReadHyperdrive({ }), enabled, queryFn: enabled - ? () => - getReadHyperdrive({ + ? () => { + const { initializationBlock } = findHyperdriveConfig({ hyperdriveAddress: address, - publicClient, - appConfig, - }) + hyperdriveChainId: chainId, + hyperdrives: appConfig.hyperdrives, + }); + return getHyperdrive({ + address, + drift, + earliestBlock: initializationBlock, + }); + } : undefined, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadWriteHyperdrive.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadWriteHyperdrive.ts index 913922112..98e261c43 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadWriteHyperdrive.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/hooks/useReadWriteHyperdrive.ts @@ -1,10 +1,12 @@ -import { appConfig } from "@delvtech/hyperdrive-appconfig"; -import { ReadWriteHyperdrive } from "@delvtech/hyperdrive-viem"; +import { + appConfig, + findHyperdriveConfig, +} from "@delvtech/hyperdrive-appconfig"; +import { getHyperdrive, ReadWriteHyperdrive } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { getReadWriteHyperdrive } from "src/hyperdrive/getReadWriteHyperdrive"; +import { useReadWriteDrift } from "src/ui/drift/useDrift"; import { Address } from "viem"; -import { usePublicClient, useWalletClient } from "wagmi"; export function useReadWriteHyperdrive({ address, @@ -13,10 +15,9 @@ export function useReadWriteHyperdrive({ address: Address | undefined; chainId: number; }): ReadWriteHyperdrive | undefined { - const publicClient = usePublicClient({ chainId }); - const { data: walletClient } = useWalletClient({ chainId }); + const drift = useReadWriteDrift({ chainId }); - const enabled = !!address && !!publicClient && !!walletClient; + const enabled = !!address && !!drift; const { data } = useQuery({ queryKey: makeQueryKey("getReadWriteHyperdrive", { @@ -25,13 +26,18 @@ export function useReadWriteHyperdrive({ }), enabled, queryFn: enabled - ? () => - getReadWriteHyperdrive({ + ? () => { + const { initializationBlock } = findHyperdriveConfig({ hyperdriveAddress: address, - publicClient, - walletClient, - appConfig, - }) + hyperdriveChainId: chainId, + hyperdrives: appConfig.hyperdrives, + }); + return getHyperdrive({ + address, + drift, + earliestBlock: initializationBlock, + }); + } : undefined, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongForm/CloseLongForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongForm/CloseLongForm.tsx index a199fa518..ad6b7c464 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongForm/CloseLongForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongForm/CloseLongForm.tsx @@ -6,7 +6,7 @@ import { HyperdriveConfig, TokenConfig, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage, Long } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage, Long } from "@delvtech/hyperdrive-js"; import { MouseEvent, ReactElement } from "react"; import { isTestnetChain } from "src/chains/isTestnetChain"; import { LoadingButton } from "src/ui/base/components/LoadingButton"; 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 88309aa73..32fa568cc 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/CloseLongModalButton/CloseLongModalButton.tsx @@ -5,7 +5,7 @@ import { findBaseToken, findToken, } from "@delvtech/hyperdrive-appconfig"; -import { Long } from "@delvtech/hyperdrive-viem"; +import { Long } from "@delvtech/hyperdrive-js"; import { ReactElement } from "react"; import { Modal } from "src/ui/base/components/Modal/Modal"; import { ModalHeader } from "src/ui/base/components/Modal/ModalHeader"; 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 d2a04dc16..189b0ccaf 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/OpenLongForm/OpenLongForm.tsx @@ -5,7 +5,7 @@ import { findToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js-core"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import { MouseEvent, ReactElement } from "react"; import { isTestnetChain } from "src/chains/isTestnetChain"; import { getIsValidTradeSize } from "src/hyperdrive/getIsValidTradeSize"; @@ -368,7 +368,6 @@ export function OpenLongForm({ primaryStats={ { - queryClient.invalidateQueries(); - toast.success( - , - { id: hash, duration: SUCCESS_TOAST_DURATION }, - ); - toastWarpcast(); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: txHash, - transactionType: "close", - positionType: "long", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { id: hash, duration: SUCCESS_TOAST_DURATION }, + ); + toastWarpcast(); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "close", + positionType: "long", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useFixedRate.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useFixedRate.ts index 05ec95cae..c8896d295 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useFixedRate.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useFixedRate.ts @@ -1,8 +1,9 @@ +import { BlockTag } from "@delvtech/drift"; import { appConfig, findHyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { getHprFromApr } from "@delvtech/hyperdrive-viem"; +import { getHprFromApr } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { formatRate } from "src/base/formatRate"; @@ -14,11 +15,11 @@ import { Address } from "viem"; export function useFixedRate({ chainId, hyperdriveAddress, - blockNumber, + block, }: { chainId: number; hyperdriveAddress: Address; - blockNumber?: bigint; + block?: BlockTag | bigint; }): { fixedApr: { apr: bigint; formatted: string } | undefined; fixedRoi: { roi: bigint; formatted: string } | undefined; @@ -40,7 +41,7 @@ export function useFixedRate({ queryKey: makeQueryKey("fixedApr", { chainId, address: hyperdriveAddress }), queryFn: queryEnabled ? async () => { - const fixedApr = await readHyperdrive.getFixedApr({ blockNumber }); + const fixedApr = await readHyperdrive.getFixedApr({ block }); const fixedRoi = getHprFromApr( fixedApr, hyperdrive.poolConfig.positionDuration, diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLong.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLong.tsx index 878a3f343..cb4062958 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLong.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLong.tsx @@ -89,30 +89,35 @@ export function useOpenLong({ }, options: { value: ethValue, - }, - onTransactionCompleted: (txHash) => { - queryClient.invalidateQueries(); - toast.success( - , - { id: txHash, duration: SUCCESS_TOAST_DURATION }, - ); - setTimeout(() => { - toastWarpcast(); - }, SUCCESS_TOAST_DURATION); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: txHash, - transactionType: "open", - positionType: "long", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { + id: receipt.transactionHash, + duration: SUCCESS_TOAST_DURATION, + }, + ); + setTimeout(() => { + toastWarpcast(); + }, SUCCESS_TOAST_DURATION); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "open", + positionType: "long", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLongs.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLongs.ts index fe568c4c7..ecbb55980 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLongs.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useOpenLongs.ts @@ -1,4 +1,4 @@ -import { Long, OpenLongPositionReceived } from "@delvtech/hyperdrive-viem"; +import { Long, OpenLongPositionReceived } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalClosedLongsValue.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalClosedLongsValue.ts index c62b0ceeb..bfe51d5d8 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalClosedLongsValue.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalClosedLongsValue.ts @@ -1,5 +1,5 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { ClosedLong } from "@delvtech/hyperdrive-viem"; +import { ClosedLong } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalOpenLongsValue.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalOpenLongsValue.ts index 35d6f43f8..14865efd8 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalOpenLongsValue.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/longs/hooks/useTotalOpenLongsValue.ts @@ -1,5 +1,5 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { Long } from "@delvtech/hyperdrive-viem"; +import { Long } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { usePoolInfo } from "src/ui/hyperdrive/hooks/usePoolInfo"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx index 7b6df0c38..806da7b3f 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/AddLiquidityForm/AddLiquidityForm.tsx @@ -6,7 +6,7 @@ import { findBaseToken, findToken, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import classNames from "classnames"; import { MouseEvent, ReactElement } from "react"; import Skeleton from "react-loading-skeleton"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/RemoveLiquidityForm/RemoveLiquidityForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/RemoveLiquidityForm/RemoveLiquidityForm.tsx index 89f7699bd..97465e5f5 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/RemoveLiquidityForm/RemoveLiquidityForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/RemoveLiquidityForm/RemoveLiquidityForm.tsx @@ -5,7 +5,7 @@ import { findToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import { MouseEvent, ReactElement } from "react"; import Skeleton from "react-loading-skeleton"; import { calculateRatio } from "src/base/calculateRatio"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useAddLiquidity.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useAddLiquidity.tsx index ae4ca7939..dbdb45ebd 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useAddLiquidity.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useAddLiquidity.tsx @@ -92,28 +92,35 @@ export function useAddLiquidity({ maxApr, destination, }, - options: { value: ethValue }, - onTransactionCompleted: (txHash: Hash) => { - queryClient.invalidateQueries(); - toast.success( - , - { id: txHash, duration: SUCCESS_TOAST_DURATION }, - ); - toastWarpcast(); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: txHash, - transactionType: "open", - positionType: "lp", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + value: ethValue, + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { + id: receipt.transactionHash, + duration: SUCCESS_TOAST_DURATION, + }, + ); + toastWarpcast(); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "open", + positionType: "lp", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemWithdrawalShares.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemWithdrawalShares.ts index 564886991..3a8bc47e4 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemWithdrawalShares.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemWithdrawalShares.ts @@ -77,17 +77,21 @@ export function useRedeemWithdrawalShares({ destination, asBase, }, - onTransactionCompleted: (transactionHash) => { - queryClient.invalidateQueries(); - window.plausible("transactionSuccess", { - props: { - transactionHash, - transactionType: "close", - positionType: "lp", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "close", + positionType: "lp", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); addTransaction({ diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemedWithdrawalShares.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemedWithdrawalShares.ts index d46e3c3da..32d4088cd 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemedWithdrawalShares.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRedeemedWithdrawalShares.ts @@ -1,4 +1,4 @@ -import { RedeemedWithdrawalShares } from "@delvtech/hyperdrive-viem"; +import { RedeemedWithdrawalShares } from "@delvtech/hyperdrive-js"; import { QueryStatus, useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRemoveLiquidity.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRemoveLiquidity.tsx index dc2ad79be..da3988da9 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRemoveLiquidity.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/lp/hooks/useRemoveLiquidity.tsx @@ -80,27 +80,34 @@ export function useRemoveLiquidity({ minOutputPerShare: finalMinOutputPerShare, destination, }, - onTransactionCompleted: (txHash: Hash) => { - queryClient.invalidateQueries(); - toast.success( - , - { id: txHash, duration: SUCCESS_TOAST_DURATION }, - ); - toastWarpcast(); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: txHash, - transactionType: "close", - positionType: "lp", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { + id: receipt.transactionHash, + duration: SUCCESS_TOAST_DURATION, + }, + ); + toastWarpcast(); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "close", + positionType: "lp", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortForm/CloseShortForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortForm/CloseShortForm.tsx index 179320f61..85f7cd1b8 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortForm/CloseShortForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortForm/CloseShortForm.tsx @@ -5,7 +5,7 @@ import { findToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage, OpenShort } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage, OpenShort } from "@delvtech/hyperdrive-js"; import { MouseEvent, ReactElement } from "react"; import { isTestnetChain } from "src/chains/isTestnetChain"; import { LoadingButton } from "src/ui/base/components/LoadingButton"; 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 38a2e1a1a..7a3db3ea2 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/CloseShortModalButton/CloseShortModalButton.tsx @@ -5,7 +5,7 @@ import { findBaseToken, findToken, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import { ReactElement } from "react"; import { Modal } from "src/ui/base/components/Modal/Modal"; import { ModalHeader } from "src/ui/base/components/Modal/ModalHeader"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/ClosedShortsTable/ClosedShortsTable.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/ClosedShortsTable/ClosedShortsTable.tsx index c02174211..497d8358d 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/ClosedShortsTable/ClosedShortsTable.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/ClosedShortsTable/ClosedShortsTable.tsx @@ -4,7 +4,7 @@ import { appConfig, findBaseToken, } from "@delvtech/hyperdrive-appconfig"; -import { ClosedShort } from "@delvtech/hyperdrive-viem"; +import { ClosedShort } from "@delvtech/hyperdrive-js"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/16/solid"; import { createColumnHelper, diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortForm/OpenShortForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortForm/OpenShortForm.tsx index 51ac626da..79a1aa12e 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortForm/OpenShortForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortForm/OpenShortForm.tsx @@ -1,5 +1,5 @@ import { fixed } from "@delvtech/fixed-point-wasm"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js-core"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import { appConfig, diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/AccruedYieldCell.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/AccruedYieldCell.tsx index 10c6ec644..48ccfde23 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/AccruedYieldCell.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/AccruedYieldCell.tsx @@ -3,7 +3,7 @@ import { appConfig, findBaseToken, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import classNames from "classnames"; import { ReactElement } from "react"; import { formatBalance } from "src/ui/base/formatting/formatBalance"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentShortsValueCell.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentShortsValueCell.tsx index 3fa93f506..6e894f812 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentShortsValueCell.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentShortsValueCell.tsx @@ -3,7 +3,7 @@ import { findBaseToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import { ExclamationTriangleIcon } from "@heroicons/react/16/solid"; import classNames from "classnames"; import { ReactElement } from "react"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentValueCell.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentValueCell.tsx index f4d6ba20c..4a7cb3327 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentValueCell.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/CurrentValueCell.tsx @@ -3,7 +3,7 @@ import { appConfig, findBaseToken, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import classNames from "classnames"; import { ReactElement } from "react"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/OpenShortsTableDesktop.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/OpenShortsTableDesktop.tsx index 35fc58181..bbda584ba 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/OpenShortsTableDesktop.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/OpenShortsTableDesktop.tsx @@ -5,7 +5,7 @@ import { findToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; import { Link } from "@tanstack/react-router"; import { diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/ShortRateAndSizeCell.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/ShortRateAndSizeCell.tsx index e5133dba4..fbdb53cf3 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/ShortRateAndSizeCell.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/OpenShortsTable/ShortRateAndSizeCell.tsx @@ -3,7 +3,7 @@ import { findBaseToken, HyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import classNames from "classnames"; import { ReactElement } from "react"; import { formatRate } from "src/base/formatRate"; @@ -33,7 +33,7 @@ export function ShortRateAndSizeCell({ const { fixedApr } = useFixedRate({ chainId: hyperdrive.chainId, hyperdriveAddress: hyperdrive.address, - blockNumber: maturityBlock?.number, + block: maturityBlock?.number, }); const rateDifference = (fixedApr?.apr || 0n) - short.fixedRatePaid; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useCloseShort.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useCloseShort.tsx index f472a9461..543374fe8 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useCloseShort.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useCloseShort.tsx @@ -83,27 +83,34 @@ export function useCloseShort({ destination, maturityTime, }, - onTransactionCompleted: (txHash: Hash) => { - queryClient.invalidateQueries(); - toast.success( - , - { id: txHash, duration: SUCCESS_TOAST_DURATION }, - ); - toastWarpcast(); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: hash, - transactionType: "close", - positionType: "short", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { + id: receipt.transactionHash, + duration: SUCCESS_TOAST_DURATION, + }, + ); + toastWarpcast(); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "close", + positionType: "short", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useClosedShorts.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useClosedShorts.ts index 2ace58600..facdd22a0 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useClosedShorts.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useClosedShorts.ts @@ -1,4 +1,4 @@ -import { ClosedShort } from "@delvtech/hyperdrive-viem"; +import { ClosedShort } from "@delvtech/hyperdrive-js"; import { QueryStatus, useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShort.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShort.tsx index 58f789096..513e9d261 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShort.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShort.tsx @@ -1,5 +1,5 @@ import { appConfig } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import { useAddRecentTransaction } from "@rainbow-me/rainbowkit"; import { MutationStatus, @@ -72,19 +72,6 @@ export function useOpenShort({ return; } - const openShortOptions = { - value: ethValue - ? // Pad the eth value by 1% so that the tx goes through, the - // contract will refund any amount that it doesn't spend - adjustAmountByPercentage({ - amount: ethValue, - decimals: 18, - direction: "up", - percentage: parseUnits("1", 18), - }) - : undefined, - }; - // All shares need to be prepared before going into the sdk const finalMaxDeposit = asBase ? maxDeposit @@ -103,28 +90,44 @@ export function useOpenShort({ minVaultSharePrice, maxDeposit: finalMaxDeposit, }, - options: openShortOptions, - onTransactionCompleted: (txHash: `0x${string}`) => { - queryClient.invalidateQueries(); - toast.success( - , - { id: txHash, duration: SUCCESS_TOAST_DURATION }, - ); - toastWarpcast(); - onExecuted?.(txHash); - window.plausible("transactionSuccess", { - props: { - transactionHash: txHash, - transactionType: "open", - positionType: "short", - poolAddress: hyperdriveAddress, - chainId, - }, - }); + options: { + value: ethValue + ? // Pad the eth value by 1% so that the tx goes through, the + // contract will refund any amount that it doesn't spend + adjustAmountByPercentage({ + amount: ethValue, + decimals: 18, + direction: "up", + percentage: parseUnits("1", 18), + }) + : undefined, + onMined: (receipt) => { + queryClient.invalidateQueries(); + if (receipt?.status === "success") { + toast.success( + , + { + id: receipt.transactionHash, + duration: SUCCESS_TOAST_DURATION, + }, + ); + toastWarpcast(); + onExecuted?.(receipt.transactionHash); + window.plausible("transactionSuccess", { + props: { + transactionHash: receipt.transactionHash, + transactionType: "open", + positionType: "short", + poolAddress: hyperdriveAddress, + chainId, + }, + }); + } + }, }, }); diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShorts.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShorts.ts index 9ef178d33..7dc03dad5 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShorts.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useOpenShorts.ts @@ -1,4 +1,4 @@ -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { OpenShort } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useShortRate.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useShortRate.ts index 80683b3f6..261e51c6f 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useShortRate.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useShortRate.ts @@ -2,7 +2,7 @@ import { appConfig, findHyperdriveConfig, } from "@delvtech/hyperdrive-appconfig"; -import { getHprFromApr } from "@delvtech/hyperdrive-viem"; +import { getHprFromApr } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { formatRate } from "src/base/formatRate"; import { makeQueryKey } from "src/base/makeQueryKey"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalClosedShortsValue.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalClosedShortsValue.ts index c355be46d..78bf51f2f 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalClosedShortsValue.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalClosedShortsValue.ts @@ -1,5 +1,5 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { ClosedShort } from "@delvtech/hyperdrive-viem"; +import { ClosedShort } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { useReadHyperdrive } from "src/ui/hyperdrive/hooks/useReadHyperdrive"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalOpenShortsValue.ts b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalOpenShortsValue.ts index 26a721584..448396afd 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalOpenShortsValue.ts +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/shorts/hooks/useTotalOpenShortsValue.ts @@ -1,5 +1,5 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { Short } from "@delvtech/hyperdrive-viem"; +import { Short } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { usePoolInfo } from "src/ui/hyperdrive/hooks/usePoolInfo"; diff --git a/apps/hyperdrive-trading/src/ui/hyperdrive/withdrawalShares/RedeemWithdrawalSharesForm/RedeemWithdrawalSharesForm.tsx b/apps/hyperdrive-trading/src/ui/hyperdrive/withdrawalShares/RedeemWithdrawalSharesForm/RedeemWithdrawalSharesForm.tsx index 9dd2a9a77..742766377 100644 --- a/apps/hyperdrive-trading/src/ui/hyperdrive/withdrawalShares/RedeemWithdrawalSharesForm/RedeemWithdrawalSharesForm.tsx +++ b/apps/hyperdrive-trading/src/ui/hyperdrive/withdrawalShares/RedeemWithdrawalSharesForm/RedeemWithdrawalSharesForm.tsx @@ -6,7 +6,7 @@ import { HyperdriveConfig, TokenConfig, } from "@delvtech/hyperdrive-appconfig"; -import { adjustAmountByPercentage } from "@delvtech/hyperdrive-viem"; +import { adjustAmountByPercentage } from "@delvtech/hyperdrive-js"; import { ReactElement } from "react"; import { convertSharesToBase } from "src/hyperdrive/convertSharesToBase"; import { LabelValue } from "src/ui/base/components/LabelValue"; diff --git a/apps/hyperdrive-trading/src/ui/markets/PoolsList.tsx b/apps/hyperdrive-trading/src/ui/markets/PoolsList.tsx index ad9aa76a5..381248d48 100644 --- a/apps/hyperdrive-trading/src/ui/markets/PoolsList.tsx +++ b/apps/hyperdrive-trading/src/ui/markets/PoolsList.tsx @@ -6,14 +6,13 @@ import { HyperdriveConfig, TokenConfig, } from "@delvtech/hyperdrive-appconfig"; +import { getHyperdrive } from "@delvtech/hyperdrive-js"; import { AdjustmentsHorizontalIcon } from "@heroicons/react/20/solid"; import { QueryStatus, useQuery } from "@tanstack/react-query"; import { useNavigate, useSearch } from "@tanstack/react-router"; -import { getPublicClient } from "@wagmi/core"; import { ReactElement, ReactNode, useMemo } from "react"; import { ZERO_ADDRESS } from "src/base/constants"; -import { getReadHyperdrive } from "src/hyperdrive/getReadHyperdrive"; -import { wagmiConfig } from "src/network/wagmiClient"; +import { getDrift } from "src/drift/getDrift"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; import LoadingState from "src/ui/base/components/LoadingState"; import { MultiSelect } from "src/ui/base/components/MultiSelect"; @@ -21,7 +20,6 @@ import { NonIdealState } from "src/ui/base/components/NonIdealState"; import { Well } from "src/ui/base/components/Well/Well"; import { LANDING_ROUTE } from "src/ui/landing/routes"; import { PoolRow } from "src/ui/markets/PoolRow/PoolRow"; -import { PublicClient } from "viem"; import { useChainId } from "wagmi"; // TODO: Re-implement sorting without blocking the list from rendering. @@ -342,13 +340,10 @@ function usePoolsList(): { queryFn: async () => { const pools = await Promise.all( appConfigForConnectedChain.hyperdrives.map(async (hyperdrive) => { - const publicClient = getPublicClient(wagmiConfig as any, { - chainId: hyperdrive.chainId, - }); - const readHyperdrive = await getReadHyperdrive({ - hyperdriveAddress: hyperdrive.address, - appConfig: appConfig, - publicClient: publicClient as PublicClient, + const readHyperdrive = await getHyperdrive({ + address: hyperdrive.address, + drift: getDrift({ chainId: hyperdrive.chainId }), + earliestBlock: hyperdrive.initializationBlock, }); // We only show hyperdrives that are not paused diff --git a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLongsData.ts b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLongsData.ts index f9a5fac4d..28ca65d92 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLongsData.ts +++ b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLongsData.ts @@ -1,10 +1,12 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { OpenLongPositionReceived } from "@delvtech/hyperdrive-viem"; +import { + getHyperdrive, + OpenLongPositionReceived, +} from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { getReadHyperdrive } from "src/hyperdrive/getReadHyperdrive"; +import { getDrift } from "src/drift/getDrift"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; -import { usePublicClients } from "src/ui/hyperdrive/hooks/usePublicClients"; import { useAccount } from "wagmi"; type OpenLongPositionsData = { @@ -17,13 +19,8 @@ export function usePortfolioLongsData(): { openLongPositionsStatus: "error" | "success" | "loading"; } { const { address: account } = useAccount(); - const appConfigForConnectedChain = useAppConfigForConnectedChain(); - - const clients = usePublicClients( - Object.keys(appConfigForConnectedChain.chains).map(Number), - ); - const queryEnabled = !!account && !!appConfigForConnectedChain && !!clients; + const queryEnabled = !!account && !!appConfigForConnectedChain; const { data: openLongPositions, status: openLongPositionsStatus } = useQuery( { @@ -33,22 +30,10 @@ export function usePortfolioLongsData(): { ? async () => await Promise.all( appConfigForConnectedChain.hyperdrives.map(async (hyperdrive) => { - const publicClient = clients[hyperdrive.chainId]?.publicClient; - - if (!publicClient) { - console.error( - `No public client found for chainId ${hyperdrive.chainId}`, - ); - return { - hyperdrive, - openLongs: [], - }; - } - - const readHyperdrive = await getReadHyperdrive({ - appConfig: appConfigForConnectedChain, - hyperdriveAddress: hyperdrive.address, - publicClient, + const readHyperdrive = await getHyperdrive({ + address: hyperdrive.address, + drift: getDrift({ chainId: hyperdrive.chainId }), + earliestBlock: hyperdrive.initializationBlock, }); const allLongs = await readHyperdrive.getOpenLongPositions({ diff --git a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLpData.ts b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLpData.ts index 1701524b0..d58c65485 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLpData.ts +++ b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioLpData.ts @@ -1,9 +1,9 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; +import { getHyperdrive } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { getReadHyperdrive } from "src/hyperdrive/getReadHyperdrive"; +import { getDrift } from "src/drift/getDrift"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; -import { usePublicClients } from "src/ui/hyperdrive/hooks/usePublicClients"; import { useAccount } from "wagmi"; type LpPosition = { @@ -17,37 +17,20 @@ export function usePortfolioLpData(): { openLpPositionStatus: "error" | "success" | "loading"; } { const { address: account } = useAccount(); - const appConfigForConnectedChain = useAppConfigForConnectedChain(); - const queryEnabled = !!account && !!appConfigForConnectedChain.hyperdrives.length; - const clients = usePublicClients( - Object.keys(appConfigForConnectedChain.chains).map(Number), - ); + const { data, status } = useQuery({ queryKey: makeQueryKey("portfolioLp", { account }), queryFn: queryEnabled ? async () => { const results = await Promise.all( appConfigForConnectedChain.hyperdrives.map(async (hyperdrive) => { - const publicClient = clients[hyperdrive.chainId]?.publicClient; - - if (!publicClient) { - console.error( - `No public client found for chainId ${hyperdrive.chainId}`, - ); - return { - hyperdrive, - lpShares: 0n, - withdrawalShares: 0n, - }; - } - - const readHyperdrive = await getReadHyperdrive({ - appConfig: appConfigForConnectedChain, - hyperdriveAddress: hyperdrive.address, - publicClient, + const readHyperdrive = await getHyperdrive({ + address: hyperdrive.address, + drift: getDrift({ chainId: hyperdrive.chainId }), + earliestBlock: hyperdrive.initializationBlock, }); const [lpShares, withdrawalShares] = await Promise.all([ diff --git a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioShortsData.ts b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioShortsData.ts index a9dd8b6cb..3557e809b 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioShortsData.ts +++ b/apps/hyperdrive-trading/src/ui/portfolio/usePortfolioShortsData.ts @@ -1,10 +1,9 @@ import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig"; -import { OpenShort } from "@delvtech/hyperdrive-viem"; +import { getHyperdrive, OpenShort } from "@delvtech/hyperdrive-js"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; -import { getReadHyperdrive } from "src/hyperdrive/getReadHyperdrive"; +import { getDrift } from "src/drift/getDrift"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; -import { usePublicClients } from "src/ui/hyperdrive/hooks/usePublicClients"; import { useAccount } from "wagmi"; type OpenShortPositionsData = { @@ -17,13 +16,8 @@ export function usePortfolioShortsData(): { openShortPositionsStatus: "error" | "success" | "loading"; } { const { address: account } = useAccount(); - const appConfigForConnectedChain = useAppConfigForConnectedChain(); - - const clients = usePublicClients( - Object.keys(appConfigForConnectedChain.chains).map(Number), - ); - const queryEnabled = !!account && !!appConfigForConnectedChain && !!clients; + const queryEnabled = !!account && !!appConfigForConnectedChain; const { data: openShortPositions, status: openShortPositionsStatus } = useQuery({ @@ -33,22 +27,10 @@ export function usePortfolioShortsData(): { ? async () => await Promise.all( appConfigForConnectedChain.hyperdrives.map(async (hyperdrive) => { - const publicClient = clients[hyperdrive.chainId]?.publicClient; - - if (!publicClient) { - console.error( - `No public client found for chainId ${hyperdrive.chainId}`, - ); - return { - hyperdrive, - openShorts: [], - }; - } - - const readHyperdrive = await getReadHyperdrive({ - appConfig: appConfigForConnectedChain, - hyperdriveAddress: hyperdrive.address, - publicClient, + const readHyperdrive = await getHyperdrive({ + address: hyperdrive.address, + drift: getDrift({ chainId: hyperdrive.chainId }), + earliestBlock: hyperdrive.initializationBlock, }); return { diff --git a/apps/hyperdrive-trading/src/ui/registry/hooks/useReadRegistry.ts b/apps/hyperdrive-trading/src/ui/registry/hooks/useReadRegistry.ts index 612affdd1..aaca40460 100644 --- a/apps/hyperdrive-trading/src/ui/registry/hooks/useReadRegistry.ts +++ b/apps/hyperdrive-trading/src/ui/registry/hooks/useReadRegistry.ts @@ -1,21 +1,20 @@ -import { ReadRegistry } from "@delvtech/hyperdrive-viem"; +import { ReadRegistry } from "@delvtech/hyperdrive-js"; import { useMemo } from "react"; -import { sdkCache } from "src/sdk/sdkCache"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; -import { usePublicClient } from "wagmi"; +import { useDrift } from "src/ui/drift/useDrift"; export function useReadRegistry(chainId: number): ReadRegistry | undefined { const { registries } = useAppConfigForConnectedChain(); - const publicClient = usePublicClient(); + const drift = useDrift(); return useMemo( () => - publicClient + drift ? new ReadRegistry({ address: registries[chainId], - publicClient, - cache: sdkCache, + drift, + cacheNamespace: chainId, }) : undefined, - [publicClient, registries, chainId], + [drift, registries, chainId], ); } diff --git a/apps/sdk-sandbox/package.json b/apps/sdk-sandbox/package.json index 9c9e1e2e9..ed83ac9ce 100644 --- a/apps/sdk-sandbox/package.json +++ b/apps/sdk-sandbox/package.json @@ -11,9 +11,11 @@ "fixed-point:watch": "tsx -r dotenv/config --watch scripts/fixed-point.ts" }, "dependencies": { + "@delvtech/drift": "^0.0.1-beta.11", + "@delvtech/drift-viem": "^0.0.1-beta.13", "@delvtech/fixed-point-wasm": "*", "@delvtech/hyperdrive-appconfig": "*", - "@delvtech/hyperdrive-viem": "*", + "@delvtech/hyperdrive-js": "*", "@delvtech/hyperdrive-wasm": "*", "viem": "^2.9.2" }, diff --git a/apps/sdk-sandbox/scripts/example.ts b/apps/sdk-sandbox/scripts/example.ts index 2626ebee5..07e3b9597 100644 --- a/apps/sdk-sandbox/scripts/example.ts +++ b/apps/sdk-sandbox/scripts/example.ts @@ -1,22 +1,21 @@ -import { IHyperdrive } from "@delvtech/hyperdrive-artifacts/IHyperdrive"; -import { ReadHyperdrive } from "@delvtech/hyperdrive-viem"; +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { ReadHyperdrive } from "@delvtech/hyperdrive-js"; import { publicClient } from "../client"; +const drift = new Drift(viemAdapter({ publicClient })); + const pool = new ReadHyperdrive({ - address: "0x1cB0E96C07910fee9a22607bb9228c73848903a3", - publicClient, + address: "0x324395D5d835F84a02A75Aa26814f6fD22F25698", + drift, }); const kind = await pool.getKind(); -const name = await publicClient.readContract({ - abi: IHyperdrive.abi, - address: "0x1cB0E96C07910fee9a22607bb9228c73848903a3", - functionName: "name", -}); const config = await pool.getPoolConfig(); + console.log(` + address: ${pool.address} kind: ${kind} - name: ${name} baseToken: ${config.baseToken} sharesToken: ${config.vaultSharesToken} `); diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/aero/getAeroHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/aero/getAeroHyperdrive.ts index 7aeec9714..eea53321b 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/aero/getAeroHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/aero/getAeroHyperdrive.ts @@ -41,7 +41,7 @@ export async function getAeroLpHyperdrive({ // safe to cast here because we know the pool was initialized const initializationBlock = await hyperdrive.getInitializationBlock(); const hyperdriveConfig: HyperdriveConfig = { - chainId: await hyperdrive.network.getChainId(), + chainId: await hyperdrive.drift.getChainId(), kind: await hyperdrive.getKind(), // safe to cast here because we know the pool was initialized initializationBlock: initializationBlock.blockNumber as bigint, diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/cbeth/getCbethHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/cbeth/getCbethHyperdrive.ts index f39369d4e..19a6b285f 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/cbeth/getCbethHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/cbeth/getCbethHyperdrive.ts @@ -18,7 +18,7 @@ export async function getCbethHyperdrive({ sharesTokenConfig: TokenConfig; hyperdriveConfig: HyperdriveConfig; }> { - const chainId = await hyperdrive.network.getChainId(); + const chainId = await hyperdrive.drift.getChainId(); const version = await hyperdrive.getVersion(); const poolConfig = await hyperdrive.getPoolConfig(); diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/custom/getCustomHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/custom/getCustomHyperdrive.ts index 80fef4d00..208ffd5f7 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/custom/getCustomHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/custom/getCustomHyperdrive.ts @@ -65,7 +65,7 @@ export async function getCustomHyperdrive({ }); const hyperdriveConfig: HyperdriveConfig = { - chainId: await hyperdrive.network.getChainId(), + chainId: await hyperdrive.drift.getChainId(), kind: await hyperdrive.getKind(), address: hyperdrive.address, version: version.string, diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/gnosisWsteth/getGnosisWstethHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/gnosisWsteth/getGnosisWstethHyperdrive.ts index 5d8be638d..dbc18700a 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/gnosisWsteth/getGnosisWstethHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/gnosisWsteth/getGnosisWstethHyperdrive.ts @@ -18,7 +18,7 @@ export async function getGnosisWstethHyperdrive({ sharesTokenConfig: TokenConfig; hyperdriveConfig: HyperdriveConfig; }> { - const chainId = await hyperdrive.network.getChainId(); + const chainId = await hyperdrive.drift.getChainId(); const version = await hyperdrive.getVersion(); const poolConfig = await hyperdrive.getPoolConfig(); diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/morpho/getMorphoHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/morpho/getMorphoHyperdrive.ts index 03593806f..923ed048f 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/morpho/getMorphoHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/morpho/getMorphoHyperdrive.ts @@ -41,7 +41,7 @@ export async function getMorphoHyperdrive({ // safe to cast here because we know the pool was initialized const initializationBlock = await hyperdrive.getInitializationBlock(); const hyperdriveConfig: HyperdriveConfig = { - chainId: await hyperdrive.network.getChainId(), + chainId: await hyperdrive.drift.getChainId(), kind: await hyperdrive.getKind(), // safe to cast here because we know the pool was initialized initializationBlock: initializationBlock.blockNumber as bigint, diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/steth/getStethHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/steth/getStethHyperdrive.ts index 30984bc5e..1678e3e31 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/steth/getStethHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/steth/getStethHyperdrive.ts @@ -15,7 +15,7 @@ export async function getStethHyperdrive({ baseTokenConfig: TokenConfig; hyperdriveConfig: HyperdriveConfig; }> { - const chainId = await hyperdrive.network.getChainId(); + const chainId = await hyperdrive.drift.getChainId(); const version = await hyperdrive.getVersion(); const poolConfig = await hyperdrive.getPoolConfig(); @@ -31,7 +31,7 @@ export async function getStethHyperdrive({ const baseTokenConfig: TokenConfig = { address: poolConfig.baseToken, - chainId: await hyperdrive.network.getChainId(), + chainId: await hyperdrive.drift.getChainId(), name: "Ether", symbol: "ETH", decimals: 18, diff --git a/packages/hyperdrive-appconfig/src/hyperdrives/susds/getSusdsHyperdrive.ts b/packages/hyperdrive-appconfig/src/hyperdrives/susds/getSusdsHyperdrive.ts index 79741ab5d..5ad95f606 100644 --- a/packages/hyperdrive-appconfig/src/hyperdrives/susds/getSusdsHyperdrive.ts +++ b/packages/hyperdrive-appconfig/src/hyperdrives/susds/getSusdsHyperdrive.ts @@ -41,7 +41,7 @@ export async function getSusdsHyperdrive({ // safe to cast here because we know the pool was initialized const initializationBlock = await hyperdrive.getInitializationBlock(); const hyperdriveConfig: HyperdriveConfig = { - chainId: await hyperdrive.network.getChainId(), + chainId: await hyperdrive.drift.getChainId(), kind: await hyperdrive.getKind(), // safe to cast here because we know the pool was initialized initializationBlock: initializationBlock.blockNumber as bigint, diff --git a/packages/hyperdrive-appconfig/src/tokens/getTokenConfig.ts b/packages/hyperdrive-appconfig/src/tokens/getTokenConfig.ts index b32a565de..36ac24ed2 100644 --- a/packages/hyperdrive-appconfig/src/tokens/getTokenConfig.ts +++ b/packages/hyperdrive-appconfig/src/tokens/getTokenConfig.ts @@ -13,7 +13,7 @@ export async function getTokenConfig({ places: number; }): Promise { return { - chainId: await token.network.getChainId(), + chainId: await token.drift.getChainId(), address: token.address, decimals: await token.getDecimals(), places, diff --git a/packages/hyperdrive-js/.eslintrc b/packages/hyperdrive-js/.eslintrc new file mode 100644 index 000000000..0f852a697 --- /dev/null +++ b/packages/hyperdrive-js/.eslintrc @@ -0,0 +1,6 @@ +{ + "root": true, + "extends": [ + "@hyperdrive/eslint-config" + ], +} \ No newline at end of file diff --git a/packages/hyperdrive-js/.gitignore b/packages/hyperdrive-js/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/packages/hyperdrive-js/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/hyperdrive-js/.prettierignore b/packages/hyperdrive-js/.prettierignore new file mode 100644 index 000000000..76add878f --- /dev/null +++ b/packages/hyperdrive-js/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/packages/hyperdrive-js/.prettierrc.mjs b/packages/hyperdrive-js/.prettierrc.mjs new file mode 100644 index 000000000..c9a63a1d6 --- /dev/null +++ b/packages/hyperdrive-js/.prettierrc.mjs @@ -0,0 +1,7 @@ +import repoConfig from "@hyperdrive/prettier-config"; + +/** @type {import("prettier").Config} */ +export default { + ...repoConfig, + plugins: ["prettier-plugin-organize-imports"], +}; diff --git a/packages/hyperdrive-js/LICENSE b/packages/hyperdrive-js/LICENSE new file mode 100644 index 000000000..4f4a9cf45 --- /dev/null +++ b/packages/hyperdrive-js/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [DELV, Inc] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/hyperdrive-js/package.json b/packages/hyperdrive-js/package.json new file mode 100644 index 000000000..46822c9b1 --- /dev/null +++ b/packages/hyperdrive-js/package.json @@ -0,0 +1,64 @@ +{ + "name": "@delvtech/hyperdrive-js", + "version": "0.0.0", + "license": "AGPL-3.0", + "type": "module", + "scripts": { + "build": "tsup", + "watch": "npm run build -- --watch", + "test": "vitest run", + "test:watch": "vitest --reporter=verbose", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@delvtech/drift": "^0.0.1-beta.11" + }, + "dependencies": { + "@delvtech/fixed-point-wasm": "^0.0.6", + "@delvtech/hyperdrive-artifacts": "^1.0.18", + "@delvtech/hyperdrive-wasm": "^0.15.3", + "lodash.groupby": "^4.6.0", + "lodash.mapvalues": "^4.6.0", + "semver": "^7.6.3" + }, + "devDependencies": { + "@delvtech/drift": "^0.0.1-beta.11", + "@hyperdrive/eslint-config": "*", + "@hyperdrive/prettier-config": "*", + "@hyperdrive/tsconfig": "*", + "@types/lodash.groupby": "^4.6.9", + "@types/lodash.mapvalues": "^4.6.9", + "@types/sinon": "^17.0.3", + "abitype": "^1.0.5", + "dotenv": "^16.4.5", + "prettier": "3.3.3", + "prettier-plugin-organize-imports": "4.0.0", + "sinon": "^18.0.0", + "tsconfig-paths": "^4.2.0", + "tsup": "^8.2.3", + "typescript": "^5.5.4", + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^2.0.4" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./v1.0.14": { + "types": "./dist/v1.0.14.d.ts", + "default": "./dist/v1.0.14.js", + "require": "./dist/v1.0.14.cjs" + }, + "./package.json": "./package.json" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/hyperdrive-js/src/HyperdriveSdkError.ts b/packages/hyperdrive-js/src/HyperdriveSdkError.ts new file mode 100644 index 000000000..32f6a41f8 --- /dev/null +++ b/packages/hyperdrive-js/src/HyperdriveSdkError.ts @@ -0,0 +1,11 @@ +import { DriftError } from "@delvtech/drift"; + +export class HyperdriveSdkError extends DriftError { + constructor(error: string, options?: ErrorOptions) { + super(error, { + ...options, + prefix: "ᛋ ", + name: "Hyperdrive SDK Error", + }); + } +} diff --git a/packages/hyperdrive-js/src/base/adjustAmountByPercentage.test.ts b/packages/hyperdrive-js/src/base/adjustAmountByPercentage.test.ts new file mode 100644 index 000000000..a704a85a4 --- /dev/null +++ b/packages/hyperdrive-js/src/base/adjustAmountByPercentage.test.ts @@ -0,0 +1,47 @@ +import { adjustAmountByPercentage } from "src/base/adjustAmountByPercentage"; +import { parseFixed } from "src/fixed-point"; +import { expect, test } from "vitest"; + +test("should return adjusted amount down when given a basic input", () => { + expect( + adjustAmountByPercentage({ + amount: parseFixed(100).bigint, + percentage: parseFixed(1).bigint, + decimals: 18, + direction: "down", + }), + ).toBe(parseFixed(99).bigint); +}); +test("should return adjusted amount up when given a basic input", () => { + expect( + adjustAmountByPercentage({ + amount: parseFixed(100).bigint, + percentage: parseFixed(1).bigint, + decimals: 18, + direction: "up", + }), + ).toBe(parseFixed(101).bigint); +}); + +test("should handle precision accurately when given precise input amounts", () => { + const amount = parseFixed("100.123456789012345678").bigint; + expect( + adjustAmountByPercentage({ + amount, + percentage: parseFixed(1).bigint, + decimals: 18, + direction: "down", + }), + ).toBe(amount - amount / 100n); +}); + +test("should return zero when input amount is zero", () => { + expect( + adjustAmountByPercentage({ + amount: 0n, + percentage: parseFixed(1).bigint, + decimals: 18, + direction: "down", + }), + ).toBe(0n); +}); diff --git a/packages/hyperdrive-js/src/base/adjustAmountByPercentage.ts b/packages/hyperdrive-js/src/base/adjustAmountByPercentage.ts new file mode 100644 index 000000000..1128ec6bb --- /dev/null +++ b/packages/hyperdrive-js/src/base/adjustAmountByPercentage.ts @@ -0,0 +1,55 @@ +import { fixed } from "src/fixed-point"; + +interface AdjustAmountByPercentageOptions { + /** + * The amount to adjust + */ + amount: bigint; + /** + * The percentage to adjust it by, eg: 1e18 for 1% + */ + percentage: bigint; + + /** + * The decimals of precision for the `amount` + */ + decimals: number; + direction: "up" | "down"; +} + +/** + * Adjusts a given amount by some percentage. Useful for slippage calculations. + * + * Example: + * + * ```ts + * adjustAmountByPercentage({ + * amount: parseUnits("100", 18), + * decimals: 18, + * percentage: BigInt(1e18), + * direction: "down", + * }) === parseUnits("99") + * ``` + * + * ```ts + * adjustAmountByPercentage({ + * amount: parseUnits("100", 18), + * decimals: 18, + * percentage: BigInt(1e18), + * direction: "up", + * }) === parseUnits("101") + * ``` + */ +export function adjustAmountByPercentage({ + amount, + percentage, + decimals, + direction, +}: AdjustAmountByPercentageOptions): bigint { + const slippageAmount = fixed(amount, decimals) + .mul(percentage, decimals) + .div(100, 0); + return direction === "down" + ? amount - slippageAmount.bigint + : amount + slippageAmount.bigint; +} diff --git a/packages/hyperdrive-js/src/base/assertNever.ts b/packages/hyperdrive-js/src/base/assertNever.ts new file mode 100644 index 000000000..d3fe8b7ae --- /dev/null +++ b/packages/hyperdrive-js/src/base/assertNever.ts @@ -0,0 +1,35 @@ +import { HyperdriveSdkError } from "src/HyperdriveSdkError"; + +/** + * Helper function for exhaustive checks of discriminated unions. + * https://basarat.gitbooks.io/typescript/docs/types/discriminated-unions.html + * + * @example + * + * type A = {type: 'a'}; + * type B = {type: 'b'}; + * type Union = A | B; + * + * function doSomething(arg: Union) { + * if (arg.type === 'a') { + * return something; + * } + * + * if (arg.type === 'b') { + * return somethingElse; + * } + * + * // TS will error if there are other types in the union + * // Will throw an Error when called at runtime. + * // Use `assertNever(arg, true)` instead to fail silently. + * return assertNever(arg); + * } + */ export function assertNever(value: never, noThrow?: boolean): never { + if (noThrow) { + return value; + } + + throw new HyperdriveSdkError( + `Unhandled discriminated union member: ${JSON.stringify(value)}`, + ); +} diff --git a/packages/hyperdrive-js/src/base/calculateAprFromPrice.test.ts b/packages/hyperdrive-js/src/base/calculateAprFromPrice.test.ts new file mode 100644 index 000000000..5cbbbd160 --- /dev/null +++ b/packages/hyperdrive-js/src/base/calculateAprFromPrice.test.ts @@ -0,0 +1,12 @@ +import { calculateAprFromPrice } from "src/base/calculateAprFromPrice"; +import { expect, test } from "vitest"; + +test("calculateAprFromPrice should return fixed rate an open long position is currently earning", async () => { + const rate = calculateAprFromPrice({ + positionDuration: 604800n, + baseAmount: 100000000000000000000n, + bondAmount: 100086217686058270990n, + }); + + expect(rate).toEqual(44956364873241333n); +}); diff --git a/packages/hyperdrive-js/src/base/calculateAprFromPrice.ts b/packages/hyperdrive-js/src/base/calculateAprFromPrice.ts new file mode 100644 index 000000000..698dc3450 --- /dev/null +++ b/packages/hyperdrive-js/src/base/calculateAprFromPrice.ts @@ -0,0 +1,30 @@ +import { fixed } from "src/fixed-point"; +import { hyperwasm } from "src/hyperwasm"; + +/** + * Calculate the APR of a position given the position duration, the amount paid + * in base, and the amount of bonds received. + */ +export function calculateAprFromPrice({ + positionDuration, + baseAmount, + bondAmount, +}: { + /** + * The position duration in seconds. + */ + positionDuration: bigint; + /** + * The amount paid in base. + */ + baseAmount: bigint; + /** + * The amount of bonds received. + */ + bondAmount: bigint; +}): bigint { + return hyperwasm.calcAprGivenFixedPrice({ + positionDuration, + price: fixed(baseAmount).div(bondAmount).bigint, + }); +} diff --git a/packages/hyperdrive-js/src/base/constants.ts b/packages/hyperdrive-js/src/base/constants.ts new file mode 100644 index 000000000..e26a49b3b --- /dev/null +++ b/packages/hyperdrive-js/src/base/constants.ts @@ -0,0 +1,8 @@ +export const SECONDS_PER_YEAR = 31536000n; + +export const MAX_UINT256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + +export const NULL_BYTES = "0x"; diff --git a/packages/hyperdrive-js/src/base/getHprFromApr.ts b/packages/hyperdrive-js/src/base/getHprFromApr.ts new file mode 100644 index 000000000..ddcde9159 --- /dev/null +++ b/packages/hyperdrive-js/src/base/getHprFromApr.ts @@ -0,0 +1,6 @@ +import { hyperwasm } from "src/hyperwasm"; +// TODO: This is now simply a renamed, re-exported hyperwasm function. We should +// consider re-exporting all hyperwasm functions directly or not at all. +export function getHprFromApr(apr: bigint, positionDuration: bigint): bigint { + return hyperwasm.calcHprGivenApr({ apr, positionDuration }); +} diff --git a/packages/hyperdrive-js/src/base/getHprFromApy.ts b/packages/hyperdrive-js/src/base/getHprFromApy.ts new file mode 100644 index 000000000..35a8d8c36 --- /dev/null +++ b/packages/hyperdrive-js/src/base/getHprFromApy.ts @@ -0,0 +1,6 @@ +import { hyperwasm } from "src/hyperwasm"; +// TODO: This is now simply a renamed, re-exported hyperwasm function. We should +// consider re-exporting all hyperwasm functions directly or not at all. +export function getHprFromApy(apy: bigint, positionDuration: bigint): bigint { + return hyperwasm.calcHprGivenApy({ apy, positionDuration }); +} diff --git a/packages/hyperdrive-js/src/base/testing/accounts.ts b/packages/hyperdrive-js/src/base/testing/accounts.ts new file mode 100644 index 000000000..f04008eed --- /dev/null +++ b/packages/hyperdrive-js/src/base/testing/accounts.ts @@ -0,0 +1,3 @@ +export const BOB = "0xBob"; +export const ALICE = "0xAlice"; +export const NANCY = "0xNancy"; diff --git a/packages/hyperdrive-js/src/base/types.ts b/packages/hyperdrive-js/src/base/types.ts new file mode 100644 index 000000000..13e33b13b --- /dev/null +++ b/packages/hyperdrive-js/src/base/types.ts @@ -0,0 +1,7 @@ +/** + * A generic constructor type. + */ +export type Constructor< + TInstanceType = any, + TArgs extends any[] = any[], +> = new (...args: TArgs) => TInstanceType; diff --git a/packages/hyperdrive-js/src/drift/ContractClient.ts b/packages/hyperdrive-js/src/drift/ContractClient.ts new file mode 100644 index 000000000..735b64c52 --- /dev/null +++ b/packages/hyperdrive-js/src/drift/ContractClient.ts @@ -0,0 +1,32 @@ +import { SimpleCache } from "@delvtech/drift"; +import { Address } from "abitype"; +import { ReadClientOptions } from "src/drift/ReadClient"; +import { ReadWriteClientOptions } from "src/drift/ReadWriteClient"; + +/** + * Additional options required for clients that represent a specific contract. + */ +export interface ContractClientOptions { + /** + * The address of the contract. + */ + address: Address; + + /** + * The cache to use for the contract. + */ + cache?: SimpleCache; + + /** + * The namespace to use for the cache. + */ + cacheNamespace?: PropertyKey; +} + +export interface ReadContractClientOptions + extends ReadClientOptions, + ContractClientOptions {} + +export interface ReadWriteContractClientOptions + extends ReadWriteClientOptions, + ContractClientOptions {} diff --git a/packages/hyperdrive-js/src/drift/ReadClient.ts b/packages/hyperdrive-js/src/drift/ReadClient.ts new file mode 100644 index 000000000..515130daa --- /dev/null +++ b/packages/hyperdrive-js/src/drift/ReadClient.ts @@ -0,0 +1,58 @@ +import { Drift } from "@delvtech/drift"; + +/** + * The base options required for all read clients. + */ +export interface ReadClientOptions { + drift: Drift; + + /** + * An arbitrary name for the instance. This is for convenience only (e.g., for + * use as a display name or in logging) and has no affect on the client's + * behavior. + */ + debugName?: string; + + /** + * The earliest block to fetch events from. + */ + earliestBlock?: bigint; +} + +/** + * A base class for read-only clients. + */ +export class ReadClient { + drift: Drift; + debugName: string; + + constructor({ debugName, drift, earliestBlock }: ReadClientOptions) { + this.debugName = debugName ?? this.constructor.name; + this.drift = drift; + + // Override the contract factory to ensure that events are fetched from the + // earliest block if necessary. + if (earliestBlock) { + const originalContractFactory = this.drift.contract; + this.drift.contract = (options) => { + const contract = originalContractFactory(options); + + // Override the getEvents method + const originalGetEvents = contract.getEvents; + contract.getEvents = async function (eventName, options) { + const _options = { ...options }; + const fromBlock = _options?.fromBlock; + const isBeforeEarliest = + typeof fromBlock === "bigint" && fromBlock < earliestBlock; + if (!fromBlock || fromBlock === "earliest" || isBeforeEarliest) { + _options.fromBlock = earliestBlock; + } + + return originalGetEvents(eventName, _options); + }; + + return contract as any; + }; + } + } +} diff --git a/packages/hyperdrive-js/src/drift/ReadWriteClient.ts b/packages/hyperdrive-js/src/drift/ReadWriteClient.ts new file mode 100644 index 000000000..fe7cefb63 --- /dev/null +++ b/packages/hyperdrive-js/src/drift/ReadWriteClient.ts @@ -0,0 +1,20 @@ +import { Drift, ReadWriteAdapter } from "@delvtech/drift"; +import { ReadClient, ReadClientOptions } from "src/drift/ReadClient"; + +/** + * The base options required for all read-write clients. + */ +export interface ReadWriteClientOptions extends ReadClientOptions { + drift: Drift; +} + +/** + * A base class for read-write clients. + */ +export class ReadWriteClient extends ReadClient { + declare drift: Drift; + + constructor(options: ReadWriteClientOptions) { + super(options); + } +} diff --git a/packages/hyperdrive-js/src/drift/getBlockOrThrow.ts b/packages/hyperdrive-js/src/drift/getBlockOrThrow.ts new file mode 100644 index 000000000..7ede661ce --- /dev/null +++ b/packages/hyperdrive-js/src/drift/getBlockOrThrow.ts @@ -0,0 +1,30 @@ +import { + Block, + ContractReadOptions, + Drift, + GetBlockParams, +} from "@delvtech/drift"; +import { HyperdriveSdkError } from "src/HyperdriveSdkError"; + +export type GetBlockOrThrowParams = GetBlockParams & ContractReadOptions; + +/** + * A utility that tries to fetch a block from a given network and throws an + * error if no block is found. Useful for unified error handling when fetching + * blocks that may not exist. + * @throws `BlockNotFoundError` + */ +export async function getBlockOrThrow( + drift: Drift, + options?: GetBlockOrThrowParams, +): Promise { + const fetched = await drift.getBlock(options); + if (!fetched) { + const block = + options?.blockHash ?? options?.blockNumber ?? options?.blockTag; + throw new HyperdriveSdkError( + `Block${block !== undefined ? ` ${block}` : ""} not found`, + ); + } + return fetched; +} diff --git a/packages/hyperdrive-js/src/exports/index.ts b/packages/hyperdrive-js/src/exports/index.ts new file mode 100644 index 000000000..9c7ac4d6f --- /dev/null +++ b/packages/hyperdrive-js/src/exports/index.ts @@ -0,0 +1,163 @@ +export { HyperdriveSdkError } from "src/HyperdriveSdkError"; + +// Hyperdrive // + +export { + getHyperdrive, + type Hyperdrive, + type HyperdriveOptions, +} from "src/hyperdrive/getHyperdrive"; + +export type { MarketState, PoolConfig, PoolInfo } from "src/hyperdrive/types"; + +export { hyperdriveAbi, type HyperdriveAbi } from "src/hyperdrive/abi"; +export { + ReadHyperdrive, + type ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +export { + ReadWriteHyperdrive, + type ReadWriteHyperdriveOptions, +} from "src/hyperdrive/ReadWriteHyperdrive"; + +// erc-4626 +export { ReadErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadErc4626Hyperdrive"; +export { ReadMockErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadMockErc4626Hyperdrive"; +export { ReadWriteErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive"; +export { ReadWriteMockErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadWriteMockErc4626Hyperdrive"; + +// ezeth +export { + ezEthHyperdriveAbi, + type EzEthHyperdriveAbi, +} from "src/hyperdrive/ezeth/abi"; +export { ReadEzEthHyperdrive } from "src/hyperdrive/ezeth/ReadEzEthHyperdrive"; +export { ReadWriteEzEthHyperdrive } from "src/hyperdrive/ezeth/ReadWriteEzEthHyperdrive"; + +// lseth +export { ReadLsEthHyperdrive } from "src/hyperdrive/lseth/ReadLsEthHyperdrive"; +export { ReadWriteLsEthHyperdrive } from "src/hyperdrive/lseth/ReadWriteLsEthHyperdrive"; + +// reth +export { ReadREthHyperdrive } from "src/hyperdrive/reth/ReadREthHyperdrive"; +export { ReadWriteREthHyperdrive } from "src/hyperdrive/reth/ReadWriteREthHyperdrive"; + +// steth +export { + ReadStEthHyperdrive, + type ReadStEthHyperdriveOptions, +} from "src/hyperdrive/steth/ReadStEthHyperdrive"; +export { + ReadWriteStEthHyperdrive, + type ReadWriteStEthHyperdriveOptions, +} from "src/hyperdrive/steth/ReadWriteStEthHyperdrive"; + +// shorts +export type { + ClosedShort, + OpenShort, + Short, +} from "src/hyperdrive/shorts/types"; + +// longs +export { calculateMatureLongYieldAfterFees } from "src/hyperdrive/longs/calculateMatureLongYieldAfterFees"; +export type { + ClosedLong, + Long, + OpenLongPositionReceived, +} from "src/hyperdrive/longs/types"; + +// lp +export type { ClosedLpShares } from "src/hyperdrive/lp/ClosedLpShares"; + +// withdrawal shares +export type { RedeemedWithdrawalShares } from "src/hyperdrive/withdrawalShares/RedeemedWithdrawalShares"; + +// Registry // + +export { registryAbi, type RegistryAbi } from "src/registry/abi"; +export { + ReadRegistry, + type ReadRegistryOptions, +} from "src/registry/ReadRegistry"; +export { + ReadWriteRegistry, + type ReadWriteRegistryOptions, +} from "src/registry/ReadWriteRegistry"; + +// Factory // + +export { factoryAbi, type FactoryAbi } from "src/factory/abi"; +export { ReadFactory, type ReadFactoryOptions } from "src/factory/ReadFactory"; +export { + ReadWriteFactory, + type ReadWriteFactoryOptions, +} from "src/factory/ReadWriteFactory"; + +// Token // + +export type { ReadToken } from "src/token/ReadToken"; +export type { ReadWriteToken } from "src/token/ReadWriteToken"; + +// eth +export { ReadEth, type ReadEthOptions } from "src/token/eth/ReadEth"; +export { + ReadWriteEth, + type ReadWriteEthOptions, +} from "src/token/eth/ReadWriteEth"; + +// erc-20 +export { erc20Abi, type Erc20Abi } from "src/token/erc20/abi"; +export { ReadErc20, type ReadErc20Options } from "src/token/erc20/ReadErc20"; +export { + ReadWriteErc20, + type ReadWriteErc20Options, +} from "src/token/erc20/ReadWriteErc20"; + +// erc-4626 +export { + erc4626Abi, + mockErc4626Abi, + type Erc4626Abi, + type MockErc4626Abi, +} from "src/token/erc4626/abi"; +export { ReadErc4626 } from "src/token/erc4626/ReadErc4626"; +export { ReadMockErc4626 } from "src/token/erc4626/ReadMockErc4626"; +export { ReadWriteErc4626 } from "src/token/erc4626/ReadWriteErc4626"; +export { ReadWriteMockErc4626 } from "src/token/erc4626/ReadWriteMockErc4626"; + +// lseth +export { lsEthAbi, type LsEthAbi } from "src/token/lseth/abi"; +export { ReadLsEth } from "src/token/lseth/ReadLsEth"; +export { ReadWriteLsEth } from "src/token/lseth/ReadWriteLsEth"; + +// reth +export { rEthAbi, type REthAbi } from "src/token/reth/abi"; +export { ReadREth } from "src/token/reth/ReadREth"; +export { ReadWriteREth } from "src/token/reth/ReadWriteREth"; + +// steth +export { stEthAbi, type StEthAbi } from "src/token/steth/abi"; +export { ReadStEth } from "src/token/steth/ReadStEth"; +export { ReadWriteStEth } from "src/token/steth/ReadWriteStEth"; + +// Drift // + +export type { + ContractClientOptions, + ReadContractClientOptions, + ReadWriteContractClientOptions, +} from "src/drift/ContractClient"; +export { ReadClient, type ReadClientOptions } from "src/drift/ReadClient"; +export { + ReadWriteClient, + type ReadWriteClientOptions, +} from "src/drift/ReadWriteClient"; + +// Base // + +export { adjustAmountByPercentage } from "src/base/adjustAmountByPercentage"; +export { calculateAprFromPrice } from "src/base/calculateAprFromPrice"; +export { getHprFromApr } from "src/base/getHprFromApr"; +export { getHprFromApy } from "src/base/getHprFromApy"; +export type { Constructor } from "src/base/types"; diff --git a/packages/hyperdrive-js/src/exports/v1.0.14.ts b/packages/hyperdrive-js/src/exports/v1.0.14.ts new file mode 100644 index 000000000..46bc42e7e --- /dev/null +++ b/packages/hyperdrive-js/src/exports/v1.0.14.ts @@ -0,0 +1,25 @@ +// base +export { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; +export { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +// erc-4626 +export { ReadErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadErc4626Hyperdrive_v1_0_14"; +export { ReadMockErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadMockErc4626Hyperdrive_v1_0_14"; +export { ReadWriteErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadWriteErc4626Hyperdrive_v1_0_14"; +export { ReadWriteMockErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadWriteMockErc4626Hyperdrive_v1_0_14"; + +// ezeth +export { ReadEzEthHyperdrive_v1_0_14 } from "src/hyperdrive/ezeth/v1.0.14/ReadEzEthHyperdrive_v1_0_14"; +export { ReadWriteEzEthHyperdrive_v1_0_14 } from "src/hyperdrive/ezeth/v1.0.14/ReadWriteEzEthHyperdrive_v1_0_14"; + +// lseth +export { ReadLsEthHyperdrive_v1_0_14 } from "src/hyperdrive/lseth/v1.0.14/ReadLsEthHyperdrive_v1_0_14"; +export { ReadWriteLsEthHyperdrive_v1_0_14 } from "src/hyperdrive/lseth/v1.0.14/ReadWriteLsEthHyperdrive_v1_0_14"; + +// reth +export { ReadREthHyperdrive_v1_0_14 } from "src/hyperdrive/reth/v1.0.14/ReadREthHyperdrive_v1_0_14"; +export { ReadWriteREthHyperdrive_v1_0_14 } from "src/hyperdrive/reth/v1.0.14/ReadWriteREthHyperdrive_v1_0_14"; + +// steth +export { ReadStEthHyperdrive_v1_0_14 } from "src/hyperdrive/steth/v1.0.14/ReadStEthHyperdrive_v1_0_14"; +export { ReadWriteStEthHyperdrive_v1_0_14 } from "src/hyperdrive/steth/v1.0.14/ReadWriteStEthHyperdrive_v1_0_14"; diff --git a/packages/hyperdrive-js/src/factory/ReadFactory.ts b/packages/hyperdrive-js/src/factory/ReadFactory.ts new file mode 100644 index 000000000..936b6b607 --- /dev/null +++ b/packages/hyperdrive-js/src/factory/ReadFactory.ts @@ -0,0 +1,131 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Address } from "abitype"; +import { ReadContractClientOptions } from "src/drift/ContractClient"; +import { ReadClient } from "src/drift/ReadClient"; +import { FactoryAbi, factoryAbi } from "src/factory/abi"; +import { ReadHyperdrive } from "src/hyperdrive/ReadHyperdrive"; + +export interface ReadFactoryOptions extends ReadContractClientOptions {} + +export class ReadFactory extends ReadClient { + address: Address; + contract: Contract; + + constructor({ + debugName = "Hyperdrive Factory", + address, + cache, + cacheNamespace, + ...rest + }: ReadFactoryOptions) { + super({ debugName, ...rest }); + this.address = address; + this.contract = this.drift.contract({ + abi: factoryAbi, + address, + cache, + cacheNamespace, + }); + } + + /** + * Find out if the given address is an instance deployed by the factory. + */ + async getIsInstance(address: Address): Promise { + return this.contract.read("isInstance", { _instance: address }); + } + + /** + * Find out if the given address is a deployer coordinator registered with the + * factory. + */ + async getIsDeployerCoordinator(address: Address): Promise { + return this.contract.read("isDeployerCoordinator", { + _deployerCoordinator: address, + }); + } + + /** + * Get the address of all registered deployer coordinators. + */ + async getDeployerCoordinatorAddresses({ + instances, + options, + }: { + /** + * Only return deployer coordinators that deployed the given instances. + */ + instances?: Address[]; + options?: ContractReadOptions; + } = {}): Promise { + if (instances) { + const readOnlyAddresses = await this.contract.read( + "getDeployerCoordinatorByInstances", + { + __instances: instances, + }, + ); + return readOnlyAddresses.slice(); + } + + const count = await this.contract.read( + "getNumberOfDeployerCoordinators", + {}, + options, + ); + + if (count === 0n) { + return []; + } + + const readOnlyAddresses = await this.contract.read( + "getDeployerCoordinatorsInRange", + { + _startIndex: 0n, + _endIndex: count, + }, + options, + ); + return readOnlyAddresses.slice(); + } + + /** + * Get a {@linkcode ReadHyperdrive} instance for each Hyperdrive instance + * deployed by the deployer factory. + */ + async getInstances(options?: ContractReadOptions): Promise { + const hyperdriveAddresses = await this.getInstanceAddresses(options); + return hyperdriveAddresses.map( + (address) => + new ReadHyperdrive({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } + + /** + * Get the address of all Hyperdrive instances deployed by the factory. + */ + async getInstanceAddresses( + options?: ContractReadOptions, + ): Promise { + const count = await this.contract.read("getNumberOfInstances", {}, options); + + if (count === 0n) { + return []; + } + + const readOnlyAddresses = await this.contract.read( + "getInstancesInRange", + { + _startIndex: 0n, + _endIndex: count, + }, + options, + ); + return readOnlyAddresses.slice(); + } +} diff --git a/packages/hyperdrive-js/src/factory/ReadWriteFactory.ts b/packages/hyperdrive-js/src/factory/ReadWriteFactory.ts new file mode 100644 index 000000000..f37740105 --- /dev/null +++ b/packages/hyperdrive-js/src/factory/ReadWriteFactory.ts @@ -0,0 +1,42 @@ +import { + ContractReadOptions, + Drift, + ReadWriteAdapter, + ReadWriteContract, + ReplaceProps, +} from "@delvtech/drift"; +import { ReadWriteContractClientOptions } from "src/drift/ContractClient"; +import { ReadFactory, ReadFactoryOptions } from "src/factory/ReadFactory"; +import { FactoryAbi } from "src/factory/abi"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; + +export interface ReadWriteFactoryOptions + extends ReplaceProps {} + +export class ReadWriteFactory extends ReadFactory { + declare contract: ReadWriteContract; + declare drift: Drift; + + constructor(options: ReadWriteFactoryOptions) { + super(options); + } + + /** + * Get a {@linkcode ReadWriteHyperdrive} instance for each Hyperdrive instance + * deployed by the deployer factory. + */ + async getInstances( + options?: ContractReadOptions, + ): Promise { + const hyperdriveAddresses = await this.getInstanceAddresses(options); + return hyperdriveAddresses.map( + (address) => + new ReadWriteHyperdrive({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } +} diff --git a/packages/hyperdrive-js/src/factory/abi.ts b/packages/hyperdrive-js/src/factory/abi.ts new file mode 100644 index 000000000..1a0f44818 --- /dev/null +++ b/packages/hyperdrive-js/src/factory/abi.ts @@ -0,0 +1,4 @@ +import { IHyperdriveFactory } from "@delvtech/hyperdrive-artifacts/IHyperdriveFactory"; + +export const factoryAbi = IHyperdriveFactory.abi; +export type FactoryAbi = typeof factoryAbi; diff --git a/packages/hyperdrive-js/src/fixed-point.ts b/packages/hyperdrive-js/src/fixed-point.ts new file mode 100644 index 000000000..ccbceb234 --- /dev/null +++ b/packages/hyperdrive-js/src/fixed-point.ts @@ -0,0 +1,6 @@ +import * as fixedPoint from "@delvtech/fixed-point-wasm"; + +fixedPoint.initSync(fixedPoint.wasmBuffer); + +export { fixedPoint }; +export const { fixed, FixedPoint, ln, parseFixed, randomFixed } = fixedPoint; diff --git a/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.test.ts b/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.test.ts new file mode 100644 index 000000000..347d55c07 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.test.ts @@ -0,0 +1,1771 @@ +import { ZERO_ADDRESS } from "@delvtech/drift"; +import { fixed, parseFixed } from "@delvtech/fixed-point-wasm"; +import { ALICE, BOB } from "src/base/testing/accounts"; +import { decodeAssetFromTransferSingleEventData } from "src/hyperdrive/assetId/decodeAssetFromTransferSingleEventData"; +import { CheckpointEvent } from "src/hyperdrive/checkpoint/types"; +import { + simplePoolConfig30Days, + simplePoolConfig7Days, +} from "src/hyperdrive/testing/PoolConfig"; +import { simplePoolInfo } from "src/hyperdrive/testing/PoolInfo"; +import { setupReadHyperdrive } from "src/hyperdrive/testing/setupReadHyperdrive"; +import { assert, expect, test } from "vitest"; + +test("getVersion should return the parsed version of the contract", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + + contract.onRead("version").resolves("v1.0.14"); + + const value = await readHyperdrive.getVersion(); + expect(value).toEqual({ + major: 1, + minor: 0, + patch: 14, + string: "v1.0.14", + }); +}); + +// The sdk should return the exact PoolConfig from the contracts. It should not +// do any conversions or transformations, eg: converting seconds to ms, +// formatting bigints, etc.. +test("getPoolConfig should return the PoolConfig from the contract as-is", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + + // stub out the contract call the sdk is going to make + contract.onRead("getPoolConfig").resolves(simplePoolConfig7Days); + + // The sdk should return the correct data + const value = await readHyperdrive.getPoolConfig(); + expect(value).toBe(simplePoolConfig7Days); +}); + +// The sdk should return the exact PoolInfo from the contracts. It should not do +// any conversions or transformations, eg: converting seconds into ms, +// formatting bigints, etc.. +test("getPoolInfo should return the PoolInfo from the contract as-is", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + + contract.onRead("getPoolInfo").resolves(simplePoolInfo); + + const value = await readHyperdrive.getPoolInfo(); + expect(value).toBe(simplePoolInfo); +}); + +// The sdk should return the exact APR from the contracts. It should not do any +// conversions or transformations, eg: formatting bigints, etc.. +test("getFixedRate should get the fixed rate as-is", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + + // These are necessary to stub, but the values won't be used since we stub + // calculateAPRFromReserves directly + contract.onRead("getPoolConfig").resolves(simplePoolConfig7Days); + contract.onRead("getPoolInfo").resolves(simplePoolInfo); + + const value = await readHyperdrive.getFixedApr(); + expect(value).toBe(50000000000000000n); +}); + +test("getTradingVolume should get the trading volume in terms of bonds", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + + contract.onGetEvents("OpenLong").resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + amount: parseFixed("1").bigint, + bondAmount: parseFixed("1.3").bigint, + maturityTime: 1729209600n, + vaultSharePrice: 1n, + asBase: false, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 2n, + amount: parseFixed("1").bigint, + bondAmount: parseFixed("1.4").bigint, + maturityTime: 1733961600n, + asBase: false, + trader: ALICE, + vaultSharePrice: 0n, + }, + }, + ]); + + contract.onGetEvents("CloseLong").resolves([ + { + eventName: "CloseLong", + args: { + extraData: "0x", + assetId: 1n, + maturityTime: 123456789n, + trader: BOB, + destination: BOB, + // received back 1 base + asBase: true, + amount: parseFixed("1").bigint, + vaultSharePrice: 0n, + // closed out 0.9 bonds + bondAmount: parseFixed("0.9").bigint, + }, + }, + ]); + + contract.onGetEvents("OpenShort").resolves([ + { + eventName: "OpenShort", + args: { + extraData: "0x", + assetId: 3n, + amount: parseFixed("1").bigint, + bondAmount: parseFixed("100").bigint, + maturityTime: 1729296000n, + vaultSharePrice: 1n, + asBase: false, + baseProceeds: parseFixed("100").bigint, + trader: BOB, + }, + }, + { + eventName: "OpenShort", + args: { + extraData: "0x", + assetId: 4n, + amount: parseFixed("2").bigint, + bondAmount: parseFixed("190").bigint, + maturityTime: 1729296000n, + vaultSharePrice: 1n, + asBase: false, + baseProceeds: parseFixed("190").bigint, + trader: BOB, + }, + }, + ]); + + contract.onGetEvents("CloseShort").resolves([]); + + const value = await readHyperdrive.getTradingVolume(); + + expect(value).toEqual({ + shortVolume: parseFixed("290").bigint, // sum of bondAmount in short events + longVolume: parseFixed("3.6").bigint, // sum of bondAmount in long events + totalVolume: parseFixed("293.6").bigint, + }); +}); + +test("getShortAccruedYield should return the amount of yield a non-mature position has earned", async () => { + const { contract, drift, readHyperdrive } = setupReadHyperdrive(); + + drift.onGetBlock().resolves({ blockNumber: 1n, timestamp: 100n }); + + contract.onRead("getPoolConfig").resolves({ + ...simplePoolConfig7Days, + positionDuration: 86400n, // one day in seconds + checkpointDuration: 86400n, // one day in seconds + }); + + // The pool info gives us the current price + contract.onRead("getPoolInfo").resolves({ + ...simplePoolInfo, + vaultSharePrice: parseFixed("1.01").bigint, + }); + + // The checkpoint gives us the price when the bond was opened + contract.onRead("getCheckpoint").resolves({ + vaultSharePrice: parseFixed("1.008").bigint, + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + + const accruedYield = await readHyperdrive.getShortAccruedYield({ + checkpointTime: 0n, + bondAmount: parseFixed("100").bigint, + }); + + // If you opened a short position on 100 bonds at a previous checkpoint price + // of 1.008 and the current price is 1.01, your accrued profit would + // be 0.20. + expect(accruedYield).toEqual(parseFixed("0.20").bigint); +}); + +test("getShortAccruedYield should return the amount of yield a mature position has earned", async () => { + const { drift, contract, readHyperdrive } = setupReadHyperdrive(); + + drift.onGetBlock().resolves({ blockNumber: 1n, timestamp: 1699503565n }); + + contract.onRead("getPoolConfig").resolves({ + ...simplePoolConfig7Days, + positionDuration: 86400n, // one day in seconds + checkpointDuration: 86400n, // one day in seconds + }); + + // This checkpoint gives us the price when the short was opened + contract.onRead("getCheckpoint", { _checkpointTime: 1n }).resolves({ + vaultSharePrice: parseFixed("1.008").bigint, + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + + // This checkpoint gives us the price when the shorts matured + contract.onRead("getCheckpoint", { _checkpointTime: 86401n }).resolves({ + vaultSharePrice: parseFixed("1.01").bigint, + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + + const accruedYield = await readHyperdrive.getShortAccruedYield({ + checkpointTime: 1n, + bondAmount: parseFixed("100").bigint, + }); + + // If you opened a short position on 100 bonds at a previous checkpoint price + // of 1.008 and the price was 1.01 at maturity, your accrued profit would + // be 0.20. + expect(accruedYield).toEqual(parseFixed("0.20").bigint); +}); + +test("getCheckpointEvents should return an array of CheckpointEvents", async () => { + const { contract, readHyperdrive } = setupReadHyperdrive(); + const checkPointEvents = [ + { + eventName: "CreateCheckpoint", + args: { + vaultSharePrice: 423890n, + checkpointTime: 1699480800n, + lpSharePrice: 1000276463406900050n, + maturedLongs: 1010694n, + maturedShorts: 0n, + }, + }, + { + eventName: "CreateCheckpoint", + args: { + sharePrice: 1000378348050038939n, + checkpointTime: 1729299000n, + lpSharePrice: 80120n, + maturedLongs: 923162n, + maturedShorts: 230904n, + }, + }, + ] as CheckpointEvent[]; + contract.onGetEvents("CreateCheckpoint").resolves(checkPointEvents); + + const events = await readHyperdrive.getCheckpointEvents(); + + expect(events).toEqual(checkPointEvents); +}); + +// opened with base +test("getOpenLongs should account for longs opened with base", async () => { + // Description: + // Bob opens up a long position over 2 txs in the same checkpoint, for a total + // cost 2 base, and receiving 2.7 bonds. As a result, he should now have + // an open position with the 2.7 bonds and a total cost of 2 base. + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + // received bonds + bondAmount: parseFixed("1.3").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: 1n, + // received bonds + bondAmount: parseFixed("1.4").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + ]); + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([]); + + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountPaid: 2000000000000000000n, // Bob paid 2 base over 2 txs that opened a long + bondAmount: 2700000000000000000n, // Bob received a total of 2.7 bonds from these txs + maturity: 1708545600n, + }, + ]); +}); + +test("getOpenLongs should account for longs opened with shares", async () => { + // Description: + // Bob opens up a long position over 2 txs, for a total cost 2 shares, and + // receiving 2.7 bonds. As a result, he should now have an open position with + // the 2.7 bonds and a total cost of 2.35 base. (because 1 share = 1.15 base in + // the first tx, and 1 share = 1.2 base in the second tx) + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in shares + vaultSharePrice: parseFixed("1.15").bigint, + amount: parseFixed("1").bigint, + // received bonds + bondAmount: parseFixed("1.3").bigint, + maturityTime: timestamp, + asBase: false, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in shares + vaultSharePrice: parseFixed("1.2").bigint, + amount: parseFixed("1").bigint, + // received bonds + bondAmount: parseFixed("1.4").bigint, + maturityTime: timestamp, + asBase: false, + trader: BOB, + }, + }, + ]); + + // Bob has not closed the position at all, these are just stubbed out + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([]); + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountPaid: parseFixed("2.35").bigint, // Bob paid in shares, for the equivalent cost of 2.35 base + bondAmount: parseFixed("2.7").bigint, // Bob received a total of 2.7 bond + maturity: 1708545600n, + }, + ]); +}); + +test("getOpenLongs should account for longs partially closed to base", async () => { + // Description: + // Bob opens up a long position over 2 txs in the same checkpoint, for a total + // cost 2 base, and receiving 2.7 bonds. He then partially closes this + // position, redeeming 0.9 bonds for 1 base. As a result, he should now have + // an open position with the remaining 1.8 bonds and a total cost of 1 base. + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // received bonds + bondAmount: parseFixed("1.3").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + // received bonds + bondAmount: parseFixed("1.4").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + ]); + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 1 base + asBase: true, + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // closed out 0.9 bonds + bondAmount: parseFixed("0.9").bigint, + }, + }, + ]); + + // mints + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountPaid: 1000000000000000000n, // Bob has now paid 1 base total + bondAmount: 1800000000000000000n, // Bob currently hold 1.8 bonds + maturity: 1708545600n, + }, + ]); +}); + +test("getOpenLongs should account for longs fully closed to base", async () => { + // Description: + // Bob opens up a long position over 2 txs in the same checkpoint, for a total + // cost 2 base, and receiving 2.7 bonds. He then completely closes this + // position, redeeming 2.7 bonds for 2.5 base. As a result, he no longer has + // any open long positions. + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // received bonds + bondAmount: parseFixed("1.3").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + // received bonds + bondAmount: parseFixed("1.4").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + ]); + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 1 base + asBase: true, + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // closed out 0.9 bonds + bondAmount: parseFixed("0.9").bigint, + }, + }, + ]); + + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // received bonds + bondAmount: parseFixed("1.3").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("1").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + // received bonds + bondAmount: parseFixed("1.4").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + ]); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 2.5 base + asBase: true, + amount: parseFixed("2.5").bigint, + vaultSharePrice: parseFixed("1.19").bigint, + + // closed out 2.7 bonds + bondAmount: parseFixed("2.7").bigint, + }, + }, + ]); + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([]); +}); + +test("getOpenLongs should handle when user fully closes then re-opens a position in the same checkpoint", async () => { + // Description: + // Bob opens a Long, then fully closes it at a loss. Then he re-opens a long + // in the same checkpoint, resulting in a single position with new accounting + // (ie: the previous loss is not factored in). + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + args: { + extraData: "0x", + trader: BOB, + assetId: + 452312848583266388373324160190187140051835877600158453279131187532625961856n, + maturityTime: 1715299200n, + amount: parseFixed("2000").bigint, + vaultSharePrice: parseFixed("1.0002871459674").bigint, + asBase: true, + bondAmount: parseFixed("2020.518819362004558105").bigint, + }, + blockNumber: 1n, + data: "0x00000000000000000000000000000000000000000000000000000000663d638000000000000000000000000000000000000000000000006c6b935b8bbd40000000000000000000000000000000000000000000000000006c639ba602f70a9a7f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000006d8854d90acff06119", + eventName: "OpenLong", + transactionHash: + "0x8b938ee12cc519b7a76debcad41aab61ef3de2cecc8a858adea7575671b1d9b4", + }, + { + args: { + extraData: "0x", + trader: BOB, + assetId: + 452312848583266388373324160190187140051835877600158453279131187532625961856n, + maturityTime: 1715299200n, + amount: parseFixed("9.0931").bigint, + vaultSharePrice: parseFixed("1.0003519789758").bigint, + asBase: true, + bondAmount: parseFixed("9.196435772384927298").bigint, + }, + blockNumber: 3n, + data: "0x00000000000000000000000000000000000000000000000000000000663d63800000000000000000000000000000000000000000000000007e312e45cf1ac0000000000000000000000000000000000000000000000000007e25d062e6d4586900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000007fa04d9c34b2de42", + eventName: "OpenLong", + transactionHash: + "0x5130c7a919f7303343e102020705cfe3db2ab5ca200410d119cef296c4693621", + } as const, + ]); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: + 452312848583266388373324160190187140051835877600158453279131187532625961856n, + maturityTime: 1715299200n, + amount: parseFixed("1998.524066158245200112").bigint, + vaultSharePrice: parseFixed("1.0002973144644").bigint, + asBase: true, + bondAmount: parseFixed("2020.518819362004558105").bigint, + }, + blockNumber: 2n, + data: "0x00000000000000000000000000000000000000000000000000000000663d638000000000000000000000000000000000000000000000006c5717c9895f7a40f000000000000000000000000000000000000000000000006c4ed96d6708a25d07000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000006d8854d90acff06119", + eventName: "CloseLong", + transactionHash: + "0x8fff6dfc2498356b665542c5517e895dd6c0364e2fa9bf011d373781dad22655", + }, + ]); + + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([ + { + assetId: + 452312848583266388373324160190187140051835877600158453279131187532625961856n, + baseAmountPaid: parseFixed("9.0931").bigint, + bondAmount: parseFixed("9.196435772384927298").bigint, + maturity: 1715299200n, + }, + ]); +}); + +test("getOpenLongs should account for longs partially closed to shares", async () => { + // Description: + // Bob opens up a long position, for a total cost 2 base, and receiving 2.2 + // bonds. He then closes half of this position, redeeming 1.1 bonds for 0.8 + // shares. Shares are worth 1.1 base at the time he closes, As a result, he + // has 1.1 bonds left with a total cost paid of 1.12 base. + + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + asBase: true, + amount: parseFixed("2").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + // received bonds + bondAmount: parseFixed("2.2").bigint, + maturityTime: timestamp, + trader: BOB, + }, + }, + ]); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + blockNumber: 5n, + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 0.8 shares + asBase: false, + amount: parseFixed("0.88").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // closed out 1.1 bonds + bondAmount: parseFixed("1.1").bigint, + }, + }, + ]); + + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountPaid: parseFixed("1.032").bigint, + bondAmount: parseFixed("1.1").bigint, + maturity: 1708545600n, + }, + ]); +}); + +test("getOpenLongs should account for longs fully closed to shares", async () => { + // Description: + // Bob opens up a long position, for a total cost 2 base, and receiving 2.2 + // bonds. He then closes the entirety of this position to shares at a time + // when shares are worth 1.1 base. As a result, he gets back 2 shares, for a + // value of 2.2 base. + const { contract, readHyperdrive } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + contract.onGetEvents("OpenLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenLong", + args: { + extraData: "0x", + assetId: 1n, + // paid for in base + amount: parseFixed("2").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // received bonds + bondAmount: parseFixed("2.2").bigint, + maturityTime: timestamp, + asBase: true, + trader: BOB, + }, + }, + ]); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + blockNumber: 5n, + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 2 shares, and no base + asBase: false, + amount: parseFixed("2.2").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + + // closed out 2.2 bonds + bondAmount: parseFixed("2.2").bigint, + }, + }, + ]); + + const value = await readHyperdrive.getOpenLongs({ account: BOB }); + + expect(value).toEqual([]); +}); + +test("getClosedLongs should account for closing out to base", async () => { + // Description: + // Bob closes a long position of 2 bonds and receives back 2.2 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + blockNumber: 5n, + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 2.2 base, and no shares + asBase: true, + amount: parseFixed("2.2").bigint, + vaultSharePrice: parseFixed("2.0").bigint, + + // closed out 2.0 bonds + bondAmount: parseFixed("2.0").bigint, + }, + }, + ]); + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + const value = await readHyperdrive.getClosedLongs({ account: BOB }); + expect(value).toEqual([ + { + assetId: 1n, + baseAmount: parseFixed("2.2").bigint, + baseAmountPaid: 0n, + bondAmount: parseFixed("2.0").bigint, + closedTimestamp: 123456789n, + maturity: 1708545600n, + }, + ]); +}); + +test("getClosedLongs should account for closing out to shares", async () => { + // Description: + // Bob closes a long position of 2 bonds and receives back 1.9 shares. Shares + // are worth 1.1 base at the time he closes, therefore his closed position is + // valued at 2.09 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + const eventData = + "0x0100000000000000000000000000000000000000000000000000000065d65640000000000000000000000000000000000000000000000001bc82c3277b2dc665"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + + contract.onGetEvents("CloseLong", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseLong", + blockNumber: 5n, + args: { + extraData: "0x", + assetId: 1n, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + + // received back 1.9 shares, and no base + asBase: false, + vaultSharePrice: parseFixed("1.1").bigint, + amount: parseFixed("1.9").bigint, + + // closed out 2 bonds + bondAmount: parseFixed("2.0").bigint, + }, + }, + ]); + + // getBlock gives us the timestamp of when he closed the position + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const value = await readHyperdrive.getClosedLongs({ account: BOB }); + expect(value).toEqual([ + { + assetId: 1n, + baseAmount: parseFixed("2.09").bigint, + baseAmountPaid: 0n, + bondAmount: parseFixed("2.0").bigint, + closedTimestamp: 123456789n, + maturity: 1708545600n, + }, + ]); +}); + +test("getOpenShorts should account for shorts opened with base", async () => { + // Description: + // Bob opens up a short position for 100 bonds over 2 txs in the same + // checkpoint, for a total cost of around 1.44 base. + + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + contract.onGetEvents("OpenShort", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("0.721996107012129147").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "OpenShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.72527013345635719").bigint, + vaultSharePrice: parseFixed("0.721952948135251528").bigint, + asBase: true, + baseProceeds: parseFixed("49.288611983218631127").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + ]); + + contract.onGetEvents("CloseShort", { filter: { trader: BOB } }).resolves([]); + + drift.onGetBlock().resolves({ + timestamp: 1713801432n, + blockNumber: 1n, + }); + + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + checkpointTime: 1713798000n, + baseAmountPaid: parseFixed("1.450580466488873595").bigint, + bondAmount: parseFixed("100").bigint, + baseProceeds: parseFixed("98.576966043666144584").bigint, + fixedRatePaid: parseFixed("0.175635145784387390").bigint, + hyperdriveAddress: ZERO_ADDRESS, + maturity: 1716336000n, + openedTimestamp: 1713801432n, + }, + ]); +}); + +test("getOpenShorts should account for shorts opened with shares", async () => { + // Description: + // Bob opens up a short position for 100 bonds over 2 txs in the same + // checkpoint, for a total cost of around 1.44 shares. + + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + contract.onGetEvents("OpenShort", { filter: { trader: BOB } }).resolves([ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.721996107012129147").bigint, + vaultSharePrice: parseFixed("1.004590365499").bigint, + asBase: false, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "OpenShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.721952948135251528").bigint, + vaultSharePrice: parseFixed("1.004594738936").bigint, + asBase: false, + baseProceeds: parseFixed("49.288611983218631127").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + ]); + + contract.onGetEvents("CloseShort", { filter: { trader: BOB } }).resolves([]); + + drift.onGetBlock().resolves({ + timestamp: 1713801432n, + blockNumber: 1n, + }); + + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + checkpointTime: 1713798000n, + baseAmountPaid: parseFixed("1.450580466488178492").bigint, + bondAmount: parseFixed("100").bigint, + baseProceeds: parseFixed("98.576966043666144584").bigint, + fixedRatePaid: parseFixed("0.175635145784387390").bigint, + hyperdriveAddress: ZERO_ADDRESS, + maturity: 1716336000n, + openedTimestamp: 1713801432n, + }, + ]); +}); + +test("getOpenShorts should account for shorts partially closed to base", async () => { + // Description: + // Bob shorts 50 bonds for a total cost of 0.73 base. He then partially + // closes this position, redeeming 25 bonds for 0.36 base. As a result, he has 25 + // bonds left with a total cost of 0.37 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + + const events = [ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("0.721996107012129147").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "CloseShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.357390566309610627").bigint, + vaultSharePrice: parseFixed("0.355730805024955393").bigint, + asBase: true, + basePayment: parseFixed("24.651318786405479294").bigint, + bondAmount: parseFixed("25").bigint, + }, + }, + ] as const; + + contract + .onGetEvents("OpenShort", { filter: { trader: BOB } }) + .resolves([events[0]]); + contract + .onGetEvents("CloseShort", { filter: { trader: BOB } }) + .resolves([events[1]]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + bondAmount: parseFixed("25").bigint, + baseAmountPaid: parseFixed("0.367919766722905778").bigint, + baseProceeds: parseFixed("24.637035274042034163").bigint, + checkpointTime: 123454800n, + hyperdriveAddress: ZERO_ADDRESS, + fixedRatePaid: parseFixed("0.179245221000329770").bigint, + maturity: 1716336000n, + openedTimestamp: 123456789n, + }, + ]); +}); + +test("getOpenShorts should account for shorts fully closed to base", async () => { + // Description: + // Bob opens up a short position, then completely closes this position, As a + // result, he no longer has any open short positions. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + + const events = [ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("0.721996107012129147").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "CloseShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.357390566309610627").bigint, + vaultSharePrice: parseFixed("0.355730805024955393").bigint, + asBase: true, + basePayment: parseFixed("24.651318786405479294").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + ] as const; + + contract + .onGetEvents("OpenShort", { filter: { trader: BOB } }) + .resolves([events[0]]); + contract + .onGetEvents("CloseShort", { filter: { trader: BOB } }) + .resolves([events[1]]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([]); +}); + +test("getOpenShorts should account for shorts partially closed to shares", async () => { + // Description: + // Bob shorts 50 bonds for a total cost of 0.73 base. He then partially + // closes this position, redeeming 25 bonds for 0.36 shares. As a result, he + // has 25 bonds left with a total cost of 0.37 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + + const events = [ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("1.004590365499").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "CloseShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.355730805024955393").bigint, + vaultSharePrice: parseFixed("1.004665778901").bigint, + asBase: false, + basePayment: parseFixed("24.651318786405479294").bigint, + bondAmount: parseFixed("25").bigint, + }, + }, + ] as const; + + contract + .onGetEvents("OpenShort", { filter: { trader: BOB } }) + .resolves([events[0]]); + contract + .onGetEvents("CloseShort", { filter: { trader: BOB } }) + .resolves([events[1]]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + bondAmount: parseFixed("25").bigint, + baseAmountPaid: parseFixed("0.367919766723039831").bigint, + baseProceeds: parseFixed("24.637035274042034163").bigint, + checkpointTime: 123454800n, + hyperdriveAddress: ZERO_ADDRESS, + fixedRatePaid: parseFixed("0.179245221000329770").bigint, + maturity: 1716336000n, + openedTimestamp: 123456789n, + }, + ]); +}); + +test("getOpenShorts should account for shorts fully closed to shares", async () => { + // Description: + // Bob opens up a short position, then completely closes this position, As a + // result, he no longer has any open short positions. + + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + const events = [ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("1.004590365499").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "CloseShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: 1n, + maturityTime: 1716336000n, + vaultSharePrice: parseFixed("1.004665778901").bigint, + amount: parseFixed("0.355730805024955393").bigint, + asBase: false, + basePayment: parseFixed("24.651318786405479294").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + ] as const; + + contract + .onGetEvents("OpenShort", { filter: { trader: BOB } }) + .resolves([events[0]]); + contract + .onGetEvents("CloseShort", { filter: { trader: BOB } }) + .resolves([events[1]]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([]); +}); + +test("getOpenShorts should handle when user fully closes then re-opens a position in the same checkpoint", async () => { + // Description: + // Bob opens a Short, then fully closes it at a loss. Then he re-opens a short + // in the same checkpoint, resulting in a single position with new accounting + // (ie: the previous loss is not factored in). + + const { contract, drift, readHyperdrive } = setupReadHyperdrive(); + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + // pool info to get the price of shares at the time he closes the short + contract + .onRead("getPoolInfo", {}, { block: 5n }) + .resolves({ ...simplePoolInfo, vaultSharePrice: parseFixed("1.1").bigint }); + + // Stub the timestamp so getOpenShorts can construct the checkpoint id + drift.onGetBlock().resolves({ + timestamp: 123456789n, + // this blockNumber is unused, but setting this to 3n, as there should be + // 3 blocks in this test flow + blockNumber: 3n, + }); + + const events = [ + { + eventName: "OpenShort", + blockNumber: 1n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("1.004590365499").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "CloseShort", + blockNumber: 2n, + args: { + extraData: "0x", + trader: BOB, + destination: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.355730805024955393").bigint, + vaultSharePrice: parseFixed("1.004665778901").bigint, + asBase: false, + basePayment: parseFixed("24.651318786405479294").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + { + eventName: "OpenShort", + blockNumber: 3n, + args: { + extraData: "0x", + trader: BOB, + assetId: 1n, + maturityTime: 1716336000n, + amount: parseFixed("0.725310333032516405").bigint, + vaultSharePrice: parseFixed("1.004590365499").bigint, + asBase: true, + baseProceeds: parseFixed("49.288354060447513457").bigint, + bondAmount: parseFixed("50").bigint, + }, + }, + ] as const; + + contract + .onGetEvents("OpenShort", { filter: { trader: BOB } }) + .resolves([events[0], events[2]]); + + contract + .onGetEvents("CloseShort", { filter: { trader: BOB } }) + .resolves([events[1]]); + + const value = await readHyperdrive.getOpenShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountPaid: parseFixed("0.725310333032516405").bigint, + bondAmount: parseFixed("50").bigint, + baseProceeds: parseFixed("49.288354060447513457").bigint, + fixedRatePaid: parseFixed("0.175667439018216348").bigint, + maturity: 1716336000n, + checkpointTime: 123454800n, + openedTimestamp: 123456789n, + hyperdriveAddress: readHyperdrive.contract.address, + }, + ]); +}); + +test("getShortBondsGivenDeposit & previewOpenShort should align within a given tolerance", async () => { + const { contract, drift, readHyperdrive } = setupReadHyperdrive(); + contract.onRead("getPoolConfig").resolves(simplePoolConfig30Days); + contract.onRead("getPoolInfo").resolves(simplePoolInfo); + contract.onRead("getCheckpointExposure").resolves(0n); + contract.onRead("getCheckpoint").resolves({ + vaultSharePrice: parseFixed(1.05).bigint, + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + drift.onGetBlock().resolves({ + timestamp: 123456789n, + blockNumber: 1n, + }); + + const targetDeposit = parseFixed(1.123); + const tolerance = fixed(1e9); + const amountOfBondsToShort = await readHyperdrive.getShortBondsGivenDeposit({ + amountIn: targetDeposit.bigint, + asBase: true, + tolerance: tolerance.bigint, + }); + const { traderDeposit } = await readHyperdrive.previewOpenShort({ + amountOfBondsToShort, + asBase: true, + }); + + assert(targetDeposit.absDiff(traderDeposit).lte(tolerance)); +}); + +test("getClosedShorts should account for shorts closed to base", async () => { + // Description: + // Bob completely closes his position, redeeming 100 shorted bonds for 2 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + const eventData = + "0x0200000000000000000000000000000000000000000000000000000065d76f800000000000000000000000000000000000000000000000056bc75e2d63100000"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig7Days); + + contract.onGetEvents("CloseShort", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseShort", + args: { + extraData: "0x", + assetId: 1n, + asBase: true, + amount: parseFixed("2").bigint, // closed out to base + vaultSharePrice: parseFixed("1.8").bigint, // did not close out to shares + bondAmount: parseFixed("100").bigint, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + basePayment: parseFixed("2").bigint, // did not close out to base + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const value = await readHyperdrive.getClosedShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountReceived: parseFixed("2").bigint, + bondAmount: parseFixed("100").bigint, + checkpointTime: 123454800n, + closedTimestamp: 123456789n, + hyperdriveAddress: ZERO_ADDRESS, + maturity: 1708617600n, + }, + ]); +}); + +test("getClosedShorts should account for shorts closed to shares", async () => { + // Description: + // Bob completely closes his position, redeeming 100 shorted bonds for 1.1 shares. + // Shares are worth 1.1 base at the time he closes, therefore his closed position + // is valued at 1.21 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + const eventData = + "0x0200000000000000000000000000000000000000000000000000000065d76f800000000000000000000000000000000000000000000000056bc75e2d63100000"; + const { timestamp } = decodeAssetFromTransferSingleEventData(eventData); + + contract.onRead("getPoolConfig").resolves(simplePoolConfig7Days); + + contract.onGetEvents("CloseShort", { filter: { trader: BOB } }).resolves([ + { + eventName: "CloseShort", + blockNumber: 5n, + args: { + extraData: "0x", + assetId: 1n, + asBase: false, + vaultSharePrice: parseFixed("1.1").bigint, + amount: parseFixed("1.1").bigint, // closed out to shares + bondAmount: parseFixed("100").bigint, + maturityTime: timestamp, + trader: BOB, + destination: BOB, + basePayment: parseFixed("1.21").bigint, + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const value = await readHyperdrive.getClosedShorts({ account: BOB }); + + expect(value).toEqual([ + { + assetId: 1n, + baseAmountReceived: parseFixed("1.21").bigint, + bondAmount: parseFixed("100").bigint, + checkpointTime: 123454800n, + closedTimestamp: 123456789n, + hyperdriveAddress: ZERO_ADDRESS, + maturity: 1708617600n, + }, + ]); +}); + +test("getOpenLpPosition should return zero when a position is fully closed", async () => { + // Description: + // Bob opens up an lp position, receiving 498 LP shares, depositing 500 base. + // Bob then closes all of his 498 LP shares, receiving 499 base (he lost + // 1 base on this position) Bob is left with 0 LP shares and 0 base paid in his + // current LP position. + + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + contract.onRead("getPoolInfo").resolves(simplePoolInfo); + contract.onSimulateWrite("removeLiquidity").resolves({ + proceeds: parseFixed("100").bigint, + withdrawalShares: 0n, + }); + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 175n }); + contract.onGetEvents("AddLiquidity", { filter: { provider: BOB } }).resolves([ + { + eventName: "AddLiquidity", + blockNumber: 174n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("500").bigint, + lpAmount: parseFixed("498").bigint, + lpSharePrice: parseFixed("1.000000590811771717").bigint, + provider: "0x020a898437E9c9DCdF3c2ffdDB94E759C0DAdFB6", + vaultSharePrice: parseFixed("498.570512905658351934").bigint, + }, + }, + ]); + + contract + .onGetEvents("RemoveLiquidity", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RemoveLiquidity", + blockNumber: 175n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("499").bigint, + lpAmount: parseFixed("498").bigint, + lpSharePrice: parseFixed("1.002867781011873985").bigint, + provider: "0x020a898437E9c9DCdF3c2ffdDB94E759C0DAdFB6", + vaultSharePrice: parseFixed("498.567723245858722697").bigint, + withdrawalShareAmount: 0n, + destination: BOB, + }, + }, + ]); + + const value = await readHyperdrive.getOpenLpPosition({ + account: BOB, + asBase: false, + }); + expect(value).toEqual({ + lpShareBalance: parseFixed("0").bigint, + baseAmountPaid: parseFixed("0").bigint, + baseValue: parseFixed("0").bigint, + sharesValue: parseFixed("0").bigint, + }); +}); + +test("getOpenLpPosition should return the current lpShareBalance and baseAmountPaid", async () => { + // Description: + // Bob opens up an lp position, receiving 498 LP shares, depositing 500 base. + // Bob then closes his entire position of 498 LP shares, receiving 499 base + // (he lost 1 base on this position). Then he opens up a new LP position, + // receiving 99 LP shares, depositing 100 base. Bob now has with 99 LP + // shares and 100 base paid in his current LP position. + + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 175n }); + contract.onSimulateWrite("removeLiquidity").resolves({ + proceeds: parseFixed("100").bigint, + withdrawalShares: 0n, + }); + contract.onRead("getPoolInfo").resolves(simplePoolInfo); + contract.onGetEvents("AddLiquidity", { filter: { provider: BOB } }).resolves([ + { + eventName: "AddLiquidity", + blockNumber: 174n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("500").bigint, + lpAmount: parseFixed("498").bigint, + lpSharePrice: parseFixed("1.000000590811771717").bigint, + provider: "0x020a898437E9c9DCdF3c2ffdDB94E759C0DAdFB6", + vaultSharePrice: parseFixed("1.002867171358").bigint, + }, + }, + { + eventName: "AddLiquidity", + blockNumber: 176n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("100").bigint, + lpAmount: parseFixed("99").bigint, + lpSharePrice: parseFixed("1.000000576182752684").bigint, + provider: "0x020a898437E9c9DCdF3c2ffdDB94E759C0DAdFB6", + vaultSharePrice: parseFixed("1.002867314461").bigint, + }, + }, + ]); + + contract + .onGetEvents("RemoveLiquidity", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RemoveLiquidity", + blockNumber: 175n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("499").bigint, + lpAmount: parseFixed("498").bigint, + lpSharePrice: parseFixed("1.002867781011873985").bigint, + provider: "0x020a898437E9c9DCdF3c2ffdDB94E759C0DAdFB6", + vaultSharePrice: parseFixed("1.0008670371827").bigint, + withdrawalShareAmount: 0n, + destination: BOB, + }, + }, + ]); + + const value = await readHyperdrive.getOpenLpPosition({ + account: BOB, + asBase: false, + }); + expect(value).toEqual({ + lpShareBalance: parseFixed("99").bigint, + baseAmountPaid: parseFixed("100").bigint, + baseValue: parseFixed("100").bigint, + sharesValue: parseFixed("100").bigint, + }); +}); + +test("getClosedLpShares should account for LP shares closed to base", async () => { + // Description: + // Bob completely closes his LP position of 5 LP shares and receives back + // base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract + .onGetEvents("RemoveLiquidity", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RemoveLiquidity", + blockNumber: 5n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("10").bigint, + vaultSharePrice: parseFixed("9").bigint, + provider: BOB, + withdrawalShareAmount: 0n, + lpAmount: parseFixed("5").bigint, + lpSharePrice: parseFixed("2").bigint, + destination: BOB, + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const closedLpShares = await readHyperdrive.getClosedLpShares({ + account: BOB, + }); + expect(closedLpShares).toEqual([ + { + lpAmount: parseFixed("5").bigint, + baseAmount: parseFixed("10").bigint, + lpSharePrice: parseFixed("2").bigint, + withdrawalShareAmount: 0n, + closedTimestamp: 123456789n, + }, + ]); +}); + +test("getClosedLpShares should account for LP shares closed to vault shares", async () => { + // Description: + // Bob completely closes his LP position of 5 LP shares and receives back + // shares. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract + .onGetEvents("RemoveLiquidity", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RemoveLiquidity", + blockNumber: 5n, + args: { + extraData: "0x", + asBase: false, + amount: parseFixed("9").bigint, + vaultSharePrice: parseFixed("1.1").bigint, + provider: BOB, + withdrawalShareAmount: 0n, + lpAmount: parseFixed("5").bigint, + lpSharePrice: parseFixed("2").bigint, + destination: BOB, + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const closedLpShares = await readHyperdrive.getClosedLpShares({ + account: BOB, + }); + expect(closedLpShares).toEqual([ + { + lpAmount: parseFixed("5").bigint, + baseAmount: parseFixed("9.9").bigint, + withdrawalShareAmount: 0n, + lpSharePrice: parseFixed("2").bigint, + closedTimestamp: 123456789n, + }, + ]); +}); + +test("getRedeemedWithdrawalShares should account for withdrawal shares closed to base", async () => { + // Description: + // Bob completely redeems 5 withdrawal shares and receives 10 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract + .onGetEvents("RedeemWithdrawalShares", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RedeemWithdrawalShares", + blockNumber: 5n, + args: { + extraData: "0x", + asBase: true, + amount: parseFixed("10").bigint, + vaultSharePrice: parseFixed("9.8").bigint, + provider: BOB, + withdrawalShareAmount: parseFixed("5").bigint, + destination: BOB, + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const redeemedWithdrawalShares = + await readHyperdrive.getRedeemedWithdrawalShares({ + account: BOB, + }); + expect(redeemedWithdrawalShares).toEqual([ + { + hyperdriveAddress: ZERO_ADDRESS, + baseAmount: parseFixed("10").bigint, + withdrawalShareAmount: parseFixed("5").bigint, + redeemedTimestamp: 123456789n, + }, + ]); +}); +test("getRedeemedWithdrawalShares should account for withdrawal shares closed to vault shares", async () => { + // Description: + // Bob completely redeems 5 withdrawal shares and receives 8 shares that are worth 10 base. + const { contract, readHyperdrive, drift } = setupReadHyperdrive(); + + contract + .onGetEvents("RedeemWithdrawalShares", { filter: { provider: BOB } }) + .resolves([ + { + eventName: "RedeemWithdrawalShares", + blockNumber: 5n, + args: { + extraData: "0x", + asBase: false, + vaultSharePrice: parseFixed("1.25").bigint, + amount: parseFixed("8").bigint, + provider: BOB, + withdrawalShareAmount: parseFixed("5").bigint, + destination: BOB, + }, + }, + ]); + + drift.onGetBlock().resolves({ timestamp: 123456789n, blockNumber: 5n }); + + const redeemedWithdrawalShares = + await readHyperdrive.getRedeemedWithdrawalShares({ + account: BOB, + }); + expect(redeemedWithdrawalShares).toEqual([ + { + hyperdriveAddress: ZERO_ADDRESS, + baseAmount: parseFixed("10").bigint, + withdrawalShareAmount: parseFixed("5").bigint, + redeemedTimestamp: 123456789n, + }, + ]); +}); diff --git a/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.ts new file mode 100644 index 000000000..4890df155 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ReadHyperdrive.ts @@ -0,0 +1,2078 @@ +import { + Address, + Block, + BlockTag, + ContractEvent, + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, + MergeKeys, + ReadContract, +} from "@delvtech/drift"; +import { HyperdriveSdkError } from "src/HyperdriveSdkError"; +import { assertNever } from "src/base/assertNever"; +import { calculateAprFromPrice } from "src/base/calculateAprFromPrice"; +import { MAX_UINT256, NULL_BYTES, SECONDS_PER_YEAR } from "src/base/constants"; +import { ReadContractClientOptions } from "src/drift/ContractClient"; +import { ReadClient } from "src/drift/ReadClient"; +import { getBlockOrThrow } from "src/drift/getBlockOrThrow"; +import { fixed } from "src/fixed-point"; +import { HyperdriveAbi, hyperdriveAbi } from "src/hyperdrive/abi"; +import { decodeAssetFromTransferSingleEventData } from "src/hyperdrive/assetId/decodeAssetFromTransferSingleEventData"; +import { getCheckpointTime } from "src/hyperdrive/checkpoint/getCheckpointTime"; +import { + Checkpoint, + CheckpointEvent, + GetCheckpointParams, + GetCheckpointTimeParams, +} from "src/hyperdrive/checkpoint/types"; +import { MAX_ITERATIONS } from "src/hyperdrive/constants"; +import { + ClosedLong, + Long, + OpenLongPositionReceivedWithoutDetails, +} from "src/hyperdrive/longs/types"; +import { ClosedLpShares } from "src/hyperdrive/lp/ClosedLpShares"; +import { LP_ASSET_ID } from "src/hyperdrive/lp/assetId"; +import { calculateShortAccruedYield } from "src/hyperdrive/shorts/calculateShortAccruedYield"; +import { ClosedShort, OpenShort } from "src/hyperdrive/shorts/types"; +import { MarketState, PoolConfig, PoolInfo } from "src/hyperdrive/types"; +import { RedeemedWithdrawalShares } from "src/hyperdrive/withdrawalShares/RedeemedWithdrawalShares"; +import { WITHDRAW_SHARES_ASSET_ID } from "src/hyperdrive/withdrawalShares/assetId"; +import { hyperwasm } from "src/hyperwasm"; +import { ReadErc20 } from "src/token/erc20/ReadErc20"; +import { ReadEth } from "src/token/eth/ReadEth"; + +export interface ReadHyperdriveOptions extends ReadContractClientOptions {} + +export class ReadHyperdrive extends ReadClient { + readonly address: Address; + readonly contract: ReadContract; + + /** + * @hidden + */ + constructor({ + debugName = "Hyperdrive", + address, + cache, + cacheNamespace, + drift, + ...rest + }: ReadHyperdriveOptions) { + super({ debugName, drift, ...rest }); + this.address = address; + this.contract = this.drift.contract({ + abi: hyperdriveAbi, + address, + cache, + cacheNamespace, + }); + } + + async getKind(): Promise { + return this.contract.read("kind"); + } + + async getVersion(): Promise<{ + major: number; + minor: number; + patch: number; + string: string; + }> { + const string = await this.contract.read("version"); + const [major, minor, patch] = string + .replace(/^\D*/, "") + .split(".") + .map((num) => parseInt(num)); + + return { + major, + minor, + patch, + string, + }; + } + + /** + * Returns the base token of the pool. + * + * @privateRemarks + * The default implementation supports ERC20 and ETH base tokens. If + * the address returned by the contract is not the ETH address, it is assumed + * to be an ERC20 token. + */ + async getBaseToken(): Promise { + const address = await this.contract.read("baseToken"); + return address === ReadEth.address + ? new ReadEth({ + drift: this.drift, + }) + : new ReadErc20({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + /** + * Returns the share token of the pool. + * + * @privateRemarks + * The share token is assumed to be an ERC20 token. This can be overwritten + * in instances of Hyperdrive to return custom tokens. + */ + async getSharesToken(): Promise { + const address = await this.contract.read("vaultSharesToken"); + return new ReadErc20({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + getDecimals(): Promise { + return this.contract.read("decimals"); + } + + /** + * Convert an amount of shares to base tokens using the current vault share price. + */ + async convertToBase({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.contract.read( + "convertToBase", + { + _shareAmount: sharesAmount, + }, + options, + ); + } + + /** + * Convert an amount of base tokens to shares using the current vault share price. + */ + async convertToShares({ + baseAmount, + options, + }: { + baseAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.contract.read( + "convertToShares", + { + _baseAmount: baseAmount, + }, + options, + ); + } + + async getInitializationBlock(options?: { + fromBlock?: BlockTag | bigint; + toBlock?: BlockTag | bigint; + }): Promise { + const events = await this.contract.getEvents("Initialize", options); + + if (!events.length || events[0].blockNumber === undefined) { + throw new HyperdriveSdkError( + "Pool has not been initialized, no block found.", + ); + } + const blockNumber = events[0].blockNumber; + + return getBlockOrThrow(this.drift, { blockNumber }); + } + + /** + * Get a standardized variable rate using vault share prices from blocks in + * the last `timeRange` seconds. + * + * Note: This function will throw an error if the pool was deployed within the + * last `timeRange` seconds. + * + * See Agent0 for calculation: + * https://github.com/delvtech/agent0/blob/854e9392e09898e65aeed0040c5e648c8d3d1380/src/agent0/ethpy/hyperdrive/interface/read_interface.py#L421 + * + * @param blockRange The block range (in blocks) to use to calculate the variable rate. + */ + async getYieldSourceRate({ + blockRange, + options, + }: { + blockRange: bigint; + options?: ContractReadOptions; + }): Promise { + const currentBlock = await getBlockOrThrow(this.drift, options); + // Clamp the start block to the pool's initialization block if the + // blockRange is too big. + let startBlockNumber = currentBlock.blockNumber! - blockRange; + const { blockNumber: initializationBlock } = + await this.getInitializationBlock(); + if (initializationBlock && initializationBlock > startBlockNumber) { + startBlockNumber = initializationBlock; + } + + // NOTE: Cloudchain will throw an error if the block number is too far back + // in history. + const startBlock = await getBlockOrThrow(this.drift, { + blockNumber: startBlockNumber, + }); + + // Get the info from fromBlock to get the starting vault share price + const { vaultSharePrice: startVaultSharePrice } = await this.getPoolInfo({ + block: startBlockNumber, + }); + + // Get the current vaultSharePrice from the latest pool info + const { vaultSharePrice: currentVaultSharePrice } = + await this.getPoolInfo(options); + + const timeFrame = currentBlock.timestamp - startBlock.timestamp; // bigint + + const vaultApy = calculateApyFromPrice({ + startPrice: startVaultSharePrice, + endPrice: currentVaultSharePrice, + timeFrame, + }); + + return vaultApy; + } + + /** + * Get the checkpoint time for a given timestamp or block number, defaulting + * to the latest block. + * @returns The time of the checkpoint. + */ + getCheckpointTime(params: GetCheckpointTimeParams = {}): Promise { + return this._getCheckpointTime(params); + } + + /** + * A protected version of `ReadHyperdrive.getCheckpointTime` with + * more relaxed types to streamline internal usage. The public API ensures + * only one of `timestamp` or `blockNumber` is provided to avoid ambiguity, + * but this function allows both to be provided, in which case `timestamp` + * will take precedence. + */ + protected async _getCheckpointTime({ + timestamp, + blockNumber, + options, + }: MergeKeys = {}): Promise { + const { checkpointDuration } = await this.getPoolConfig(options); + + // If no timestamp is provided, try to get one from the block number + if (timestamp === undefined) { + // Default to the block from read options + const getBlockOptions = blockNumber ? { blockNumber } : options; + const block = await getBlockOrThrow(this.drift, getBlockOptions); + timestamp = block.timestamp; + } + + return getCheckpointTime(timestamp, checkpointDuration); + } + + async getCheckpoint({ + checkpointTime, + timestamp, + blockNumber, + options, + }: GetCheckpointParams = {}): Promise { + if (checkpointTime === undefined) { + checkpointTime = await this._getCheckpointTime({ + timestamp, + blockNumber, + options, + }); + } + + const { + lastWeightedSpotPriceUpdateTime, + vaultSharePrice, + weightedSpotPrice, + } = await this.contract.read( + "getCheckpoint", + { _checkpointTime: checkpointTime }, + options, + ); + + return { + checkpointTime, + lastWeightedSpotPriceUpdateTime, + vaultSharePrice, + weightedSpotPrice, + }; + } + + async getCheckpointExposure({ + checkpointTime, + blockNumber, + timestamp, + options, + }: GetCheckpointParams = {}): Promise { + if (checkpointTime === undefined) { + checkpointTime = await this._getCheckpointTime({ + blockNumber, + timestamp, + options, + }); + } + + return this.contract.read( + "getCheckpointExposure", + { _checkpointTime: checkpointTime }, + options, + ); + } + + /** + * + * This function retrieves the market state. This is helpful for retrieving + * general market state statistics, such as whether the market has been + * paused. + */ + getMarketState(options?: ContractReadOptions): Promise { + return this.contract.read("getMarketState", undefined, options); + } + + /** + * Gets the pool's configuration parameters + */ + getPoolConfig(options?: ContractReadOptions): Promise { + return this.contract.read("getPoolConfig", undefined, options); + } + + /** + * Gets info about the pool's reserves and other state that is important to + * evaluate potential trades. + */ + getPoolInfo(options?: ContractReadOptions): Promise { + return this.contract.read("getPoolInfo", undefined, options); + } + + /** + * Gets the pool's fixed APR, i.e. the fixed rate a user locks in when they + * open a long. + */ + async getFixedApr(options?: ContractReadOptions): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + return hyperwasm.spotRate({ poolConfig, poolInfo }); + } + + /** + * Gets the implied variable rate of opening a short. + */ + async getImpliedRate({ + bondAmount, + timestamp, + variableApy, + options, + }: { + bondAmount: bigint; + timestamp: bigint; + // TODO: Get this from sdk instead + variableApy: bigint; + options?: ContractReadOptions; + }): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + // The vault share price at the time the current checkpoint was minted is + // the most accurate, however if there is no current checkpoint we should + // just use the current vault share price. + let { vaultSharePrice: openVaultSharePrice } = await this.getCheckpoint({ + timestamp, + }); + if (!openVaultSharePrice) { + openVaultSharePrice = (await this.getPoolInfo()).vaultSharePrice; + } + + return hyperwasm.calcImpliedRate({ + poolInfo, + poolConfig, + bondAmount, + openVaultSharePrice, + variableApy, + }); + } + + /** + * Gets the market liquidity available for trading and removing LP. + */ + async getIdleLiquidity(options?: ContractReadOptions): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + return hyperwasm.idleShareReservesInBase({ poolInfo, poolConfig }); + } + + /** + * Gets the total present value of the pool in base. + * @param options + * @returns + */ + async getPresentValue(options?: ContractReadOptions): Promise { + // presentValueInShares 33997119981629446n + // Start block: {blockNumber: 20486359n, timestamp: 1723150091n} + // Start vault share price: 1015896019620210959n + // Current vault share price: 1017273050693203130n + // Time frame: 1203624n + // Vault APY: 36128140020150719n + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + const presentValueInShares = hyperwasm.presentValue({ + poolInfo, + poolConfig, + currentTime: BigInt(Date.now()) / 1000n, + }); + + return this.convertToBase({ + sharesAmount: presentValueInShares, + options, + }); + } + + /** + * Gets the yield accrued on an amount of bonds shorted in a given checkpoint. + * Note that shorts stop accruing yield once they reach maturity. + * @param checkpointTime - The checkpoint the short was opened in + * @param bondAmount - The number of bonds shorted + * @param decimals + * @param options + */ + async getShortAccruedYield({ + checkpointTime, + bondAmount, + options, + }: { + checkpointTime: bigint; + bondAmount: bigint; + options?: ContractReadOptions; + }): Promise { + // Get the vault share price when the short was opened + let { vaultSharePrice: openVaultSharePrice } = await this.getCheckpoint({ + checkpointTime, + options, + }); + + const { positionDuration } = await this.getPoolConfig(options); + const maturityTime = checkpointTime + positionDuration; + const latestCheckpointTime = await this.getCheckpointTime({ options }); + const isMatured = latestCheckpointTime >= maturityTime; + + // If the short is mature, get the vault share price at maturity + let endingVaultSharePrice; + if (isMatured) { + const checkpointAtMaturity = await this.getCheckpoint({ + checkpointTime: maturityTime, + options, + }); + endingVaultSharePrice = checkpointAtMaturity.vaultSharePrice; + } else { + // Otherwise get the current vault share price + const poolInfo = await this.getPoolInfo(options); + endingVaultSharePrice = poolInfo.vaultSharePrice; + // Also check if the latest checkpoint was minted + const checkpointIsMinted = openVaultSharePrice != 0n; + // If not, this tx will mint it and set its vaultSharePrice + if (!checkpointIsMinted) { + openVaultSharePrice = poolInfo.vaultSharePrice; + } + } + + return calculateShortAccruedYield({ + openVaultSharePrice, + endingVaultSharePrice, + bondAmount, + decimals: await this.getDecimals(), + }); + } + + /** + * Calculates the total trading volume in bonds given a block window. + * @param options.fromBlock - The start block, defaults to "earliest" + * @param options.toBlock - The end block, defaults to "latest" + * @returns the total amount of bonds traded + */ + async getTradingVolume(options?: { + fromBlock?: BlockTag | bigint; + toBlock?: BlockTag | bigint; + }): Promise<{ + totalVolume: bigint; + longVolume: bigint; + shortVolume: bigint; + }> { + const { fromBlock, toBlock } = options || {}; + const longEvents = await this.getLongEvents({ + fromBlock, + toBlock, + }); + const shortEvents = await this.getShortEvents({ + fromBlock, + toBlock, + }); + + const longVolume = longEvents.reduce( + (sum, { bondAmount }) => sum + bondAmount, + 0n, + ); + const shortVolume = shortEvents.reduce( + (sum, { bondAmount }) => sum + bondAmount, + 0n, + ); + return { + longVolume, + shortVolume, + totalVolume: longVolume + shortVolume, + }; + } + + /** + * Gets the spot price of a long + * @param options - The read options + * @returns the spot price of a long + */ + async getLongPrice(options?: ContractReadOptions): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + return hyperwasm.spotPrice({ poolConfig, poolInfo }); + } + + async getLongEvents( + options?: ContractGetEventsOptions & + ContractGetEventsOptions, + ): Promise< + { + trader: `0x${string}`; + assetId: bigint; + bondAmount: bigint; + baseAmount: bigint; + eventName: "OpenLong" | "CloseLong"; + blockNumber: bigint | undefined; + transactionHash: `0x${string}` | undefined; + }[] + > { + const openLongEvents = await this.contract.getEvents("OpenLong", options); + const closeLongEvents = await this.contract.getEvents("CloseLong", options); + return [...openLongEvents, ...closeLongEvents] + .map(({ args, eventName, blockNumber, transactionHash }) => { + const baseAmount = args.asBase + ? args.amount + : fixed(args.amount).mul(args.vaultSharePrice).bigint; + return { + trader: args.trader, + assetId: args.assetId, + bondAmount: args.bondAmount, + baseAmount, + eventName, + blockNumber, + transactionHash, + }; + }) + .sort((a, b) => Number(a.blockNumber) - Number(b.blockNumber)); + } + + async getShortEvents( + options?: ContractGetEventsOptions & + ContractGetEventsOptions, + ): Promise< + { + trader: `0x${string}`; + assetId: bigint; + bondAmount: bigint; + baseAmount: bigint; + eventName: "OpenShort" | "CloseShort"; + blockNumber: bigint | undefined; + transactionHash: `0x${string}` | undefined; + }[] + > { + const openShortEvents = await this.contract.getEvents("OpenShort", options); + const closeShortEvents = await this.contract.getEvents( + "CloseShort", + options, + ); + return [...openShortEvents, ...closeShortEvents] + .map(({ args, eventName, blockNumber, transactionHash }) => { + const baseAmount = args.asBase + ? args.amount + : fixed(args.amount).mul(args.vaultSharePrice).bigint; + return { + trader: args.trader, + assetId: args.assetId, + bondAmount: args.bondAmount, + baseAmount, + eventName, + blockNumber, + transactionHash, + }; + }) + .sort((a, b) => Number(a.blockNumber) - Number(b.blockNumber)); + } + + async getLpEvents( + options?: ContractGetEventsOptions & + ContractGetEventsOptions & + ContractGetEventsOptions, + ): Promise<{ + addLiquidity: ContractEvent[]; + removeLiquidity: ContractEvent[]; + redeemWithdrawalShares: ContractEvent< + HyperdriveAbi, + "RedeemWithdrawalShares" + >[]; + }> { + const addLiquidityEvents = await this.contract.getEvents( + "AddLiquidity", + options, + ); + const removeLiquidityEvents = await this.contract.getEvents( + "RemoveLiquidity", + options, + ); + const redeemWithdrawalSharesEvents = await this.contract.getEvents( + "RedeemWithdrawalShares", + options, + ); + + return { + addLiquidity: addLiquidityEvents, + removeLiquidity: removeLiquidityEvents, + redeemWithdrawalShares: redeemWithdrawalSharesEvents, + }; + } + + /** + * This returns the LP APY using the following formula for continuous compounding: + * r = rate of return + * p_0 = from lpSharePrice + * p_1 = to lpSharePrice + * t = time frame between p_0 and p_1 + * + * r = (p_1 / p_0) ^ (1 / t) - 1 + */ + async getLpApy({ + fromBlock, + options, + }: { + fromBlock: bigint; + options?: ContractReadOptions; + }): Promise<{ lpApy: bigint }> { + // If the 24 hour rate doesn't exist, assume the pool was initialized less + // than 24 hours before and try to get the all-time rate until toBlock + const { blockNumber: initializationBlock } = + await this.getInitializationBlock(); + if (initializationBlock && initializationBlock > fromBlock) { + fromBlock = initializationBlock; + } + + // Attempt to fetch the blocks first to fail early if the block is not found + const currentBlock = await getBlockOrThrow(this.drift, options); + const startBlock = await getBlockOrThrow(this.drift, { + blockNumber: fromBlock, + }); + + // Get the info from fromBlock to get the starting lp share price + const { lpSharePrice: startLpSharePrice } = await this.getPoolInfo({ + block: fromBlock, + }); + + // Get the current lpSharePrice from the latest pool info + const { lpSharePrice: currentLpSharePrice } = await this.getPoolInfo(); + + const timeFrame = currentBlock.timestamp - startBlock.timestamp; + + const lpApy = calculateApyFromPrice({ + startPrice: startLpSharePrice, + endPrice: currentLpSharePrice, + timeFrame, + }); + + return { lpApy }; + } + + async getCheckpointEvents( + options?: ContractGetEventsOptions, + ): Promise { + const checkPointEvents = await this.contract.getEvents( + "CreateCheckpoint", + options, + ); + return checkPointEvents; + } + + private _calcOpenLongs({ + openLongEvents, + closeLongEvents, + }: { + openLongEvents: ContractEvent[]; + closeLongEvents: ContractEvent[]; + }): Long[] { + // Put open and long events in block order. We spread openLongEvents first + // since you have to open a long before you can close one. + const orderedLongEvents = [...openLongEvents, ...closeLongEvents].sort( + (a, b) => Number(a.blockNumber) - Number(b.blockNumber), + ); + + const openLongs: Record = {}; + + orderedLongEvents.forEach((event) => { + const assetId = event.args.assetId.toString(); + + const long: Long = openLongs[assetId] || { + assetId: event.args.assetId, + maturity: event.args.maturityTime, + baseAmountPaid: 0n, + bondAmount: 0n, + }; + + const baseAmount = event.args.asBase + ? event.args.amount + : fixed(event.args.amount).mul(event.args.vaultSharePrice).bigint; + + switch (event.eventName) { + case "OpenLong": + openLongs[assetId] = { + ...long, + baseAmountPaid: long.baseAmountPaid + baseAmount, + bondAmount: long.bondAmount + event.args.bondAmount, + }; + return; + + case "CloseLong": + // If a user closes their whole position, we should remove the whole + // position since it's basically starting over + if (event.args.bondAmount === long.bondAmount) { + delete openLongs[assetId]; + } else { + // otherwise just subtract the amount of bonds they closed and baseAmount + // they received back from the running total + openLongs[assetId] = { + ...long, + baseAmountPaid: long.baseAmountPaid - baseAmount, + bondAmount: long.bondAmount - event.args.bondAmount, + }; + } + return; + + default: + assertNever(event, true); + } + }); + + return Object.values(openLongs).filter((long) => long.bondAmount); + } + + // TODO: Rename this to getOpenLongs once this function replaces the existing getOpenLongs + async getOpenLongPositions({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const transfersReceived = await this.contract.getEvents("TransferSingle", { + filter: { to: account }, + toBlock: options?.block, + }); + const transfersSent = await this.contract.getEvents("TransferSingle", { + filter: { from: account }, + toBlock: options?.block, + }); + + const longsReceived = transfersReceived.filter((event) => { + const { assetType } = decodeAssetFromTransferSingleEventData( + event.data as `0x${string}`, + ); + return assetType === "LONG"; + }); + + const longsSent = transfersSent.filter((event) => { + const { assetType } = decodeAssetFromTransferSingleEventData( + event.data as `0x${string}`, + ); + return assetType === "LONG"; + }); + + // Put open and long events in block order. We spread openLongEvents first + // since you have to open a long before you can close one. + const orderedLongEvents = [...longsReceived, ...longsSent].sort( + (a, b) => Number(a.blockNumber) - Number(b.blockNumber), + ); + + const openLongs: Record = + {}; + + orderedLongEvents.forEach((event) => { + const assetId = event.args.id.toString(); + + const long: OpenLongPositionReceivedWithoutDetails = openLongs[ + assetId + ] || { + assetId, + maturity: decodeAssetFromTransferSingleEventData( + event.data as `0x${string}`, + ).timestamp, + value: 0n, + }; + + const isLongReceived = event.args.to === account; + if (isLongReceived) { + const updatedLong: OpenLongPositionReceivedWithoutDetails = { + ...long, + value: long.value + event.args.value, + }; + openLongs[assetId] = updatedLong; + return; + } + + const isLongSent = event.args.from === account; + if (isLongSent) { + // If a user closes their whole position, we should remove the whole + // position since it's basically starting over + if (event.args.value === long.value) { + delete openLongs[assetId]; + return; + } + // otherwise just subtract the amount of bonds they closed and baseAmount + // they received back from the running total + const updatedLong: OpenLongPositionReceivedWithoutDetails = { + ...long, + value: long.value - event.args.value, + }; + openLongs[assetId] = updatedLong; + } + }); + return Object.values(openLongs).filter((long) => long.value); + } + + async getOpenLongDetails({ + assetId, + account, + options, + }: { + assetId: bigint; + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const allLongPositions = await this.getOpenLongPositions({ + account, + options, + }); + + const longPosition = allLongPositions.find((p) => p.assetId === assetId); + + if (!longPosition) { + throw new HyperdriveSdkError( + `No position with asset id: ${assetId} found for account ${account}`, + ); + } + + const openLongEvents = await this.contract.getEvents("OpenLong", { + filter: { trader: account }, + }); + + const closeLongEvents = await this.contract.getEvents("CloseLong", { + filter: { trader: account }, + }); + + const allOpenLongDetails = this._calcOpenLongs({ + openLongEvents, + closeLongEvents, + }); + + const openLongDetails = allOpenLongDetails.find( + (details) => + details.assetId.toString() === longPosition.assetId.toString(), + ); + // If no details exists for the position, the user must have just received + // some longs via transfer but never opened them themselves. + // OR If the amounts aren't the same, then they may have opened some and + // received some from another wallet. In this case, we still can't be sure + // of the details, so we return undefined. + if (!openLongDetails || openLongDetails.bondAmount !== longPosition.value) { + return; + } + + return openLongDetails; + } + /** + * @deprecated Use ReadHyperdrive.getOpenLongPositions and ReadHyperdrive.getOpenLongDetails instead to retrieve all longs opened or received by a user. + * Gets the active longs opened by a specific user. + * @param account - The user's address + * @param options.toBlock - The end block, defaults to "latest" + * @returns the active longs opened by a specific user + */ + async getOpenLongs({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const openLongEvents = await this.contract.getEvents("OpenLong", { + filter: { trader: account }, + toBlock: options?.block, + }); + const closeLongEvents = await this.contract.getEvents("CloseLong", { + filter: { trader: account }, + toBlock: options?.block, + }); + + return this._calcOpenLongs({ + openLongEvents, + closeLongEvents, + }); + } + + /** + * Gets the active shorts opened by a specific user. + * @param account - The user's address + * @param options.toBlock - The end block, defaults to "latest" + * @returns the active shorts opened by a specific user + * */ + async getOpenShorts({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const { checkpointDuration, positionDuration } = + await this.getPoolConfig(options); + + const openShortEvents = await this.contract.getEvents("OpenShort", { + filter: { trader: account }, + toBlock: options?.block, + }); + const closeShortEvents = await this.contract.getEvents("CloseShort", { + filter: { trader: account }, + toBlock: options?.block, + }); + + return this._calcOpenShorts({ + hyperdriveAddress: this.address, + checkpointDuration, + positionDuration, + openShortEvents, + closeShortEvents, + }); + } + + private async _calcOpenShorts({ + hyperdriveAddress, + checkpointDuration, + positionDuration, + closeShortEvents, + openShortEvents, + }: { + hyperdriveAddress: Address; + checkpointDuration: bigint; + positionDuration: bigint; + openShortEvents: ContractEvent[]; + closeShortEvents: ContractEvent[]; + }): Promise { + // Put open and short events in block order. We spread openShortEvents first + // since you have to open a short before you can close one. + const orderedShortEvents = [...openShortEvents, ...closeShortEvents].sort( + (a, b) => Number(a.blockNumber) - Number(b.blockNumber), + ); + + const openShorts: Record = {}; + + for (const event of orderedShortEvents) { + const assetId = event.args.assetId.toString(); + const { timestamp } = await getBlockOrThrow(this.drift, { + blockNumber: event.blockNumber, + }); + + // Create a default empty short that we will update based on the events + // const short: OpenShort = openShorts[assetId] || { + openShorts[assetId] = openShorts[assetId] || { + hyperdriveAddress, + assetId: event.args.assetId, + maturity: event.args.maturityTime, + checkpointTime: getCheckpointTime(timestamp, checkpointDuration), + // The openedTimestamp will always reflect the latest short, if you open + // twice in the same checkpoint + openedTimestamp: timestamp, + baseAmountPaid: 0n, + bondAmount: 0n, + baseProceeds: 0n, + fixedRatePaid: 0n, + }; + + const baseAmount = event.args.asBase + ? event.args.amount + : fixed(event.args.amount).mul(event.args.vaultSharePrice).bigint; + + const { eventName } = event; + switch (eventName) { + // When you open a short, we add up how much you've paid and your new + // total bond amount, then update the average price and fixed rate + // paid + case "OpenShort": + openShorts[assetId].baseAmountPaid += baseAmount; + openShorts[assetId].bondAmount += event.args.bondAmount; + openShorts[assetId].baseProceeds += event.args.baseProceeds; + openShorts[assetId].fixedRatePaid = calculateAprFromPrice({ + positionDuration, + baseAmount: openShorts[assetId].baseProceeds, + bondAmount: openShorts[assetId].bondAmount, + }); + continue; + + case "CloseShort": { + // If a user closes their whole position, we should remove the whole + // position since it's basically starting over + if (event.args.bondAmount === openShorts[assetId].bondAmount) { + delete openShorts[assetId]; + continue; + } + // otherwise just subtract the amount of bonds they closed and baseAmount + // they received back from the running total + openShorts[assetId].baseAmountPaid -= baseAmount; + openShorts[assetId].bondAmount -= event.args.bondAmount; + openShorts[assetId].baseProceeds -= event.args.basePayment; + openShorts[assetId].fixedRatePaid = calculateAprFromPrice({ + positionDuration, + baseAmount: openShorts[assetId].baseProceeds, + bondAmount: openShorts[assetId].bondAmount, + }); + continue; + } + + default: + assertNever(eventName, true); + } + } + + return Object.values(openShorts).filter((short) => short.bondAmount); + } + + /** + * Gets the closed longs by a specific user. + */ + async getClosedLongs({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const closedLongs = await this.contract.getEvents("CloseLong", { + filter: { trader: account }, + toBlock: options?.block, + }); + + const closedLongsList: ClosedLong[] = await Promise.all( + closedLongs.map(async (event) => { + const assetId = event.args.assetId; + + const baseAmount = event.args.asBase + ? event.args.amount + : fixed(event.args.amount).mul(event.args.vaultSharePrice).bigint; + + return { + assetId, + bondAmount: event.args.bondAmount, + baseAmount, + baseAmountPaid: 0n, // TODO: Remove this field, this is copy/paste from @hyperdrive/queries + maturity: event.args.maturityTime, + closedTimestamp: ( + await getBlockOrThrow(this.drift, { + blockNumber: event.blockNumber, + }) + ).timestamp, + }; + }), + ); + return closedLongsList.filter((long) => long.bondAmount); + } + + /** + * Gets the inactive shorts opened by a specific user. + */ + async getClosedShorts({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const closedShorts = await this.contract.getEvents("CloseShort", { + filter: { trader: account }, + toBlock: options?.block, + }); + + const { checkpointDuration } = await this.getPoolConfig(options); + const closedShortsList: ClosedShort[] = await Promise.all( + closedShorts.map(async (event) => { + const { assetId, maturityTime } = event.args; + const { timestamp } = await getBlockOrThrow(this.drift, { + blockNumber: event.blockNumber, + }); + + const baseAmount = event.args.asBase + ? event.args.amount + : fixed(event.args.amount).mul(event.args.vaultSharePrice).bigint; + + return { + hyperdriveAddress: this.address, + assetId, + bondAmount: event.args.bondAmount, + baseAmountReceived: baseAmount, + maturity: maturityTime, + closedTimestamp: timestamp, + checkpointTime: getCheckpointTime(timestamp, checkpointDuration), + }; + }), + ); + + return closedShortsList.filter((short) => short.bondAmount); + } + /** + * Gets the maximum amount of bonds a user can open a short for. + */ + async getMaxShort({ + budget, + options, + }: { + budget: bigint; + options?: ContractReadOptions; + }): Promise<{ + maxBaseIn: bigint; + maxSharesIn: bigint; + maxBondsOut: bigint; + }> { + const poolInfo = await this.getPoolInfo(options); + const poolConfig = await this.getPoolConfig(options); + const checkpointExposure = await this.getCheckpointExposure({ options }); + const { vaultSharePrice: openVaultSharePrice } = await this.getCheckpoint({ + options, + }); + + const maxBondsOut = hyperwasm.maxShort({ + budget, + poolInfo, + poolConfig, + maxIterations: MAX_ITERATIONS, + openVaultSharePrice, + checkpointExposure, + }); + + const maxBaseIn = hyperwasm.calcOpenShort({ + poolInfo, + poolConfig, + bondAmount: maxBondsOut, + openVaultSharePrice, + }); + const maxSharesIn = await this.convertToShares({ + baseAmount: maxBaseIn, + options, + }); + + return { + maxBaseIn, + maxSharesIn, + maxBondsOut, + }; + } + /** + * Gets the maximum amount of bonds a user can open a long for. + */ + async getMaxLong(options?: ContractReadOptions): Promise<{ + maxBaseIn: bigint; + maxSharesIn: bigint; + maxBondsOut: bigint; + }> { + const poolInfo = await this.getPoolInfo(options); + const poolConfig = await this.getPoolConfig(options); + const checkpointExposure = await this.getCheckpointExposure({ options }); + + const maxBaseIn = hyperwasm.maxLong({ + poolInfo, + poolConfig, + budget: MAX_UINT256, + checkpointExposure, + }); + + const maxSharesIn = await this.convertToShares({ + baseAmount: maxBaseIn, + options, + }); + + const maxBondsOut = hyperwasm.calcOpenLong({ + poolInfo, + poolConfig, + baseAmount: maxBaseIn, + }); + + return { + maxBaseIn, + maxSharesIn, + maxBondsOut, + }; + } + + getLpSharesTotalSupply(args?: { + options?: ContractReadOptions; + }): Promise { + return this.contract.read( + "totalSupply", + { tokenId: LP_ASSET_ID }, + args?.options, + ); + } + + /** + * Gets the amount of LP shares a user has. + */ + getLpShares({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.contract.read( + "balanceOf", + { tokenId: LP_ASSET_ID, owner: account }, + options, + ); + } + + /** + * Gets a user's current LP position. + */ + async getOpenLpPosition({ + account, + // TODO: Remove asBase parameter when we can use hyperwasm to calculate the + // preview remove liquidity + asBase, + options, + }: { + asBase: boolean; + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise<{ + lpShareBalance: bigint; + baseAmountPaid: bigint; + baseValue: bigint; + sharesValue: bigint; + }> { + const addLiquidityEvents = await this.contract.getEvents("AddLiquidity", { + filter: { provider: account }, + toBlock: options?.block, + }); + const removeLiquidityEvents = await this.contract.getEvents( + "RemoveLiquidity", + { + filter: { provider: account }, + toBlock: options?.block, + }, + ); + + const decimals = await this.getDecimals(); + const { lpShareBalance, baseAmountPaid } = this._calcOpenLpPosition({ + addLiquidityEvents, + removeLiquidityEvents, + }); + + if (!lpShareBalance) { + return { + lpShareBalance, + baseAmountPaid, + baseValue: 0n, + sharesValue: 0n, + }; + } + + // Note: `previewRemoveLiquidity` uses the `simulateWrite` method which + // simulates the transaction at the current block. This means that the + // calculated value of the position will always be based on the current + // state of the pool, even if the lp balance and amount paid were + // calculated using past events via the block in options. + + const { proceeds, withdrawalShares } = await this.previewRemoveLiquidity({ + lpSharesIn: lpShareBalance, + minOutputPerShare: 1n, + asBase, + destination: account, + }); + + // Note: we don't pass in the options here because we want the current + // prices that were used in the previewRemoveLiquidity call. + const proceedsBaseValue = asBase + ? proceeds + : await this.convertToBase({ + sharesAmount: proceeds, + }); + + // convert the withdrawal shares into base using lpSharePrice + const { lpSharePrice } = await this.getPoolInfo(); + const withdrawalSharesBaseValue = fixed(lpSharePrice, decimals).mul( + withdrawalShares, + decimals, + ).bigint; + const withdrawalSharesSharesValue = await this.convertToShares({ + baseAmount: withdrawalSharesBaseValue, + }); + + return { + lpShareBalance, + baseAmountPaid, + baseValue: proceedsBaseValue + withdrawalSharesBaseValue, + sharesValue: proceeds + withdrawalSharesSharesValue, + }; + } + + /** + * Combine the adds and removes in order of block number to get the full + * transaction history in the order the user executed them + */ + private _calcOpenLpPosition({ + addLiquidityEvents, + removeLiquidityEvents, + }: { + addLiquidityEvents: ContractEvent[]; + removeLiquidityEvents: ContractEvent< + typeof hyperdriveAbi, + "RemoveLiquidity" + >[]; + }) { + const combinedEventsInOrder = [ + ...addLiquidityEvents, + ...removeLiquidityEvents, + ].sort((a, b) => Number(a.blockNumber) - Number(b.blockNumber)); + + let baseAmountPaid = 0n; + let lpShareBalance = 0n; + combinedEventsInOrder.forEach((event) => { + const baseAmount = event.args.asBase + ? event.args.amount + : fixed(event.args.amount).mul(event.args.vaultSharePrice).bigint; + + switch (event.eventName) { + case "AddLiquidity": + lpShareBalance += event.args.lpAmount; + baseAmountPaid += baseAmount; + return; + + case "RemoveLiquidity": { + lpShareBalance -= event.args.lpAmount; + + // If a user removes all their lp shares, we should zero out + // baseAmountPaid, since it's basically starting over + if (lpShareBalance <= 0n) { + baseAmountPaid = 0n; + } else { + // Include the base value of withdrawal shares received when + // recalculating baseAmountPaid. + const withdrawalSharesBaseValue = fixed( + event.args.withdrawalShareAmount, + ).mul(event.args.lpSharePrice).bigint; + baseAmountPaid = + baseAmountPaid - baseAmount - withdrawalSharesBaseValue; + } + return; + } + + default: + assertNever(event, true); + } + }); + + return { lpShareBalance, baseAmountPaid }; + } + + /** + * Gets the amount of closed LP shares a user has. + */ + async getClosedLpShares({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const removeLiquidityEvents = await this.contract.getEvents( + "RemoveLiquidity", + { + filter: { provider: account }, + toBlock: options?.block, + }, + ); + return Promise.all( + removeLiquidityEvents.map(async ({ blockNumber, args }) => { + const { + lpAmount, + withdrawalShareAmount, + asBase, + amount, + lpSharePrice, + } = args; + + const baseAmount = asBase + ? amount + : fixed(args.amount).mul(args.vaultSharePrice).bigint; + + return { + lpAmount, + baseAmount, + withdrawalShareAmount, + lpSharePrice, + closedTimestamp: ( + await getBlockOrThrow(this.drift, { + blockNumber, + }) + ).timestamp, + }; + }), + ); + } + + /** + * Gets the amount of withdrawal shares a user has. + */ + getWithdrawalShares({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.contract.read( + "balanceOf", + { tokenId: WITHDRAW_SHARES_ASSET_ID, owner: account }, + options, + ); + } + + /** + * Gets the amount of redeemed withdrawal shares a user has. + */ + async getRedeemedWithdrawalShares({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const redeemedWithdrawalShareEvents = await this.contract.getEvents( + "RedeemWithdrawalShares", + { + filter: { provider: account }, + toBlock: options?.block, + }, + ); + + return Promise.all( + redeemedWithdrawalShareEvents.map(async ({ blockNumber, args }) => { + const { withdrawalShareAmount, amount, asBase, vaultSharePrice } = args; + const baseAmount = asBase + ? args.amount + : fixed(amount).mul(vaultSharePrice).bigint; + + return { + hyperdriveAddress: this.address, + withdrawalShareAmount, + baseAmount, + redeemedTimestamp: ( + await getBlockOrThrow(this.drift, { blockNumber }) + ).timestamp, + }; + }), + ); + } + + /** + * Predicts the amount of bonds a user will receive when opening a long in + * either base or shares. The curve fee returned from this function is paid in bonds. + */ + async previewOpenLong({ + amountIn, + asBase, + options, + }: { + amountIn: bigint; + asBase: boolean; + options?: ContractReadOptions; + }): Promise<{ + maturityTime: bigint; + bondProceeds: bigint; + spotPriceAfterOpen: bigint; + spotRateAfterOpen: bigint; + curveFee: bigint; + }> { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + const checkpointTime = await this.getCheckpointTime({ options }); + + // calcOpenLong only accepts base, so if the user is depositing shares we + // need to convert that value to base before we can preview the trade for them + let depositAmountConvertedToBase = amountIn; + if (!asBase) { + depositAmountConvertedToBase = await this.convertToBase({ + sharesAmount: amountIn, + options, + }); + } + + const spotPriceAfterOpen = hyperwasm.spotPriceAfterLong({ + poolInfo, + poolConfig, + baseAmount: depositAmountConvertedToBase, + }); + + const spotRateAfterOpen = hyperwasm.calcAprGivenFixedPrice({ + price: spotPriceAfterOpen, + positionDuration: poolConfig.positionDuration, + }); + + const bondProceeds = hyperwasm.calcOpenLong({ + poolInfo, + poolConfig, + baseAmount: depositAmountConvertedToBase, + }); + + const curveFeeInBonds = hyperwasm.openLongCurveFee({ + poolInfo, + poolConfig, + baseAmount: depositAmountConvertedToBase, + }); + + return { + maturityTime: checkpointTime + poolConfig.positionDuration, + bondProceeds, + spotPriceAfterOpen, + spotRateAfterOpen, + curveFee: curveFeeInBonds, + }; + } + + /** + * Calculates the cost to open a short given the current pool state and the + * amount of bonds the user wants to short. + * @param amountOfBondsToShort The number of bonds to short + * @param asBase If true, the traderDeposit will be in base. If false, the traderDeposit will be in shares + */ + async previewOpenShort({ + amountOfBondsToShort, + asBase, + options, + }: { + amountOfBondsToShort: bigint; + asBase: boolean; + options?: ContractReadOptions; + }): Promise<{ + maturityTime: bigint; + traderDeposit: bigint; + spotPriceAfterOpen: bigint; + spotRateAfterOpen: bigint; + curveFee: bigint; + fixedRatePaid: bigint; + }> { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + const latestCheckpoint = await this.getCheckpoint({ options }); + const accruedYield = await this.getShortAccruedYield({ + checkpointTime: latestCheckpoint.checkpointTime, + bondAmount: amountOfBondsToShort, + options, + }); + + const baseDepositAmount = hyperwasm.calcOpenShort({ + poolInfo, + poolConfig, + bondAmount: amountOfBondsToShort, + openVaultSharePrice: latestCheckpoint.vaultSharePrice, + }); + + // Calculation for fixed rate paid. This is the rate the user pays upfront + // to open a short. + const fixedTimeRangeInYears = fixed(poolConfig.positionDuration).div( + SECONDS_PER_YEAR, + ); + const baseAmountMinusYield = fixed(baseDepositAmount).sub(accruedYield); + const bondsMinusBaseAmount = + fixed(amountOfBondsToShort).sub(baseAmountMinusYield); + const fixedRatePaid = baseAmountMinusYield + .div(bondsMinusBaseAmount) + .div(fixedTimeRangeInYears).bigint; + + const spotPriceAfterOpen = hyperwasm.spotPriceAfterShort({ + poolInfo, + poolConfig, + bondAmount: amountOfBondsToShort, + }); + + const spotRateAfterOpen = hyperwasm.calcAprGivenFixedPrice({ + price: spotPriceAfterOpen, + positionDuration: poolConfig.positionDuration, + }); + + const curveFeeInBase = hyperwasm.openShortCurveFee({ + poolInfo, + poolConfig, + bondAmount: amountOfBondsToShort, + }); + + if (asBase) { + return { + maturityTime: + latestCheckpoint.checkpointTime + poolConfig.positionDuration, + traderDeposit: baseDepositAmount, + spotPriceAfterOpen, + spotRateAfterOpen, + curveFee: curveFeeInBase, + fixedRatePaid, + }; + } + + return { + maturityTime: + latestCheckpoint.checkpointTime + poolConfig.positionDuration, + traderDeposit: await this.convertToShares({ + baseAmount: baseDepositAmount, + options, + }), + spotPriceAfterOpen, + spotRateAfterOpen, + curveFee: await this.convertToShares({ + baseAmount: curveFeeInBase, + options, + }), + fixedRatePaid, + }; + } + + /** + * Predicts the amount of bonds that can be shorted given a target deposit + * amount in either base or shares. + */ + async getShortBondsGivenDeposit({ + amountIn, + asBase, + tolerance, + options, + }: { + amountIn: bigint; + asBase: boolean; + /** + * The maximum difference between the target and actual base amount. + * + * @default 1e9 + */ + tolerance?: bigint; + options?: ContractReadOptions; + }): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + const latestCheckpoint = await this.getCheckpoint({ options }); + const checkpointExposure = await this.getCheckpointExposure({ options }); + + let targetBaseAmount = amountIn; + if (!asBase) { + targetBaseAmount = await this.convertToBase({ + sharesAmount: amountIn, + options, + }); + } + + const absoluteMaxBondAmount = hyperwasm.absoluteMaxShort({ + poolInfo, + poolConfig, + checkpointExposure, + }); + + return hyperwasm.shortBondsGivenDeposit({ + poolInfo, + poolConfig, + targetBaseAmount, + absoluteMaxBondAmount, + openVaultSharePrice: latestCheckpoint.vaultSharePrice, + maybeTolerance: tolerance, + }); + } + + /** + * Predicts the amount of base asset a user will receive when closing a long. + */ + async previewCloseLong({ + maturityTime, + bondAmountIn, + asBase, + options, + }: { + maturityTime: bigint; + bondAmountIn: bigint; + asBase: boolean; + options?: ContractReadOptions; + }): Promise<{ amountOut: bigint; flatPlusCurveFee: bigint }> { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + + const flatFeeInShares = hyperwasm.closeLongFlatFee({ + poolInfo, + poolConfig, + bondAmount: bondAmountIn, + maturityTime, + currentTime, + }); + const curveFeeInShares = hyperwasm.closeLongCurveFee({ + poolInfo, + poolConfig, + bondAmount: bondAmountIn, + maturityTime, + currentTime, + }); + const flatPlusCurveFee = flatFeeInShares + curveFeeInShares; + + const amountOutInShares = hyperwasm.calcCloseLong({ + poolInfo, + poolConfig, + bondAmount: bondAmountIn, + maturityTime, + currentTime, + }); + + if (!asBase) { + return { + amountOut: amountOutInShares, + flatPlusCurveFee, + }; + } + + return { + amountOut: await this.convertToBase({ + sharesAmount: amountOutInShares, + options, + }), + flatPlusCurveFee: await this.convertToBase({ + sharesAmount: flatPlusCurveFee, + options, + }), + }; + } + /** + * Get a rough estimate of the market value of a short. This can be used to + * value a position that cannot be fully closed. + */ + async estimateShortMarketValue({ + maturityTime, + asBase, + shortAmountIn, + options, + }: { + maturityTime: bigint; + shortAmountIn: bigint; + asBase: boolean; + extraData?: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + // The checkpoint in which this position was opened. + // This is always maturity time - position duration thanks to mint on demand + const openCheckpointTimestamp = maturityTime - poolConfig.positionDuration; + const { vaultSharePrice: openVaultSharePrice } = await this.getCheckpoint({ + timestamp: openCheckpointTimestamp, + options, + }); + + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + + // If the position is mature, we use the closing vault share price otherwise + // use the current vault share price + let closeVaultSharePrice = poolInfo.vaultSharePrice; + if (maturityTime <= currentTime) { + const closingCheckpoint = await this.getCheckpoint({ + timestamp: maturityTime, + options, + }); + closeVaultSharePrice = closingCheckpoint.vaultSharePrice; + } + + const marketEstimateInShares = hyperwasm.calcShortMarketValue({ + poolInfo, + poolConfig, + bondAmount: shortAmountIn, + openVaultSharePrice, + closeVaultSharePrice, + maturityTime, + currentTime, + }); + + if (!asBase) { + return marketEstimateInShares; + } + + return this.convertToBase({ + sharesAmount: marketEstimateInShares, + options, + }); + } + + /** + * Predicts the amount of base asset a user will receive when closing a short. + * If closing the short would result in negative interest, an error will be + * thrown. + */ + async previewCloseShort({ + maturityTime, + shortAmountIn, + asBase, + options, + }: { + maturityTime: bigint; + shortAmountIn: bigint; + asBase: boolean; + extraData?: `0x${string}`; + options?: ContractReadOptions; + }): Promise<{ + amountOut: bigint; + flatPlusCurveFee: bigint; + }> { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + + // The checkpoint in which this position was opened. + // This is always maturity time - position duration thanks to mint on demand + const openCheckpointTimestamp = maturityTime - poolConfig.positionDuration; + const { vaultSharePrice: openVaultSharePrice } = await this.getCheckpoint({ + timestamp: openCheckpointTimestamp, + options, + }); + + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + + // If the position is mature, we use the closing vault share price otherwise + // use the current vault share price + let closeVaultSharePrice = poolInfo.vaultSharePrice; + if (maturityTime <= currentTime) { + const closingCheckpoint = await this.getCheckpoint({ + timestamp: maturityTime, + options, + }); + closeVaultSharePrice = closingCheckpoint.vaultSharePrice; + } + + const flatFeeInShares = hyperwasm.closeShortFlatFee({ + poolInfo, + poolConfig, + bondAmount: shortAmountIn, + maturityTime, + currentTime, + }); + const curveFeeInShares = hyperwasm.closeShortCurveFee({ + poolInfo, + poolConfig, + bondAmount: shortAmountIn, + maturityTime, + currentTime, + }); + const flatPlusCurveFee = flatFeeInShares + curveFeeInShares; + + const amountOutInShares = hyperwasm.calcCloseShort({ + poolInfo, + poolConfig, + bondAmount: shortAmountIn, + openVaultSharePrice, + closeVaultSharePrice, + maturityTime, + currentTime, + }); + + if (!asBase) { + return { + amountOut: amountOutInShares, + flatPlusCurveFee, + }; + } + + return { + amountOut: await this.convertToBase({ + sharesAmount: amountOutInShares, + options, + }), + flatPlusCurveFee: await this.convertToBase({ + sharesAmount: flatPlusCurveFee, + options, + }), + }; + } + + /** + * Predicts the amount of LP shares a user will receive when adding liquidity. + */ + async previewAddLiquidity({ + contribution, + minApr, + minLpSharePrice, + maxApr, + asBase, + options, + }: { + contribution: bigint; + minApr: bigint; + minLpSharePrice: bigint; + maxApr: bigint; + destination: `0x${string}`; + asBase: boolean; + extraData?: `0x${string}`; + options?: ContractReadOptions; + }): Promise<{ lpSharesOut: bigint; slippagePaid: bigint }> { + const poolConfig = await this.getPoolConfig(options); + const poolInfo = await this.getPoolInfo(options); + const currentTime = BigInt(Math.floor(Date.now() / 1000)); + const lpSharesOut = hyperwasm.calcAddLiquidity({ + poolInfo, + poolConfig, + currentTime, + contribution, + asBase, + minLpSharePrice, + minApr: minApr, + maxApr: maxApr, + }); + const decimals = await this.getDecimals(); + + const lpSharesOutInBase = fixed(lpSharesOut, decimals).mul( + poolInfo.lpSharePrice, + decimals, + ).bigint; + const valueOfLpShares = asBase + ? lpSharesOutInBase + : await this.convertToShares({ + baseAmount: lpSharesOutInBase, + options, + }); + + return { + lpSharesOut, + slippagePaid: contribution - valueOfLpShares, + }; + } + + /** + * Predicts the amount of base asset and withdrawal shares a user will receive when removing liquidity. + */ + async previewRemoveLiquidity({ + lpSharesIn, + minOutputPerShare, + destination, + asBase, + extraData = NULL_BYTES, + options, + }: { + lpSharesIn: bigint; + minOutputPerShare: bigint; + destination: `0x${string}`; + asBase: boolean; + extraData?: `0x${string}`; + options?: ContractWriteOptions; + }): Promise<{ proceeds: bigint; withdrawalShares: bigint }> { + const { proceeds, withdrawalShares } = await this.contract.simulateWrite( + "removeLiquidity", + { + _lpShares: lpSharesIn, + _minOutputPerShare: minOutputPerShare, + _options: { destination, asBase, extraData }, + }, + { + ...options, + // since this is calling a write method in view mode, we must specify + // the `from` in order to have an account to preview with + from: destination, + }, + ); + + return { + proceeds, + withdrawalShares, + }; + } + + /** + * Predicts the amount of base asset and redeemed shares a user will receive when redeeming withdrawal shares. + */ + async previewRedeemWithdrawalShares({ + withdrawalSharesIn, + minOutputPerShare, + destination, + asBase, + extraData = NULL_BYTES, + options, + }: { + withdrawalSharesIn: bigint; + minOutputPerShare: bigint; + destination: `0x${string}`; + asBase: boolean; + extraData?: `0x${string}`; + options?: ContractWriteOptions; + }): Promise<{ + baseProceeds: bigint; + withdrawalSharesRedeemed: bigint; + asBase: boolean; + sharesProceeds: bigint; + }> { + const { proceeds, withdrawalSharesRedeemed } = + await this.contract.simulateWrite( + "redeemWithdrawalShares", + { + _withdrawalShares: withdrawalSharesIn, + _minOutputPerShare: minOutputPerShare, + _options: { destination, asBase, extraData }, + }, + options, + ); + + return { + asBase, + baseProceeds: asBase + ? proceeds + : await this.convertToBase({ sharesAmount: proceeds }), + sharesProceeds: asBase + ? await this.convertToShares({ baseAmount: proceeds }) + : proceeds, + withdrawalSharesRedeemed, + }; + } +} + +/* + * This returns the APY using the following formula for continuous compounding: + + * r = rate of return + * p_0 = start price + * p_1 = end price + * t = term length in fractions of a year + * + * r = (p_1 / p_0) ^ (1 / t) - 1 + */ +function calculateApyFromPrice({ + startPrice, + endPrice, + timeFrame, // seconds +}: { + startPrice: bigint; + endPrice: bigint; + timeFrame: bigint; +}): bigint { + const priceRatio = fixed(endPrice).div(startPrice); + const yearFraction = fixed(timeFrame).div(SECONDS_PER_YEAR); + return priceRatio.pow(fixed(1e18).div(yearFraction)).sub(1e18).bigint; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/ReadWriteHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/ReadWriteHyperdrive.ts new file mode 100644 index 000000000..9b4506979 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ReadWriteHyperdrive.ts @@ -0,0 +1,468 @@ +import { + ContractReadOptions, + ContractWriteOptions, + Drift, + ReadWriteAdapter, + ReadWriteContract, +} from "@delvtech/drift"; +import { NULL_BYTES } from "src/base/constants"; +import { ReadWriteContractClientOptions } from "src/drift/ContractClient"; +import { HyperdriveAbi } from "src/hyperdrive/abi"; +import { ReadHyperdrive } from "src/hyperdrive/ReadHyperdrive"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { ReadWriteEth } from "src/token/eth/ReadWriteEth"; + +type ReadWriteParams = { + args: Args; + options?: ContractWriteOptions; +}; + +export interface ReadWriteHyperdriveOptions + extends ReadWriteContractClientOptions {} + +export class ReadWriteHyperdrive extends ReadHyperdrive { + declare drift: Drift; + declare contract: ReadWriteContract; + + constructor(options: ReadWriteHyperdriveOptions) { + super(options); + } + + async getBaseToken( + options?: ContractReadOptions, + ): Promise { + const address = await this.contract.read("baseToken", {}, options); + return address === ReadWriteEth.address + ? new ReadWriteEth({ + drift: this.drift, + }) + : new ReadWriteErc20({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + async getSharesToken(): Promise { + const address = await this.contract.read("vaultSharesToken"); + return new ReadWriteErc20({ + address, + drift: this.drift, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + /** + * Allows anyone to mint a new checkpoint. + * @param time - The time (in seconds) of the checkpoint to create. + */ + async checkpoint({ + args: { time }, + options, + }: ReadWriteParams<{ time: number }>): Promise<`0x${string}`> { + return this.contract.write( + "checkpoint", + { _checkpointTime: BigInt(time), _maxIterations: 4n }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Allows an authorized address to pause this contract + * @param paused - True to pause all deposits and false to unpause them + */ + async pause({ + args: { paused }, + options, + }: ReadWriteParams<{ + paused: boolean; + }>): Promise<`0x${string}`> { + return this.contract.write( + "pause", + { _status: paused }, + { + ...options, + onMined: (receipt) => { + this.contract.invalidateReadsMatching("getMarketState"); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Allows the first LP to initialize the market with a target APR. + * @param args.contribution - The amount of base to supply. + * @param args.apr - The target APR. + * @param args.destination - The destination of the LP shares. + * @param args.asBase - If true the user is charged in underlying if false + * the contract transfers in yield source directly. Note + * - for some paths one choice may be disabled or + * blocked. + * @returns The initial number of LP shares created. + */ + async initialize({ + args: { + contribution, + apr, + destination, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + contribution: bigint; + apr: bigint; + destination: `0x${string}`; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "initialize", + { + _apr: apr, + _contribution: contribution, + _options: { + destination: destination, + asBase: asBase, + extraData: extraData, + }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Opens a new long position. + * @param destination - The account opening the position + * @param baseAmount - The amount of base supplied to the position + * @param bondAmountOut - The amount of bonds to send to the destination + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return bondProceeds - The amount of bonds the user received + * + */ + async openLong({ + args: { + destination, + amount, + minBondsOut, + minVaultSharePrice, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + destination: `0x${string}`; + amount: bigint; + minVaultSharePrice: bigint; + minBondsOut: bigint; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "openLong", + { + _amount: amount, + _minOutput: minBondsOut, + _minVaultSharePrice: minVaultSharePrice, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Opens a new short position. + * @param destination - The account opening the position + * @param baseAmount - The amount of base supplied to the position + * @param bondAmountOut - The amount of bonds to send to the destination + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return maturityTime - The maturity time of the short. + * @return traderDeposit - The amount the user deposited for this trade. + */ + async openShort({ + args: { + destination, + bondAmount, + minVaultSharePrice, + maxDeposit, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + destination: `0x${string}`; + minVaultSharePrice: bigint; + bondAmount: bigint; + maxDeposit: bigint; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "openShort", + { + _bondAmount: bondAmount, + _maxDeposit: maxDeposit, + _minVaultSharePrice: minVaultSharePrice, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Closes a long position. + * @param maturityTime - The maturity time of the long + * @param bondAmountIn - The amount of of bonds to remove from the position + * @param minBaseAmountOut - The minimum amount of base to send to the destination + * @param destination - The account receiving the base + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return The amount of underlying asset the user receives. + */ + async closeLong({ + args: { + maturityTime, + bondAmountIn, + minAmountOut, + destination, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + maturityTime: bigint; + bondAmountIn: bigint; + minAmountOut: bigint; + destination: `0x${string}`; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "closeLong", + { + _maturityTime: maturityTime, + _bondAmount: bondAmountIn, + _minOutput: minAmountOut, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Closes a short position. + * @param maturityTime - The maturity time of the short + * @param bondAmountIn - The amount of bonds to remove from the position + * @param minBaseAmountOut - The minimum amount of base to send to the destination + * @param destination - The account receiving the base + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return The amount of base tokens produced by closing this short + */ + async closeShort({ + args: { + maturityTime, + bondAmountIn, + minAmountOut, + destination, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + maturityTime: bigint; + bondAmountIn: bigint; + minAmountOut: bigint; + destination: `0x${string}`; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "closeShort", + { + _maturityTime: maturityTime, + _bondAmount: bondAmountIn, + _minOutput: minAmountOut, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Adds liquidity to the pool. + * @param destination - The account adding liquidity + * @param contribution - The amount of base to supply + * @param minApr - The minimum APR to accept + * @param minLpSharePrice - The minimum LP share price to accept + * @param maxApr - The maximum APR to accept + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return lpShares The number of LP tokens created + */ + async addLiquidity({ + args: { + destination, + contribution, + minApr, + minLpSharePrice, + maxApr, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + destination: `0x${string}`; + contribution: bigint; + minApr: bigint; + minLpSharePrice: bigint; + maxApr: bigint; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "addLiquidity", + { + _contribution: contribution, + _minLpSharePrice: minLpSharePrice, + _minApr: minApr, + _maxApr: maxApr, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Removes liquidity from the pool. + * @param destination - The account removing liquidity + * @param lpSharesIn - The amount of LP shares to remove + * @param minBaseAmountOut - The minimum amount of base to send to the destination + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @returns baseProceeds - The base the LP removing liquidity receives. The LP + receives a proportional amount of the pool's idle capital + * @returns withdrawShares - The base that the LP receives buys out some of their LP shares, but it may not be sufficient to fully buy the LP out. In this case, the LP receives withdrawal shares equal in value to the present value they are owed. As idle capital becomes available, the pool will buy back these shares. + */ + async removeLiquidity({ + args: { + destination, + lpSharesIn, + minOutputPerShare, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + destination: `0x${string}`; + lpSharesIn: bigint; + minOutputPerShare: bigint; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "removeLiquidity", + { + _lpShares: lpSharesIn, + _minOutputPerShare: minOutputPerShare, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } + + /** + * Redeems withdrawal shares. + * @param withdrawalSharesIn - The amount of withdrawal shares to redeem + * @param minBaseAmountOutPerShare - The minimum amount of base to send to the destination per share + * @param destination - The account receiving the base + * @param asUnderlying - A flag indicating whether the sender will pay in base or using another currency. Implementations choose which currencies they accept. + * @param options - Contract Write Options + * @return baseProceeds The amount of base the LP received. + * @return sharesRedeemed The amount of withdrawal shares that were redeemed. + */ + async redeemWithdrawalShares({ + args: { + withdrawalSharesIn, + minOutputPerShare, + destination, + asBase = true, + extraData = NULL_BYTES, + }, + options, + }: ReadWriteParams<{ + withdrawalSharesIn: bigint; + minOutputPerShare: bigint; + destination: `0x${string}`; + asBase?: boolean; + extraData?: `0x${string}`; + }>): Promise<`0x${string}`> { + return this.contract.write( + "redeemWithdrawalShares", + { + _withdrawalShares: withdrawalSharesIn, + _minOutputPerShare: minOutputPerShare, + _options: { destination, asBase, extraData }, + }, + { + ...options, + onMined: (receipt) => { + this.contract.cache.clear(); + options?.onMined?.(receipt); + }, + }, + ); + } +} diff --git a/packages/hyperdrive-js/src/hyperdrive/abi.ts b/packages/hyperdrive-js/src/hyperdrive/abi.ts new file mode 100644 index 000000000..406da38f7 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/abi.ts @@ -0,0 +1,4 @@ +import { IHyperdrive } from "@delvtech/hyperdrive-artifacts/IHyperdrive"; + +export const hyperdriveAbi = IHyperdrive.abi; +export type HyperdriveAbi = typeof IHyperdrive.abi; diff --git a/packages/hyperdrive-js/src/hyperdrive/assetId/decodeAssetFromTransferSingleEventData.ts b/packages/hyperdrive-js/src/hyperdrive/assetId/decodeAssetFromTransferSingleEventData.ts new file mode 100644 index 000000000..fd5bbd464 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/assetId/decodeAssetFromTransferSingleEventData.ts @@ -0,0 +1,26 @@ +import { + AssetType, + parseAssetType, +} from "src/hyperdrive/assetId/parseAssetType"; + +export function decodeAssetFromTransferSingleEventData( + eventData: `0x${string}`, +): { + assetType: AssetType; + /** + * in seconds + */ + timestamp: bigint; +} { + const cleanEventData = eventData.slice(2); + + const identifier = Number(cleanEventData.slice(0, 2)); + const assetType = parseAssetType(identifier); + // 62 hexadecimal digits (248 bits) = timestamp (in seconds) + const timestampPart = cleanEventData.slice(2, 64); + const timestamp = BigInt(parseInt(timestampPart, 16)); + return { + assetType, + timestamp, + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/assetId/parseAssetType.ts b/packages/hyperdrive-js/src/hyperdrive/assetId/parseAssetType.ts new file mode 100644 index 000000000..807cbfe39 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/assetId/parseAssetType.ts @@ -0,0 +1,21 @@ +import { HyperdriveSdkError } from "src/HyperdriveSdkError"; + +export type AssetType = "LP" | "LONG" | "SHORT" | "WITHDRAWAL_SHARE"; +export function parseAssetType(identifier: number): AssetType { + if (identifier === 0) { + return "LP"; + } + if (identifier === 1) { + return "LONG"; + } + if (identifier === 2) { + return "SHORT"; + } + if (identifier === 3) { + return "WITHDRAWAL_SHARE"; + } + + throw new HyperdriveSdkError( + `parseAssetType(${identifier}) did not match a valid asset type.`, + ); +} diff --git a/packages/hyperdrive-js/src/hyperdrive/checkpoint/getCheckpointTime.ts b/packages/hyperdrive-js/src/hyperdrive/checkpoint/getCheckpointTime.ts new file mode 100644 index 000000000..55bc54ada --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/checkpoint/getCheckpointTime.ts @@ -0,0 +1,7 @@ +export function getCheckpointTime( + blockTimestamp: bigint, + checkpointDuration: bigint, +): bigint { + // https://github.com/delvtech/hyperdrive/blob/314cd408ffa4968d5a6daff95167cd9af17607d6/contracts/src/HyperdriveStorage.sol#L193 + return blockTimestamp - (blockTimestamp % checkpointDuration); +} diff --git a/packages/hyperdrive-js/src/hyperdrive/checkpoint/types.ts b/packages/hyperdrive-js/src/hyperdrive/checkpoint/types.ts new file mode 100644 index 000000000..67b0c0450 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/checkpoint/types.ts @@ -0,0 +1,63 @@ +import { + ContractEvent, + ContractReadOptions, + FunctionReturn, + Pretty, +} from "@delvtech/drift"; +import { HyperdriveAbi } from "src/hyperdrive/abi"; + +export type Checkpoint = Pretty< + { + checkpointTime: bigint; + } & FunctionReturn +>; + +export type CheckpointEvent = ContractEvent; + +export type GetCheckpointTimeParams = ( + | { + /** + * A timestamp that falls within the checkpoint. + */ + timestamp?: bigint; + blockNumber?: never; + } + | { + timestamp?: never; + /** + * A block number that falls within the checkpoint. + */ + blockNumber?: bigint; + } +) & { + options?: ContractReadOptions; +}; + +export type GetCheckpointParams = ( + | { + /** + * The time of the checkpoint. + */ + checkpointTime?: bigint; + timestamp?: never; + blockNumber?: never; + } + | { + checkpointTime?: never; + /** + * A timestamp that falls within the checkpoint. + */ + timestamp?: bigint; + blockNumber?: never; + } + | { + checkpointTime?: never; + timestamp?: never; + /** + * A block number that falls within the checkpoint. + */ + blockNumber?: bigint; + } +) & { + options?: ContractReadOptions; +}; diff --git a/packages/hyperdrive-js/src/hyperdrive/constants.ts b/packages/hyperdrive-js/src/hyperdrive/constants.ts new file mode 100644 index 000000000..362892bfb --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/constants.ts @@ -0,0 +1,2 @@ +// The maximum number of iterations to run the Newton's method for. Used in `readHyperdrive.getMaxShort`. +export const MAX_ITERATIONS = 14; diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadErc4626Hyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadErc4626Hyperdrive.ts new file mode 100644 index 000000000..0d571bdcd --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadErc4626Hyperdrive.ts @@ -0,0 +1,47 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +import { ReadErc4626 } from "src/token/erc4626/ReadErc4626"; + +export class ReadErc4626Hyperdrive extends readErc4626HyperdriveMixin( + ReadHyperdrive, +) {} + +/** + * @internal + */ +export interface ReadErc4626HyperdriveMixin { + /** + * Get a client for the tokenized vault for this Hyperdrive instance. + */ + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readErc4626HyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends Base { + constructor(...[options]: any[]) { + const { debugName = "ERC-4626 Hyperdrive", ...restOptions } = + options as ReadHyperdriveOptions; + super({ debugName, ...restOptions }); + } + + async getSharesToken(options?: ContractReadOptions): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + + return new ReadErc4626({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadMockErc4626Hyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadMockErc4626Hyperdrive.ts new file mode 100644 index 000000000..598ed4e22 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadMockErc4626Hyperdrive.ts @@ -0,0 +1,43 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadHyperdriveOptions } from "src/hyperdrive/ReadHyperdrive"; +import { ReadErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadErc4626Hyperdrive"; +import { ReadMockErc4626 } from "src/token/erc4626/ReadMockErc4626"; + +export class ReadMockErc4626Hyperdrive extends readMockErc4626HyperdriveMixin( + ReadErc4626Hyperdrive, +) {} + +/** + * @internal + */ +export interface ReadMockErc4626HyperdriveMixin { + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readMockErc4626HyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends Base { + constructor(...[options]: any[]) { + const { debugName } = options as ReadHyperdriveOptions; + super({ debugName: debugName ?? "Mock ERC-4626 Hyperdrive", ...options }); + } + + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + + return new ReadMockErc4626({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive.ts new file mode 100644 index 000000000..dd3765b9c --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive.ts @@ -0,0 +1,39 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { + ReadErc4626HyperdriveMixin, + readErc4626HyperdriveMixin, +} from "src/hyperdrive/erc4626/ReadErc4626Hyperdrive"; +import { ReadWriteErc4626 } from "src/token/erc4626/ReadWriteErc4626"; + +export class ReadWriteErc4626Hyperdrive extends readWriteErc4626HyperdriveMixin( + ReadWriteHyperdrive, +) {} + +export interface ReadWriteErc4626HyperdriveMixin + extends ReadErc4626HyperdriveMixin { + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readWriteErc4626HyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends readErc4626HyperdriveMixin(Base) { + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + + return new ReadWriteErc4626({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteMockErc4626Hyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteMockErc4626Hyperdrive.ts new file mode 100644 index 000000000..1bbd691d3 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/ReadWriteMockErc4626Hyperdrive.ts @@ -0,0 +1,40 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadWriteErc4626Hyperdrive } from "src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive"; +import { ReadWriteMockErc4626 } from "src/token/erc4626/ReadWriteMockErc4626"; +import { + ReadMockErc4626HyperdriveMixin, + readMockErc4626HyperdriveMixin, +} from "./ReadMockErc4626Hyperdrive"; + +export class ReadWriteMockErc4626Hyperdrive extends readWriteMockErc4626HyperdriveMixin( + ReadWriteErc4626Hyperdrive, +) {} + +export interface ReadWriteMockErc4626HyperdriveMixin + extends ReadMockErc4626HyperdriveMixin { + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readWriteMockErc4626HyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + // return class extends readErc4626HyperdriveMixin(Base) { + return class extends readMockErc4626HyperdriveMixin(Base) { + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + + return new ReadWriteMockErc4626({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadErc4626Hyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadErc4626Hyperdrive_v1_0_14.ts new file mode 100644 index 000000000..53027b227 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadErc4626Hyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readErc4626HyperdriveMixin } from "src/hyperdrive/erc4626/ReadErc4626Hyperdrive"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadErc4626Hyperdrive_v1_0_14 extends readErc4626HyperdriveMixin( + ReadHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadMockErc4626Hyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadMockErc4626Hyperdrive_v1_0_14.ts new file mode 100644 index 000000000..97ef34a21 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadMockErc4626Hyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readMockErc4626HyperdriveMixin } from "src/hyperdrive/erc4626/ReadMockErc4626Hyperdrive"; +import { ReadErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadErc4626Hyperdrive_v1_0_14"; + +export class ReadMockErc4626Hyperdrive_v1_0_14 extends readMockErc4626HyperdriveMixin( + ReadErc4626Hyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteErc4626Hyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteErc4626Hyperdrive_v1_0_14.ts new file mode 100644 index 000000000..d15d937ba --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteErc4626Hyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteErc4626HyperdriveMixin } from "src/hyperdrive/erc4626/ReadWriteErc4626Hyperdrive"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export class ReadWriteErc4626Hyperdrive_v1_0_14 extends readWriteErc4626HyperdriveMixin( + ReadWriteHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteMockErc4626Hyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteMockErc4626Hyperdrive_v1_0_14.ts new file mode 100644 index 000000000..6dd576969 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/erc4626/v1.0.14/ReadWriteMockErc4626Hyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteMockErc4626HyperdriveMixin } from "src/hyperdrive/erc4626/ReadWriteMockErc4626Hyperdrive"; +import { ReadWriteErc4626Hyperdrive_v1_0_14 } from "src/hyperdrive/erc4626/v1.0.14/ReadWriteErc4626Hyperdrive_v1_0_14"; + +export class ReadWriteMockErc4626Hyperdrive_v1_0_14 extends readWriteMockErc4626HyperdriveMixin( + ReadWriteErc4626Hyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadEzEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadEzEthHyperdrive.ts new file mode 100644 index 000000000..3371f388e --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadEzEthHyperdrive.ts @@ -0,0 +1,74 @@ +import { Contract, ContractReadOptions, ReadContract } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadHyperdrive } from "src/hyperdrive/ReadHyperdrive"; +import { + EzEthHyperdriveAbi, + ezEthHyperdriveAbi, +} from "src/hyperdrive/ezeth/abi"; +import { ReadErc20 } from "src/token/erc20/ReadErc20"; +import { ReadEth } from "src/token/eth/ReadEth"; + +export class ReadEzEthHyperdrive extends readEzEthHyperdriveMixin( + ReadHyperdrive, +) {} + +/** + * @internal + */ +export interface ReadEzEthHyperdriveMixin { + ezEthHyperdriveContract: Contract; + + /** + * Get a client for ETH, the base token for this Hyperdrive instance. + */ + getBaseToken(options?: ContractReadOptions): Promise; + + /** + * Get a client for the EzETH token for this Hyperdrive instance. + */ + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readEzEthHyperdriveMixin>( + Base: T, +): Constructor & T { + return class extends Base { + ezEthHyperdriveContract: ReadContract; + + constructor(...[options]: any[]) { + const { + debugName = "EzETH Hyperdrive", + address, + cache, + cacheNamespace, + ...rest + } = options as ConstructorParameters[0]; + super({ debugName, address, cache, cacheNamespace, ...rest }); + this.ezEthHyperdriveContract = this.drift.contract({ + abi: ezEthHyperdriveAbi, + address, + cache, + cacheNamespace, + }); + } + + async getBaseToken(): Promise { + return new ReadEth({ + drift: this.drift, + }); + } + + async getSharesToken(): Promise { + const { vaultSharesToken } = await this.getPoolConfig(); + return new ReadErc20({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadWriteEzEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadWriteEzEthHyperdrive.ts new file mode 100644 index 000000000..9b46bdc3d --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ezeth/ReadWriteEzEthHyperdrive.ts @@ -0,0 +1,50 @@ +import { ContractReadOptions, ReadWriteContract } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { + ReadEzEthHyperdriveMixin, + readEzEthHyperdriveMixin, +} from "src/hyperdrive/ezeth/ReadEzEthHyperdrive"; +import { EzEthHyperdriveAbi } from "src/hyperdrive/ezeth/abi"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { ReadWriteEth } from "src/token/eth/ReadWriteEth"; + +export class ReadWriteEzEthHyperdrive extends readWriteEzEthHyperdriveMixin( + ReadWriteHyperdrive, +) {} + +export interface ReadWriteEzEthHyperdriveMixin + extends ReadEzEthHyperdriveMixin { + ezEthHyperdriveContract: ReadWriteContract; + getBaseToken(options?: ContractReadOptions): Promise; + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readWriteEzEthHyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends readEzEthHyperdriveMixin(Base) { + declare ezEthHyperdriveContract: ReadWriteContract; + + async getBaseToken(): Promise { + return new ReadWriteEth({ + drift: this.drift, + }); + } + + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + return new ReadWriteErc20({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/ezeth/abi.ts b/packages/hyperdrive-js/src/hyperdrive/ezeth/abi.ts new file mode 100644 index 000000000..a0ccb4885 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ezeth/abi.ts @@ -0,0 +1,4 @@ +import { IEzETHHyperdrive } from "@delvtech/hyperdrive-artifacts/IEzETHHyperdrive"; + +export const ezEthHyperdriveAbi = IEzETHHyperdrive.abi; +export type EzEthHyperdriveAbi = typeof ezEthHyperdriveAbi; diff --git a/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadEzEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadEzEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..d4988ed07 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadEzEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readEzEthHyperdriveMixin } from "src/hyperdrive/ezeth/ReadEzEthHyperdrive"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadEzEthHyperdrive_v1_0_14 extends readEzEthHyperdriveMixin( + ReadHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadWriteEzEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadWriteEzEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..5df3ff2bd --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/ezeth/v1.0.14/ReadWriteEzEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteEzEthHyperdriveMixin } from "src/hyperdrive/ezeth/ReadWriteEzEthHyperdrive"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export class ReadWriteEzEthHyperdrive_v1_0_14 extends readWriteEzEthHyperdriveMixin( + ReadWriteHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/getHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/getHyperdrive.ts new file mode 100644 index 000000000..062016ff7 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/getHyperdrive.ts @@ -0,0 +1,101 @@ +import { Drift, ReadWriteAdapter } from "@delvtech/drift"; +import semver from "semver"; +import { hyperdriveAbi } from "src/hyperdrive/abi"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +import { + ReadWriteHyperdrive, + ReadWriteHyperdriveOptions, +} from "src/hyperdrive/ReadWriteHyperdrive"; +import { ReadStEthHyperdrive } from "src/hyperdrive/steth/ReadStEthHyperdrive"; +import { ReadWriteStEthHyperdrive } from "src/hyperdrive/steth/ReadWriteStEthHyperdrive"; +import { ReadStEthHyperdrive_v1_0_14 } from "src/hyperdrive/steth/v1.0.14/ReadStEthHyperdrive_v1_0_14"; +import { ReadWriteStEthHyperdrive_v1_0_14 } from "src/hyperdrive/steth/v1.0.14/ReadWriteStEthHyperdrive_v1_0_14"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export interface HyperdriveOptions + extends ReadHyperdriveOptions { + drift: T; +} + +export type Hyperdrive = + T extends Drift ? ReadWriteHyperdrive : ReadHyperdrive; + +export async function getHyperdrive({ + address, + drift, + cache = drift.cache, + cacheNamespace, + earliestBlock, + debugName, +}: HyperdriveOptions): Promise> { + cacheNamespace ??= await drift.getChainId(); + + const options: HyperdriveOptions = { + address, + drift, + cache, + cacheNamespace, + earliestBlock, + debugName, + }; + const isReadWrite = isReadWriteOptions(options); + + const version = await drift.read({ + abi: hyperdriveAbi, + address, + fn: "version", + cacheNamespace, + }); + const isV1_0_14 = semver.lte(version, "1.0.14"); + + const kind = isV1_0_14 + ? undefined + : await drift.read({ + abi: hyperdriveAbi, + address, + fn: "kind", + cacheNamespace, + }); + + switch (kind) { + case "StETHHyperdrive": + if (isReadWrite && isV1_0_14) { + return new ReadWriteStEthHyperdrive_v1_0_14(options); + } + + if (isReadWrite) { + return new ReadWriteStEthHyperdrive(options); + } + + if (isV1_0_14) { + return new ReadStEthHyperdrive_v1_0_14(options) as any; + } + + return new ReadStEthHyperdrive(options) as any; + + default: + if (isReadWrite && isV1_0_14) { + return new ReadWriteHyperdrive_v1_0_14(options); + } + + if (isReadWrite) { + return new ReadWriteHyperdrive(options); + } + + if (isV1_0_14) { + return new ReadHyperdrive_v1_0_14(options) as any; + } + + return new ReadHyperdrive(options) as any; + } +} + +function isReadWriteOptions( + options: HyperdriveOptions, +): options is ReadWriteHyperdriveOptions { + return typeof options.drift.write === "function"; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.test.ts b/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.test.ts new file mode 100644 index 000000000..59e8c0693 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from "vitest"; + +import { calculateMatureLongYieldAfterFees } from "src/hyperdrive/longs/calculateMatureLongYieldAfterFees"; + +test("calculateMatureLongYieldAfterFees should return the yield a mature long has earned after fees", async () => { + const value = calculateMatureLongYieldAfterFees({ + flatFee: 500000000000000n, + bondAmount: 1000862573239041776123n, + baseAmountPaid: 1000000000000000000000n, + decimals: 18, + }); + + expect(value).toEqual(362141952422255235n); +}); + +test("calculateMatureLongYieldAfterFees should work with mixed decimals", async () => { + const value = calculateMatureLongYieldAfterFees({ + flatFee: 500000000000000n, + bondAmount: 5000862n, + baseAmountPaid: 4000000n, + decimals: 6, + }); + + expect(value).toEqual(998362n); +}); diff --git a/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.ts b/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.ts new file mode 100644 index 000000000..cd44194ba --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/longs/calculateMatureLongYieldAfterFees.ts @@ -0,0 +1,16 @@ +import { fixed } from "src/fixed-point"; +export function calculateMatureLongYieldAfterFees({ + flatFee, + bondAmount, + baseAmountPaid, + decimals, +}: { + flatFee: bigint; + bondAmount: bigint; + baseAmountPaid: bigint; + decimals: number; +}): bigint { + // Flat fee is always 18 decimals + const poolFee = fixed(bondAmount, decimals).mul(flatFee).bigint; + return bondAmount - baseAmountPaid - poolFee; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/longs/types.ts b/packages/hyperdrive-js/src/hyperdrive/longs/types.ts new file mode 100644 index 000000000..242ba269b --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/longs/types.ts @@ -0,0 +1,29 @@ +export interface Long { + assetId: bigint; + bondAmount: bigint; + /** + * Time in seconds when this long will mature + */ + maturity: bigint; + + baseAmountPaid: bigint; +} + +export interface ClosedLong extends Long { + // A closed long will include the baseAmount that was redeemed + baseAmount: bigint; + closedTimestamp: bigint; +} + +// TODO: This is a temporary type until all the long positions are migrated to the new format with details used for the long which includes all bond details if those are able to be calculated. +export interface OpenLongPositionReceived { + assetId: bigint; + value: bigint; + maturity: bigint; + details: Long | undefined; +} +// TODO: This is a temporary type for describing the OpenLongPositionReceived without the details field. This will be a position a user has received from another account and we will not be able to calculate the bond details for it. +export type OpenLongPositionReceivedWithoutDetails = Omit< + OpenLongPositionReceived, + "details" +>; diff --git a/packages/hyperdrive-js/src/hyperdrive/lp/ClosedLpShares.ts b/packages/hyperdrive-js/src/hyperdrive/lp/ClosedLpShares.ts new file mode 100644 index 000000000..44ce183dd --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lp/ClosedLpShares.ts @@ -0,0 +1,7 @@ +export interface ClosedLpShares { + lpAmount: bigint; + baseAmount: bigint; + withdrawalShareAmount: bigint; + lpSharePrice: bigint; + closedTimestamp: bigint; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/lp/assetId.ts b/packages/hyperdrive-js/src/hyperdrive/lp/assetId.ts new file mode 100644 index 000000000..ca10f51c6 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lp/assetId.ts @@ -0,0 +1 @@ +export const LP_ASSET_ID = 0n; diff --git a/packages/hyperdrive-js/src/hyperdrive/lseth/ReadLsEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/lseth/ReadLsEthHyperdrive.ts new file mode 100644 index 000000000..d3cc5f1c4 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lseth/ReadLsEthHyperdrive.ts @@ -0,0 +1,59 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +import { ReadEth } from "src/token/eth/ReadEth"; +import { ReadLsEth } from "src/token/lseth/ReadLsEth"; + +export class ReadLsEthHyperdrive extends readLsEthHyperdriveMixin( + ReadHyperdrive, +) {} + +/** + * @internal + */ +export interface ReadLsEthHyperdriveMixin { + /** + * Get a client for ETH, the base token for this Hyperdrive instance. + */ + getBaseToken(options?: ContractReadOptions): Promise; + + /** + * Get a client for the LsETH token for this Hyperdrive instance. + */ + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readLsEthHyperdriveMixin>( + Base: T, +): Constructor & T { + return class extends Base { + constructor(...[options]: any[]) { + const { debugName = "lsETH Hyperdrive", ...restOptions } = + options as ReadHyperdriveOptions; + super({ debugName, ...restOptions }); + } + + async getBaseToken(): Promise { + return new ReadEth({ + drift: this.drift, + }); + } + + async getSharesToken(options?: ContractReadOptions): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + + return new ReadLsEth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/lseth/ReadWriteLsEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/lseth/ReadWriteLsEthHyperdrive.ts new file mode 100644 index 000000000..c612beac8 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lseth/ReadWriteLsEthHyperdrive.ts @@ -0,0 +1,43 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { + ReadLsEthHyperdriveMixin, + readLsEthHyperdriveMixin, +} from "src/hyperdrive/lseth/ReadLsEthHyperdrive"; +import { ReadWriteEth } from "src/token/eth/ReadWriteEth"; +import { ReadWriteLsEth } from "src/token/lseth/ReadWriteLsEth"; + +export class ReadWriteLsEthHyperdrive extends readWriteLsEthHyperdriveMixin( + ReadWriteHyperdrive, +) {} + +export interface ReadWriteLsEthHyperdriveMixin + extends ReadLsEthHyperdriveMixin { + getBaseToken(options?: ContractReadOptions): Promise; + getSharesToken(options?: ContractReadOptions): Promise; +} + +export function readWriteLsEthHyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends readLsEthHyperdriveMixin(Base) { + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + return new ReadWriteLsEth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + async getBaseToken(): Promise { + return new ReadWriteEth({ + drift: this.drift, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadLsEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadLsEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..dda8c19ff --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadLsEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readLsEthHyperdriveMixin } from "src/hyperdrive/lseth/ReadLsEthHyperdrive"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadLsEthHyperdrive_v1_0_14 extends readLsEthHyperdriveMixin( + ReadHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadWriteLsEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadWriteLsEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..9c1af2886 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/lseth/v1.0.14/ReadWriteLsEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteLsEthHyperdriveMixin } from "src/hyperdrive/lseth/ReadWriteLsEthHyperdrive"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export class ReadWriteLsEthHyperdrive_v1_0_14 extends readWriteLsEthHyperdriveMixin( + ReadWriteHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/reth/ReadREthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/reth/ReadREthHyperdrive.ts new file mode 100644 index 000000000..379f9744f --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/reth/ReadREthHyperdrive.ts @@ -0,0 +1,97 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { fixed } from "@delvtech/fixed-point-wasm"; +import { Constructor } from "src/base/types"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +import { ReadEth } from "src/token/eth/ReadEth"; +import { ReadREth } from "src/token/reth/ReadREth"; + +export class ReadREthHyperdrive extends readREthHyperdriveMixin( + ReadHyperdrive, +) {} + +/** + * @internal + */ +export interface ReadREthHyperdriveMixin { + /** + * Get a client for ETH, the base token for this Hyperdrive instance. + */ + getBaseToken(options?: ContractReadOptions): Promise; + + /** + * Get a client for the rETH token for this Hyperdrive instance. + */ + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readREthHyperdriveMixin>( + Base: T, +): Constructor & T { + return class extends Base { + constructor(...[options]: any[]) { + const { debugName = "rETH Hyperdrive", ...restOptions } = + options as ReadHyperdriveOptions; + super({ debugName, ...restOptions }); + } + + async getBaseToken(): Promise { + return new ReadEth({ + drift: this.drift, + }); + } + + async getSharesToken(options?: ContractReadOptions): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + return new ReadREth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + // Calculations + + async getMaxShort({ + budget, + options, + }: { + budget: bigint; + options: Parameters[0]["options"]; + }): ReturnType { + const result = await super.getMaxShort({ budget, options }); + const decimals = await this.getDecimals(); + return { + ...result, + // FIXME: MockRocketPool updates its price based on the current + // timestamp, so the accuracy of max calculations will slowly drift + // every second. This pads the max shares to avoid errors trying to open + // the max, but may not be needed for mainnet. + maxSharesIn: fixed(result.maxSharesIn, decimals).mul(1e18 - 1e12, 18) + .bigint, + }; + } + + async getMaxLong( + options?: Parameters[0], + ): ReturnType { + const result = await super.getMaxLong(options); + const decimals = await this.getDecimals(); + return { + ...result, + // FIXME: MockRocketPool updates its price based on the current + // timestamp, so the accuracy of max calculations will slowly drift + // every second. This pads the max shares to avoid errors trying to open + // the max, but may not be needed for mainnet. + maxSharesIn: fixed(result.maxSharesIn, decimals).mul(1e18 - 1e12, 18) + .bigint, + }; + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/reth/ReadWriteREthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/reth/ReadWriteREthHyperdrive.ts new file mode 100644 index 000000000..6c3f708d3 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/reth/ReadWriteREthHyperdrive.ts @@ -0,0 +1,42 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { readREthHyperdriveMixin } from "src/hyperdrive/reth/ReadREthHyperdrive"; +import { ReadWriteEth } from "src/token/eth/ReadWriteEth"; +import { ReadWriteREth } from "src/token/reth/ReadWriteREth"; + +export class ReadWriteREthHyperdrive extends readWriteREthHyperdriveMixin( + ReadWriteHyperdrive, +) {} + +export interface ReadWriteREthHyperdriveMixin { + getBaseToken(options?: ContractReadOptions): Promise; + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readWriteREthHyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends readREthHyperdriveMixin(Base) { + async getBaseToken(): Promise { + return new ReadWriteEth({ + drift: this.drift, + }); + } + + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + return new ReadWriteREth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadREthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadREthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..210b61cd8 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadREthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readREthHyperdriveMixin } from "src/hyperdrive/reth/ReadREthHyperdrive"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadREthHyperdrive_v1_0_14 extends readREthHyperdriveMixin( + ReadHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadWriteREthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadWriteREthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..9e0fe9a60 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/reth/v1.0.14/ReadWriteREthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteREthHyperdriveMixin } from "src/hyperdrive/reth/ReadWriteREthHyperdrive"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export class ReadWriteREthHyperdrive_v1_0_14 extends readWriteREthHyperdriveMixin( + ReadWriteHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.test.ts b/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.test.ts new file mode 100644 index 000000000..52cfb34f7 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.test.ts @@ -0,0 +1,15 @@ +import { calculateShortAccruedYield } from "src/hyperdrive/shorts/calculateShortAccruedYield"; +import { expect, test } from "vitest"; + +test("calculateShortAccruedYield should return the yield a short has accrued since it was opened", async () => { + const value = calculateShortAccruedYield({ + bondAmount: 100_000n, + openVaultSharePrice: 1_008n, + endingVaultSharePrice: 1_010n, + decimals: 3, + }); + // If you opened a short position on 100 bonds at a previous checkpoint price + // of 1.008 and the current checkpoint price is 1.01, your accrued profit would + // be 0.20. + expect(value).toEqual(200n); +}); diff --git a/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.ts b/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.ts new file mode 100644 index 000000000..ebb3d5d3f --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/shorts/calculateShortAccruedYield.ts @@ -0,0 +1,23 @@ +import { fixed } from "src/fixed-point"; + +export function calculateShortAccruedYield({ + openVaultSharePrice, + endingVaultSharePrice, + bondAmount, + decimals, +}: { + openVaultSharePrice: bigint; + endingVaultSharePrice: bigint; + bondAmount: bigint; + decimals: number; +}): bigint { + // Current Accrued yield = (current share price - checkpoint share price) x + // number of bonds + const result = + // vaultSharePrice is always 18 decimals + fixed(endingVaultSharePrice - openVaultSharePrice).mul( + bondAmount, + decimals, + ).bigint; + return result; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/shorts/types.ts b/packages/hyperdrive-js/src/hyperdrive/shorts/types.ts new file mode 100644 index 000000000..2e13f8a5b --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/shorts/types.ts @@ -0,0 +1,23 @@ +export interface Short { + hyperdriveAddress: `0x${string}`; + assetId: bigint; + bondAmount: bigint; + checkpointTime: bigint; + /** + * Time in seconds when this short will mature + */ + maturity: bigint; +} + +export interface ClosedShort extends Short { + baseAmountReceived: bigint; + closedTimestamp: bigint; +} + +export interface OpenShort extends Short { + baseAmountPaid: bigint; + baseProceeds: bigint; + fixedRatePaid: bigint; + + openedTimestamp: bigint; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/steth/ReadStEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/steth/ReadStEthHyperdrive.ts new file mode 100644 index 000000000..b6fffb32e --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/steth/ReadStEthHyperdrive.ts @@ -0,0 +1,103 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { fixed } from "src/fixed-point"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; +import { ReadEth } from "src/token/eth/ReadEth"; +import { ReadStEth } from "src/token/steth/ReadStEth"; + +export interface ReadStEthHyperdriveOptions extends ReadHyperdriveOptions {} + +export class ReadStEthHyperdrive extends readStEthHyperdriveMixin( + ReadHyperdrive, +) { + constructor(options: ReadStEthHyperdriveOptions) { + super(options); + } +} + +/** + * @internal + */ +export interface ReadStEthHyperdriveMixin { + /** + * Get a client for ETH, the base token for this Hyperdrive instance. + */ + getBaseToken(options?: ContractReadOptions): Promise; + + /** + * Get a client for the Lido stETH token for this Hyperdrive instance. + */ + getSharesToken(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readStEthHyperdriveMixin>( + Base: T, +): Constructor & T { + return class extends Base { + constructor(...[options]: any[]) { + const { debugName = "stETH Hyperdrive", ...restOptions } = + options as ReadStEthHyperdriveOptions; + super({ debugName, ...restOptions }); + } + + async getBaseToken(): Promise { + return new ReadEth({ + drift: this.drift, + }); + } + + async getSharesToken(): Promise { + const { vaultSharesToken } = await this.getPoolConfig(); + return new ReadStEth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + + // Calculations + + async getMaxShort({ + budget, + options, + }: { + budget: bigint; + options?: Parameters[0]["options"]; + }): ReturnType { + const result = await super.getMaxShort({ budget, options }); + const decimals = await this.getDecimals(); + return { + ...result, + // FIXME: MockLido updates its price based on the current timestamp, so + // the accuracy of max calculations will slowly drift every second. + // This pads the max shares to avoid errors trying to open the max, + // but may not be needed for mainnet. + maxSharesIn: fixed(result.maxSharesIn, decimals).mul(1e18 - 1e12, 18) + .bigint, + }; + } + + async getMaxLong( + options?: Parameters[0], + ): ReturnType { + const result = await super.getMaxLong(options); + const decimals = await this.getDecimals(); + return { + ...result, + // FIXME: MockLido updates its price based on the current timestamp, so + // the accuracy of max calculations will slowly drift every second. + // This pads the max shares to avoid errors trying to open the max, + // but may not be needed for mainnet. + maxSharesIn: fixed(result.maxSharesIn, decimals).mul(1e18 - 1e12, 18) + .bigint, + }; + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/steth/ReadWriteStEthHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/steth/ReadWriteStEthHyperdrive.ts new file mode 100644 index 000000000..2e9713b95 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/steth/ReadWriteStEthHyperdrive.ts @@ -0,0 +1,53 @@ +import { ContractReadOptions, ReplaceProps } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { + ReadWriteHyperdrive, + ReadWriteHyperdriveOptions, +} from "src/hyperdrive/ReadWriteHyperdrive"; +import { + ReadStEthHyperdriveMixin, + readStEthHyperdriveMixin, + ReadStEthHyperdriveOptions, +} from "src/hyperdrive/steth/ReadStEthHyperdrive"; +import { ReadWriteEth } from "src/token/eth/ReadWriteEth"; +import { ReadWriteStEth } from "src/token/steth/ReadWriteStEth"; + +export class ReadWriteStEthHyperdrive extends readWriteStEthHyperdriveMixin( + ReadWriteHyperdrive, +) {} + +export interface ReadWriteStEthHyperdriveMixin + extends ReadStEthHyperdriveMixin { + getBaseToken(options?: ContractReadOptions): Promise; + getSharesToken(options?: ContractReadOptions): Promise; +} + +export interface ReadWriteStEthHyperdriveOptions + extends ReplaceProps< + ReadWriteHyperdriveOptions, + ReadStEthHyperdriveOptions + > {} + +export function readWriteStEthHyperdriveMixin< + T extends Constructor, +>(Base: T): Constructor & T { + return class extends readStEthHyperdriveMixin(Base) { + async getBaseToken(): Promise { + return new ReadWriteEth({ + drift: this.drift, + }); + } + + async getSharesToken( + options?: ContractReadOptions, + ): Promise { + const { vaultSharesToken } = await this.getPoolConfig(options); + return new ReadWriteStEth({ + address: vaultSharesToken, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }); + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadStEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadStEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..5e0fcec8c --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadStEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readStEthHyperdriveMixin } from "src/hyperdrive/steth/ReadStEthHyperdrive"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadStEthHyperdrive_v1_0_14 extends readStEthHyperdriveMixin( + ReadHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadWriteStEthHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadWriteStEthHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..fceb48f75 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/steth/v1.0.14/ReadWriteStEthHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { readWriteStEthHyperdriveMixin } from "src/hyperdrive/steth/ReadWriteStEthHyperdrive"; +import { ReadWriteHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14"; + +export class ReadWriteStEthHyperdrive_v1_0_14 extends readWriteStEthHyperdriveMixin( + ReadWriteHyperdrive_v1_0_14, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/testing/PoolConfig.ts b/packages/hyperdrive-js/src/hyperdrive/testing/PoolConfig.ts new file mode 100644 index 000000000..8a4b9ddb9 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/testing/PoolConfig.ts @@ -0,0 +1,33 @@ +import { ZERO_ADDRESS } from "src/base/constants"; +import { PoolConfig } from "src/hyperdrive/types"; + +const SEVEN_DAYS = 604_800n; +export const simplePoolConfig7Days: PoolConfig = { + baseToken: ZERO_ADDRESS, + vaultSharesToken: ZERO_ADDRESS, + governance: ZERO_ADDRESS, + feeCollector: ZERO_ADDRESS, + sweepCollector: ZERO_ADDRESS, + fees: { + curve: 100000000000000000n, + flat: 500000000000000n, + governanceLP: 10000000000000000n, + governanceZombie: 100000000000000000n, + }, + initialVaultSharePrice: 1000000000000000000n, + minimumShareReserves: 10000000000000000000n, + minimumTransactionAmount: 1000000000000000n, + timeStretch: 44463125629060298n, + positionDuration: SEVEN_DAYS, + checkpointDuration: 3600n, + linkerCodeHash: "0x".padEnd(66, "0") as `0x${string}`, + linkerFactory: ZERO_ADDRESS, + circuitBreakerDelta: 0n, + checkpointRewarder: ZERO_ADDRESS, +}; + +const THIRTY_DAYS = 2_592_000n; +export const simplePoolConfig30Days: PoolConfig = { + ...simplePoolConfig7Days, + positionDuration: THIRTY_DAYS, +}; diff --git a/packages/hyperdrive-js/src/hyperdrive/testing/PoolInfo.ts b/packages/hyperdrive-js/src/hyperdrive/testing/PoolInfo.ts new file mode 100644 index 000000000..e5c0eab58 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/testing/PoolInfo.ts @@ -0,0 +1,19 @@ +import { PoolInfo } from "src/hyperdrive/types"; + +export const simplePoolInfo: PoolInfo = { + shareReserves: 10000000000000000000000000n, + bondReserves: 10217899519533796120000000n, + vaultSharePrice: 1000000000000000000n, + longsOutstanding: 0n, + shortsOutstanding: 0n, + longExposure: 0n, + shareAdjustment: 0n, + longAverageMaturityTime: 0n, + shortAverageMaturityTime: 0n, + lpTotalSupply: 9999990000000000000000000n, + lpSharePrice: 1000000000000000000n, + withdrawalSharesProceeds: 0n, + withdrawalSharesReadyToWithdraw: 0n, + zombieBaseProceeds: 0n, + zombieShareReserves: 0n, +}; diff --git a/packages/hyperdrive-js/src/hyperdrive/testing/setupReadHyperdrive.ts b/packages/hyperdrive-js/src/hyperdrive/testing/setupReadHyperdrive.ts new file mode 100644 index 000000000..dc618f56f --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/testing/setupReadHyperdrive.ts @@ -0,0 +1,31 @@ +import { ZERO_ADDRESS } from "@delvtech/drift"; +import { MockDrift } from "@delvtech/drift/testing"; +import { ReadHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +// No need to explicitly set return types as they are already set in the Stubs +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function setupReadHyperdrive() { + const drift = new MockDrift(); + // TODO: We use the v1.0.14 version of ReadHyperdrive to avoid the need to + // stub every `convertToShares` and `convertToBase` call since drift only + // supports stubbing calls with static values. It should be refactored to + // support stubbing a call with a function to dynamically calculate the return + // value based on arguments and options. + const readHyperdrive = new ReadHyperdrive_v1_0_14({ + address: ZERO_ADDRESS, + drift, + }); + + // The ReadHyperdrive class doesn't infer that the contract is a MockContract. + const contract = drift.contract({ + abi: readHyperdrive.contract.abi, + address: readHyperdrive.contract.address, + }); + + contract.onRead("decimals").resolves(18); + return { + drift, + contract, + readHyperdrive, + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/types.ts b/packages/hyperdrive-js/src/hyperdrive/types.ts new file mode 100644 index 000000000..a32a85a16 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/types.ts @@ -0,0 +1,6 @@ +import { FunctionReturn } from "@delvtech/drift"; +import { HyperdriveAbi } from "src/hyperdrive/abi"; + +export type PoolConfig = FunctionReturn; +export type PoolInfo = FunctionReturn; +export type MarketState = FunctionReturn; diff --git a/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..167b4df45 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14.ts @@ -0,0 +1,60 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { fixed } from "src/fixed-point"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, +} from "src/hyperdrive/ReadHyperdrive"; + +/** + * A Hyperdrive instance that is compatible with Hyperdrive <= v1.0.14. + */ +export class ReadHyperdrive_v1_0_14 extends readHyperdriveMixin_v1_0_14( + ReadHyperdrive, +) {} + +/** + * Overrides for compatibility with Hyperdrive <= v1.0.14. + */ +export function readHyperdriveMixin_v1_0_14< + T extends Constructor, +>(Base: T): T { + return class extends Base { + /** + * @hidden + */ + constructor(...[options]: any[]) { + const { debugName = "Hyperdrive v1.0.14", ...restOptions } = + options as ReadHyperdriveOptions; + super({ + debugName, + ...restOptions, + }); + } + + async convertToBase({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise { + const { vaultSharePrice } = await this.getPoolInfo(options); + const decimals = await this.getDecimals(); + return fixed(sharesAmount, decimals).mul(vaultSharePrice, decimals) + .bigint; + } + + async convertToShares({ + baseAmount, + options, + }: { + baseAmount: bigint; + options?: ContractReadOptions; + }): Promise { + const { vaultSharePrice } = await this.getPoolInfo(options); + const decimals = await this.getDecimals(); + return fixed(baseAmount, decimals).div(vaultSharePrice).bigint; + } + }; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14.ts b/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14.ts new file mode 100644 index 000000000..904c72b86 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/v1.0.14/ReadWriteHyperdrive_v1_0_14.ts @@ -0,0 +1,6 @@ +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { readHyperdriveMixin_v1_0_14 } from "src/hyperdrive/v1.0.14/ReadHyperdrive_v1_0_14"; + +export class ReadWriteHyperdrive_v1_0_14 extends readHyperdriveMixin_v1_0_14( + ReadWriteHyperdrive, +) {} diff --git a/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/RedeemedWithdrawalShares.ts b/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/RedeemedWithdrawalShares.ts new file mode 100644 index 000000000..b62e8a608 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/RedeemedWithdrawalShares.ts @@ -0,0 +1,6 @@ +export interface RedeemedWithdrawalShares { + hyperdriveAddress: `0x${string}`; + withdrawalShareAmount: bigint; + baseAmount: bigint; + redeemedTimestamp: bigint; +} diff --git a/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/assetId.ts b/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/assetId.ts new file mode 100644 index 000000000..807a2cbf1 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperdrive/withdrawalShares/assetId.ts @@ -0,0 +1,2 @@ +export const WITHDRAW_SHARES_ASSET_ID = + 0x0300000000000000000000000000000000000000000000000000000000000000n; diff --git a/packages/hyperdrive-js/src/hyperwasm.ts b/packages/hyperdrive-js/src/hyperwasm.ts new file mode 100644 index 000000000..b36e39c74 --- /dev/null +++ b/packages/hyperdrive-js/src/hyperwasm.ts @@ -0,0 +1,5 @@ +import * as hyperwasm from "@delvtech/hyperdrive-wasm"; + +hyperwasm.initSync(hyperwasm.wasmBuffer); + +export { hyperwasm }; diff --git a/packages/hyperdrive-js/src/registry/ReadRegistry.ts b/packages/hyperdrive-js/src/registry/ReadRegistry.ts new file mode 100644 index 000000000..1ea364381 --- /dev/null +++ b/packages/hyperdrive-js/src/registry/ReadRegistry.ts @@ -0,0 +1,188 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Address } from "abitype"; +import { ReadContractClientOptions } from "src/drift/ContractClient"; +import { ReadClient } from "src/drift/ReadClient"; +import { ReadFactory } from "src/factory/ReadFactory"; +import { ReadHyperdrive } from "src/hyperdrive/ReadHyperdrive"; +import { RegistryAbi, registryAbi } from "src/registry/abi"; +import { + FactoryInfoWithMetadata, + ReadInstanceInfoWithMetadata, +} from "src/registry/types"; + +export interface ReadRegistryOptions extends ReadContractClientOptions {} + +export class ReadRegistry extends ReadClient { + address: Address; + contract: Contract; + + constructor({ + debugName = "Hyperdrive Registry", + address, + cache, + cacheNamespace, + ...rest + }: ReadRegistryOptions) { + super({ debugName, ...rest }); + this.address = address; + this.contract = this.drift.contract({ + abi: registryAbi, + address, + cache, + cacheNamespace, + }); + } + + /** + * Get a {@linkcode ReadFactory} instance for each registered factory. + */ + async getFactories(options?: ContractReadOptions): Promise { + const factoryAddresses = await this.getFactoryAddresses(options); + return factoryAddresses.map( + (address) => + new ReadFactory({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } + + /** + * Get the address of all registered factories. + */ + async getFactoryAddresses(options?: ContractReadOptions): Promise { + const count = await this.contract.read("getNumberOfFactories", {}, options); + + if (count === 0n) { + return []; + } + + const readOnlyAddresses = await this.contract.read( + "getFactoriesInRange", + { + _startIndex: 0n, + _endIndex: count, + }, + options, + ); + return readOnlyAddresses.slice(); + } + + /** + * Gets the Hyperdrive factory info with associated metadata for a factory. + */ + async getFactoryInfo( + factoryAddress: Address, + options?: ContractReadOptions, + ): Promise { + return this.contract.read( + "getFactoryInfoWithMetadata", + { _factory: factoryAddress }, + options, + ); + } + + /** + * Gets the Hyperdrive factory info with associated metadata for a list of + * factories + */ + async getFactoryInfos( + factoryAddresses: Address[], + options?: ContractReadOptions, + ): Promise { + const readonlyInfos = await this.contract.read( + "getFactoryInfosWithMetadata", + { __factories: factoryAddresses }, + options, + ); + return readonlyInfos.slice(); + } + + /** + * Get a {@linkcode ReadHyperdrive} instance for each Hyperdrive instance + * registered in the registry. + */ + async getInstances(options?: ContractReadOptions): Promise { + const hyperdriveAddresses = await this.getInstanceAddresses(options); + return hyperdriveAddresses.map( + (address) => + new ReadHyperdrive({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } + + /** + * Get the address of all Hyperdrive instances registered in the registry. + */ + async getInstanceAddresses( + options?: ContractReadOptions, + ): Promise { + const count = await this.contract.read("getNumberOfInstances", {}, options); + + if (count === 0n) { + return []; + } + + const readOnlyAddresses = await this.contract.read( + "getInstancesInRange", + { + _startIndex: 0n, + _endIndex: count, + }, + options, + ); + return readOnlyAddresses.slice(); + } + + /** + * Gets the instance info with associated metadata for an instance. + */ + async getInstanceInfo( + instanceAddress: Address, + options?: ContractReadOptions, + ): Promise { + const { factory, ...rest } = await this.contract.read( + "getInstanceInfoWithMetadata", + { _instance: instanceAddress }, + options, + ); + return { + factory: new ReadFactory({ + address: factory, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ...rest, + }; + } + + /** + * Gets the instance info with associated metadata for a list of instances. + */ + async getInstanceInfos( + instanceAddresses: Address[], + options?: ContractReadOptions, + ): Promise { + const infos = await this.contract.read( + "getInstanceInfosWithMetadata", + { __instances: instanceAddresses }, + options, + ); + return infos.map(({ factory, ...rest }) => ({ + factory: new ReadFactory({ + address: factory, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ...rest, + })); + } +} diff --git a/packages/hyperdrive-js/src/registry/ReadWriteRegistry.ts b/packages/hyperdrive-js/src/registry/ReadWriteRegistry.ts new file mode 100644 index 000000000..a539c77b0 --- /dev/null +++ b/packages/hyperdrive-js/src/registry/ReadWriteRegistry.ts @@ -0,0 +1,119 @@ +import { + ContractReadOptions, + Drift, + ReadWriteAdapter, + ReadWriteContract, + ReplaceProps, +} from "@delvtech/drift"; +import { Address } from "abitype"; +import { ReadWriteContractClientOptions } from "src/drift/ContractClient"; +import { ReadWriteFactory } from "src/factory/ReadWriteFactory"; +import { ReadWriteHyperdrive } from "src/hyperdrive/ReadWriteHyperdrive"; +import { ReadRegistry, ReadRegistryOptions } from "src/registry/ReadRegistry"; +import { RegistryAbi } from "src/registry/abi"; +import { ReadWriteInstanceInfoWithMetadata } from "src/registry/types"; + +export interface ReadWriteRegistryOptions + extends ReplaceProps {} + +export class ReadWriteRegistry extends ReadRegistry { + declare drift: Drift; + declare contract: ReadWriteContract; + + constructor(options: ReadWriteRegistryOptions) { + super(options); + } + + /** + * Get a {@linkcode ReadWriteFactory} instance for each registered factory. + */ + async getFactories( + options?: ContractReadOptions, + ): Promise { + const factoryAddresses = await this.getFactoryAddresses(options); + return factoryAddresses.map( + (address) => + new ReadWriteFactory({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } + + /** + * Get a {@linkcode ReadWriteHyperdrive} instance for each Hyperdrive instance + * registered in the registry. + */ + async getInstances( + options?: ContractReadOptions, + ): Promise { + const count = await this.contract.read("getNumberOfInstances", {}, options); + + if (count === 0n) { + return []; + } + + const hyperdriveAddresses = await this.contract.read( + "getInstancesInRange", + { + _startIndex: 0n, + _endIndex: count, + }, + options, + ); + return hyperdriveAddresses.map( + (address) => + new ReadWriteHyperdrive({ + address, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ); + } + + async getInstanceInfo( + instanceAddress: Address, + options?: ContractReadOptions, + ): Promise { + const { factory, ...rest } = await this.contract.read( + "getInstanceInfoWithMetadata", + { _instance: instanceAddress }, + options, + ); + return { + factory: new ReadWriteFactory({ + address: factory, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ...rest, + }; + } + + /** + * Gets the instance info with associated metadata for a list of instances. + */ + async getInstanceInfos( + instanceAddresses: Address[], + options?: ContractReadOptions, + ): Promise { + const infos = await this.contract.read( + "getInstanceInfosWithMetadata", + { __instances: instanceAddresses }, + options, + ); + return infos.map(({ factory, ...rest }) => ({ + factory: new ReadWriteFactory({ + address: factory, + drift: this.drift, + cache: this.contract.cache, + cacheNamespace: this.contract.cacheNamespace, + }), + ...rest, + })); + } +} diff --git a/packages/hyperdrive-js/src/registry/abi.ts b/packages/hyperdrive-js/src/registry/abi.ts new file mode 100644 index 000000000..00aada691 --- /dev/null +++ b/packages/hyperdrive-js/src/registry/abi.ts @@ -0,0 +1,4 @@ +import { IHyperdriveRegistry } from "@delvtech/hyperdrive-artifacts/IHyperdriveRegistry"; + +export const registryAbi = IHyperdriveRegistry.abi; +export type RegistryAbi = typeof registryAbi; diff --git a/packages/hyperdrive-js/src/registry/types.ts b/packages/hyperdrive-js/src/registry/types.ts new file mode 100644 index 000000000..137c7469e --- /dev/null +++ b/packages/hyperdrive-js/src/registry/types.ts @@ -0,0 +1,36 @@ +import { FunctionReturn, ReplaceProps } from "@delvtech/drift"; +import { ReadFactory } from "src/factory/ReadFactory"; +import { ReadWriteFactory } from "src/factory/ReadWriteFactory"; +import { RegistryAbi } from "src/registry/abi"; + +/** + * The info collected for each Hyperdrive factory along with the metadata + * associated with each instance. + */ +export type FactoryInfoWithMetadata = FunctionReturn< + RegistryAbi, + "getFactoryInfoWithMetadata" +>; + +/** + * The info related to each Hyperdrive instance along with the metadata + * associated with each instance. + */ +export type ReadInstanceInfoWithMetadata = ReplaceProps< + FunctionReturn, + { + /** + * The factory that deployed this instance. + */ + factory: ReadFactory; + } +>; + +/** {@inheritDoc ReadInstanceInfoWithMetadata} */ +export type ReadWriteInstanceInfoWithMetadata = ReplaceProps< + ReadInstanceInfoWithMetadata, + { + /** {@inheritDoc ReadInstanceInfoWithMetadata.factory} */ + factory: ReadWriteFactory; + } +>; diff --git a/packages/hyperdrive-js/src/token/ReadToken.ts b/packages/hyperdrive-js/src/token/ReadToken.ts new file mode 100644 index 000000000..a582e2f25 --- /dev/null +++ b/packages/hyperdrive-js/src/token/ReadToken.ts @@ -0,0 +1,46 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { ReadClient } from "src/drift/ReadClient"; + +export interface ReadToken extends ReadClient { + address: `0x${string}`; + + /** + * Get the name of this token + */ + getName(): Promise; + + /** + * Get the symbol for this token. + */ + getSymbol(): Promise; + + /** + * Get the number of decimal places this token uses. + */ + getDecimals(): Promise; + + /** + * Get the spending allowance of a given spender for a given owner of this + * token. + */ + getAllowance({ + owner, + spender, + options, + }: { + owner: `0x${string}`; + spender: `0x${string}`; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the token balance of a given address + */ + getBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise; +} diff --git a/packages/hyperdrive-js/src/token/ReadWriteToken.ts b/packages/hyperdrive-js/src/token/ReadWriteToken.ts new file mode 100644 index 000000000..8515c6bbd --- /dev/null +++ b/packages/hyperdrive-js/src/token/ReadWriteToken.ts @@ -0,0 +1,23 @@ +import { ContractWriteOptions, ReplaceProps } from "@delvtech/drift"; +import { ReadWriteClient } from "src/drift/ReadWriteClient"; +import { ReadToken } from "src/token/ReadToken"; + +export interface ReadWriteToken + extends ReplaceProps { + /** + * Give a spending allowance to a given spender. + * @param spender - The address of the spender. + * @param amount - The amount of tokens the spender can spend. + * @returns The transaction hash. + */ + approve({ + spender, + amount, + options, + }: { + owner?: `0x${string}`; + spender: `0x${string}`; + amount: bigint; + options?: ContractWriteOptions; + }): Promise<`0x${string}`>; +} diff --git a/packages/hyperdrive-js/src/token/erc20/ReadErc20.ts b/packages/hyperdrive-js/src/token/erc20/ReadErc20.ts new file mode 100644 index 000000000..9fac7e2b0 --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc20/ReadErc20.ts @@ -0,0 +1,76 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Address } from "abitype"; +import { ReadContractClientOptions } from "src/drift/ContractClient"; +import { ReadClient } from "src/drift/ReadClient"; +import { erc20Abi, Erc20Abi } from "src/token/erc20/abi"; +import { ReadToken } from "src/token/ReadToken"; + +export interface ReadErc20Options extends ReadContractClientOptions {} + +export class ReadErc20 extends ReadClient implements ReadToken { + contract: Contract; + + constructor({ + debugName = "ERC-20 Token", + address, + cache, + cacheNamespace, + ...rest + }: ReadErc20Options) { + super({ debugName, ...rest }); + this.contract = this.drift.contract({ + abi: erc20Abi, + address, + cache, + cacheNamespace, + }); + } + + get address(): Address { + return this.contract.address; + } + get namespace(): PropertyKey | undefined { + return this.contract.cacheNamespace; + } + + getName(): Promise { + return this.contract.read("name"); + } + + getSymbol(): Promise { + return this.contract.read("symbol"); + } + + getDecimals(): Promise { + return this.contract.read("decimals"); + } + + getAllowance({ + owner, + spender, + options, + }: { + owner: `0x${string}`; + spender: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.contract.read("allowance", { owner, spender }, options); + } + + getBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.contract.read("balanceOf", { account }, options); + } + + /** + * Get the total supply of the token. + */ + getTotalSupply(options?: ContractReadOptions): Promise { + return this.contract.read("totalSupply", {}, options); + } +} diff --git a/packages/hyperdrive-js/src/token/erc20/ReadWriteErc20.ts b/packages/hyperdrive-js/src/token/erc20/ReadWriteErc20.ts new file mode 100644 index 000000000..f1075bd3d --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc20/ReadWriteErc20.ts @@ -0,0 +1,45 @@ +import { + ContractWriteOptions, + Drift, + ReadWriteAdapter, + ReadWriteContract, +} from "@delvtech/drift"; +import { ReadWriteContractClientOptions } from "src/drift/ContractClient"; +import { ReadWriteToken } from "src/token/ReadWriteToken"; +import { ReadErc20 } from "src/token/erc20/ReadErc20"; +import { Erc20Abi } from "src/token/erc20/abi"; + +export interface ReadWriteErc20Options extends ReadWriteContractClientOptions {} + +export class ReadWriteErc20 extends ReadErc20 implements ReadWriteToken { + declare drift: Drift; + declare contract: ReadWriteContract; + + constructor(options: ReadWriteErc20Options) { + super(options); + } + + async approve({ + spender, + amount, + options, + }: { + owner?: `0x${string}`; + spender: `0x${string}`; + amount: bigint; + options?: ContractWriteOptions; + }): Promise<`0x${string}`> { + const hash = await this.contract.write( + "approve", + { spender, amount }, + { + ...options, + onMined: (receipt) => { + this.contract.invalidateReadsMatching("allowance"); + options?.onMined?.(receipt); + }, + }, + ); + return hash; + } +} diff --git a/packages/hyperdrive-js/src/token/erc20/abi.ts b/packages/hyperdrive-js/src/token/erc20/abi.ts new file mode 100644 index 000000000..1d5e35c1c --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc20/abi.ts @@ -0,0 +1,4 @@ +import { IERC20 } from "@delvtech/hyperdrive-artifacts/IERC20"; + +export const erc20Abi = IERC20.abi; +export type Erc20Abi = typeof erc20Abi; diff --git a/packages/hyperdrive-js/src/token/erc4626/ReadErc4626.ts b/packages/hyperdrive-js/src/token/erc4626/ReadErc4626.ts new file mode 100644 index 000000000..d263fe299 --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc4626/ReadErc4626.ts @@ -0,0 +1,100 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadErc20, ReadErc20Options } from "src/token/erc20/ReadErc20"; +import { Erc4626Abi, erc4626Abi } from "src/token/erc4626/abi"; + +export class ReadErc4626 extends readErc4626Mixin(ReadErc20) {} + +/** + * @internal + */ +export interface ReadErc4626Mixin { + erc4626Contract: Contract; + + /** + * Get the total supply of assets in the vault. + */ + getTotalAssets(options?: ContractReadOptions): Promise; + + /** + * Convert a shares amount to an assets amount. + */ + convertToAssets({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise; + + /** + * Convert an assets amount to a shares amount. + */ + convertToShares({ + assetsAmount, + options, + }: { + assetsAmount: bigint; + options?: ContractReadOptions; + }): Promise; +} + +/** + * @internal + */ +export function readErc4626Mixin>( + Base: T, +): Constructor & T { + return class extends Base implements ReadErc4626Mixin { + erc4626Contract: Contract; + + constructor(...[options]: any[]) { + const { + debugName = "ERC-4626 Tokenized Vault", + address, + cache, + cacheNamespace, + ...rest + } = options as ReadErc20Options; + super({ debugName, address, cache, cacheNamespace, ...rest }); + this.erc4626Contract = this.drift.contract({ + abi: erc4626Abi, + address, + cache, + cacheNamespace, + }); + } + + getTotalAssets(options?: ContractReadOptions): Promise { + return this.erc4626Contract.read("totalAssets", {}, options); + } + + convertToAssets({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.erc4626Contract.read( + "convertToAssets", + { shares: sharesAmount }, + options, + ); + } + + convertToShares({ + assetsAmount, + options, + }: { + assetsAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.erc4626Contract.read( + "convertToShares", + { assets: assetsAmount }, + options, + ); + } + }; +} diff --git a/packages/hyperdrive-js/src/token/erc4626/ReadMockErc4626.ts b/packages/hyperdrive-js/src/token/erc4626/ReadMockErc4626.ts new file mode 100644 index 000000000..657c2b13a --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc4626/ReadMockErc4626.ts @@ -0,0 +1,53 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { mockErc4626Abi, MockErc4626Abi } from "src/token/erc4626/abi"; +import { ReadErc4626 } from "src/token/erc4626/ReadErc4626"; + +export class ReadMockErc4626 extends readMockErc4626Mixin(ReadErc4626) {} + +/** + * @internal + */ +export interface ReadMockErc4626Mixin { + mockErc4626Contract: Contract; + + /** + * Get the rate of the vault. + */ + getRate(options?: ContractReadOptions): Promise; +} + +/** + * @internal + */ +export function readMockErc4626Mixin>( + BaseReadErc4626: T, +): Constructor & T { + return class extends BaseReadErc4626 implements ReadMockErc4626Mixin { + mockErc4626Contract: Contract; + + constructor(...[options]: any[]) { + const { + debugName = "Mock ERC-4626 Tokenized Vault", + address, + cache, + cacheNamespace, + ...rest + } = options as ConstructorParameters[0]; + super({ debugName, address, cache, cacheNamespace, ...rest }); + this.mockErc4626Contract = this.drift.contract({ + abi: mockErc4626Abi, + address, + cache, + cacheNamespace, + }); + } + + /** + * Get the rate of the vault. + */ + getRate(options?: ContractReadOptions): Promise { + return this.mockErc4626Contract.read("getRate", {}, options); + } + }; +} diff --git a/packages/hyperdrive-js/src/token/erc4626/ReadWriteErc4626.ts b/packages/hyperdrive-js/src/token/erc4626/ReadWriteErc4626.ts new file mode 100644 index 000000000..8521db317 --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc4626/ReadWriteErc4626.ts @@ -0,0 +1,8 @@ +import { ReadWriteContract } from "@delvtech/drift"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { readErc4626Mixin } from "src/token/erc4626/ReadErc4626"; +import { Erc4626Abi } from "src/token/erc4626/abi"; + +export class ReadWriteErc4626 extends readErc4626Mixin(ReadWriteErc20) { + declare erc4626Contract: ReadWriteContract; +} diff --git a/packages/hyperdrive-js/src/token/erc4626/ReadWriteMockErc4626.ts b/packages/hyperdrive-js/src/token/erc4626/ReadWriteMockErc4626.ts new file mode 100644 index 000000000..4c04a4ae0 --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc4626/ReadWriteMockErc4626.ts @@ -0,0 +1,45 @@ +import { ContractWriteOptions, ReadWriteContract } from "@delvtech/drift"; +import { MockErc4626Abi } from "src/token/erc4626/abi"; +import { readMockErc4626Mixin } from "src/token/erc4626/ReadMockErc4626"; +import { ReadWriteErc4626 } from "src/token/erc4626/ReadWriteErc4626"; + +export class ReadWriteMockErc4626 extends readMockErc4626Mixin( + ReadWriteErc4626, +) { + declare mockErc4626Contract: ReadWriteContract; + + /** + * Mint shares. + */ + mint({ + receiver, + sharesAmount, + options, + }: { + receiver: `0x${string}`; + sharesAmount: bigint; + options?: ContractWriteOptions; + }): Promise<`0x${string}`> { + return this.mockErc4626Contract.write( + "mint", + { + _receiver: receiver, + _shares: sharesAmount, + }, + options, + ); + } + + /** + * Set the vault's rate. + */ + setRate({ + rate, + options, + }: { + rate: bigint; + options?: ContractWriteOptions; + }): Promise<`0x${string}`> { + return this.mockErc4626Contract.write("setRate", { _rate_: rate }, options); + } +} diff --git a/packages/hyperdrive-js/src/token/erc4626/abi.ts b/packages/hyperdrive-js/src/token/erc4626/abi.ts new file mode 100644 index 000000000..374dfc855 --- /dev/null +++ b/packages/hyperdrive-js/src/token/erc4626/abi.ts @@ -0,0 +1,8 @@ +import { IERC4626 } from "@delvtech/hyperdrive-artifacts/IERC4626"; +import { MockERC4626 } from "@delvtech/hyperdrive-artifacts/MockERC4626"; + +export const erc4626Abi = IERC4626.abi; +export type Erc4626Abi = typeof erc4626Abi; + +export const mockErc4626Abi = MockERC4626.abi; +export type MockErc4626Abi = typeof mockErc4626Abi; diff --git a/packages/hyperdrive-js/src/token/eth/ReadEth.ts b/packages/hyperdrive-js/src/token/eth/ReadEth.ts new file mode 100644 index 000000000..48a4f552e --- /dev/null +++ b/packages/hyperdrive-js/src/token/eth/ReadEth.ts @@ -0,0 +1,50 @@ +import { ContractReadOptions } from "@delvtech/drift"; +import { ReadClient, ReadClientOptions } from "src/drift/ReadClient"; +import { ReadToken } from "src/token/ReadToken"; + +export interface ReadEthOptions extends ReadClientOptions {} + +export class ReadEth extends ReadClient implements ReadToken { + static address = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as const; + address = ReadEth.address; + + constructor({ debugName = "ETH", ...restOptions }: ReadEthOptions) { + super({ debugName, ...restOptions }); + } + + async getName(): Promise { + return "Ethereum"; + } + + async getSymbol(): Promise { + return "ETH"; + } + + async getDecimals(): Promise { + return 18; + } + + /** + * @remarks + * Native ETH does not require allowances as it is sent directly as the + * message value when used in Hyperdrive. This method returns a maximum + * value to indicate the absence of an allowance mechanism for ETH. + */ + async getAllowance(): Promise { + // Max value for uint256 + return 2n ** 256n - 1n; + } + + async getBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.drift.getBalance({ + address: account, + ...options, + }); + } +} diff --git a/packages/hyperdrive-js/src/token/eth/ReadWriteEth.ts b/packages/hyperdrive-js/src/token/eth/ReadWriteEth.ts new file mode 100644 index 000000000..6128d7ede --- /dev/null +++ b/packages/hyperdrive-js/src/token/eth/ReadWriteEth.ts @@ -0,0 +1,36 @@ +import { Drift, ReadWriteAdapter } from "@delvtech/drift"; +import { ReadWriteClientOptions } from "src/drift/ReadWriteClient"; +import { HyperdriveSdkError } from "src/HyperdriveSdkError"; +import { ReadEth } from "src/token/eth/ReadEth"; +import { ReadWriteToken } from "src/token/ReadWriteToken"; + +export interface ReadWriteEthOptions extends ReadWriteClientOptions {} + +export class ReadWriteEth extends ReadEth implements ReadWriteToken { + declare drift: Drift; + + constructor(options: ReadWriteEthOptions) { + super(options); + } + + /** + * This method is not available for the native ETH token. + * @throws A {@linkcode HyperdriveSdkError} + */ + async approve(): Promise<`0x${string}`> { + throw new HyperdriveSdkError( + "This method is not available for the native ETH token.", + ); + } +} + +export interface MethodNotImplementedErrorOptions { + /** + * The name of the object that the method was not implemented in. + */ + objectName: string; + /** + * The name of the method that was not implemented. + */ + methodName: string; +} diff --git a/packages/hyperdrive-js/src/token/lseth/ReadLsEth.ts b/packages/hyperdrive-js/src/token/lseth/ReadLsEth.ts new file mode 100644 index 000000000..5fd3792e3 --- /dev/null +++ b/packages/hyperdrive-js/src/token/lseth/ReadLsEth.ts @@ -0,0 +1,124 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadErc20, ReadErc20Options } from "src/token/erc20/ReadErc20"; +import { LsEthAbi, lsEthAbi } from "src/token/lseth/abi"; + +export class ReadLsEth extends readLsEthMixin(ReadErc20) {} + +/** + * @internal + */ +export interface ReadLsEthMixin { + lsEthContract: Contract; + + /** + * Get the total supply of underlying eth in the lsEth contract. + */ + getTotalEthSupply(options?: ContractReadOptions): Promise; + + /** + * Get the underlying eth balance of an account. + */ + getEthBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the amount of ETH that would be received for a given number of shares. + */ + getEthBalanceFromShares({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the number of shares that would be received for a given amount of ETH. + */ + getSharesFromEthBalance({ + ethBalance, + options, + }: { + ethBalance: bigint; + options?: ContractReadOptions; + }): Promise; +} + +/** + * @internal + */ +export function readLsEthMixin>( + Base: T, +): Constructor & T { + return class extends Base implements ReadLsEthMixin { + lsEthContract: Contract; + + constructor(...[options]: any[]) { + const { drift, address, cache, cacheNamespace } = + options as ReadErc20Options; + super({ address, drift, cache, cacheNamespace }); + this.lsEthContract = drift.contract({ + abi: lsEthAbi, + address, + cache, + cacheNamespace, + }); + } + + async getTotalEthSupply(options?: ContractReadOptions): Promise { + return this.lsEthContract.read("totalUnderlyingSupply", {}, options); + } + + async getEthBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.lsEthContract.read( + "balanceOfUnderlying", + { _owner: account }, + options, + ); + } + + async getEthBalanceFromShares({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.lsEthContract.read( + "underlyingBalanceFromShares", + { + _shares: sharesAmount, + }, + options, + ); + } + + async getSharesFromEthBalance({ + ethBalance, + options, + }: { + ethBalance: bigint; + options?: ContractReadOptions; + }): Promise { + return this.lsEthContract.read( + "sharesFromUnderlyingBalance", + { + _underlyingAssetAmount: ethBalance, + }, + options, + ); + } + }; +} diff --git a/packages/hyperdrive-js/src/token/lseth/ReadWriteLsEth.ts b/packages/hyperdrive-js/src/token/lseth/ReadWriteLsEth.ts new file mode 100644 index 000000000..f1e8237b7 --- /dev/null +++ b/packages/hyperdrive-js/src/token/lseth/ReadWriteLsEth.ts @@ -0,0 +1,8 @@ +import { ReadWriteContract } from "@delvtech/drift"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { readLsEthMixin } from "src/token/lseth/ReadLsEth"; +import { LsEthAbi } from "src/token/lseth/abi"; + +export class ReadWriteLsEth extends readLsEthMixin(ReadWriteErc20) { + declare lsEthContract: ReadWriteContract; +} diff --git a/packages/hyperdrive-js/src/token/lseth/abi.ts b/packages/hyperdrive-js/src/token/lseth/abi.ts new file mode 100644 index 000000000..5d936fa8c --- /dev/null +++ b/packages/hyperdrive-js/src/token/lseth/abi.ts @@ -0,0 +1,4 @@ +import { IRiverV1 } from "@delvtech/hyperdrive-artifacts/IRiverV1"; + +export const lsEthAbi = IRiverV1.abi; +export type LsEthAbi = typeof lsEthAbi; diff --git a/packages/hyperdrive-js/src/token/reth/ReadREth.ts b/packages/hyperdrive-js/src/token/reth/ReadREth.ts new file mode 100644 index 000000000..2bcdb7691 --- /dev/null +++ b/packages/hyperdrive-js/src/token/reth/ReadREth.ts @@ -0,0 +1,125 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadErc20, ReadErc20Options } from "src/token/erc20/ReadErc20"; +import { REthAbi, rEthAbi } from "src/token/reth/abi"; + +export class ReadREth extends readREthMixin(ReadErc20) {} + +/** + * @internal + */ +export interface ReadREthMixin { + rEthContract: Contract; + + /** + * Get the total supply of underlying eth in the rETH contract. + */ + getTotalEthSupply(options?: ContractReadOptions): Promise; + + /** + * Get the underlying eth balance of an account. + */ + getEthBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the amount of ETH backing an amount of rETH. + */ + getEthValue({ + rEthAmount, + options, + }: { + rEthAmount: bigint; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the amount of rETH backing an amount of ETH. + */ + getREthValue({ + ethAmount, + options, + }: { + ethAmount: bigint; + options?: ContractReadOptions; + }): Promise; +} + +/** + * @internal + */ +export function readREthMixin>( + Base: T, +): Constructor & T { + return class extends Base implements ReadREthMixin { + rEthContract: Contract; + + constructor(...[options]: any[]) { + const { drift, address, cache, cacheNamespace } = + options as ReadErc20Options; + super({ address, drift, cache, cacheNamespace }); + this.rEthContract = drift.contract({ + abi: rEthAbi, + address, + cache, + cacheNamespace, + }); + } + + async getTotalEthSupply(options?: ContractReadOptions): Promise { + return this.rEthContract.read("getTotalCollateral", {}, options); + } + + async getEthBalanceOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + const rEthBalance = await this.getBalanceOf({ account, options }); + return this.rEthContract.read( + "getEthValue", + { _rethAmount: rEthBalance }, + options, + ); + } + + async getEthValue({ + rEthAmount, + options, + }: { + rEthAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.rEthContract.read( + "getEthValue", + { + _rethAmount: rEthAmount, + }, + options, + ); + } + + async getREthValue({ + ethAmount, + options, + }: { + ethAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.rEthContract.read( + "getRethValue", + { + _ethAmount: ethAmount, + }, + options, + ); + } + }; +} diff --git a/packages/hyperdrive-js/src/token/reth/ReadWriteREth.ts b/packages/hyperdrive-js/src/token/reth/ReadWriteREth.ts new file mode 100644 index 000000000..3b5ef80c7 --- /dev/null +++ b/packages/hyperdrive-js/src/token/reth/ReadWriteREth.ts @@ -0,0 +1,8 @@ +import { ReadWriteContract } from "@delvtech/drift"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { readREthMixin } from "src/token/reth/ReadREth"; +import { REthAbi } from "src/token/reth/abi"; + +export class ReadWriteREth extends readREthMixin(ReadWriteErc20) { + declare rEthContract: ReadWriteContract; +} diff --git a/packages/hyperdrive-js/src/token/reth/abi.ts b/packages/hyperdrive-js/src/token/reth/abi.ts new file mode 100644 index 000000000..50f68b410 --- /dev/null +++ b/packages/hyperdrive-js/src/token/reth/abi.ts @@ -0,0 +1,4 @@ +import { IRocketTokenRETH } from "@delvtech/hyperdrive-artifacts/IRocketTokenRETH"; + +export const rEthAbi = IRocketTokenRETH.abi; +export type REthAbi = typeof rEthAbi; diff --git a/packages/hyperdrive-js/src/token/steth/ReadStEth.ts b/packages/hyperdrive-js/src/token/steth/ReadStEth.ts new file mode 100644 index 000000000..dadbc9f19 --- /dev/null +++ b/packages/hyperdrive-js/src/token/steth/ReadStEth.ts @@ -0,0 +1,118 @@ +import { Contract, ContractReadOptions } from "@delvtech/drift"; +import { Constructor } from "src/base/types"; +import { ReadErc20, ReadErc20Options } from "src/token/erc20/ReadErc20"; +import { StEthAbi, stEthAbi } from "src/token/steth/abi"; + +export class ReadStEth extends readStEthMixin(ReadErc20) {} + +/** + * @internal + */ +export interface ReadStEthMixin { + stEthContract: Contract; + + /** + * Get the number of stETH shares held by an account. + */ + getSharesOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the amount of pooled ETH (stETH) that would be received for a given + * number of shares. + */ + getPooledEthByShares({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise; + + /** + * Get the number of shares that would be received for a given amount of + * pooled ETH (stETH). + */ + getSharesByPooledEth({ + ethAmount, + options, + }: { + ethAmount: bigint; + options?: ContractReadOptions; + }): Promise; +} + +/** + * @internal + */ +export function readStEthMixin>( + Base: T, +): Constructor & T { + return class extends Base implements ReadStEthMixin { + stEthContract: Contract; + + constructor(...[options]: any[]) { + const { + debugName = "stETH Token", + address, + cache, + cacheNamespace, + ...rest + } = options as ReadErc20Options; + super({ debugName, address, cache, cacheNamespace, ...rest }); + this.stEthContract = this.drift.contract({ + abi: stEthAbi, + address, + cache, + cacheNamespace, + }); + } + + getSharesOf({ + account, + options, + }: { + account: `0x${string}`; + options?: ContractReadOptions; + }): Promise { + return this.stEthContract.read( + "sharesOf", + { _account: account }, + options, + ); + } + + getPooledEthByShares({ + sharesAmount, + options, + }: { + sharesAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.stEthContract.read( + "getPooledEthByShares", + { _sharesAmount: sharesAmount }, + options, + ); + } + + getSharesByPooledEth({ + ethAmount, + options, + }: { + ethAmount: bigint; + options?: ContractReadOptions; + }): Promise { + return this.stEthContract.read( + "getSharesByPooledEth", + { _ethAmount: ethAmount }, + options, + ); + } + }; +} diff --git a/packages/hyperdrive-js/src/token/steth/ReadWriteStEth.ts b/packages/hyperdrive-js/src/token/steth/ReadWriteStEth.ts new file mode 100644 index 000000000..66ec0b8ff --- /dev/null +++ b/packages/hyperdrive-js/src/token/steth/ReadWriteStEth.ts @@ -0,0 +1,8 @@ +import { ReadWriteContract } from "@delvtech/drift"; +import { ReadWriteErc20 } from "src/token/erc20/ReadWriteErc20"; +import { StEthAbi } from "src/token/steth/abi"; +import { readStEthMixin } from "src/token/steth/ReadStEth"; + +export class ReadWriteStEth extends readStEthMixin(ReadWriteErc20) { + declare stEthContract: ReadWriteContract; +} diff --git a/packages/hyperdrive-js/src/token/steth/abi.ts b/packages/hyperdrive-js/src/token/steth/abi.ts new file mode 100644 index 000000000..9ca120fd4 --- /dev/null +++ b/packages/hyperdrive-js/src/token/steth/abi.ts @@ -0,0 +1,4 @@ +import { ILido } from "@delvtech/hyperdrive-artifacts/ILido"; + +export const stEthAbi = ILido.abi; +export type StEthAbi = typeof stEthAbi; diff --git a/packages/hyperdrive-js/tsconfig.json b/packages/hyperdrive-js/tsconfig.json new file mode 100644 index 000000000..f92f27a67 --- /dev/null +++ b/packages/hyperdrive-js/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@hyperdrive/tsconfig/base.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "moduleResolution": "Bundler", + "experimentalDecorators": true, + "target": "esnext", + "module": "ESNext", + "paths": { + "src/*": ["src/*"] + }, + "resolveJsonModule": true + } +} diff --git a/packages/hyperdrive-js/tsup.config.ts b/packages/hyperdrive-js/tsup.config.ts new file mode 100644 index 000000000..07ca24f96 --- /dev/null +++ b/packages/hyperdrive-js/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/exports/index.ts", "src/exports/v1.0.14.ts"], + format: ["esm", "cjs"], + sourcemap: true, + dts: true, + clean: true, + minify: true, + cjsInterop: true, +}); diff --git a/packages/hyperdrive-js/vite.config.ts b/packages/hyperdrive-js/vite.config.ts new file mode 100644 index 000000000..5f5a285c1 --- /dev/null +++ b/packages/hyperdrive-js/vite.config.ts @@ -0,0 +1,6 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths()], +}); diff --git a/packages/hyperdrive-viem/package.json b/packages/hyperdrive-viem/package.json index ff5a27881..62d705bab 100644 --- a/packages/hyperdrive-viem/package.json +++ b/packages/hyperdrive-viem/package.json @@ -12,8 +12,9 @@ "viem": "^2.7.8" }, "dependencies": { - "@delvtech/hyperdrive-js-core": "3.0.6", - "@delvtech/evm-client-viem": "^0.6.3" + "@delvtech/drift": "^0.0.1-beta.11", + "@delvtech/drift-viem": "^0.0.1-beta.13", + "@delvtech/hyperdrive-js": "0.0.0" }, "devDependencies": { "@hyperdrive/eslint-config": "*", @@ -31,33 +32,16 @@ ], "exports": { ".": { - "import": "./dist/index.js", - "require": "./dist/index.js" - }, - "./factory": { - "import": "./dist/factory.js", - "require": "./dist/factory.js" - }, - "./hyperdrive": { - "import": "./dist/hyperdrive.js", - "require": "./dist/hyperdrive.js" - }, - "./registry": { - "import": "./dist/registry.js", - "require": "./dist/registry.js" - }, - "./token": { - "import": "./dist/token.js", - "require": "./dist/token.js" + "types": "./dist/index.d.ts", + "default": "./dist/index.js", + "require": "./dist/index.cjs" }, "./v1.0.14": { - "import": "./dist/v1.0.14/index.js", - "require": "./dist/v1.0.14/index.js" + "types": "./dist/v1.0.14.d.ts", + "default": "./dist/v1.0.14.js", + "require": "./dist/v1.0.14.cjs" }, - "./v1.0.14/*": { - "import": "./dist/v1.0.14/*.js", - "require": "./dist/v1.0.14/*.js" - } + "./package.json": "./package.json" }, "publishConfig": { "access": "public" diff --git a/packages/hyperdrive-viem/src/exports/factory.ts b/packages/hyperdrive-viem/src/exports/factory.ts deleted file mode 100644 index a67b59bb6..000000000 --- a/packages/hyperdrive-viem/src/exports/factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - ReadFactory, - ReadWriteFactory, - type ReadFactoryOptions, - type ReadWriteFactoryOptions, -} from "src/factory"; - -// re-exports -export * from "@delvtech/hyperdrive-js-core/factory/rest"; diff --git a/packages/hyperdrive-viem/src/exports/hyperdrive.ts b/packages/hyperdrive-viem/src/exports/hyperdrive.ts deleted file mode 100644 index dad0a95a8..000000000 --- a/packages/hyperdrive-viem/src/exports/hyperdrive.ts +++ /dev/null @@ -1,50 +0,0 @@ -// base -export { - ReadHyperdrive, - ReadWriteHyperdrive, - type ReadHyperdriveOptions, - type ReadWriteHyperdriveOptions, -} from "src/hyperdrive/base"; - -// erc-4626 -export { - ReadErc4626Hyperdrive, - ReadMockErc4626Hyperdrive, - ReadWriteErc4626Hyperdrive, - ReadWriteMockErc4626Hyperdrive, -} from "src/hyperdrive/erc4626"; - -// ezeth -export { - ReadEzEthHyperdrive, - ReadWriteEzEthHyperdrive, -} from "src/hyperdrive/ezeth"; - -// lseth -export { - ReadLsEthHyperdrive, - ReadWriteLsEthHyperdrive, -} from "src/hyperdrive/lseth"; - -// morpho -export { - ReadMetaMorphoHyperdrive, - ReadWriteMetaMorphoHyperdrive, -} from "src/hyperdrive/morpho"; - -// reth -export { - ReadREthHyperdrive, - ReadWriteREthHyperdrive, -} from "src/hyperdrive/reth"; - -// steth -export { - ReadStEthHyperdrive, - ReadWriteStEthHyperdrive, - type ReadStEthHyperdriveOptions, - type ReadWriteStEthHyperdriveOptions, -} from "src/hyperdrive/steth"; - -// re-exports -export * from "@delvtech/hyperdrive-js-core/hyperdrive/rest"; diff --git a/packages/hyperdrive-viem/src/exports/index.ts b/packages/hyperdrive-viem/src/exports/index.ts index 432a95c54..9f724f516 100644 --- a/packages/hyperdrive-viem/src/exports/index.ts +++ b/packages/hyperdrive-viem/src/exports/index.ts @@ -1,14 +1,147 @@ -// models -export * from "./factory"; -export * from "./hyperdrive"; -export * from "./registry"; -export * from "./token"; - -// re-exports -export * from "@delvtech/hyperdrive-js-core/contract"; -export * from "@delvtech/hyperdrive-js-core/errors"; -export * from "@delvtech/hyperdrive-js-core/model"; -export * from "@delvtech/hyperdrive-js-core/utils"; - -// client re-exports -export * from "@delvtech/evm-client-viem"; +import { + ReadClientOptions as BaseReadClientOptions, + ReadWriteClientOptions as BaseReadWriteClientOptions, +} from "@delvtech/hyperdrive-js"; +import { ViemReadClientOptions } from "src/viem/viemReadMixin"; +import { ViemReadWriteClientOptions } from "src/viem/viemReadWriteMixin"; + +// Factory +export { + ReadFactory, + ReadWriteFactory, + type ReadFactoryOptions, + type ReadWriteFactoryOptions, +} from "src/factory"; + +// Registry +export { + ReadRegistry, + ReadWriteRegistry, + type ReadRegistryOptions, + type ReadWriteRegistryOptions, +} from "src/registry"; + +// Hyperdrive // + +export { + getHyperdrive, + type Hyperdrive, + type HyperdriveOptions, +} from "src/hyperdrive/getHyperdrive"; + +// base +export { + ReadHyperdrive, + ReadWriteHyperdrive, + type ReadHyperdriveOptions, + type ReadWriteHyperdriveOptions, +} from "src/hyperdrive/base"; + +// erc-4626 +export { + ReadErc4626Hyperdrive, + ReadMockErc4626Hyperdrive, + ReadWriteErc4626Hyperdrive, + ReadWriteMockErc4626Hyperdrive, +} from "src/hyperdrive/erc4626"; + +// ezeth +export { + ReadEzEthHyperdrive, + ReadWriteEzEthHyperdrive, +} from "src/hyperdrive/ezeth"; + +// lseth +export { + ReadLsEthHyperdrive, + ReadWriteLsEthHyperdrive, +} from "src/hyperdrive/lseth"; + +// reth +export { + ReadREthHyperdrive, + ReadWriteREthHyperdrive, +} from "src/hyperdrive/reth"; + +// steth +export { + ReadStEthHyperdrive, + ReadWriteStEthHyperdrive, + type ReadStEthHyperdriveOptions, + type ReadWriteStEthHyperdriveOptions, +} from "src/hyperdrive/steth"; + +// Client +export type ReadClientOptions = ViemReadClientOptions; +export type ReadWriteClientOptions = + ViemReadWriteClientOptions; + +// Re-exports // + +export type { BaseReadClientOptions, BaseReadWriteClientOptions }; + +export { + adjustAmountByPercentage, + calculateAprFromPrice, + calculateMatureLongYieldAfterFees, + erc20Abi, + erc4626Abi, + ezEthHyperdriveAbi, + factoryAbi, + getHprFromApr, + getHprFromApy, + hyperdriveAbi, + HyperdriveSdkError, + lsEthAbi, + mockErc4626Abi, + ReadClient, + ReadErc20, + ReadErc4626, + ReadEth, + ReadLsEth, + ReadMockErc4626, + ReadREth, + ReadStEth, + ReadWriteClient, + ReadWriteErc20, + ReadWriteErc4626, + ReadWriteEth, + ReadWriteLsEth, + ReadWriteMockErc4626, + ReadWriteREth, + ReadWriteStEth, + registryAbi, + rEthAbi, + stEthAbi, + type ClosedLong, + type ClosedLpShares, + type ClosedShort, + type Constructor, + type ContractClientOptions, + type Erc20Abi, + type Erc4626Abi, + type EzEthHyperdriveAbi, + type FactoryAbi, + type HyperdriveAbi, + type Long, + type LsEthAbi, + type MarketState, + type MockErc4626Abi, + type OpenLongPositionReceived, + type OpenShort, + type PoolConfig, + type PoolInfo, + type ReadContractClientOptions, + type ReadErc20Options, + type ReadEthOptions, + type ReadToken, + type ReadWriteContractClientOptions, + type ReadWriteErc20Options, + type ReadWriteEthOptions, + type ReadWriteToken, + type RedeemedWithdrawalShares, + type RegistryAbi, + type REthAbi, + type Short, + type StEthAbi, +} from "@delvtech/hyperdrive-js"; diff --git a/packages/hyperdrive-viem/src/exports/registry.ts b/packages/hyperdrive-viem/src/exports/registry.ts deleted file mode 100644 index 950df4bb1..000000000 --- a/packages/hyperdrive-viem/src/exports/registry.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - ReadRegistry, - ReadWriteRegistry, - type ReadRegistryOptions, - type ReadWriteRegistryOptions, -} from "src/registry"; - -// re-exports -export * from "@delvtech/hyperdrive-js-core/registry/rest"; diff --git a/packages/hyperdrive-viem/src/exports/token.ts b/packages/hyperdrive-viem/src/exports/token.ts deleted file mode 100644 index 5ca60eb46..000000000 --- a/packages/hyperdrive-viem/src/exports/token.ts +++ /dev/null @@ -1,2 +0,0 @@ -// re-exports -export * from "@delvtech/hyperdrive-js-core/token/index"; diff --git a/packages/hyperdrive-viem/src/exports/v1.0.14/hyperdrive.ts b/packages/hyperdrive-viem/src/exports/v1.0.14.ts similarity index 89% rename from packages/hyperdrive-viem/src/exports/v1.0.14/hyperdrive.ts rename to packages/hyperdrive-viem/src/exports/v1.0.14.ts index e6b7dadc5..ba4875e5c 100644 --- a/packages/hyperdrive-viem/src/exports/v1.0.14/hyperdrive.ts +++ b/packages/hyperdrive-viem/src/exports/v1.0.14.ts @@ -24,9 +24,6 @@ export { ReadWriteLsEthHyperdrive_v1_0_14, } from "src/hyperdrive/v1.0.14/lseth"; -// morpho -export { ReadMetaMorphoHyperdrive_v1_0_14 } from "src/hyperdrive/v1.0.14/morpho"; - // reth export { ReadREthHyperdrive_v1_0_14, diff --git a/packages/hyperdrive-viem/src/exports/v1.0.14/index.ts b/packages/hyperdrive-viem/src/exports/v1.0.14/index.ts deleted file mode 100644 index 85fbc6375..000000000 --- a/packages/hyperdrive-viem/src/exports/v1.0.14/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./hyperdrive"; diff --git a/packages/hyperdrive-viem/src/factory.ts b/packages/hyperdrive-viem/src/factory.ts index 007d757a7..e4e056b05 100644 --- a/packages/hyperdrive-viem/src/factory.ts +++ b/packages/hyperdrive-viem/src/factory.ts @@ -1,18 +1,23 @@ -import * as core from "@delvtech/hyperdrive-js-core/factory/model"; -import { viemReadMixin, ViemReadModelOptions } from "src/viem/viemReadMixin"; import { + ReadFactory as BaseReadFactory, + ReadFactoryOptions as BaseReadFactoryOptions, + ReadWriteFactory as BaseReadWriteFactory, + ReadWriteFactoryOptions as BaseReadWriteFactoryOptions, +} from "@delvtech/hyperdrive-js"; +import { ViemReadClientOptions, viemReadMixin } from "src/viem/viemReadMixin"; +import { + ViemReadWriteClientOptions, viemReadWriteMixin, - ViemReadWriteModelOptions, } from "src/viem/viemReadWriteMixin"; export interface ReadFactoryOptions - extends ViemReadModelOptions {} + extends ViemReadClientOptions {} -export class ReadFactory extends viemReadMixin(core.ReadFactory) {} +export class ReadFactory extends viemReadMixin(BaseReadFactory) {} export interface ReadWriteFactoryOptions - extends ViemReadWriteModelOptions {} + extends ViemReadWriteClientOptions {} export class ReadWriteFactory extends viemReadWriteMixin( - core.ReadWriteFactory + BaseReadWriteFactory, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/base.ts b/packages/hyperdrive-viem/src/hyperdrive/base.ts index 71a41997a..544fe5414 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/base.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/base.ts @@ -1,18 +1,23 @@ -import * as core from "@delvtech/hyperdrive-js-core"; -import { viemReadMixin, ViemReadModelOptions } from "src/viem/viemReadMixin"; import { + ReadHyperdrive as BaseReadHyperdrive, + ReadHyperdriveOptions as BaseReadHyperdriveOptions, + ReadWriteHyperdrive as BaseReadWriteHyperdrive, + ReadWriteHyperdriveOptions as BaseReadWriteHyperdriveOptions, +} from "@delvtech/hyperdrive-js"; +import { ViemReadClientOptions, viemReadMixin } from "src/viem/viemReadMixin"; +import { + ViemReadWriteClientOptions, viemReadWriteMixin, - ViemReadWriteModelOptions, } from "src/viem/viemReadWriteMixin"; export interface ReadHyperdriveOptions - extends ViemReadModelOptions {} + extends ViemReadClientOptions {} -export class ReadHyperdrive extends viemReadMixin(core.ReadHyperdrive) {} +export class ReadHyperdrive extends viemReadMixin(BaseReadHyperdrive) {} export interface ReadWriteHyperdriveOptions - extends ViemReadWriteModelOptions {} + extends ViemReadWriteClientOptions {} export class ReadWriteHyperdrive extends viemReadWriteMixin( - core.ReadWriteHyperdrive + BaseReadWriteHyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/erc4626.ts b/packages/hyperdrive-viem/src/hyperdrive/erc4626.ts index ebeee4858..337f70212 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/erc4626.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/erc4626.ts @@ -1,19 +1,24 @@ -import * as core from "@delvtech/hyperdrive-js-core/hyperdrive/model"; +import { + ReadErc4626Hyperdrive as BaseReadErc4626Hyperdrive, + ReadMockErc4626Hyperdrive as BaseReadMockErc4626Hyperdrive, + ReadWriteErc4626Hyperdrive as BaseReadWriteErc4626Hyperdrive, + ReadWriteMockErc4626Hyperdrive as BaseReadWriteMockErc4626Hyperdrive, +} from "@delvtech/hyperdrive-js"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadErc4626Hyperdrive extends viemReadMixin( - core.ReadErc4626Hyperdrive + BaseReadErc4626Hyperdrive, ) {} export class ReadWriteErc4626Hyperdrive extends viemReadWriteMixin( - core.ReadWriteErc4626Hyperdrive + BaseReadWriteErc4626Hyperdrive, ) {} export class ReadMockErc4626Hyperdrive extends viemReadMixin( - core.ReadMockErc4626Hyperdrive + BaseReadMockErc4626Hyperdrive, ) {} export class ReadWriteMockErc4626Hyperdrive extends viemReadWriteMixin( - core.ReadWriteMockErc4626Hyperdrive + BaseReadWriteMockErc4626Hyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/ezeth.ts b/packages/hyperdrive-viem/src/hyperdrive/ezeth.ts index 99726f973..82c173924 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/ezeth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/ezeth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core"; +import { + ReadEzEthHyperdrive as BaseReadEzEthHyperdrive, + ReadWriteEzEthHyperdrive as BaseReadWriteEzEthHyperdrive, +} from "@delvtech/hyperdrive-js"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadEzEthHyperdrive extends viemReadMixin( - core.ReadEzEthHyperdrive + BaseReadEzEthHyperdrive, ) {} export class ReadWriteEzEthHyperdrive extends viemReadWriteMixin( - core.ReadWriteEzEthHyperdrive + BaseReadWriteEzEthHyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/getHyperdrive.ts b/packages/hyperdrive-viem/src/hyperdrive/getHyperdrive.ts new file mode 100644 index 000000000..d2c5767c4 --- /dev/null +++ b/packages/hyperdrive-viem/src/hyperdrive/getHyperdrive.ts @@ -0,0 +1,45 @@ +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { getHyperdrive as baseGetHyperdrive } from "@delvtech/hyperdrive-js"; +import { + ReadHyperdrive, + ReadHyperdriveOptions, + ReadWriteHyperdrive, +} from "src/hyperdrive/base"; +import { WalletClient } from "viem"; + +export interface HyperdriveOptions< + T extends WalletClient | undefined = undefined, +> extends ReadHyperdriveOptions { + walletClient?: T; +} + +export type Hyperdrive = + T extends void ? ReadHyperdrive : ReadWriteHyperdrive; + +export async function getHyperdrive< + T extends WalletClient | undefined = undefined, +>({ + address, + publicClient, + walletClient, + cache, + cacheNamespace = publicClient.chain?.id, + debugName, + earliestBlock, +}: HyperdriveOptions): Promise> { + const drift = new Drift( + viemAdapter({ + publicClient, + walletClient, + }), + ); + return baseGetHyperdrive({ + address, + drift, + cache, + cacheNamespace, + debugName, + earliestBlock, + }) as Promise>; +} diff --git a/packages/hyperdrive-viem/src/hyperdrive/lseth.ts b/packages/hyperdrive-viem/src/hyperdrive/lseth.ts index ec498c050..588bf78b0 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/lseth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/lseth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core"; +import { + ReadLsEthHyperdrive as BaseReadLsEthHyperdrive, + ReadWriteLsEthHyperdrive as BaseReadWriteLsEthHyperdrive, +} from "@delvtech/hyperdrive-js"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadLsEthHyperdrive extends viemReadMixin( - core.ReadLsEthHyperdrive + BaseReadLsEthHyperdrive, ) {} export class ReadWriteLsEthHyperdrive extends viemReadWriteMixin( - core.ReadWriteLsEthHyperdrive + BaseReadWriteLsEthHyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/morpho.ts b/packages/hyperdrive-viem/src/hyperdrive/morpho.ts deleted file mode 100644 index 10a16b516..000000000 --- a/packages/hyperdrive-viem/src/hyperdrive/morpho.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as core from "@delvtech/hyperdrive-js-core"; -import { viemReadMixin } from "src/viem/viemReadMixin"; -import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; - -export class ReadMetaMorphoHyperdrive extends viemReadMixin( - core.ReadMetaMorphoHyperdrive -) {} - -export class ReadWriteMetaMorphoHyperdrive extends viemReadWriteMixin( - core.ReadWriteMetaMorphoHyperdrive -) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/reth.ts b/packages/hyperdrive-viem/src/hyperdrive/reth.ts index 2c542c85d..55b30cf91 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/reth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/reth.ts @@ -1,11 +1,12 @@ -import * as core from "@delvtech/hyperdrive-js-core"; +import { + ReadREthHyperdrive as BaseReadREthHyperdrive, + ReadWriteREthHyperdrive as BaseReadWriteREthHyperdrive, +} from "@delvtech/hyperdrive-js"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; -export class ReadREthHyperdrive extends viemReadMixin( - core.ReadREthHyperdrive -) {} +export class ReadREthHyperdrive extends viemReadMixin(BaseReadREthHyperdrive) {} export class ReadWriteREthHyperdrive extends viemReadWriteMixin( - core.ReadWriteREthHyperdrive + BaseReadWriteREthHyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/steth.ts b/packages/hyperdrive-viem/src/hyperdrive/steth.ts index 0f0d7babb..b05cb951f 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/steth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/steth.ts @@ -1,17 +1,22 @@ -import * as core from "@delvtech/hyperdrive-js-core"; -import { viemReadMixin, ViemReadModelOptions } from "src/viem/viemReadMixin"; +import { + ReadStEthHyperdrive as BaseReadStEthHyperdrive, + ReadStEthHyperdriveOptions as BaseReadStEthHyperdriveOptions, + ReadWriteStEthHyperdrive as BaseReadWriteStEthHyperdrive, + ReadWriteStEthHyperdriveOptions as BaseReadWriteStEthHyperdriveOptions, +} from "@delvtech/hyperdrive-js"; +import { ViemReadClientOptions, viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export interface ReadStEthHyperdriveOptions - extends ViemReadModelOptions {} + extends ViemReadClientOptions {} export class ReadStEthHyperdrive extends viemReadMixin( - core.ReadStEthHyperdrive + BaseReadStEthHyperdrive, ) {} export interface ReadWriteStEthHyperdriveOptions - extends ViemReadModelOptions {} + extends ViemReadClientOptions {} export class ReadWriteStEthHyperdrive extends viemReadWriteMixin( - core.ReadWriteStEthHyperdrive + BaseReadWriteStEthHyperdrive, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/base.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/base.ts index f44eddf21..f621def98 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/base.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/base.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadHyperdrive_v1_0_14 as BaseReadHyperdrive_v1_0_14, + ReadWriteHyperdrive_v1_0_14 as BaseReadWriteHyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadHyperdrive_v1_0_14 + BaseReadHyperdrive_v1_0_14, ) {} export class ReadWriteHyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteHyperdrive_v1_0_14 + BaseReadWriteHyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/erc4626.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/erc4626.ts index b4f974a9b..c20ea66fb 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/erc4626.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/erc4626.ts @@ -1,19 +1,24 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadErc4626Hyperdrive_v1_0_14 as BaseReadErc4626Hyperdrive_v1_0_14, + ReadMockErc4626Hyperdrive_v1_0_14 as BaseReadMockErc4626Hyperdrive_v1_0_14, + ReadWriteErc4626Hyperdrive_v1_0_14 as BaseReadWriteErc4626Hyperdrive_v1_0_14, + ReadWriteMockErc4626Hyperdrive_v1_0_14 as BaseReadWriteMockErc4626Hyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadErc4626Hyperdrive_v1_0_14 extends viemReadMixin( - core.ReadErc4626Hyperdrive_v1_0_14 + BaseReadErc4626Hyperdrive_v1_0_14, ) {} export class ReadWriteErc4626Hyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteErc4626Hyperdrive_v1_0_14 + BaseReadWriteErc4626Hyperdrive_v1_0_14, ) {} export class ReadMockErc4626Hyperdrive_v1_0_14 extends viemReadMixin( - core.ReadMockErc4626Hyperdrive_v1_0_14 + BaseReadMockErc4626Hyperdrive_v1_0_14, ) {} export class ReadWriteMockErc4626Hyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteMockErc4626Hyperdrive_v1_0_14 + BaseReadWriteMockErc4626Hyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/ezeth.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/ezeth.ts index 16bee02cc..8437db49c 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/ezeth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/ezeth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadEzEthHyperdrive_v1_0_14 as BaseReadEzEthHyperdrive_v1_0_14, + ReadWriteEzEthHyperdrive_v1_0_14 as BaseReadWriteEzEthHyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadEzEthHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadEzEthHyperdrive_v1_0_14 + BaseReadEzEthHyperdrive_v1_0_14, ) {} export class ReadWriteEzEthHyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteEzEthHyperdrive_v1_0_14 + BaseReadWriteEzEthHyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/lseth.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/lseth.ts index 156393c44..141bb1235 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/lseth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/lseth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadLsEthHyperdrive_v1_0_14 as BaseReadLsEthHyperdrive_v1_0_14, + ReadWriteLsEthHyperdrive_v1_0_14 as BaseReadWriteLsEthHyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadLsEthHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadLsEthHyperdrive_v1_0_14 + BaseReadLsEthHyperdrive_v1_0_14, ) {} export class ReadWriteLsEthHyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteLsEthHyperdrive_v1_0_14 + BaseReadWriteLsEthHyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/morpho.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/morpho.ts deleted file mode 100644 index 0fe6d8da9..000000000 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/morpho.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; -import { viemReadMixin } from "src/viem/viemReadMixin"; - -export class ReadMetaMorphoHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadMetaMorphoHyperdrive_v1_0_14 -) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/reth.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/reth.ts index 06b6fd259..c37efdd12 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/reth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/reth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadREthHyperdrive_v1_0_14 as BaseReadREthHyperdrive_v1_0_14, + ReadWriteREthHyperdrive_v1_0_14 as BaseReadWriteREthHyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadREthHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadREthHyperdrive_v1_0_14 + BaseReadREthHyperdrive_v1_0_14, ) {} export class ReadWriteREthHyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteREthHyperdrive_v1_0_14 + BaseReadWriteREthHyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/steth.ts b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/steth.ts index d51b8e35f..5446c873f 100644 --- a/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/steth.ts +++ b/packages/hyperdrive-viem/src/hyperdrive/v1.0.14/steth.ts @@ -1,11 +1,14 @@ -import * as core from "@delvtech/hyperdrive-js-core/v1.0.14"; +import { + ReadStEthHyperdrive_v1_0_14 as BaseReadStEthHyperdrive_v1_0_14, + ReadWriteStEthHyperdrive_v1_0_14 as BaseReadWriteStEthHyperdrive_v1_0_14, +} from "@delvtech/hyperdrive-js/v1.0.14"; import { viemReadMixin } from "src/viem/viemReadMixin"; import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; export class ReadStEthHyperdrive_v1_0_14 extends viemReadMixin( - core.ReadStEthHyperdrive_v1_0_14 + BaseReadStEthHyperdrive_v1_0_14, ) {} export class ReadWriteStEthHyperdrive_v1_0_14 extends viemReadWriteMixin( - core.ReadWriteStEthHyperdrive_v1_0_14 + BaseReadWriteStEthHyperdrive_v1_0_14, ) {} diff --git a/packages/hyperdrive-viem/src/registry.ts b/packages/hyperdrive-viem/src/registry.ts index 2f3e878d6..de36bd1fc 100644 --- a/packages/hyperdrive-viem/src/registry.ts +++ b/packages/hyperdrive-viem/src/registry.ts @@ -1,15 +1,23 @@ -import * as core from "@delvtech/hyperdrive-js-core/registry/model"; -import { viemReadMixin, ViemReadModelOptions } from "src/viem/viemReadMixin"; -import { viemReadWriteMixin } from "src/viem/viemReadWriteMixin"; +import { + ReadRegistry as BaseReadRegistry, + ReadRegistryOptions as BaseReadRegistryOptions, + ReadWriteRegistry as BaseReadWriteRegistry, + ReadWriteRegistryOptions as BaseReadWriteRegistryOptions, +} from "@delvtech/hyperdrive-js"; +import { ViemReadClientOptions, viemReadMixin } from "src/viem/viemReadMixin"; +import { + ViemReadWriteClientOptions, + viemReadWriteMixin, +} from "src/viem/viemReadWriteMixin"; export type ReadRegistryOptions = - ViemReadModelOptions; + ViemReadClientOptions; -export class ReadRegistry extends viemReadMixin(core.ReadRegistry) {} +export class ReadRegistry extends viemReadMixin(BaseReadRegistry) {} export type ReadWriteRegistryOptions = - ViemReadModelOptions; + ViemReadWriteClientOptions; export class ReadWriteRegistry extends viemReadWriteMixin( - core.ReadWriteRegistry + BaseReadWriteRegistry, ) {} diff --git a/packages/hyperdrive-viem/src/viem/viemReadMixin.ts b/packages/hyperdrive-viem/src/viem/viemReadMixin.ts index 19aa2027e..6a2afd4d7 100644 --- a/packages/hyperdrive-viem/src/viem/viemReadMixin.ts +++ b/packages/hyperdrive-viem/src/viem/viemReadMixin.ts @@ -1,54 +1,46 @@ -import { - createCachedReadContract, - createNetwork, -} from "@delvtech/evm-client-viem"; +import { Drift, Pretty } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; import { Constructor, - ContractFactoryOptions, - ReadContractModelOptions, - ReadModelOptions, -} from "@delvtech/hyperdrive-js-core"; + ReadClient, + ReadClientOptions, +} from "@delvtech/hyperdrive-js"; import { PublicClient } from "viem"; -import { Prettify } from "viem/chains"; -export type ViemReadMixin = new ( - options: ViemReadModelOptions[0]> -) => InstanceType; +// Replace the `drift` option with `publicClient`. +export type ViemReadClientOptions = Pretty< + Omit & { + publicClient: PublicClient; + } +>; -export function viemReadMixin( - Base: T -): ViemReadMixin { +/** + * A mixin that overrides a read client's `drift` option with a `publicClient` + * option. + */ +export function viemReadMixin>( + Base: T, +): ViemReadClientConstructor { return class extends (Base as Constructor) { constructor(...[options, ...restArgs]: any[]) { - const { publicClient, cache, namespace, ...restOptions } = - options as ViemReadModelOptions; - super( - { - contractFactory: (options: ContractFactoryOptions) => { - return createCachedReadContract({ - publicClient, - cache, - namespace, - ...options, - }); - }, - network: createNetwork(publicClient), - ...restOptions, - }, - ...restArgs - ); + const { publicClient, ...restOptions } = + options as ViemReadClientOptions; + + const clientOptions: ReadClientOptions = { + // TODO: Fix type casting needed for conflicting viem versions + drift: new Drift(viemAdapter({ publicClient: publicClient as any })), + ...restOptions, + }; + + super(clientOptions, ...restArgs); } - } as ViemReadMixin; + } as ViemReadClientConstructor; } -export type ReadContractModelConstructor = new ( - ...args: [options: ReadContractModelOptions, ...any[]] -) => any; - -export type ViemReadModelOptions = Prettify< - // Replace the properties the mixin will create with the ones it will use to - // create them. - Omit & { - publicClient: PublicClient; - } ->; +// A `ReadClient` class constructor that takes a `publicClient` instead of a +// `drift` option. +export type ViemReadClientConstructor> = + Constructor< + InstanceType, + [options: ViemReadClientOptions[0]>] + >; diff --git a/packages/hyperdrive-viem/src/viem/viemReadWriteMixin.ts b/packages/hyperdrive-viem/src/viem/viemReadWriteMixin.ts index 6099b746b..73891ce0f 100644 --- a/packages/hyperdrive-viem/src/viem/viemReadWriteMixin.ts +++ b/packages/hyperdrive-viem/src/viem/viemReadWriteMixin.ts @@ -1,58 +1,50 @@ -import { - createCachedReadWriteContract, - createNetwork, -} from "@delvtech/evm-client-viem"; +import { Drift, Pretty, ReadWriteAdapter } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; import { Constructor, - ContractFactoryOptions, - ReadWriteContractModelOptions, - ReadWriteModelOptions, -} from "@delvtech/hyperdrive-js-core"; + ReadWriteClient, + ReadWriteClientOptions, +} from "@delvtech/hyperdrive-js"; import { PublicClient, WalletClient } from "viem"; -import { Prettify } from "viem/chains"; -export type ViemReadWriteMixin = - new ( - options: ViemReadWriteModelOptions[0]> - ) => InstanceType; +// Replace the `drift` option with `publicClient` and `walletClient`. +export type ViemReadWriteClientOptions = + Pretty< + Omit & { + publicClient: PublicClient; + walletClient: WalletClient; + } + >; -export function viemReadWriteMixin( - Base: T -): ViemReadWriteMixin { +export function viemReadWriteMixin>( + Base: T, +): ViemReadWriteClientConstructor { return class extends (Base as Constructor) { constructor(...[options, ...restArgs]: any[]) { - const { publicClient, walletClient, cache, namespace, ...restOptions } = - options as ViemReadWriteModelOptions; - super( - { - contractFactory: (options: ContractFactoryOptions) => { - return createCachedReadWriteContract({ - publicClient, - walletClient, - cache, - namespace, - ...options, - }); - }, - network: createNetwork(publicClient), - ...restOptions, - }, - ...restArgs - ); - } - } as ViemReadWriteMixin; -} + const { publicClient, walletClient, ...restOptions } = + options as ViemReadWriteClientOptions; -export type ReadWriteContractModelConstructor = new ( - ...args: [options: ReadWriteContractModelOptions, ...any[]] -) => any; + const clientOptions: ReadWriteClientOptions = { + // TODO: Fix type casting needed for conflicting viem versions + drift: new Drift( + viemAdapter({ + publicClient: publicClient as any, + walletClient: walletClient as any, + }) as ReadWriteAdapter, + ), + ...restOptions, + }; -export type ViemReadWriteModelOptions = - Prettify< - // Replace the properties the wrapper will create with the ones it will use - // to create them. - Omit & { - publicClient: PublicClient; - walletClient: WalletClient; + super(clientOptions, ...restArgs); } - >; + } as ViemReadWriteClientConstructor; +} + +// A `ReadWriteClient` class constructor that takes a `publicClient` instead of +// a `drift` option. +export type ViemReadWriteClientConstructor< + T extends Constructor, +> = Constructor< + InstanceType, + [options: ViemReadWriteClientOptions[0]>] +>; diff --git a/packages/hyperdrive-viem/tsup.config.ts b/packages/hyperdrive-viem/tsup.config.ts index 5c5a22e9e..07ca24f96 100644 --- a/packages/hyperdrive-viem/tsup.config.ts +++ b/packages/hyperdrive-viem/tsup.config.ts @@ -1,19 +1,9 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: [ - "src/exports/index.ts", - - "src/exports/factory.ts", - "src/exports/hyperdrive.ts", - "src/exports/registry.ts", - "src/exports/token.ts", - - // v1.0.14 - "src/exports/v1.0.14/index.ts", - "src/exports/v1.0.14/hyperdrive.ts", - ], - format: ["esm"], + entry: ["src/exports/index.ts", "src/exports/v1.0.14.ts"], + format: ["esm", "cjs"], + sourcemap: true, dts: true, clean: true, minify: true, diff --git a/yarn.lock b/yarn.lock index 999e88963..7dc49bfaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1479,14 +1479,21 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@delvtech/evm-client-viem@^0.6.3": - version "0.6.3" - resolved "https://registry.yarnpkg.com/@delvtech/evm-client-viem/-/evm-client-viem-0.6.3.tgz#9b45833cd509fe676df5b765126bf6dc3c19413e" - integrity sha512-n2qVLGk7v4WPIHsvwaWTmjVyK1pmkzoFu/jqxXhgHD8ArWCccfXu3LGpRUFT7LVI/ksshW7hnMbvJ3r5mpZuzA== +"@delvtech/drift-viem@^0.0.1-beta.13": + version "0.0.1-beta.13" + resolved "https://registry.yarnpkg.com/@delvtech/drift-viem/-/drift-viem-0.0.1-beta.13.tgz#9f5f38a50f72ea887319ea9480559fa01956c29b" + integrity sha512-i7PUAmUdq7IRRSjQO4JozV7swvYFcED3X4TT1l5RF0kXAD5drbMHIcpIB1D+2N7V2H2MCd2D4PDWDqVu+Hotzg== + +"@delvtech/drift@^0.0.1-beta.11": + version "0.0.1-beta.11" + resolved "https://registry.yarnpkg.com/@delvtech/drift/-/drift-0.0.1-beta.11.tgz#939c2fe6a07dacfa58ba72cf30ba4917fde7e91d" + integrity sha512-YjMLT4xJszZPVfNVK+t7rINwereQ4yMgVOS+VNm0NsMqWmv5vXxjnDGtLHg1RyeZy93DVnxPxTFj0YJxB/4t3g== dependencies: - "@delvtech/evm-client" "0.5.1" + lodash.ismatch "^4.4.0" + lru-cache "^10.0.1" + safe-stable-stringify "^2.5.0" -"@delvtech/evm-client@0.5.1", "@delvtech/evm-client@^0.5.1": +"@delvtech/evm-client@^0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@delvtech/evm-client/-/evm-client-0.5.1.tgz#b69bd04ecb9f1690c1ce94acb7362e4d30b9e1e5" integrity sha512-Ixx7coKko1l9i2CkrrkhzRVnxzf+b3ROEppWvN7Q0+Gx3DLq1lq8oorXjZb5dZhs5K07LtJM7sUJLztxx8BIEA== @@ -15003,6 +15010,11 @@ safe-stable-stringify@^2.1.0: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== +safe-stable-stringify@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"