From 96b9c077ea028ad7bf7984323f443cbd2ca13d89 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Fri, 31 Jan 2025 15:12:26 +0000 Subject: [PATCH 1/6] InterestRateField fixes & improvements - In the env vars, add an auto delegate per branch - Enable the IC strategies - Fetch the min & max interest rates from the contracts - Split delegation modal components in separate files - Start migrating some subgraph-hooks functions into liquity-utils.ts --- .../comps/InterestRateField/DelegateBox.tsx | 189 ++++++ .../comps/InterestRateField/DelegateModal.tsx | 154 +++++ .../InterestRateField/IcStrategiesModal.tsx | 103 ++++ .../InterestRateField/InterestRateField.tsx | 543 ++---------------- .../src/comps/InterestRateField/MiniChart.tsx | 31 + .../src/comps/InterestRateField/ShadowBox.tsx | 29 + frontend/app/src/demo-mode/demo-data.ts | 3 - frontend/app/src/env.ts | 73 +-- frontend/app/src/liquity-utils.ts | 69 ++- frontend/app/src/subgraph-hooks.ts | 63 +- 10 files changed, 643 insertions(+), 614 deletions(-) create mode 100644 frontend/app/src/comps/InterestRateField/DelegateBox.tsx create mode 100644 frontend/app/src/comps/InterestRateField/DelegateModal.tsx create mode 100644 frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx create mode 100644 frontend/app/src/comps/InterestRateField/MiniChart.tsx create mode 100644 frontend/app/src/comps/InterestRateField/ShadowBox.tsx diff --git a/frontend/app/src/comps/InterestRateField/DelegateBox.tsx b/frontend/app/src/comps/InterestRateField/DelegateBox.tsx new file mode 100644 index 000000000..565614287 --- /dev/null +++ b/frontend/app/src/comps/InterestRateField/DelegateBox.tsx @@ -0,0 +1,189 @@ +import type { Delegate } from "@/src/types"; + +import { Amount } from "@/src/comps/Amount/Amount"; +import { fmtnum, formatRedemptionRisk } from "@/src/formatting"; +import { getRedemptionRisk } from "@/src/liquity-math"; +import { riskLevelToStatusMode } from "@/src/uikit-utils"; +import { css } from "@/styled-system/css"; +import { Button, IconCopy, StatusDot, TextButton } from "@liquity2/uikit"; +import { MiniChart } from "./MiniChart"; +import { ShadowBox } from "./ShadowBox"; + +export function DelegateBox({ + delegate, + onSelect, + selectLabel = "Select", +}: { + delegate: Delegate; + onSelect: (delegate: Delegate) => void; + selectLabel: string; +}) { + const delegationRisk = getRedemptionRisk(delegate.interestRate); + return ( + +
+
+
+

+ {delegate.name} +

+
+ + {fmtnum(delegate.interestRate, "pct1z")}% +
+
+
+
+ +
+
+ + {formatRedemptionRisk(delegationRisk)} +
+
+
+
+
+
Interest rate range
+
+ {fmtnum(delegate.interestRateChange[0], "pct2")} + - + {fmtnum(delegate.interestRateChange[1], "pct2")}% +
+
+ {delegate.fee && ( +
+
+ Fees p.a. +
+
+ {fmtnum(delegate.fee, { digits: 4, scale: 100 })}% +
+
+ )} +
+
+
+ + Copy address + + + } + className={css({ + fontSize: 14, + })} + /> +
+
+
+
+
+
+ ); +} diff --git a/frontend/app/src/comps/InterestRateField/DelegateModal.tsx b/frontend/app/src/comps/InterestRateField/DelegateModal.tsx new file mode 100644 index 000000000..6bc2bbd53 --- /dev/null +++ b/frontend/app/src/comps/InterestRateField/DelegateModal.tsx @@ -0,0 +1,154 @@ +import type { Address, CollIndex, Delegate } from "@/src/types"; + +import content from "@/src/content"; +import { useInterestBatchDelegate } from "@/src/liquity-utils"; +import { css } from "@/styled-system/css"; +import { AddressField, AnchorTextButton, Modal } from "@liquity2/uikit"; +import { useState } from "react"; +import { DelegateBox } from "./DelegateBox"; + +const URL_WHAT_IS_DELEGATION = + "https://docs.liquity.org/v2-faq/redemptions-and-delegation#what-is-delegation-of-interest-rates"; + +const DELEGATES_LIST_URL = + "https://docs.liquity.org/v2-faq/redemptions-and-delegation#docs-internal-guid-441d8c3f-7fff-4efa-6319-4ba00d908597"; + +export function DelegateModal({ + collIndex, + onClose, + onSelectDelegate, + visible, +}: { + collIndex: CollIndex; + onClose: () => void; + onSelectDelegate: (delegate: Delegate) => void; + visible: boolean; +}) { + const [delegateAddress, setDelegateAddress] = useState(null); + const [delegateAddressValue, setDelegateAddressValue] = useState(""); + + const delegate = useInterestBatchDelegate(collIndex, delegateAddress); + + return ( + +
+ {content.interestRateField.delegatesModal.intro} +
+
+
{ + event.preventDefault(); + if (delegate.data) { + onSelectDelegate(delegate.data); + } + }} + > + + +
+
+ {delegateAddress + ? ( +
+ {delegate.status === "pending" + ? ( +
+ Loading… +
+ ) + : delegate.status === "error" + ? ( +
+ Error: {delegate.error?.name} +
+ ) + : ( + delegate.data + ? ( + + ) + : ( +
+ The address is not a valid{" "} + . +
+ ) + )} +
+ ) + : ( + <> +
+ Set a valid{" "} + {" "} + address. +
+ +
+ Delegate addresses can be found{" "} + . +
+ + )} +
+
+ ); +} diff --git a/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx new file mode 100644 index 000000000..3586a6056 --- /dev/null +++ b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx @@ -0,0 +1,103 @@ +import type { CollIndex, Delegate } from "@/src/types"; + +import content from "@/src/content"; +import { IC_STRATEGIES } from "@/src/demo-mode"; +import { COLLATERAL_CONTRACTS } from "@/src/env"; +import { css } from "@/styled-system/css"; +import { Modal, TextButton } from "@liquity2/uikit"; +import Image from "next/image"; +import { useState } from "react"; +import { DelegateBox } from "./DelegateBox"; +import { ShadowBox } from "./ShadowBox"; + +import icLogo from "./ic-logo.svg"; + +export function IcStrategiesModal({ + collIndex, + onClose, + onSelectDelegate, + visible, +}: { + collIndex: CollIndex; + onClose: () => void; + onSelectDelegate: (delegate: Delegate) => void; + visible: boolean; +}) { + const collateral = COLLATERAL_CONTRACTS[collIndex]; + + if (!collateral) { + throw new Error(`No collateral for index ${collIndex}`); + } + + const [displayedDelegates, setDisplayedDelegates] = useState(5); + + console.log(collateral.delegateAuto); + + return ( + +
{content.interestRateField.icStrategyModal.title}
+ + + } + visible={visible} + > +
+ {content.interestRateField.icStrategyModal.intro} +
+
+ {IC_STRATEGIES.slice(0, displayedDelegates).map((delegate) => { + return ( + + ); + })} + {displayedDelegates < IC_STRATEGIES.length && ( + + setDisplayedDelegates(displayedDelegates + 5)} + className={css({ + width: "100%", + padding: "24px 0", + justifyContent: "center", + })} + /> + + )} +
+
+ ); +} diff --git a/frontend/app/src/comps/InterestRateField/InterestRateField.tsx b/frontend/app/src/comps/InterestRateField/InterestRateField.tsx index 0e81154c2..f9b79b4b2 100644 --- a/frontend/app/src/comps/InterestRateField/InterestRateField.tsx +++ b/frontend/app/src/comps/InterestRateField/InterestRateField.tsx @@ -1,35 +1,25 @@ import type { Address, CollIndex, Delegate } from "@/src/types"; import type { Dnum } from "dnum"; -import type { ReactNode } from "react"; -import { Amount } from "@/src/comps/Amount/Amount"; import { INTEREST_RATE_DEFAULT, INTEREST_RATE_MAX, INTEREST_RATE_MIN } from "@/src/constants"; import content from "@/src/content"; import { IC_STRATEGIES } from "@/src/demo-mode"; import { jsonStringifyWithDnum } from "@/src/dnum-utils"; import { useInputFieldValue } from "@/src/form-utils"; -import { fmtnum, formatRedemptionRisk } from "@/src/formatting"; -import { getRedemptionRisk } from "@/src/liquity-math"; +import { fmtnum } from "@/src/formatting"; import { useInterestRateChartData } from "@/src/liquity-utils"; -import { useInterestBatchDelegate } from "@/src/subgraph-hooks"; -import { infoTooltipProps, riskLevelToStatusMode } from "@/src/uikit-utils"; +import { infoTooltipProps } from "@/src/uikit-utils"; import { noop } from "@/src/utils"; import { css } from "@/styled-system/css"; import { - AddressField, - AnchorTextButton, - Button, Dropdown, HFlex, - IconCopy, InfoTooltip, InputField, lerp, - Modal, norm, shortenAddress, Slider, - StatusDot, TextButton, } from "@liquity2/uikit"; import { blo } from "blo"; @@ -37,15 +27,16 @@ import * as dn from "dnum"; import Image from "next/image"; import { memo, useId, useState } from "react"; import { match } from "ts-pattern"; - -import icLogo from "./ic-logo.svg"; +import { DelegateModal } from "./DelegateModal"; +import { IcStrategiesModal } from "./IcStrategiesModal"; +import { MiniChart } from "./MiniChart"; export type DelegateMode = "manual" | "strategy" | "delegate"; const DELEGATE_MODES: DelegateMode[] = [ "manual", "delegate", - // "strategy", + "strategy", ]; export const InterestRateField = memo( @@ -92,8 +83,12 @@ export const InterestRateField = memo( const interestChartData = useInterestRateChartData(collIndex); - const interestRateNumber = interestRate && dn.toNumber(dn.mul(interestRate, 100)); - const chartdataPoint = interestChartData.data?.find(({ rate }) => rate === interestRateNumber); + const interestRateNumber = interestRate && dn.toNumber( + dn.mul(interestRate, 100), + ); + const chartdataPoint = interestChartData.data?.find( + ({ rate }) => rate === interestRateNumber, + ); const boldRedeemableInFront = chartdataPoint?.debtInFront ?? dn.from(0, 18); const handleDelegateSelect = (delegate: Delegate) => { @@ -122,7 +117,9 @@ export const InterestRateField = memo( Math.max(0.1, size)) ?? []} + chart={interestChartData.data?.map( + ({ size }) => Math.max(0.1, size), + ) ?? []} onChange={(value) => { fieldValue.setValue(String( Math.round( @@ -146,7 +143,9 @@ export const InterestRateField = memo( address === delegate)?.name + ? IC_STRATEGIES.find( + ({ address }) => address === delegate, + )?.name : "Choose strategy"} onClick={() => { setDelegatePicker("strategy"); @@ -192,15 +191,9 @@ export const InterestRateField = memo( end: (
{ - const modeContent = content.interestRateField.delegateModes[mode]; - return ({ - label: modeContent.label, - secondary: modeContent.secondary, - disabled: mode === "strategy", - disabledReason: "Coming soon", - }); - })} + items={DELEGATE_MODES.map((mode) => ( + content.interestRateField.delegateModes[mode] + ))} menuWidth={300} menuPlacement="end" onSelect={(index) => { @@ -225,12 +218,16 @@ export const InterestRateField = memo( ? fmtnum(boldInterestPerYear) : "−"} BOLD / year
- + ), end: ( - {"Redeemable before you: "} + Redeemable before you:{" "} - { setDelegatePicker(null); }} - title={ -
-
- {content.interestRateField.icStrategyModal.title} -
- -
- } - visible={delegatePicker === "strategy"} - > - -
- + { setDelegatePicker(null); }} - title={content.interestRateField.delegatesModal.title} - visible={delegatePicker === "delegate"} - > - - + onSelectDelegate={handleDelegateSelect} + visible={delegatePicker === "strategy"} + /> ); }, @@ -342,438 +308,3 @@ export const InterestRateField = memo( jsonStringifyWithDnum(prev) === jsonStringifyWithDnum(next) ), ); - -function CustomDelegateModalContent({ - collIndex, - intro, - onSelectDelegate, -}: { - collIndex: CollIndex; - chooseLabel: string; - intro: ReactNode; - onSelectDelegate: (delegate: Delegate) => void; -}) { - const [delegateAddress, setDelegateAddress] = useState(null); - const [delegateAddressValue, setDelegateAddressValue] = useState(""); - - const delegate = useInterestBatchDelegate(collIndex, delegateAddress); - - return ( - <> -
- {intro} -
-
-
{ - event.preventDefault(); - if (delegate.data) { - onSelectDelegate(delegate.data); - } - }} - > - - -
- -
- {delegateAddress - ? ( -
- {delegate.status === "pending" - ? ( -
- Loading… -
- ) - : delegate.status === "error" - ? ( -
- Error: {delegate.error?.name} -
- ) - : ( - delegate.data - ? ( - - ) - : ( -
- The address is not a valid{" "} - . -
- ) - )} -
- ) - : ( - <> -
- Set a valid{" "} - {" "} - address. -
- -
- Delegate addresses can be found{" "} - . -
- - )} -
- - ); -} - -function DelegatesModalContent({ - collIndex: _collIndex, - delegates = [], - chooseLabel: _chooseLabel, - intro, - onSelectDelegate: _onSelectDelegate, -}: { - collIndex: CollIndex; - delegates?: Delegate[]; - chooseLabel: string; - intro: ReactNode; - onSelectDelegate: (delegate: Delegate) => void; -}) { - const [displayedDelegates, setDisplayedDelegates] = useState(5); - return ( - <> -
- {intro} -
- -
- {delegates.slice(0, displayedDelegates).map((delegate) => { - return ( - - ); - })} - {displayedDelegates < delegates.length && ( - - setDisplayedDelegates(displayedDelegates + 5)} - className={css({ - width: "100%", - padding: "24px 0", - justifyContent: "center", - })} - /> - - )} -
- - ); -} - -const MiniChart = memo(function MiniChart({ - size = "small", -}: { - size?: "small" | "medium"; -}) { - return ( - size === "medium" - ? ( - - - - ) - : ( - - - - ) - ); -}); - -function ShadowBox({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} - -function DelegateBox({ - delegate, - onSelect, - selectLabel = "Select", -}: { - delegate: Delegate; - onSelect: (delegate: Delegate) => void; - selectLabel: string; -}) { - const delegationRisk = getRedemptionRisk(delegate.interestRate); - return ( - -
-
-
-

- {delegate.name} -

-
- - {fmtnum(delegate.interestRate, "pct1z")}% -
-
-
-
- -
-
- - {formatRedemptionRisk(delegationRisk)} -
-
-
-
-
-
Interest rate range
-
- {fmtnum(delegate.interestRateChange[0], "pct2")} - - - {fmtnum(delegate.interestRateChange[1], "pct2")}% -
-
- {delegate.fee && ( -
-
- Fees p.a. -
-
- {fmtnum(delegate.fee, { digits: 4, scale: 100 })}% -
-
- )} -
-
-
- - Copy address - - - } - className={css({ - fontSize: 14, - })} - /> -
-
-
-
-
-
- ); -} diff --git a/frontend/app/src/comps/InterestRateField/MiniChart.tsx b/frontend/app/src/comps/InterestRateField/MiniChart.tsx new file mode 100644 index 000000000..0991b3a32 --- /dev/null +++ b/frontend/app/src/comps/InterestRateField/MiniChart.tsx @@ -0,0 +1,31 @@ +import { memo } from "react"; + +export const MiniChart = memo(function MiniChart({ + size = "small", +}: { + size?: "small" | "medium"; +}) { + return ( + size === "medium" + ? ( + + + + ) + : ( + + + + ) + ); +}); diff --git a/frontend/app/src/comps/InterestRateField/ShadowBox.tsx b/frontend/app/src/comps/InterestRateField/ShadowBox.tsx new file mode 100644 index 000000000..61afb0c1a --- /dev/null +++ b/frontend/app/src/comps/InterestRateField/ShadowBox.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from "react"; + +import { css } from "@/styled-system/css"; + +export function ShadowBox({ + children, +}: { + children: ReactNode; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/app/src/demo-mode/demo-data.ts b/frontend/app/src/demo-mode/demo-data.ts index 0ac1e4e7a..b29e7a010 100644 --- a/frontend/app/src/demo-mode/demo-data.ts +++ b/frontend/app/src/demo-mode/demo-data.ts @@ -394,6 +394,3 @@ export const IC_STRATEGIES: Delegate[] = [ fee: dn.from(0.00003, 18), }, ]; - -// Delegates + IC strategies -export const DELEGATES_FULL = DELEGATES.concat(IC_STRATEGIES); diff --git a/frontend/app/src/env.ts b/frontend/app/src/env.ts index caec9ba35..21eff90c9 100644 --- a/frontend/app/src/env.ts +++ b/frontend/app/src/env.ts @@ -13,6 +13,25 @@ export const CollateralSymbolSchema = v.union([ v.literal("WSTETH"), ]); +function vCollateralEnvVars(collIndex: CollIndex) { + const prefix = `COLL_${collIndex}`; + return v.object({ + [`${prefix}_CONTRACT_ACTIVE_POOL`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_BORROWER_OPERATIONS`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_COLL_SURPLUS_POOL`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_COLL_TOKEN`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_DEFAULT_POOL`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_LEVERAGE_ZAPPER`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_PRICE_FEED`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_SORTED_TROVES`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_STABILITY_POOL`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_TROVE_MANAGER`]: v.optional(vAddress()), + [`${prefix}_CONTRACT_TROVE_NFT`]: v.optional(vAddress()), + [`${prefix}_DELEGATE_AUTO`]: v.optional(vAddress()), + [`${prefix}_TOKEN_ID`]: v.optional(CollateralSymbolSchema), + }); +} + export const EnvSchema = v.pipe( v.object({ ACCOUNT_SCREEN: v.optional(vEnvFlag(), "false"), @@ -109,7 +128,6 @@ export const EnvSchema = v.pipe( `Invalid CONTRACTS_COMMIT_URL (must contain "{commit}")`, ), ), - DELEGATE_AUTO: vAddress(), DEMO_MODE: v.optional(vEnvFlag(), "false"), DEPLOYMENT_FLAVOR: v.pipe( v.optional(v.string(), ""), @@ -140,44 +158,9 @@ export const EnvSchema = v.pipe( CONTRACT_MULTI_TROVE_GETTER: vAddress(), CONTRACT_WETH: vAddress(), - COLL_0_CONTRACT_ACTIVE_POOL: v.optional(vAddress()), - COLL_0_CONTRACT_BORROWER_OPERATIONS: v.optional(vAddress()), - COLL_0_CONTRACT_COLL_SURPLUS_POOL: v.optional(vAddress()), - COLL_0_CONTRACT_COLL_TOKEN: v.optional(vAddress()), - COLL_0_CONTRACT_DEFAULT_POOL: v.optional(vAddress()), - COLL_0_CONTRACT_LEVERAGE_ZAPPER: v.optional(vAddress()), - COLL_0_CONTRACT_PRICE_FEED: v.optional(vAddress()), - COLL_0_CONTRACT_SORTED_TROVES: v.optional(vAddress()), - COLL_0_CONTRACT_STABILITY_POOL: v.optional(vAddress()), - COLL_0_CONTRACT_TROVE_MANAGER: v.optional(vAddress()), - COLL_0_CONTRACT_TROVE_NFT: v.optional(vAddress()), - COLL_0_TOKEN_ID: v.optional(CollateralSymbolSchema), - - COLL_1_CONTRACT_ACTIVE_POOL: v.optional(vAddress()), - COLL_1_CONTRACT_BORROWER_OPERATIONS: v.optional(vAddress()), - COLL_1_CONTRACT_COLL_SURPLUS_POOL: v.optional(vAddress()), - COLL_1_CONTRACT_COLL_TOKEN: v.optional(vAddress()), - COLL_1_CONTRACT_DEFAULT_POOL: v.optional(vAddress()), - COLL_1_CONTRACT_LEVERAGE_ZAPPER: v.optional(vAddress()), - COLL_1_CONTRACT_PRICE_FEED: v.optional(vAddress()), - COLL_1_CONTRACT_SORTED_TROVES: v.optional(vAddress()), - COLL_1_CONTRACT_STABILITY_POOL: v.optional(vAddress()), - COLL_1_CONTRACT_TROVE_MANAGER: v.optional(vAddress()), - COLL_1_CONTRACT_TROVE_NFT: v.optional(vAddress()), - COLL_1_TOKEN_ID: v.optional(CollateralSymbolSchema), - - COLL_2_CONTRACT_ACTIVE_POOL: v.optional(vAddress()), - COLL_2_CONTRACT_BORROWER_OPERATIONS: v.optional(vAddress()), - COLL_2_CONTRACT_COLL_SURPLUS_POOL: v.optional(vAddress()), - COLL_2_CONTRACT_COLL_TOKEN: v.optional(vAddress()), - COLL_2_CONTRACT_DEFAULT_POOL: v.optional(vAddress()), - COLL_2_CONTRACT_LEVERAGE_ZAPPER: v.optional(vAddress()), - COLL_2_CONTRACT_PRICE_FEED: v.optional(vAddress()), - COLL_2_CONTRACT_SORTED_TROVES: v.optional(vAddress()), - COLL_2_CONTRACT_STABILITY_POOL: v.optional(vAddress()), - COLL_2_CONTRACT_TROVE_MANAGER: v.optional(vAddress()), - COLL_2_CONTRACT_TROVE_NFT: v.optional(vAddress()), - COLL_2_TOKEN_ID: v.optional(CollateralSymbolSchema), + ...vCollateralEnvVars(0).entries, + ...vCollateralEnvVars(1).entries, + ...vCollateralEnvVars(2).entries, }), v.transform((data) => { const env = { ...data }; @@ -200,12 +183,13 @@ export const EnvSchema = v.pipe( const collateralContracts: Array<{ collIndex: CollIndex; - symbol: v.InferOutput; contracts: Record; + delegateAuto: Address | null; + symbol: v.InferOutput; }> = []; for (const index of Array(10).keys()) { - const collEnvName = `COLL_${index}`; + const collEnvName = `COLL_${index}` as const; const contracts: Partial> = {}; for (const name of contractsEnvNames) { @@ -231,6 +215,7 @@ export const EnvSchema = v.pipe( collateralContracts[index] = { collIndex: index, contracts: contracts as Record, + delegateAuto: (env[`${collEnvName}_DELEGATE_AUTO` as keyof typeof env] ?? null) as Address | null, symbol: env[`${collEnvName}_TOKEN_ID` as keyof typeof env] as v.InferOutput, }; } @@ -281,7 +266,6 @@ const parsedEnv = v.safeParse(EnvSchema, { ?? process.env.CONTRACTS_COMMIT_HASH_FROM_BUILD ), CONTRACTS_COMMIT_URL: process.env.NEXT_PUBLIC_CONTRACTS_COMMIT_URL, - DELEGATE_AUTO: process.env.NEXT_PUBLIC_DELEGATE_AUTO, DEMO_MODE: process.env.NEXT_PUBLIC_DEMO_MODE, DEPLOYMENT_FLAVOR: process.env.NEXT_PUBLIC_DEPLOYMENT_FLAVOR, KNOWN_INITIATIVES_URL: process.env.NEXT_PUBLIC_KNOWN_INITIATIVES_URL, @@ -306,6 +290,10 @@ const parsedEnv = v.safeParse(EnvSchema, { COLL_1_TOKEN_ID: process.env.NEXT_PUBLIC_COLL_1_TOKEN_ID, COLL_2_TOKEN_ID: process.env.NEXT_PUBLIC_COLL_2_TOKEN_ID, + COLL_0_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_0_DELEGATE_AUTO, + COLL_1_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_1_DELEGATE_AUTO, + COLL_2_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_2_DELEGATE_AUTO, + COLL_0_CONTRACT_ACTIVE_POOL: process.env.NEXT_PUBLIC_COLL_0_CONTRACT_ACTIVE_POOL, COLL_0_CONTRACT_BORROWER_OPERATIONS: process.env.NEXT_PUBLIC_COLL_0_CONTRACT_BORROWER_OPERATIONS, COLL_0_CONTRACT_COLL_SURPLUS_POOL: process.env.NEXT_PUBLIC_COLL_0_CONTRACT_COLL_SURPLUS_POOL, @@ -387,7 +375,6 @@ export const { CONTRACT_LUSD_TOKEN, CONTRACT_MULTI_TROVE_GETTER, CONTRACT_WETH, - DELEGATE_AUTO, DEMO_MODE, DEPLOYMENT_FLAVOR, KNOWN_INITIATIVES_URL, diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index a5fdb0a7d..fd795721a 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -1,5 +1,5 @@ import type { Contracts } from "@/src/contracts"; -import type { StabilityPoolDepositQuery } from "@/src/graphql/graphql"; +import type { StabilityPoolDepositQuery as StabilityPoolDepositQueryType } from "@/src/graphql/graphql"; import type { CollIndex, Dnum, @@ -28,14 +28,15 @@ import { useStabilityPoolEpochScale, } from "@/src/subgraph-hooks"; import { isCollIndex, isTroveId } from "@/src/types"; -import { COLLATERALS, isAddress } from "@liquity2/uikit"; +import { COLLATERALS, isAddress, shortenAddress } from "@liquity2/uikit"; import { useQuery } from "@tanstack/react-query"; import * as dn from "dnum"; import { useMemo } from "react"; import * as v from "valibot"; import { encodeAbiParameters, keccak256, parseAbiParameters } from "viem"; -import { useBalance, useReadContract, useReadContracts } from "wagmi"; +import { useBalance, useConfig as useWagmiConfig, useReadContract, useReadContracts } from "wagmi"; import { readContract } from "wagmi/actions"; +import { graphQuery, InterestBatchQuery } from "./subgraph-queries"; export function shortenTroveId(troveId: TroveId, chars = 8) { return troveId.length < chars * 2 + 2 @@ -174,7 +175,9 @@ export function useEarnPosition( } function earnPositionFromGraph( - spDeposit: NonNullable, + spDeposit: NonNullable< + StabilityPoolDepositQueryType["stabilityPoolDeposit"] + >, rewards: { bold: Dnum; coll: Dnum }, ): PositionEarn { const collIndex = spDeposit.collateral.collIndex; @@ -643,3 +646,61 @@ export function useLoan(collIndex: CollIndex, troveId: TroveId): UseQueryResult< }, }; } + +export function useInterestBatchDelegate( + collIndex: null | CollIndex, + address: null | Address, +) { + const BorrowerOperations = getCollateralContract(collIndex, "BorrowerOperations"); + if (!BorrowerOperations) { + throw new Error(`Invalid collateral index: ${collIndex}`); + } + + const wagmiConfig = useWagmiConfig(); + + const id = address && collIndex !== null + ? `${collIndex}:${address.toLowerCase()}` + : null; + + let queryFn = async () => { + if (!id || !address) return null; + + const [{ interestBatch: batch }, batchFromChain] = await Promise.all([ + graphQuery(InterestBatchQuery, { id }), + readContract(wagmiConfig, { + ...BorrowerOperations, + functionName: "getInterestBatchManager", + args: [address], + }), + ]); + + if (!isAddress(batch?.batchManager)) { + return null; + } + + return { + id: batch.batchManager, + address: batch.batchManager, + name: shortenAddress(batch.batchManager, 4), + interestRate: dnum18(batch.annualInterestRate), + boldAmount: dnum18(batch.debt), + interestRateChange: [ + dnum18(batchFromChain.minInterestRate), + dnum18(batchFromChain.maxInterestRate), + ], + fee: dnum18(batch.annualManagementFee), + + // not available in the subgraph yet + followers: 0, + lastDays: 0, + redemptions: dnum18(0), + }; + }; + + return useQuery({ + queryKey: ["InterestBatch", id], + queryFn, + refetchInterval: DATA_REFRESH_INTERVAL, + enabled: id !== null, + }); +} diff --git a/frontend/app/src/subgraph-hooks.ts b/frontend/app/src/subgraph-hooks.ts index 9fd269891..eefd92355 100644 --- a/frontend/app/src/subgraph-hooks.ts +++ b/frontend/app/src/subgraph-hooks.ts @@ -1,9 +1,11 @@ +// TODO: Remove this file and move the hooks into liquity-*.ts files instead, +// since some need to do more than just fetch data from the subgraph. + import type { - InterestBatchQuery as InterestBatchQueryType, StabilityPoolDepositQuery as StabilityPoolDepositQueryType, TrovesByAccountQuery as TrovesByAccountQueryType, } from "@/src/graphql/graphql"; -import type { Address, CollIndex, Delegate, PositionEarn, PositionLoanCommitted, PrefixedTroveId } from "@/src/types"; +import type { Address, CollIndex, PositionEarn, PositionLoanCommitted, PrefixedTroveId } from "@/src/types"; import { DATA_REFRESH_INTERVAL } from "@/src/constants"; import { ACCOUNT_POSITIONS } from "@/src/demo-mode"; @@ -11,9 +13,8 @@ import { dnum18 } from "@/src/dnum-utils"; import { DEMO_MODE } from "@/src/env"; import { isCollIndex, isPositionLoanCommitted, isPrefixedtroveId, isTroveId } from "@/src/types"; import { sleep } from "@/src/utils"; -import { isAddress, shortenAddress } from "@liquity2/uikit"; +import { isAddress } from "@liquity2/uikit"; import { useQuery } from "@tanstack/react-query"; -import * as dn from "dnum"; import { useCallback } from "react"; import { AllInterestRateBracketsQuery, @@ -22,7 +23,6 @@ import { GovernanceStats, GovernanceUser, graphQuery, - InterestBatchQuery, StabilityPoolDepositQuery, StabilityPoolDepositsByAccountQuery, StabilityPoolEpochScaleQuery, @@ -105,59 +105,6 @@ export function useLoansByAccount( }); } -function subgraphBatchToDelegate( - batch: NonNullable< - InterestBatchQueryType["interestBatch"] - >, -): Delegate { - if (!isAddress(batch.batchManager)) { - throw new Error(`Invalid batch manager: ${batch.batchManager}`); - } - return { - id: batch.batchManager, - address: batch.batchManager, - name: shortenAddress(batch.batchManager, 4), - interestRate: dnum18(batch.annualInterestRate), - boldAmount: dnum18(batch.debt), - interestRateChange: [dn.from(0.015), dn.from(0.05)], - fee: dnum18(batch.annualManagementFee), - - // not available in the subgraph yet - followers: 0, - lastDays: 0, - redemptions: dnum18(0), - }; -} - -export function useInterestBatchDelegate( - collIndex: null | CollIndex, - address: null | Address, - options?: Options, -) { - const id = address && collIndex !== null - ? `${collIndex}:${address.toLowerCase()}` - : null; - - let queryFn = async () => { - if (!id) return null; - const { interestBatch } = await graphQuery(InterestBatchQuery, { id }); - return isAddress(interestBatch?.batchManager) - ? subgraphBatchToDelegate(interestBatch) - : null; - }; - - if (DEMO_MODE) { - queryFn = async () => null; - } - - return useQuery({ - queryKey: ["InterestBatch", id], - queryFn, - ...prepareOptions(options), - enabled: id !== null, - }); -} - export function useLoanById( id?: null | PrefixedTroveId, options?: Options, From 3f31ef57bb99a43cb3f0844067700a0ab9043c91 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Thu, 6 Feb 2025 17:32:05 +0000 Subject: [PATCH 2/6] Collateral => Branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `collIndex` is now `branchId` - A new type, Branch, has been added. It replaces what used to be called a collateral (which now only corresponds to the branch collateral, when used). Branches objects can now contain additional data rather than contract addresses only. - Branch.strategies corresponds to the interest rate delegate strategies - getBranch() now throws if the branch doesn’t exist, so it can be called without checking the returned value + throwing an error if null. - getCollToken() follows the same behaviour for errors. - COLL_*_DELEGATE_AUTO is now COLL_*_IC_STRATEGIES and allows to define multiple strategies. --- frontend/app/src/comps/About/About.tsx | 10 +- .../EarnPositionSummary.tsx | 10 +- .../comps/InterestRateField/DelegateModal.tsx | 8 +- .../InterestRateField/IcStrategiesModal.tsx | 16 +- .../InterestRateField/InterestRateField.tsx | 12 +- .../comps/Positions/PositionCardBorrow.tsx | 8 +- .../src/comps/Positions/PositionCardEarn.tsx | 10 +- .../comps/Positions/PositionCardLeverage.tsx | 10 +- .../src/comps/Positions/PositionCardLoan.tsx | 6 +- frontend/app/src/contracts.ts | 45 +++-- frontend/app/src/demo-mode/demo-data.ts | 12 +- frontend/app/src/env.ts | 45 +++-- frontend/app/src/liquity-leverage.ts | 34 ++-- frontend/app/src/liquity-stability-pool.ts | 12 +- frontend/app/src/liquity-utils.ts | 180 +++++++++++------- .../screens/AccountScreen/AccountScreen.tsx | 7 +- .../src/screens/BorrowScreen/BorrowScreen.tsx | 46 ++--- .../screens/EarnPoolScreen/EarnPoolScreen.tsx | 37 ++-- .../EarnPoolScreen/PanelClaimRewards.tsx | 10 +- .../EarnPoolScreen/PanelUpdateDeposit.tsx | 14 +- .../EarnPoolsListScreen.tsx | 26 ++- .../app/src/screens/HomeScreen/HomeScreen.tsx | 29 +-- .../screens/LeverageScreen/LeverageScreen.tsx | 69 +++---- .../app/src/screens/LoanScreen/LoanScreen.tsx | 20 +- .../src/screens/LoanScreen/LoanScreenCard.tsx | 2 +- .../screens/LoanScreen/PanelClosePosition.tsx | 22 +-- .../LoanScreen/PanelUpdateBorrowPosition.tsx | 4 +- .../PanelUpdateLeveragePosition.tsx | 4 +- .../screens/LoanScreen/PanelUpdateRate.tsx | 6 +- .../screens/TransactionsScreen/LoanCard.tsx | 8 +- frontend/app/src/services/Ethereum.tsx | 6 +- frontend/app/src/subgraph-hooks.ts | 96 ++++++---- .../src/tx-flows/claimCollateralSurplus.tsx | 30 ++- .../app/src/tx-flows/closeLoanPosition.tsx | 57 ++---- .../app/src/tx-flows/earnClaimRewards.tsx | 17 +- frontend/app/src/tx-flows/earnDeposit.tsx | 15 +- frontend/app/src/tx-flows/earnWithdraw.tsx | 16 +- .../app/src/tx-flows/openBorrowPosition.tsx | 72 +++---- .../app/src/tx-flows/openLeveragePosition.tsx | 56 ++---- frontend/app/src/tx-flows/shared.ts | 6 +- .../app/src/tx-flows/updateBorrowPosition.tsx | 120 +++++------- .../src/tx-flows/updateLeveragePosition.tsx | 83 +++----- .../src/tx-flows/updateLoanInterestRate.tsx | 44 ++--- frontend/app/src/types.ts | 19 +- frontend/app/src/valibot-utils.ts | 6 +- 45 files changed, 633 insertions(+), 732 deletions(-) diff --git a/frontend/app/src/comps/About/About.tsx b/frontend/app/src/comps/About/About.tsx index 156a3f00a..7f7f2339c 100644 --- a/frontend/app/src/comps/About/About.tsx +++ b/frontend/app/src/comps/About/About.tsx @@ -46,14 +46,14 @@ function getEnvGroups() { } const branches: { - collIndex: number; + branchId: number; symbol: string; contracts: [string, string][]; }[] = []; - for (const { collIndex, symbol, contracts } of config.COLLATERAL_CONTRACTS) { + for (const { branchId, symbol, contracts } of config.COLLATERAL_CONTRACTS) { branches.push({ - collIndex, + branchId, symbol, contracts: Object .entries(contracts) @@ -279,9 +279,9 @@ export function About({ children }: { children: ReactNode }) { } entries={envGroups.contracts} /> - {envGroups.branches.map(({ collIndex, symbol, contracts }) => ( + {envGroups.branches.map(({ branchId, symbol, contracts }) => ( diff --git a/frontend/app/src/comps/EarnPositionSummary/EarnPositionSummary.tsx b/frontend/app/src/comps/EarnPositionSummary/EarnPositionSummary.tsx index 66f085cad..18f9bc6df 100644 --- a/frontend/app/src/comps/EarnPositionSummary/EarnPositionSummary.tsx +++ b/frontend/app/src/comps/EarnPositionSummary/EarnPositionSummary.tsx @@ -1,4 +1,4 @@ -import type { CollIndex, PositionEarn } from "@/src/types"; +import type { BranchId, PositionEarn } from "@/src/types"; import type { ReactNode } from "react"; import { Amount } from "@/src/comps/Amount/Amount"; @@ -11,22 +11,22 @@ import * as dn from "dnum"; import Link from "next/link"; export function EarnPositionSummary({ - collIndex, + branchId, prevEarnPosition, earnPosition, linkToScreen, title, txPreviewMode, }: { - collIndex: CollIndex; + branchId: BranchId; prevEarnPosition?: PositionEarn | null; earnPosition: PositionEarn | null; linkToScreen?: boolean; title?: ReactNode; txPreviewMode?: boolean; }) { - const collToken = getCollToken(collIndex); - const earnPool = useEarnPool(collIndex); + const collToken = getCollToken(branchId); + const earnPool = useEarnPool(branchId); const { totalDeposited: totalPoolDeposit } = earnPool.data; diff --git a/frontend/app/src/comps/InterestRateField/DelegateModal.tsx b/frontend/app/src/comps/InterestRateField/DelegateModal.tsx index 6bc2bbd53..b524c1784 100644 --- a/frontend/app/src/comps/InterestRateField/DelegateModal.tsx +++ b/frontend/app/src/comps/InterestRateField/DelegateModal.tsx @@ -1,4 +1,4 @@ -import type { Address, CollIndex, Delegate } from "@/src/types"; +import type { Address, BranchId, Delegate } from "@/src/types"; import content from "@/src/content"; import { useInterestBatchDelegate } from "@/src/liquity-utils"; @@ -14,12 +14,12 @@ const DELEGATES_LIST_URL = "https://docs.liquity.org/v2-faq/redemptions-and-delegation#docs-internal-guid-441d8c3f-7fff-4efa-6319-4ba00d908597"; export function DelegateModal({ - collIndex, + branchId, onClose, onSelectDelegate, visible, }: { - collIndex: CollIndex; + branchId: BranchId; onClose: () => void; onSelectDelegate: (delegate: Delegate) => void; visible: boolean; @@ -27,7 +27,7 @@ export function DelegateModal({ const [delegateAddress, setDelegateAddress] = useState(null); const [delegateAddressValue, setDelegateAddressValue] = useState(""); - const delegate = useInterestBatchDelegate(collIndex, delegateAddress); + const delegate = useInterestBatchDelegate(branchId, delegateAddress); return ( void; onSelectDelegate: (delegate: Delegate) => void; visible: boolean; }) { - const collateral = COLLATERAL_CONTRACTS[collIndex]; - - if (!collateral) { - throw new Error(`No collateral for index ${collIndex}`); - } + const collateral = getCollateralContracts(branchId); const [displayedDelegates, setDisplayedDelegates] = useState(5); - console.log(collateral.delegateAuto); + console.log(collateral); return ( { setDelegatePicker(null); }} @@ -294,7 +294,7 @@ export const InterestRateField = memo( visible={delegatePicker === "delegate"} /> { setDelegatePicker(null); }} diff --git a/frontend/app/src/comps/Positions/PositionCardBorrow.tsx b/frontend/app/src/comps/Positions/PositionCardBorrow.tsx index 7677ef12a..221030636 100644 --- a/frontend/app/src/comps/Positions/PositionCardBorrow.tsx +++ b/frontend/app/src/comps/Positions/PositionCardBorrow.tsx @@ -19,7 +19,7 @@ import { CardRow, CardRows } from "./shared"; export function PositionCardBorrow({ batchManager, debt, - collIndex, + branchId, deposit, interestRate, statusTag, @@ -28,7 +28,7 @@ export function PositionCardBorrow({ & Pick< PositionLoanCommitted, | "batchManager" - | "collIndex" + | "branchId" | "deposit" | "interestRate" | "troveId" @@ -38,7 +38,7 @@ export function PositionCardBorrow({ statusTag?: ReactNode; }) { - const token = getCollToken(collIndex); + const token = getCollToken(branchId); const collateralPriceUsd = usePrice(token?.symbol ?? null); const ltv = debt && collateralPriceUsd.data @@ -59,7 +59,7 @@ export function PositionCardBorrow({ return ( diff --git a/frontend/app/src/comps/Positions/PositionCardEarn.tsx b/frontend/app/src/comps/Positions/PositionCardEarn.tsx index ae845b5ed..16f24383c 100644 --- a/frontend/app/src/comps/Positions/PositionCardEarn.tsx +++ b/frontend/app/src/comps/Positions/PositionCardEarn.tsx @@ -10,15 +10,15 @@ import { CardRow, CardRows } from "./shared"; export function PositionCardEarn({ owner, - collIndex, + branchId, }: Pick< PositionEarn, | "owner" - | "collIndex" + | "branchId" >) { - const token = getCollToken(collIndex); - const earnPool = useEarnPool(collIndex); - const earnPosition = useEarnPosition(collIndex, owner ?? null); + const token = getCollToken(branchId); + const earnPool = useEarnPool(branchId); + const earnPosition = useEarnPosition(branchId, owner ?? null); return ( diff --git a/frontend/app/src/comps/Positions/PositionCardLoan.tsx b/frontend/app/src/comps/Positions/PositionCardLoan.tsx index ea6561352..375c3bcde 100644 --- a/frontend/app/src/comps/Positions/PositionCardLoan.tsx +++ b/frontend/app/src/comps/Positions/PositionCardLoan.tsx @@ -12,7 +12,7 @@ export function PositionCardLoan( | "type" | "batchManager" | "borrowed" - | "collIndex" + | "branchId" | "deposit" | "interestRate" | "status" @@ -21,9 +21,9 @@ export function PositionCardLoan( ) { const storedState = useStoredState(); - const loan = useLoan(props.collIndex, props.troveId); + const loan = useLoan(props.branchId, props.troveId); - const prefixedTroveId = getPrefixedTroveId(props.collIndex, props.troveId); + const prefixedTroveId = getPrefixedTroveId(props.branchId, props.troveId); const loanMode = storedState.loanModes[prefixedTroveId] ?? props.type; const Card = loanMode === "multiply" ? PositionCardLeverage : PositionCardBorrow; diff --git a/frontend/app/src/contracts.ts b/frontend/app/src/contracts.ts index d1d397e28..7550d94e2 100644 --- a/frontend/app/src/contracts.ts +++ b/frontend/app/src/contracts.ts @@ -1,4 +1,4 @@ -import type { CollateralSymbol, CollIndex } from "@/src/types"; +import type { Branch, BranchId, CollateralSymbol } from "@/src/types"; import type { Address } from "@liquity2/uikit"; import { ActivePool } from "@/src/abi/ActivePool"; @@ -91,18 +91,12 @@ type Contract = { address: Address; }; -type CollateralContracts = { +export type BranchContracts = { [K in CollateralContractName]: Contract; }; -type Collaterals = Array<{ - collIndex: CollIndex; - contracts: CollateralContracts; - symbol: CollateralSymbol; -}>; - export type Contracts = ProtocolContractMap & { - collaterals: Collaterals; + branches: Branch[]; }; const CONTRACTS: Contracts = { @@ -117,8 +111,9 @@ const CONTRACTS: Contracts = { MultiTroveGetter: { abi: abis.MultiTroveGetter, address: CONTRACT_MULTI_TROVE_GETTER }, WETH: { abi: abis.WETH, address: CONTRACT_WETH }, - collaterals: COLLATERAL_CONTRACTS.map(({ collIndex, symbol, contracts }) => ({ - collIndex, + branches: COLLATERAL_CONTRACTS.map(({ branchId, symbol, contracts }) => ({ + id: branchId, + branchId, symbol, contracts: { ActivePool: { address: contracts.ACTIVE_POOL, abi: abis.ActivePool }, @@ -153,23 +148,31 @@ export function getProtocolContract( return CONTRACTS[name]; } +export function getCollateralContracts(branchIdOrSymbol: null): null; +export function getCollateralContracts(branchIdOrSymbol: CollateralSymbol | BranchId): BranchContracts; +export function getCollateralContracts( + branchIdOrSymbol: CollateralSymbol | BranchId | null, +): BranchContracts | null; export function getCollateralContracts( - collIndexOrSymbol: CollateralSymbol | CollIndex | null, -): CollateralContracts | null { - if (collIndexOrSymbol === null) { + branchIdOrSymbol: CollateralSymbol | BranchId | null, +): BranchContracts | null { + if (branchIdOrSymbol === null) { return null; } - const { collaterals } = getContracts(); - const collateral = typeof collIndexOrSymbol === "number" - ? collaterals[collIndexOrSymbol] - : collaterals.find((c) => c.symbol === collIndexOrSymbol); - return collateral?.contracts ?? null; + const { branches } = getContracts(); + const collateral = typeof branchIdOrSymbol === "number" + ? branches[branchIdOrSymbol] + : branches.find((c) => c.symbol === branchIdOrSymbol); + if (collateral?.contracts) { + return collateral.contracts; + } + throw new Error(`No collateral for index or symbol ${branchIdOrSymbol}`); } export function getCollateralContract( - collIndexOrSymbol: CollateralSymbol | CollIndex | null, + branchIdOrSymbol: CollateralSymbol | BranchId | null, contractName: CN, ): Contract | null { - const contracts = getCollateralContracts(collIndexOrSymbol); + const contracts = getCollateralContracts(branchIdOrSymbol); return contracts?.[contractName] ?? null; } diff --git a/frontend/app/src/demo-mode/demo-data.ts b/frontend/app/src/demo-mode/demo-data.ts index b29e7a010..52b8b2249 100644 --- a/frontend/app/src/demo-mode/demo-data.ts +++ b/frontend/app/src/demo-mode/demo-data.ts @@ -38,7 +38,7 @@ export const ACCOUNT_POSITIONS: Exclude[] = [ deposit: dn.from(5.5, 18), interestRate: dn.from(0.067, 18), troveId: "0x01", - collIndex: 1, + branchId: 1, batchManager: null, createdAt: getTime(), updatedAt: getTime(), @@ -51,7 +51,7 @@ export const ACCOUNT_POSITIONS: Exclude[] = [ deposit: dn.from(19.20, 18), // 8 ETH @ 2.4 leverage interestRate: dn.from(0.045, 18), troveId: "0x02", - collIndex: 0, + branchId: 0, batchManager: null, createdAt: getTime(), updatedAt: getTime(), @@ -59,7 +59,7 @@ export const ACCOUNT_POSITIONS: Exclude[] = [ { type: "earn", owner: DEMO_ACCOUNT, - collIndex: 0, + branchId: 0, deposit: dn.from(5_000, 18), rewards: { bold: dn.from(789.438, 18), @@ -81,15 +81,15 @@ export const ACCOUNT_POSITIONS: Exclude[] = [ export const BORROW_STATS = { ETH: { - collIndex: 0, + branchId: 0, totalDeposited: dn.from(30_330.9548, 18), }, RETH: { - collIndex: 1, + branchId: 1, totalDeposited: dn.from(22_330.9548, 18), }, WSTETH: { - collIndex: 2, + branchId: 2, totalDeposited: dn.from(18_030.9548, 18), }, } as const; diff --git a/frontend/app/src/env.ts b/frontend/app/src/env.ts index 21eff90c9..1c2716523 100644 --- a/frontend/app/src/env.ts +++ b/frontend/app/src/env.ts @@ -1,6 +1,6 @@ -import type { Address, CollIndex } from "@/src/types"; +import type { Address, BranchId } from "@/src/types"; -import { isCollIndex } from "@/src/types"; +import { isBranchId } from "@/src/types"; import { vAddress, vEnvAddressAndBlock, vEnvCurrency, vEnvFlag, vEnvLink, vEnvUrlOrDefault } from "@/src/valibot-utils"; import * as v from "valibot"; @@ -13,8 +13,8 @@ export const CollateralSymbolSchema = v.union([ v.literal("WSTETH"), ]); -function vCollateralEnvVars(collIndex: CollIndex) { - const prefix = `COLL_${collIndex}`; +function vCollateralEnvVars(branchId: BranchId) { + const prefix = `COLL_${branchId}`; return v.object({ [`${prefix}_CONTRACT_ACTIVE_POOL`]: v.optional(vAddress()), [`${prefix}_CONTRACT_BORROWER_OPERATIONS`]: v.optional(vAddress()), @@ -27,7 +27,22 @@ function vCollateralEnvVars(collIndex: CollIndex) { [`${prefix}_CONTRACT_STABILITY_POOL`]: v.optional(vAddress()), [`${prefix}_CONTRACT_TROVE_MANAGER`]: v.optional(vAddress()), [`${prefix}_CONTRACT_TROVE_NFT`]: v.optional(vAddress()), - [`${prefix}_DELEGATE_AUTO`]: v.optional(vAddress()), + [`${prefix}_IC_STRATEGIES`]: v.optional( + v.pipe( + v.string(), + v.regex(/^\s?([^:]+:0x[0-9a-fA-F]{40},?)+\s?$/), + v.transform((value) => { + value = value.trim(); + if (value.endsWith(",")) { + value = value.slice(0, -1); + } + return value.split(",").map((s) => { + const [name, address] = s.split(":"); + return { address, name }; + }); + }), + ), + ), [`${prefix}_TOKEN_ID`]: v.optional(CollateralSymbolSchema), }); } @@ -182,9 +197,9 @@ export const EnvSchema = v.pipe( type ContractEnvName = typeof contractsEnvNames[number]; const collateralContracts: Array<{ - collIndex: CollIndex; + branchId: BranchId; contracts: Record; - delegateAuto: Address | null; + strategies: Array<{ address: Address; name: string }>; symbol: v.InferOutput; }> = []; @@ -208,14 +223,16 @@ export const EnvSchema = v.pipe( throw new Error(`Incomplete contracts for collateral ${index} (${contractsCount}/${contractsEnvNames.length})`); } - if (!isCollIndex(index)) { - throw new Error(`Invalid collateral index: ${index}`); + if (!isBranchId(index)) { + throw new Error(`Invalid branch: ${index}`); } collateralContracts[index] = { - collIndex: index, + branchId: index, contracts: contracts as Record, - delegateAuto: (env[`${collEnvName}_DELEGATE_AUTO` as keyof typeof env] ?? null) as Address | null, + strategies: (env[`${collEnvName}_IC_STRATEGIES` as keyof typeof env] ?? []) as Array< + { address: Address; name: string } + >, symbol: env[`${collEnvName}_TOKEN_ID` as keyof typeof env] as v.InferOutput, }; } @@ -290,9 +307,9 @@ const parsedEnv = v.safeParse(EnvSchema, { COLL_1_TOKEN_ID: process.env.NEXT_PUBLIC_COLL_1_TOKEN_ID, COLL_2_TOKEN_ID: process.env.NEXT_PUBLIC_COLL_2_TOKEN_ID, - COLL_0_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_0_DELEGATE_AUTO, - COLL_1_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_1_DELEGATE_AUTO, - COLL_2_DELEGATE_AUTO: process.env.NEXT_PUBLIC_COLL_2_DELEGATE_AUTO, + COLL_0_IC_STRATEGIES: process.env.NEXT_PUBLIC_COLL_0_IC_STRATEGIES, + COLL_1_IC_STRATEGIES: process.env.NEXT_PUBLIC_COLL_1_IC_STRATEGIES, + COLL_2_IC_STRATEGIES: process.env.NEXT_PUBLIC_COLL_2_IC_STRATEGIES, COLL_0_CONTRACT_ACTIVE_POOL: process.env.NEXT_PUBLIC_COLL_0_CONTRACT_ACTIVE_POOL, COLL_0_CONTRACT_BORROWER_OPERATIONS: process.env.NEXT_PUBLIC_COLL_0_CONTRACT_BORROWER_OPERATIONS, diff --git a/frontend/app/src/liquity-leverage.ts b/frontend/app/src/liquity-leverage.ts index e22b578d0..31f9d0f57 100644 --- a/frontend/app/src/liquity-leverage.ts +++ b/frontend/app/src/liquity-leverage.ts @@ -1,4 +1,4 @@ -import type { CollIndex, Dnum, TroveId } from "@/src/types"; +import type { BranchId, Dnum, TroveId } from "@/src/types"; import type { Config as WagmiConfig } from "wagmi"; import { CLOSE_FROM_COLLATERAL_SLIPPAGE, DATA_REFRESH_INTERVAL } from "@/src/constants"; @@ -14,15 +14,15 @@ import { readContract, readContracts } from "wagmi/actions"; const DECIMAL_PRECISION = 10n ** 18n; export async function getLeverUpTroveParams( - collIndex: CollIndex, + branchId: BranchId, troveId: TroveId, leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(collIndex); + const collContracts = getCollateralContracts(branchId); if (!collContracts) { - throw new Error("Invalid collateral index: " + collIndex); + throw new Error("Invalid branch: " + branchId); } const { PriceFeed, TroveManager } = collContracts; @@ -73,15 +73,15 @@ export async function getLeverUpTroveParams( } export async function getLeverDownTroveParams( - collIndex: CollIndex, + branchId: BranchId, troveId: TroveId, leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(collIndex); + const collContracts = getCollateralContracts(branchId); if (!collContracts) { - throw new Error("Invalid collateral index: " + collIndex); + throw new Error("Invalid branch: " + branchId); } const { PriceFeed, TroveManager } = collContracts; @@ -133,15 +133,15 @@ export async function getLeverDownTroveParams( // from openLeveragedTroveWithIndex() in contracts/src/test/zapperLeverage.t.sol export async function getOpenLeveragedTroveParams( - collIndex: CollIndex, + branchId: BranchId, collAmount: bigint, leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(collIndex); + const collContracts = getCollateralContracts(branchId); if (!collContracts) { - throw new Error("Invalid collateral index: " + collIndex); + throw new Error("Invalid branch: " + branchId); } const { PriceFeed } = collContracts; @@ -168,14 +168,14 @@ export async function getOpenLeveragedTroveParams( // from _getCloseFlashLoanAmount() in contracts/src/test/zapperLeverage.t.sol export async function getCloseFlashLoanAmount( - collIndex: CollIndex, + branchId: BranchId, troveId: TroveId, wagmiConfig: WagmiConfig, ): Promise { - const collContracts = getCollateralContracts(collIndex); + const collContracts = getCollateralContracts(branchId); if (!collContracts) { - throw new Error("Invalid collateral index: " + collIndex); + throw new Error("Invalid branch: " + branchId); } const { PriceFeed, TroveManager } = collContracts; @@ -216,12 +216,12 @@ function leverageRatioToCollateralRatio(inputRatio: bigint) { } export function useCheckLeverageSlippage({ - collIndex, + branchId, initialDeposit, leverageFactor, ownerIndex, }: { - collIndex: CollIndex; + branchId: BranchId; initialDeposit: Dnum | null; leverageFactor: number; ownerIndex: number | null; @@ -232,7 +232,7 @@ export function useCheckLeverageSlippage({ const debouncedQueryKey = useDebouncedQueryKey([ "openLeveragedTroveParams", - collIndex, + branchId, String(!initialDeposit || initialDeposit[0]), leverageFactor, ownerIndex, @@ -242,7 +242,7 @@ export function useCheckLeverageSlippage({ queryKey: debouncedQueryKey, queryFn: async () => { const params = initialDeposit && await getOpenLeveragedTroveParams( - collIndex, + branchId, initialDeposit[0], leverageFactor, wagmiConfig, diff --git a/frontend/app/src/liquity-stability-pool.ts b/frontend/app/src/liquity-stability-pool.ts index 27573e729..75daee6f8 100644 --- a/frontend/app/src/liquity-stability-pool.ts +++ b/frontend/app/src/liquity-stability-pool.ts @@ -1,4 +1,4 @@ -import type { Address, CollateralSymbol, CollIndex } from "@/src/types"; +import type { Address, CollateralSymbol, BranchId } from "@/src/types"; import { SP_YIELD_SPLIT } from "@/src/constants"; import { getCollateralContract } from "@/src/contracts"; @@ -13,19 +13,19 @@ const DECIMAL_PRECISION = 10n ** 18n; const SCALE_FACTOR = 10n ** 9n; const ONE_YEAR = 365n * 24n * 60n * 60n * 1000n; -export function useContinuousBoldGains(account: null | Address, collIndex: null | CollIndex) { - const collateral = getCollToken(collIndex); +export function useContinuousBoldGains(account: null | Address, branchId: null | BranchId) { + const collateral = getCollToken(branchId); const spYieldGainParams = useSpYieldGainParameters(collateral?.symbol ?? null); - const deposit = useStabilityPoolDeposit(collIndex, account); + const deposit = useStabilityPoolDeposit(branchId, account); const epochScale1 = useStabilityPoolEpochScale( - collIndex, + branchId, deposit.data?.snapshot.epoch ?? null, deposit.data?.snapshot.scale ?? null, ); const epochScale2 = useStabilityPoolEpochScale( - collIndex, + branchId, deposit.data?.snapshot.epoch ?? null, deposit.data?.snapshot.scale ? deposit.data?.snapshot.scale + 1n : null, ); diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index 9ee66bfdf..2fed01ff1 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -1,5 +1,14 @@ import type { Contracts } from "@/src/contracts"; -import type { CollIndex, Dnum, PositionLoanCommitted, PositionStake, PrefixedTroveId, TroveId } from "@/src/types"; +import type { + Branch, + BranchId, + Delegate, + Dnum, + PositionLoanCommitted, + PositionStake, + PrefixedTroveId, + TroveId, +} from "@/src/types"; import type { Address, CollateralSymbol, CollateralToken } from "@liquity2/uikit"; import type { UseQueryResult } from "@tanstack/react-query"; import type { Config as WagmiConfig } from "wagmi"; @@ -16,7 +25,7 @@ import { useLoanById, useStabilityPool, } from "@/src/subgraph-hooks"; -import { isCollIndex, isTroveId } from "@/src/types"; +import { isBranchId, isTroveId } from "@/src/types"; import { COLLATERALS, isAddress, shortenAddress } from "@liquity2/uikit"; import { useQuery } from "@tanstack/react-query"; import * as dn from "dnum"; @@ -42,48 +51,78 @@ export function getTroveId(owner: Address, ownerIndex: bigint | number) { } export function parsePrefixedTroveId(value: PrefixedTroveId): { - collIndex: CollIndex; + branchId: BranchId; troveId: TroveId; } { - const [collIndex_, troveId] = value.split(":"); - if (!collIndex_ || !troveId) { + const [branchId_, troveId] = value.split(":"); + if (!branchId_ || !troveId) { throw new Error(`Invalid prefixed trove ID: ${value}`); } - const collIndex = parseInt(collIndex_, 10); - if (!isCollIndex(collIndex) || !isTroveId(troveId)) { + const branchId = parseInt(branchId_, 10); + if (!isBranchId(branchId) || !isTroveId(troveId)) { throw new Error(`Invalid prefixed trove ID: ${value}`); } - return { collIndex, troveId }; + return { branchId, troveId }; } -export function getPrefixedTroveId(collIndex: CollIndex, troveId: TroveId): PrefixedTroveId { - return `${collIndex}:${troveId}`; +export function getPrefixedTroveId(branchId: BranchId, troveId: TroveId): PrefixedTroveId { + return `${branchId}:${troveId}`; } -export function getCollToken(collIndex: CollIndex | null): CollateralToken | null { - const { collaterals } = getContracts(); - if (collIndex === null) { +export function getCollToken(branchId: null): null; +export function getCollToken(branchId: BranchId): CollateralToken; +export function getCollToken(branchId: BranchId | null): CollateralToken | null; +export function getCollToken(branchId: BranchId | null): CollateralToken | null { + if (branchId === null) { return null; } - return collaterals.map(({ symbol }) => { - const collateral = COLLATERALS.find((c) => c.symbol === symbol); - if (!collateral) { - throw new Error(`Unknown collateral symbol: ${symbol}`); - } - return collateral; - })[collIndex] ?? null; + const branch = getBranch(branchId); + const token = COLLATERALS.find((c) => c.symbol === branch.symbol); + if (!token) { + throw new Error(`Unknown collateral symbol: ${branch.symbol}`); + } + return token; +} + +export function getBranchIdFromSymbol(symbol: null): null; +export function getBranchIdFromSymbol(symbol: CollateralSymbol): BranchId; +export function getBranchIdFromSymbol(symbol: CollateralSymbol | null): BranchId | null { + if (symbol === null) { + return null; + } + const branch = getBranch(symbol); + return branch.id; +} + +export function getBranches(): Branch[] { + return getContracts().branches; } -export function getCollIndexFromSymbol(symbol: CollateralSymbol | null): CollIndex | null { - if (symbol === null) return null; - const { collaterals } = getContracts(); - const collIndex = collaterals.findIndex((coll) => coll.symbol === symbol); - return isCollIndex(collIndex) ? collIndex : null; +export function getBranch(idOrSymbol: null): null; +export function getBranch(idOrSymbol: CollateralSymbol | BranchId): Branch; +export function getBranch( + idOrSymbol: CollateralSymbol | BranchId | null, +): Branch | null { + if (idOrSymbol === null) { + return null; + } + + const branch = getBranches().find((b) => ( + typeof idOrSymbol === "string" + ? b.symbol === idOrSymbol + : b.id === idOrSymbol + )); + + if (!branch) { + throw new Error("Invalid branch ID or symbol: " + idOrSymbol); + } + + return branch; } -export function useEarnPool(collIndex: null | CollIndex) { - const collateral = getCollToken(collIndex); - const pool = useStabilityPool(collIndex ?? undefined); +export function useEarnPool(branchId: null | BranchId) { + const collateral = getCollToken(branchId); + const pool = useStabilityPool(branchId ?? undefined); const stats = useLiquityStats(); const branchStats = collateral && stats.data?.branch[collateral?.symbol]; return { @@ -98,10 +137,10 @@ export function useEarnPool(collIndex: null | CollIndex) { } export function useEarnPosition( - collIndex: null | CollIndex, + branchId: null | BranchId, account: null | Address, ) { - const getBoldGains = useContinuousBoldGains(account, collIndex); + const getBoldGains = useContinuousBoldGains(account, branchId); const getBoldGains_ = () => { return getBoldGains.data?.(Date.now()) ?? null; @@ -109,14 +148,14 @@ export function useEarnPosition( const yieldGainsInBold = useQuery({ queryFn: () => getBoldGains_(), - queryKey: ["useEarnPosition:getBoldGains", collIndex, account], + queryKey: ["useEarnPosition:getBoldGains", branchId, account], refetchInterval: 10_000, enabled: getBoldGains.status === "success", }); - const StabilityPool = getCollateralContract(collIndex, "StabilityPool"); + const StabilityPool = getCollateralContract(branchId, "StabilityPool"); if (!StabilityPool) { - throw new Error(`Invalid collateral index: ${collIndex}`); + throw new Error(`Invalid branch: ${branchId}`); } const spContractReads = useReadContracts({ @@ -145,7 +184,7 @@ export function useEarnPosition( ].find((r) => r && r.status !== "success") ?? yieldGainsInBold; if ( - !account || collIndex === null + !account || branchId === null || spDepositCompounded?.status !== "success" || spCollGain?.status !== "success" || yieldGainsInBold.status !== "success" @@ -162,7 +201,7 @@ export function useEarnPosition( type: "earn" as const, owner: account, deposit: dnum18(spDepositCompounded.result), - collIndex, + branchId, rewards: { bold: yieldGainsInBold.data ?? dnum18(0), coll: dnum18(spCollGain.result), @@ -278,15 +317,15 @@ export function useStakePosition(address: null | Address) { : stakePosition; } -export function useTroveNftUrl(collIndex: null | CollIndex, troveId: null | TroveId) { - const TroveNft = getCollateralContract(collIndex, "TroveNFT"); +export function useTroveNftUrl(branchId: null | BranchId, troveId: null | TroveId) { + const TroveNft = getCollateralContract(branchId, "TroveNFT"); return TroveNft && troveId && `${CHAIN_BLOCK_EXPLORER?.url}nft/${TroveNft.address}/${BigInt(troveId)}`; } const RATE_STEPS = Math.round((INTEREST_RATE_MAX - INTEREST_RATE_MIN) / INTEREST_RATE_INCREMENT) + 1; -export function useAverageInterestRate(collIndex: null | CollIndex) { - const brackets = useInterestRateBrackets(collIndex); +export function useAverageInterestRate(branchId: null | BranchId) { + const brackets = useInterestRateBrackets(branchId); const data = useMemo(() => { if (!brackets.isSuccess) { @@ -315,13 +354,13 @@ export function useAverageInterestRate(collIndex: null | CollIndex) { }; } -export function useInterestRateChartData(collIndex: null | CollIndex) { - const brackets = useInterestRateBrackets(collIndex); +export function useInterestRateChartData(branchId: null | BranchId) { + const brackets = useInterestRateBrackets(branchId); const chartData = useQuery({ queryKey: [ "useInterestRateChartData", - collIndex, + branchId, jsonStringifyWithDnum(brackets.data), ], queryFn: () => { @@ -370,7 +409,7 @@ export function useInterestRateChartData(collIndex: null | CollIndex) { } export function usePredictOpenTroveUpfrontFee( - collIndex: CollIndex, + branchId: BranchId, borrowedAmount: Dnum, interestRateOrBatch: Address | Dnum, ) { @@ -382,8 +421,8 @@ export function usePredictOpenTroveUpfrontFee( ? "predictOpenTroveAndJoinBatchUpfrontFee" : "predictOpenTroveUpfrontFee", args: batch - ? [BigInt(collIndex), borrowedAmount[0], interestRateOrBatch] - : [BigInt(collIndex), borrowedAmount[0], interestRateOrBatch[0]], + ? [BigInt(branchId), borrowedAmount[0], interestRateOrBatch] + : [BigInt(branchId), borrowedAmount[0], interestRateOrBatch[0]], query: { refetchInterval: DATA_REFRESH_INTERVAL, select: dnum18, @@ -392,7 +431,7 @@ export function usePredictOpenTroveUpfrontFee( } export function usePredictAdjustTroveUpfrontFee( - collIndex: CollIndex, + branchId: BranchId, troveId: TroveId, debtIncrease: Dnum, ) { @@ -400,7 +439,7 @@ export function usePredictAdjustTroveUpfrontFee( ...getProtocolContract("HintHelpers"), functionName: "predictAdjustTroveUpfrontFee", args: [ - BigInt(collIndex), + BigInt(branchId), BigInt(troveId), debtIncrease[0], ], @@ -416,7 +455,7 @@ export function usePredictAdjustTroveUpfrontFee( // - joining a batch with a new interest rate (non-batch => batch or batch => batch) // - removing a trove from a batch (batch => non-batch) export function usePredictAdjustInterestRateUpfrontFee( - collIndex: CollIndex, + branchId: BranchId, troveId: TroveId, newInterestRateOrBatch: Address | Dnum, fromBatch: boolean, @@ -431,7 +470,7 @@ export function usePredictAdjustInterestRateUpfrontFee( ...getProtocolContract("HintHelpers"), functionName, args: [ - BigInt(collIndex), + BigInt(branchId), BigInt(troveId), typeof newInterestRateOrBatch === "string" ? newInterestRateOrBatch @@ -448,24 +487,21 @@ export function usePredictAdjustInterestRateUpfrontFee( export async function getTroveOperationHints({ wagmiConfig, contracts, - collIndex, + branchId, interestRate, }: { wagmiConfig: WagmiConfig; contracts: Contracts; - collIndex: number; + branchId: BranchId; interestRate: bigint; }): Promise<{ upperHint: bigint; lowerHint: bigint; }> { - const collateral = contracts.collaterals[collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${collIndex}`); - } + const branch = getBranch(branchId); const numTroves = await readContract(wagmiConfig, { - ...collateral.contracts.SortedTroves, + ...branch.contracts.SortedTroves, functionName: "getSize", }); @@ -473,7 +509,7 @@ export async function getTroveOperationHints({ ...contracts.HintHelpers, functionName: "getApproxHint", args: [ - BigInt(collIndex), + BigInt(branchId), interestRate, // (10 * sqrt(troves)) gives a hint close to the right position 10n * BigInt(Math.ceil(Math.sqrt(Number(numTroves)))), @@ -482,7 +518,7 @@ export async function getTroveOperationHints({ }); const [upperHint, lowerHint] = await readContract(wagmiConfig, { - ...collateral.contracts.SortedTroves, + ...branch.contracts.SortedTroves, functionName: "findInsertPosition", args: [ interestRate, @@ -566,10 +602,10 @@ export function useLiquityStats() { }); } -export function useLatestTroveData(collIndex: CollIndex, troveId: TroveId) { - const TroveManager = getCollateralContract(collIndex, "TroveManager"); +export function useLatestTroveData(branchId: BranchId, troveId: TroveId) { + const TroveManager = getCollateralContract(branchId, "TroveManager"); if (!TroveManager) { - throw new Error(`Invalid collateral index: ${collIndex}`); + throw new Error(`Invalid branch: ${branchId}`); } return useReadContract({ ...TroveManager, @@ -581,17 +617,17 @@ export function useLatestTroveData(collIndex: CollIndex, troveId: TroveId) { }); } -export function useLoanLiveDebt(collIndex: CollIndex, troveId: TroveId) { - const latestTroveData = useLatestTroveData(collIndex, troveId); +export function useLoanLiveDebt(branchId: BranchId, troveId: TroveId) { + const latestTroveData = useLatestTroveData(branchId, troveId); return { ...latestTroveData, data: latestTroveData.data?.entireDebt ?? null, }; } -export function useLoan(collIndex: CollIndex, troveId: TroveId): UseQueryResult { - const liveDebt = useLoanLiveDebt(collIndex, troveId); - const loan = useLoanById(getPrefixedTroveId(collIndex, troveId)); +export function useLoan(branchId: BranchId, troveId: TroveId): UseQueryResult { + const liveDebt = useLoanLiveDebt(branchId, troveId); + const loan = useLoanById(getPrefixedTroveId(branchId, troveId)); if (liveDebt.status === "pending" || loan.status === "pending") { return { @@ -623,18 +659,18 @@ export function useLoan(collIndex: CollIndex, troveId: TroveId): UseQueryResult< } export function useInterestBatchDelegate( - collIndex: null | CollIndex, + branchId: null | BranchId, address: null | Address, -) { - const BorrowerOperations = getCollateralContract(collIndex, "BorrowerOperations"); +): UseQueryResult { + const BorrowerOperations = getCollateralContract(branchId, "BorrowerOperations"); if (!BorrowerOperations) { - throw new Error(`Invalid collateral index: ${collIndex}`); + throw new Error(`Invalid branch: ${branchId}`); } const wagmiConfig = useWagmiConfig(); - const id = address && collIndex !== null - ? `${collIndex}:${address.toLowerCase()}` + const id = address && branchId !== null + ? `${branchId}:${address.toLowerCase()}` : null; let queryFn = async () => { diff --git a/frontend/app/src/screens/AccountScreen/AccountScreen.tsx b/frontend/app/src/screens/AccountScreen/AccountScreen.tsx index 6097e91fe..81f9ade62 100644 --- a/frontend/app/src/screens/AccountScreen/AccountScreen.tsx +++ b/frontend/app/src/screens/AccountScreen/AccountScreen.tsx @@ -6,9 +6,10 @@ import type { ReactNode } from "react"; import { ERC20Faucet } from "@/src/abi/ERC20Faucet"; import { Positions } from "@/src/comps/Positions/Positions"; import { Screen } from "@/src/comps/Screen/Screen"; -import { getCollateralContract, getContracts, getProtocolContract } from "@/src/contracts"; +import { getCollateralContract, getProtocolContract } from "@/src/contracts"; import { CHAIN_ID } from "@/src/env"; import { fmtnum } from "@/src/formatting"; +import { getBranches } from "@/src/liquity-utils"; import { useAccount, useBalance } from "@/src/services/Ethereum"; import { css } from "@/styled-system/css"; import { @@ -30,7 +31,7 @@ export function AccountScreen({ address: Address; }) { const account = useAccount(); - const collSymbols = getContracts().collaterals.map((coll) => coll.symbol); + const branches = getBranches(); const tapEnabled = CHAIN_ID !== 1; return ( @@ -126,7 +127,7 @@ export function AccountScreen({ tokenSymbol="LUSD" /> - {collSymbols.map((symbol) => ( + {branches.map(({ symbol }) => ( symbol); export function BorrowScreen() { - const router = useRouter(); - - const account = useAccount(); - const txFlow = useTransactionFlow(); - const contracts = getContracts(); - + const branches = getBranches(); // useParams() can return an array but not with the current // routing setup, so we can safely cast it to a string - const collSymbol = String(useParams().collateral ?? contracts.collaterals[0]?.symbol ?? "").toUpperCase(); + const collSymbol = `${useParams().collateral ?? branches[0]?.symbol}`.toUpperCase(); if (!isCollateralSymbol(collSymbol)) { throw new Error(`Invalid collateral symbol: ${collSymbol}`); } - const collIndex = contracts.collaterals.findIndex(({ symbol }) => symbol === collSymbol); - if (!isCollIndex(collIndex)) { - throw new Error(`Unknown collateral symbol: ${collSymbol}`); - } - - const collaterals = contracts.collaterals.map(({ symbol }) => { - const collateral = KNOWN_COLLATERALS.find((c) => c.symbol === symbol); - if (!collateral) { - throw new Error(`Unknown collateral symbol: ${symbol}`); - } - return collateral; - }); + const router = useRouter(); + const account = useAccount(); + const txFlow = useTransactionFlow(); - const collateral = collaterals[collIndex]; - if (!collateral) { - throw new Error(`Unknown collateral index: ${collIndex}`); - } + const branch = getBranch(collSymbol); + const collateral = getCollToken(branch.id); + const collaterals = branches.map((b) => getCollToken(b.branchId)); const maxCollDeposit = MAX_COLLATERAL_DEPOSITS[collSymbol] ?? null; @@ -113,7 +97,7 @@ export function BorrowScreen() { throw new Error(`Unknown collateral symbol: ${collateral.symbol}`); } - const nextOwnerIndex = useNextOwnerIndex(account.address ?? null, collIndex); + const nextOwnerIndex = useNextOwnerIndex(account.address ?? null, branch.id); const loanDetails = getLoanDetails( deposit.isEmpty ? null : deposit.parsed, @@ -180,7 +164,7 @@ export function BorrowScreen() { {content.borrowScreen.headline( - {contracts.collaterals.map(({ symbol }) => ( + {collaterals.map(({ symbol }) => ( { const coll = collaterals[index]; if (!coll) { - throw new Error(`Unknown collateral index: ${index}`); + throw new Error(`Unknown branch: ${index}`); } deposit.setValue(""); @@ -229,7 +213,7 @@ export function BorrowScreen() { { scroll: false }, ); }} - selected={collIndex} + selected={branch.id} /> } label="Collateral" @@ -346,7 +330,7 @@ export function BorrowScreen() { // “Interest rate” field={ tab.action === params.action) ?? TABS[0]; + if (!tab) { + throw new Error("Invalid tab action: " + params.action); + } - const hasDeposit = earnPosition.data?.deposit && dn.gt(earnPosition.data.deposit, 0); + const router = useRouter(); + const account = useAccount(); - const tab = TABS.find((tab) => tab.action === params.action) ?? TABS[0]; + const branch = getBranch(collateralSymbol); + const earnPosition = useEarnPosition(branch.id, account.address ?? null); + const earnPool = useEarnPool(branch.id); + + const hasDeposit = earnPosition.data?.deposit && dn.gt(earnPosition.data.deposit, 0); const loadingState = earnPool.isLoading || earnPosition.status === "pending" ? "loading" : "success"; @@ -51,11 +56,7 @@ export function EarnPoolScreen() { }, }); - if (collIndex === null || !isCollSymbolOk) { - return null; - } - - return earnPool.data && tab && ( + return ( ) : ( @@ -160,14 +161,14 @@ export function EarnPoolScreen() { )} {tab.action === "deposit" && ( )} {tab.action === "claim" && ( )} diff --git a/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx b/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx index fc1185cd0..7c6afbd5f 100644 --- a/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx +++ b/frontend/app/src/screens/EarnPoolScreen/PanelClaimRewards.tsx @@ -1,4 +1,4 @@ -import type { CollIndex, PositionEarn, TokenSymbol } from "@/src/types"; +import type { BranchId, PositionEarn, TokenSymbol } from "@/src/types"; import type { Dnum } from "dnum"; import { ReactNode } from "react"; @@ -15,18 +15,18 @@ import { Button, HFlex, TokenIcon, VFlex } from "@liquity2/uikit"; import * as dn from "dnum"; export function PanelClaimRewards({ - collIndex, + branchId, position, }: { - collIndex: null | CollIndex; + branchId: null | BranchId; position?: PositionEarn; }) { const account = useAccount(); const txFlow = useTransactionFlow(); - const collateral = getCollToken(collIndex); + const collateral = getCollToken(branchId); if (!collateral) { - throw new Error(`Invalid collateral index: ${collIndex}`); + throw new Error(`Invalid branch: ${branchId}`); } const boldPriceUsd = usePrice("BOLD"); diff --git a/frontend/app/src/screens/EarnPoolScreen/PanelUpdateDeposit.tsx b/frontend/app/src/screens/EarnPoolScreen/PanelUpdateDeposit.tsx index a1981c65e..578284a4a 100644 --- a/frontend/app/src/screens/EarnPoolScreen/PanelUpdateDeposit.tsx +++ b/frontend/app/src/screens/EarnPoolScreen/PanelUpdateDeposit.tsx @@ -1,4 +1,4 @@ -import type { CollIndex, PositionEarn } from "@/src/types"; +import type { BranchId, PositionEarn } from "@/src/types"; import type { Dnum } from "dnum"; import { Amount } from "@/src/comps/Amount/Amount"; @@ -22,11 +22,11 @@ type ValueUpdateMode = "add" | "remove"; export function PanelUpdateDeposit({ deposited, - collIndex, + branchId, position, }: { deposited: Dnum; - collIndex: CollIndex; + branchId: BranchId; position?: PositionEarn; }) { const account = useAccount(); @@ -58,7 +58,7 @@ export function PanelUpdateDeposit({ ? dn.div(updatedDeposit, updatedBoldQty) : DNUM_0; - const collateral = getCollToken(collIndex); + const collateral = getCollToken(branchId); const insufficientBalance = mode === "add" && parsedValue @@ -265,7 +265,7 @@ export function PanelUpdateDeposit({ : { type: "earn" as const, owner: account.address, - collIndex: collIndex, + branchId: branchId, deposit: updatedDeposit, rewards: { bold: DNUM_0, coll: DNUM_0 }, }; @@ -280,7 +280,7 @@ export function PanelUpdateDeposit({ successLink: ["/", "Go to the Dashboard"], successMessage: "The withdrawal has been processed successfully.", claim: claimRewards, - collIndex, + branchId, prevEarnPosition: position, earnPosition: newPosition, }); @@ -297,7 +297,7 @@ export function PanelUpdateDeposit({ successLink: ["/", "Go to the Dashboard"], successMessage: "The deposit has been processed successfully.", claim: claimRewards, - collIndex, + branchId, prevEarnPosition: position ?? null, earnPosition: newPosition, }); diff --git a/frontend/app/src/screens/EarnPoolsListScreen/EarnPoolsListScreen.tsx b/frontend/app/src/screens/EarnPoolsListScreen/EarnPoolsListScreen.tsx index cf5e1d60e..1d6c733e9 100644 --- a/frontend/app/src/screens/EarnPoolsListScreen/EarnPoolsListScreen.tsx +++ b/frontend/app/src/screens/EarnPoolsListScreen/EarnPoolsListScreen.tsx @@ -1,21 +1,21 @@ "use client"; -import type { CollIndex } from "@/src/types"; +import type { BranchId } from "@/src/types"; import { EarnPositionSummary } from "@/src/comps/EarnPositionSummary/EarnPositionSummary"; import { Screen } from "@/src/comps/Screen/Screen"; import content from "@/src/content"; -import { getContracts } from "@/src/contracts"; -import { useEarnPosition } from "@/src/liquity-utils"; +import { getBranches, useEarnPosition } from "@/src/liquity-utils"; import { useAccount } from "@/src/services/Ethereum"; import { css } from "@/styled-system/css"; import { TokenIcon } from "@liquity2/uikit"; import { a, useTransition } from "@react-spring/web"; export function EarnPoolsListScreen() { - const { collaterals } = getContracts(); + const branches = getBranches(); + const collSymbols = branches.map((b) => b.symbol); - const poolsTransition = useTransition(collaterals.map((c) => c.collIndex), { + const poolsTransition = useTransition(branches.map((c) => c.branchId), { from: { opacity: 0, transform: "scale(1.1) translateY(64px)" }, enter: { opacity: 1, transform: "scale(1) translateY(0px)" }, leave: { opacity: 0, transform: "scale(1) translateY(0px)" }, @@ -41,7 +41,7 @@ export function EarnPoolsListScreen() { > {content.earnHome.headline( - {["BOLD" as const, ...collaterals.map((coll) => coll.symbol)].map((symbol) => ( + {["BOLD" as const, ...collSymbols].map((symbol) => ( - {poolsTransition((style, collIndex) => ( + {poolsTransition((style, branchId) => ( - + ))} @@ -69,15 +67,15 @@ export function EarnPoolsListScreen() { } function EarnPool({ - collIndex, + branchId, }: { - collIndex: CollIndex; + branchId: BranchId; }) { const account = useAccount(); - const earnPosition = useEarnPosition(collIndex, account.address ?? null); + const earnPosition = useEarnPosition(branchId, account.address ?? null); return ( diff --git a/frontend/app/src/screens/HomeScreen/HomeScreen.tsx b/frontend/app/src/screens/HomeScreen/HomeScreen.tsx index 8a590b6cb..747e214ea 100644 --- a/frontend/app/src/screens/HomeScreen/HomeScreen.tsx +++ b/frontend/app/src/screens/HomeScreen/HomeScreen.tsx @@ -4,9 +4,8 @@ import type { CollateralSymbol } from "@/src/types"; import { Amount } from "@/src/comps/Amount/Amount"; import { Positions } from "@/src/comps/Positions/Positions"; -import { getContracts } from "@/src/contracts"; import { DNUM_1 } from "@/src/dnum-utils"; -import { getCollIndexFromSymbol, getCollToken, useAverageInterestRate, useEarnPool } from "@/src/liquity-utils"; +import { getBranch, getBranches, getCollToken, useAverageInterestRate, useEarnPool } from "@/src/liquity-utils"; import { useAccount } from "@/src/services/Ethereum"; import { css } from "@/styled-system/css"; import { AnchorTextButton, IconBorrow, IconEarn, TokenIcon } from "@liquity2/uikit"; @@ -16,10 +15,7 @@ import { HomeTable } from "./HomeTable"; export function HomeScreen() { const account = useAccount(); - - const { collaterals } = getContracts(); - const collSymbols = collaterals.map((coll) => coll.symbol); - + const branches = getBranches(); return (
, null, ] as const} - rows={collSymbols.map((symbol) => ( + rows={branches.map(({ symbol }) => ( )} + rows={branches.map(({ symbol }) => ( + + ))} />
@@ -84,9 +85,9 @@ function BorrowingRow({ }: { symbol: CollateralSymbol; }) { - const collIndex = getCollIndexFromSymbol(symbol); - const collateral = getCollToken(collIndex); - const avgInterestRate = useAverageInterestRate(collIndex); + const branch = getBranch(symbol); + const collateral = getCollToken(branch.id); + const avgInterestRate = useAverageInterestRate(branch.id); const maxLtv = collateral?.collateralRatio && dn.gt(collateral.collateralRatio, 0) ? dn.div(DNUM_1, collateral.collateralRatio) @@ -182,9 +183,9 @@ function EarnRewardsRow({ }: { symbol: CollateralSymbol; }) { - const collIndex = getCollIndexFromSymbol(symbol); - const collateral = getCollToken(collIndex); - const earnPool = useEarnPool(collIndex); + const branch = getBranch(symbol); + const collateral = getCollToken(branch.id); + const earnPool = useEarnPool(branch.id); return ( diff --git a/frontend/app/src/screens/LeverageScreen/LeverageScreen.tsx b/frontend/app/src/screens/LeverageScreen/LeverageScreen.tsx index 3471231e1..ba5858a75 100644 --- a/frontend/app/src/screens/LeverageScreen/LeverageScreen.tsx +++ b/frontend/app/src/screens/LeverageScreen/LeverageScreen.tsx @@ -19,13 +19,12 @@ import { MIN_DEBT, } from "@/src/constants"; import content from "@/src/content"; -import { getContracts } from "@/src/contracts"; import { dnum18, dnumMax } from "@/src/dnum-utils"; import { useInputFieldValue } from "@/src/form-utils"; import { fmtnum } from "@/src/formatting"; import { useCheckLeverageSlippage } from "@/src/liquity-leverage"; import { getRedemptionRisk } from "@/src/liquity-math"; -import { getCollIndexFromSymbol } from "@/src/liquity-utils"; +import { getBranch, getBranches, getCollToken } from "@/src/liquity-utils"; import { useAccount, useBalance } from "@/src/services/Ethereum"; import { usePrice } from "@/src/services/Prices"; import { useTransactionFlow } from "@/src/services/TransactionFlow"; @@ -35,7 +34,6 @@ import { css } from "@/styled-system/css"; import { ADDRESS_ZERO, Button, - COLLATERALS as COLL_TOKENS, Dropdown, HFlex, IconSuggestion, @@ -51,43 +49,30 @@ import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; export function LeverageScreen() { - const router = useRouter(); - const account = useAccount(); - const txFlow = useTransactionFlow(); - const contracts = getContracts(); + const branches = getBranches(); // useParams() can return an array but not with the current // routing setup, so we can safely cast it to a string - const collSymbol = String(useParams().collateral ?? contracts.collaterals[0]?.symbol ?? "").toUpperCase(); + const collSymbol = `${useParams().collateral ?? branches[0]?.symbol}`.toUpperCase(); if (!isCollateralSymbol(collSymbol)) { throw new Error(`Invalid collateral symbol: ${collSymbol}`); } - const collIndex = getCollIndexFromSymbol(collSymbol); - if (collIndex === null) { - throw new Error(`Unknown collateral symbol: ${collSymbol}`); - } - - const collateralTokens = contracts.collaterals.map(({ symbol }) => { - const collateral = COLL_TOKENS.find((c) => c.symbol === symbol); - if (!collateral) { - throw new Error(`Unknown collateral symbol: ${symbol}`); - } - return collateral; - }); + const router = useRouter(); + const account = useAccount(); + const txFlow = useTransactionFlow(); - const collToken = collateralTokens[collIndex]; - if (!collToken) { - throw new Error(`Unknown collateral index: ${collIndex}`); - } + const branch = getBranch(collSymbol); + const collaterals = branches.map((b) => getCollToken(b.branchId)); + const collateral = getCollToken(branch.id); - const balances = Object.fromEntries(collateralTokens.map(({ symbol }) => ( + const balances = Object.fromEntries(collaterals.map(({ symbol }) => ( [symbol, useBalance(account.address, symbol)] as const ))); - const nextOwnerIndex = useNextOwnerIndex(account.address ?? null, collIndex); + const nextOwnerIndex = useNextOwnerIndex(account.address ?? null, branch.id); - const collPrice = usePrice(collToken.symbol); + const collPrice = usePrice(collateral.symbol); const maxCollDeposit = MAX_COLLATERAL_DEPOSITS[collSymbol] ?? null; const depositPreLeverage = useInputFieldValue(fmtnum, { @@ -107,13 +92,13 @@ export function LeverageScreen() { const leverageField = useLeverageField({ depositPreLeverage: depositPreLeverage.parsed, collPrice: collPrice.data ?? dn.from(0, 18), - collToken, + collToken: collateral, }); // reset leverage when collateral changes useEffect(() => { leverageField.updateLeverageFactor(leverageField.leverageFactorSuggestions[0] ?? 1.1); - }, [collToken.symbol, leverageField.leverageFactorSuggestions]); + }, [collateral.symbol, leverageField.leverageFactorSuggestions]); const redemptionRisk = getRedemptionRisk(interestRate); const depositUsd = depositPreLeverage.parsed && collPrice.data && dn.mul( @@ -121,7 +106,7 @@ export function LeverageScreen() { collPrice.data, ); - const collBalance = balances[collToken.symbol]?.data; + const collBalance = balances[collateral.symbol]?.data; const maxAmount = collBalance && dnumMax( dn.sub(collBalance, collSymbol === "ETH" ? ETH_MAX_RESERVE : 0), // Only keep a reserve for ETH, not LSTs @@ -134,7 +119,7 @@ export function LeverageScreen() { batchManager: interestRateDelegate, borrowed: leverageField.debt ?? dn.from(0, 18), borrower: account.address ?? ADDRESS_ZERO, - collIndex, + branchId: branch.id, deposit: depositPreLeverage.parsed ? dn.mul(depositPreLeverage.parsed, leverageField.leverageFactor) : dn.from(0, 18), @@ -145,7 +130,7 @@ export function LeverageScreen() { const hasDeposit = Boolean(depositPreLeverage.parsed && dn.gt(depositPreLeverage.parsed, 0)); const leverageSlippage = useCheckLeverageSlippage({ - collIndex, + branchId: branch.id, initialDeposit: depositPreLeverage.parsed, leverageFactor: leverageField.leverageFactor, ownerIndex: nextOwnerIndex.data ?? null, @@ -176,7 +161,7 @@ export function LeverageScreen() { {content.leverageScreen.headline( - {contracts.collaterals.map(({ symbol }) => ( + {collaterals.map(({ symbol }) => ( ({ + items={collaterals.map(({ symbol, name }) => ({ icon: , label: name, value: account.isConnected @@ -216,9 +201,9 @@ export function LeverageScreen() { depositPreLeverage.setValue(""); depositPreLeverage.focus(); }, 0); - const collToken = collateralTokens[index]; + const collToken = collaterals[index]; if (!collToken) { - throw new Error(`Unknown collateral index: ${index}`); + throw new Error(`Unknown branch: ${index}`); } const { symbol } = collToken; router.push( @@ -226,7 +211,7 @@ export function LeverageScreen() { { scroll: false }, ); }} - selected={collIndex} + selected={branch.id} /> } label={content.leverageScreen.depositField.label} @@ -236,7 +221,7 @@ export function LeverageScreen() { end: maxAmount ? ( { depositPreLeverage.setValue(dn.toString(maxAmount)); }} @@ -250,13 +235,13 @@ export function LeverageScreen() { footer={{ start: collPrice.data && ( ), end: ( ), }} @@ -299,7 +284,7 @@ export function LeverageScreen() { value={leverageField.deposit && dn.gt(leverageField.deposit, 0) ? leverageField.deposit : null} format="2z" fallback="−" - suffix={` ${collToken.name}`} + suffix={` ${collateral.name}`} /> @@ -313,7 +298,7 @@ export function LeverageScreen() { ), })} - items={(["BOLD", collToken.symbol] as const).map((symbol) => ({ + items={(["BOLD", collateral.symbol] as const).map((symbol) => ({ icon: , label: (
- -
{collToken.name}
+ +
{collateral.name}
@@ -268,7 +264,7 @@ export function PanelClosePosition({ txFlow.start({ flowId: "closeLoanPosition", backLink: [ - `/loan/close?id=${loan.collIndex}:${loan.troveId}`, + `/loan/close?id=${loan.branchId}:${loan.troveId}`, "Back to editing", ], successLink: ["/", "Go to the dashboard"], diff --git a/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx b/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx index 1d9b45ade..7be4667fa 100644 --- a/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx +++ b/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx @@ -46,7 +46,7 @@ export function PanelUpdateBorrowPosition({ const account = useAccount(); const txFlow = useTransactionFlow(); - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { throw new Error("collToken not found"); } @@ -425,7 +425,7 @@ export function PanelUpdateBorrowPosition({ if (account.address) { txFlow.start({ flowId: "updateBorrowPosition", - backLink: [`/loan?id=${loan.collIndex}:${loan.troveId}`, "Back to editing"], + backLink: [`/loan?id=${loan.branchId}:${loan.troveId}`, "Back to editing"], successLink: ["/", "Go to the dashboard"], successMessage: "The position has been updated successfully.", diff --git a/frontend/app/src/screens/LoanScreen/PanelUpdateLeveragePosition.tsx b/frontend/app/src/screens/LoanScreen/PanelUpdateLeveragePosition.tsx index 62228389d..22f779121 100644 --- a/frontend/app/src/screens/LoanScreen/PanelUpdateLeveragePosition.tsx +++ b/frontend/app/src/screens/LoanScreen/PanelUpdateLeveragePosition.tsx @@ -44,7 +44,7 @@ export function PanelUpdateLeveragePosition({ const account = useAccount(); const txFlow = useTransactionFlow(); - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { throw new Error("collToken not found"); } @@ -486,7 +486,7 @@ export function PanelUpdateLeveragePosition({ txFlow.start({ flowId: "updateLeveragePosition", backLink: [ - `/loan?id=${loan.collIndex}:${loan.troveId}`, + `/loan?id=${loan.branchId}:${loan.troveId}`, "Back to editing", ], successLink: ["/", "Go to the dashboard"], diff --git a/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx b/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx index 0fae204c2..f4663b627 100644 --- a/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx +++ b/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx @@ -31,7 +31,7 @@ export function PanelUpdateRate({ const account = useAccount(); const txFlow = useTransactionFlow(); - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { return null; @@ -90,7 +90,7 @@ export function PanelUpdateRate({ field={ ; prevLoanDetails: null | ReturnType; }) { - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { return null; } diff --git a/frontend/app/src/services/Ethereum.tsx b/frontend/app/src/services/Ethereum.tsx index 588771059..29e8afde0 100644 --- a/frontend/app/src/services/Ethereum.tsx +++ b/frontend/app/src/services/Ethereum.tsx @@ -7,7 +7,6 @@ import type { Address } from "@liquity2/uikit"; import type { ComponentProps, ReactNode } from "react"; import type { Chain } from "wagmi/chains"; -import { getContracts } from "@/src/contracts"; import { ACCOUNT_BALANCES } from "@/src/demo-mode"; import { useDemoMode } from "@/src/demo-mode"; import { dnum18 } from "@/src/dnum-utils"; @@ -25,6 +24,7 @@ import { CONTRACT_LUSD_TOKEN, WALLET_CONNECT_PROJECT_ID, } from "@/src/env"; +import { getBranch } from "@/src/liquity-utils"; import { getSafeStatus } from "@/src/safe-utils"; import { noop } from "@/src/utils"; import { isCollateralSymbol, useTheme } from "@liquity2/uikit"; @@ -114,7 +114,6 @@ export function useBalance( token: Token["symbol"] | undefined, ) { const demoMode = useDemoMode(); - const contracts = getContracts(); const tokenAddress = match(token) .when( @@ -123,8 +122,7 @@ export function useBalance( if (!symbol || !isCollateralSymbol(symbol) || symbol === "ETH") { return null; } - const collateral = contracts.collaterals.find((c) => c.symbol === symbol); - return collateral?.contracts.CollToken.address ?? null; + return getBranch(symbol).contracts.CollToken.address; }, ) .with("LUSD", () => CONTRACT_LUSD_TOKEN) diff --git a/frontend/app/src/subgraph-hooks.ts b/frontend/app/src/subgraph-hooks.ts index ac6dfbf95..a157f819c 100644 --- a/frontend/app/src/subgraph-hooks.ts +++ b/frontend/app/src/subgraph-hooks.ts @@ -5,13 +5,13 @@ import type { StabilityPoolDepositQuery as StabilityPoolDepositQueryType, TrovesByAccountQuery as TrovesByAccountQueryType, } from "@/src/graphql/graphql"; -import type { Address, CollIndex, PositionEarn, PositionLoanCommitted, PrefixedTroveId } from "@/src/types"; +import type { Address, BranchId, PositionEarn, PositionLoanCommitted, PrefixedTroveId } from "@/src/types"; import { DATA_REFRESH_INTERVAL } from "@/src/constants"; import { ACCOUNT_POSITIONS } from "@/src/demo-mode"; import { dnum18 } from "@/src/dnum-utils"; import { DEMO_MODE } from "@/src/env"; -import { isCollIndex, isPositionLoanCommitted, isPrefixedtroveId, isTroveId } from "@/src/types"; +import { isBranchId, isPositionLoanCommitted, isPrefixedtroveId, isTroveId } from "@/src/types"; import { sleep } from "@/src/utils"; import { isAddress } from "@liquity2/uikit"; import { useQuery } from "@tanstack/react-query"; @@ -44,11 +44,11 @@ function prepareOptions(options?: Options) { export function useNextOwnerIndex( borrower: null | Address, - collIndex: null | CollIndex, + branchId: null | BranchId, options?: Options, ) { let queryFn = async () => { - if (!borrower || collIndex === null) { + if (!borrower || branchId === null) { return null; } @@ -58,7 +58,7 @@ export function useNextOwnerIndex( ); // if borrowerInfo doesn’t exist, start at 0 - return borrowerInfo?.nextOwnerIndexes[collIndex] ?? 0; + return borrowerInfo?.nextOwnerIndexes[branchId] ?? 0; }; if (DEMO_MODE) { @@ -72,7 +72,7 @@ export function useNextOwnerIndex( } return useQuery({ - queryKey: ["NextTroveId", borrower, collIndex], + queryKey: ["NextTroveId", borrower, branchId], queryFn, ...prepareOptions(options), }); @@ -120,7 +120,7 @@ export function useLoanById( if (!isPrefixedtroveId(id)) return null; await sleep(500); for (const pos of ACCOUNT_POSITIONS) { - if (isPositionLoanCommitted(pos) && `${pos.collIndex}:${pos.troveId}` === id) { + if (isPositionLoanCommitted(pos) && `${pos.branchId}:${pos.troveId}` === id) { return pos; } } @@ -166,11 +166,17 @@ export function useStabilityPoolDeposits( return ACCOUNT_POSITIONS .filter((position) => position.type === "earn") .map((position) => ({ - id: `${position.collIndex}:${account}`.toLowerCase(), - collateral: { collIndex: position.collIndex }, + id: `${position.branchId}:${account}`.toLowerCase(), + collateral: { collIndex: position.branchId }, deposit: position.deposit[0], depositor: account.toLowerCase(), - snapshot: { B: 0n, P: 0n, S: 0n, epoch: 0n, scale: 0n }, + snapshot: { + B: 0n, + P: 0n, + S: 0n, + epoch: 0n, + scale: 0n, + }, })); }; } @@ -185,18 +191,18 @@ export function useStabilityPoolDeposits( } export function useStabilityPoolDeposit( - collIndex: null | number, + branchId: null | number, account: null | Address, options?: Options, ) { let queryFn = async () => { - if (account === null || collIndex === null) return null; + if (account === null || branchId === null) return null; const { stabilityPoolDeposit } = await graphQuery(StabilityPoolDepositQuery, { - id: `${collIndex}:${account}`.toLowerCase(), + id: `${branchId}:${account}`.toLowerCase(), }); return !stabilityPoolDeposit ? null : { - id: `${collIndex}:${account}`.toLowerCase(), - collateral: { collIndex }, + id: `${branchId}:${account}`.toLowerCase(), + collateral: { collIndex: branchId }, deposit: BigInt(stabilityPoolDeposit.deposit), depositor: account.toLowerCase(), snapshot: { @@ -211,18 +217,24 @@ export function useStabilityPoolDeposit( if (DEMO_MODE) { queryFn = async () => { - if (account === null || collIndex === null) return null; + if (account === null || branchId === null) return null; const position = ACCOUNT_POSITIONS.find( (position): position is PositionEarn => ( - position.type === "earn" && position.collIndex === collIndex + position.type === "earn" && position.branchId === branchId ), ); return !position ? null : { - id: `${collIndex}:${account}`.toLowerCase(), - collateral: { collIndex }, + id: `${branchId}:${account}`.toLowerCase(), + collateral: { collIndex: branchId }, deposit: position.deposit[0], depositor: account.toLowerCase(), - snapshot: { B: 0n, P: 0n, S: 0n, epoch: 0n, scale: 0n }, + snapshot: { + B: 0n, + P: 0n, + S: 0n, + epoch: 0n, + scale: 0n, + }, }; }; } @@ -233,14 +245,14 @@ export function useStabilityPoolDeposit( StabilityPoolDepositQueryType["stabilityPoolDeposit"] > >({ - queryKey: ["StabilityPoolDeposit", account, collIndex], + queryKey: ["StabilityPoolDeposit", account, branchId], queryFn, ...prepareOptions(options), }); } export function useStabilityPool( - collIndex?: null | number, + branchId?: null | number, options?: Options, ) { let queryFn = async () => { @@ -248,7 +260,7 @@ export function useStabilityPool( StabilityPoolsQuery, ); return stabilityPools.map((stabilityPool) => ({ - collIndex: parseInt(stabilityPool.id, 10), + branchId: parseInt(stabilityPool.id, 10), apr: dnum18(0), totalDeposited: dnum18(stabilityPool.totalDeposited), })); @@ -256,8 +268,8 @@ export function useStabilityPool( if (DEMO_MODE) { queryFn = async () => - Array.from({ length: 10 }, (_, collIndex) => ({ - collIndex, + Array.from({ length: 10 }, (_, branchId) => ({ + branchId, apr: dnum18(0), totalDeposited: dnum18(0), })); @@ -267,12 +279,12 @@ export function useStabilityPool( queryKey: ["StabilityPool"], queryFn, select: (pools) => { - if (typeof collIndex !== "number") { + if (typeof branchId !== "number") { return null; } - const pool = pools.find((pool) => pool.collIndex === collIndex); + const pool = pools.find((pool) => pool.branchId === branchId); if (pool === undefined) { - throw new Error(`Stability pool not found: ${collIndex}`); + throw new Error(`Stability pool not found: ${branchId}`); } return pool; }, @@ -281,7 +293,7 @@ export function useStabilityPool( } export function useStabilityPoolEpochScale( - collIndex: null | number, + branchId: null | number, epoch: null | bigint, scale: null | bigint, options?: Options, @@ -289,7 +301,7 @@ export function useStabilityPoolEpochScale( let queryFn = async () => { const { stabilityPoolEpochScale } = await graphQuery( StabilityPoolEpochScaleQuery, - { id: `${collIndex}:${epoch}:${scale}` }, + { id: `${branchId}:${epoch}:${scale}` }, ); return { B: BigInt(stabilityPoolEpochScale?.B ?? 0n), @@ -302,7 +314,7 @@ export function useStabilityPoolEpochScale( } return useQuery<{ B: bigint; S: bigint }>({ - queryKey: ["StabilityPoolEpochScale", collIndex, String(epoch), String(scale)], + queryKey: ["StabilityPoolEpochScale", branchId, String(epoch), String(scale)], queryFn, ...prepareOptions(options), }); @@ -336,7 +348,7 @@ export function useEarnPositionsByAccount( } export function useInterestRateBrackets( - collIndex: null | CollIndex, + branchId: null | BranchId, options?: Options, ) { let queryFn = async () => ( @@ -351,10 +363,10 @@ export function useInterestRateBrackets( queryKey: ["AllInterestRateBrackets"], queryFn, select: useCallback((brackets: Awaited>) => { - // only filter by collIndex in the select() + // only filter by branchId in the select() // so that we can query all the brackets at once return brackets - .filter((bracket) => bracket.collateral.collIndex === collIndex) + .filter((bracket) => bracket.collateral.collIndex === branchId) .sort((a, b) => (a.rate > b.rate ? 1 : -1)) .map((bracket) => ({ rate: dnum18(bracket.rate), @@ -443,9 +455,9 @@ function subgraphTroveToLoan( throw new Error(`Invalid trove ID: ${trove.id} / ${trove.troveId}`); } - const collIndex = trove.collateral.collIndex; - if (!isCollIndex(collIndex)) { - throw new Error(`Invalid collateral index: ${collIndex}`); + const branchId = trove.collateral.id; + if (!isBranchId(branchId)) { + throw new Error(`Invalid branch: ${branchId}`); } if (!isAddress(trove.borrower)) { @@ -459,7 +471,7 @@ function subgraphTroveToLoan( : null, borrowed: dnum18(trove.debt), borrower: trove.borrower, - collIndex, + branchId, createdAt: Number(trove.createdAt) * 1000, deposit: dnum18(trove.deposit), interestRate: dnum18(trove.interestBatch?.annualInterestRate ?? trove.interestRate), @@ -474,9 +486,9 @@ function subgraphStabilityPoolDepositToEarnPosition( StabilityPoolDepositQueryType["stabilityPoolDeposit"] >, ): PositionEarn { - const collIndex = spDeposit.collateral.collIndex; - if (!isCollIndex(collIndex)) { - throw new Error(`Invalid collateral index: ${collIndex}`); + const branchId = spDeposit.collateral.collIndex; + if (!isBranchId(branchId)) { + throw new Error(`Invalid branch: ${branchId}`); } if (!isAddress(spDeposit.depositor)) { throw new Error(`Invalid depositor address: ${spDeposit.depositor}`); @@ -484,7 +496,7 @@ function subgraphStabilityPoolDepositToEarnPosition( return { type: "earn", owner: spDeposit.depositor, - collIndex, + branchId, deposit: dnum18(0), rewards: { bold: dnum18(0), diff --git a/frontend/app/src/tx-flows/claimCollateralSurplus.tsx b/frontend/app/src/tx-flows/claimCollateralSurplus.tsx index cad32097d..3f1b057c7 100644 --- a/frontend/app/src/tx-flows/claimCollateralSurplus.tsx +++ b/frontend/app/src/tx-flows/claimCollateralSurplus.tsx @@ -3,9 +3,9 @@ import type { ReactNode } from "react"; import { getCollateralContract } from "@/src/contracts"; import { fmtnum } from "@/src/formatting"; -import { getCollToken } from "@/src/liquity-utils"; +import { getBranch, getCollToken } from "@/src/liquity-utils"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; -import { vAddress, vCollIndex, vDnum } from "@/src/valibot-utils"; +import { vAddress, vBranchId, vDnum } from "@/src/valibot-utils"; import { css } from "@/styled-system/css"; import { shortenAddress, TokenIcon } from "@liquity2/uikit"; import { blo } from "blo"; @@ -18,7 +18,7 @@ const RequestSchema = createRequestSchema( { borrower: vAddress(), collSurplus: vDnum(), - collIndex: vCollIndex(), + branchId: vBranchId(), }, ); @@ -28,16 +28,16 @@ export const claimCollateralSurplus: FlowDeclaration @@ -176,14 +176,10 @@ export const claimCollateralSurplus: FlowDeclaration = { Details({ request }) { const { loan, repayWithCollateral } = request; - const collateral = getCollToken(loan.collIndex); - - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - + const collateral = getCollToken(loan.branchId); const collPrice = usePrice(collateral.symbol); if (!collPrice.data) { @@ -116,19 +111,16 @@ export const closeLoanPosition: FlowDeclaration = { ), async commit(ctx) { const { loan } = ctx.request; - const coll = ctx.contracts.collaterals[loan.collIndex]; - if (!coll) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } + const branch = getBranch(loan.branchId); const { entireDebt } = await readContract(ctx.wagmiConfig, { - ...coll.contracts.TroveManager, + ...branch.contracts.TroveManager, functionName: "getLatestTroveData", args: [BigInt(loan.troveId)], }); - const Zapper = coll.symbol === "ETH" - ? coll.contracts.LeverageWETHZapper - : coll.contracts.LeverageLSTZapper; + const Zapper = branch.symbol === "ETH" + ? branch.contracts.LeverageWETHZapper + : branch.contracts.LeverageLSTZapper; return ctx.writeContract({ ...ctx.contracts.BoldToken, @@ -153,15 +145,12 @@ export const closeLoanPosition: FlowDeclaration = { async commit(ctx) { const { loan } = ctx.request; - const coll = ctx.contracts.collaterals[loan.collIndex]; - if (!coll) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } + const branch = getBranch(loan.branchId); // repay with BOLD => get ETH - if (!ctx.request.repayWithCollateral && coll.symbol === "ETH") { + if (!ctx.request.repayWithCollateral && branch.symbol === "ETH") { return ctx.writeContract({ - ...coll.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "closeTroveToRawETH", args: [BigInt(loan.troveId)], }); @@ -170,7 +159,7 @@ export const closeLoanPosition: FlowDeclaration = { // repay with BOLD => get LST if (!ctx.request.repayWithCollateral) { return ctx.writeContract({ - ...coll.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "closeTroveToRawETH", args: [BigInt(loan.troveId)], }); @@ -179,7 +168,7 @@ export const closeLoanPosition: FlowDeclaration = { // from here, we are repaying with the collateral const closeFlashLoanAmount = await getCloseFlashLoanAmount( - loan.collIndex, + loan.branchId, loan.troveId, ctx.wagmiConfig, ); @@ -189,9 +178,9 @@ export const closeLoanPosition: FlowDeclaration = { } // repay with collateral => get ETH - if (coll.symbol === "ETH") { + if (branch.symbol === "ETH") { return ctx.writeContract({ - ...coll.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "closeTroveFromCollateral", args: [BigInt(loan.troveId), closeFlashLoanAmount], }); @@ -199,7 +188,7 @@ export const closeLoanPosition: FlowDeclaration = { // repay with collateral => get LST return ctx.writeContract({ - ...coll.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "closeTroveFromCollateral", args: [BigInt(loan.troveId), closeFlashLoanAmount], }); @@ -209,7 +198,7 @@ export const closeLoanPosition: FlowDeclaration = { await verifyTransaction(ctx.wagmiConfig, hash, ctx.isSafe); const prefixedTroveId = getPrefixedTroveId( - ctx.request.loan.collIndex, + ctx.request.loan.branchId, ctx.request.loan.troveId, ); @@ -231,18 +220,14 @@ export const closeLoanPosition: FlowDeclaration = { } const { loan } = ctx.request; + const branch = getBranch(loan.branchId); - const coll = ctx.contracts.collaterals[loan.collIndex]; - if (!coll) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - - const Zapper = coll.symbol === "ETH" - ? coll.contracts.LeverageWETHZapper - : coll.contracts.LeverageLSTZapper; + const Zapper = branch.symbol === "ETH" + ? branch.contracts.LeverageWETHZapper + : branch.contracts.LeverageLSTZapper; const { entireDebt } = await ctx.readContract({ - ...coll.contracts.TroveManager, + ...branch.contracts.TroveManager, functionName: "getLatestTroveData", args: [BigInt(loan.troveId)], }); diff --git a/frontend/app/src/tx-flows/earnClaimRewards.tsx b/frontend/app/src/tx-flows/earnClaimRewards.tsx index 0890353d4..d7299202b 100644 --- a/frontend/app/src/tx-flows/earnClaimRewards.tsx +++ b/frontend/app/src/tx-flows/earnClaimRewards.tsx @@ -2,7 +2,7 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; import { Amount } from "@/src/comps/Amount/Amount"; import { EarnPositionSummary } from "@/src/comps/EarnPositionSummary/EarnPositionSummary"; -import { getCollToken } from "@/src/liquity-utils"; +import { getBranch, getCollToken } from "@/src/liquity-utils"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; import { usePrice } from "@/src/services/Prices"; @@ -26,7 +26,7 @@ export const earnClaimRewards: FlowDeclaration = { Summary({ request }) { return ( @@ -34,7 +34,7 @@ export const earnClaimRewards: FlowDeclaration = { }, Details({ request }) { - const collateral = getCollToken(request.earnPosition.collIndex); + const collateral = getCollToken(request.earnPosition.branchId); if (!collateral) { return null; @@ -90,15 +90,10 @@ export const earnClaimRewards: FlowDeclaration = { Status: TransactionStatus, async commit(ctx) { - const { collIndex } = ctx.request.earnPosition; - const collateral = ctx.contracts.collaterals[collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + collIndex); - } - - const { StabilityPool } = collateral.contracts; + const { branchId } = ctx.request.earnPosition; + const branch = getBranch(branchId); return ctx.writeContract({ - ...StabilityPool, + ...branch.contracts.StabilityPool, functionName: "withdrawFromSP", args: [0n, true], }); diff --git a/frontend/app/src/tx-flows/earnDeposit.tsx b/frontend/app/src/tx-flows/earnDeposit.tsx index 9925b901b..40622b07e 100644 --- a/frontend/app/src/tx-flows/earnDeposit.tsx +++ b/frontend/app/src/tx-flows/earnDeposit.tsx @@ -2,10 +2,11 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; import { Amount } from "@/src/comps/Amount/Amount"; import { EarnPositionSummary } from "@/src/comps/EarnPositionSummary/EarnPositionSummary"; +import { getBranch } from "@/src/liquity-utils"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; import { usePrice } from "@/src/services/Prices"; -import { vCollIndex, vPositionEarn } from "@/src/valibot-utils"; +import { vBranchId, vPositionEarn } from "@/src/valibot-utils"; import * as dn from "dnum"; import * as v from "valibot"; import { createRequestSchema, verifyTransaction } from "./shared"; @@ -18,7 +19,7 @@ const RequestSchema = createRequestSchema( vPositionEarn(), ]), earnPosition: vPositionEarn(), - collIndex: vCollIndex(), + branchId: vBranchId(), claim: v.boolean(), }, ); @@ -31,7 +32,7 @@ export const earnDeposit: FlowDeclaration = { Summary({ request }) { return ( = { Status: TransactionStatus, async commit(ctx) { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + ctx.request.collIndex); - } + const branch = getBranch(ctx.request.branchId); const boldAmount = dn.sub( ctx.request.earnPosition.deposit, ctx.request.prevEarnPosition?.deposit ?? dn.from(0, 18), ); - return ctx.writeContract({ - ...collateral.contracts.StabilityPool, + ...branch.contracts.StabilityPool, functionName: "provideToSP", args: [ boldAmount[0], diff --git a/frontend/app/src/tx-flows/earnWithdraw.tsx b/frontend/app/src/tx-flows/earnWithdraw.tsx index cae2c35c6..f64847870 100644 --- a/frontend/app/src/tx-flows/earnWithdraw.tsx +++ b/frontend/app/src/tx-flows/earnWithdraw.tsx @@ -2,10 +2,11 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; import { Amount } from "@/src/comps/Amount/Amount"; import { EarnPositionSummary } from "@/src/comps/EarnPositionSummary/EarnPositionSummary"; +import { getBranch } from "@/src/liquity-utils"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; import { usePrice } from "@/src/services/Prices"; -import { vCollIndex, vPositionEarn } from "@/src/valibot-utils"; +import { vBranchId, vPositionEarn } from "@/src/valibot-utils"; import * as dn from "dnum"; import * as v from "valibot"; import { createRequestSchema, verifyTransaction } from "./shared"; @@ -15,7 +16,7 @@ const RequestSchema = createRequestSchema( { prevEarnPosition: vPositionEarn(), earnPosition: vPositionEarn(), - collIndex: vCollIndex(), + branchId: vBranchId(), claim: v.boolean(), }, ); @@ -28,7 +29,7 @@ export const earnWithdraw: FlowDeclaration = { Summary({ request }) { return ( = { Status: TransactionStatus, async commit(ctx) { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + ctx.request.collIndex); - } - const { StabilityPool } = collateral.contracts; - const boldAmount = dn.abs(dn.sub( ctx.request.earnPosition.deposit, ctx.request.prevEarnPosition.deposit, )); + const branch = getBranch(ctx.request.branchId); return ctx.writeContract({ - ...StabilityPool, + ...branch.contracts.StabilityPool, functionName: "withdrawFromSP", args: [boldAmount[0], ctx.request.claim], }); diff --git a/frontend/app/src/tx-flows/openBorrowPosition.tsx b/frontend/app/src/tx-flows/openBorrowPosition.tsx index 4a4726898..7589c6ce4 100644 --- a/frontend/app/src/tx-flows/openBorrowPosition.tsx +++ b/frontend/app/src/tx-flows/openBorrowPosition.tsx @@ -5,6 +5,7 @@ import { ETH_GAS_COMPENSATION } from "@/src/constants"; import { dnum18 } from "@/src/dnum-utils"; import { fmtnum } from "@/src/formatting"; import { + getBranch, getCollToken, getPrefixedTroveId, getTroveOperationHints, @@ -16,7 +17,7 @@ import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionS import { usePrice } from "@/src/services/Prices"; import { graphQuery, TroveByIdQuery } from "@/src/subgraph-queries"; import { sleep } from "@/src/utils"; -import { vAddress, vCollIndex, vDnum } from "@/src/valibot-utils"; +import { vAddress, vBranchId, vDnum } from "@/src/valibot-utils"; import { css } from "@/styled-system/css"; import { ADDRESS_ZERO, InfoTooltip, shortenAddress } from "@liquity2/uikit"; import * as dn from "dnum"; @@ -28,7 +29,7 @@ import { createRequestSchema, verifyTransaction } from "./shared"; const RequestSchema = createRequestSchema( "openBorrowPosition", { - collIndex: vCollIndex(), + branchId: vBranchId(), owner: vAddress(), ownerIndex: v.number(), collAmount: vDnum(), @@ -53,7 +54,7 @@ export const openBorrowPosition: FlowDeclaration = { Summary({ request }) { const upfrontFee = usePredictOpenTroveUpfrontFee( - request.collIndex, + request.branchId, request.boldAmount, request.interestRateDelegate?.[0] ?? request.annualInterestRate, ); @@ -70,7 +71,7 @@ export const openBorrowPosition: FlowDeclaration = { borrower: request.owner, batchManager: request.interestRateDelegate?.[0] ?? null, borrowed: boldAmountWithFee ?? dnum18(0), - collIndex: request.collIndex, + branchId: request.branchId, deposit: request.collAmount, interestRate: request.annualInterestRate, }} @@ -81,15 +82,15 @@ export const openBorrowPosition: FlowDeclaration = { }, Details({ request }) { - const collateral = getCollToken(request.collIndex); + const collateral = getCollToken(request.branchId); if (!collateral) { - throw new Error(`Invalid collateral index: ${request.collIndex}`); + throw new Error(`Invalid branch: ${request.branchId}`); } const collPrice = usePrice(collateral.symbol); const upfrontFee = usePredictOpenTroveUpfrontFee( - request.collIndex, + request.branchId, request.boldAmount, request.interestRateDelegate?.[0] ?? request.annualInterestRate, ); @@ -189,11 +190,8 @@ export const openBorrowPosition: FlowDeclaration = { // Approve LST approveLst: { name: (ctx) => { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } - return `Approve ${collateral.symbol}`; + const branch = getBranch(ctx.request.branchId); + return `Approve ${branch.symbol}`; }, Status: (props) => ( = { /> ), async commit(ctx) { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } - const { LeverageLSTZapper, CollToken } = collateral.contracts; + const branch = getBranch(ctx.request.branchId); + const { LeverageLSTZapper, CollToken } = branch.contracts; return ctx.writeContract({ ...CollToken, @@ -230,20 +225,16 @@ export const openBorrowPosition: FlowDeclaration = { Status: TransactionStatus, async commit(ctx) { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } - const { upperHint, lowerHint } = await getTroveOperationHints({ wagmiConfig: ctx.wagmiConfig, contracts: ctx.contracts, - collIndex: ctx.request.collIndex, + branchId: ctx.request.branchId, interestRate: ctx.request.annualInterestRate[0], }); + const branch = getBranch(ctx.request.branchId); return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "openTroveWithRawETH" as const, args: [{ owner: ctx.request.owner, @@ -271,12 +262,9 @@ export const openBorrowPosition: FlowDeclaration = { const receipt = await verifyTransaction(ctx.wagmiConfig, hash, ctx.isSafe); // extract trove ID from logs - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } + const branch = getBranch(ctx.request.branchId); const [troveOperation] = parseEventLogs({ - abi: collateral.contracts.TroveManager.abi, + abi: branch.contracts.TroveManager.abi, logs: receipt.logs, eventName: "TroveOperation", }); @@ -286,7 +274,7 @@ export const openBorrowPosition: FlowDeclaration = { } const prefixedTroveId = getPrefixedTroveId( - ctx.request.collIndex, + ctx.request.branchId, `0x${troveOperation.args._troveId.toString(16)}`, ); @@ -309,20 +297,16 @@ export const openBorrowPosition: FlowDeclaration = { Status: TransactionStatus, async commit(ctx) { - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } - const { upperHint, lowerHint } = await getTroveOperationHints({ wagmiConfig: ctx.wagmiConfig, contracts: ctx.contracts, - collIndex: ctx.request.collIndex, + branchId: ctx.request.branchId, interestRate: ctx.request.annualInterestRate[0], }); + const branch = getBranch(ctx.request.branchId); return ctx.writeContract({ - ...collateral.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "openTroveWithRawETH", args: [{ owner: ctx.request.owner, @@ -358,22 +342,18 @@ export const openBorrowPosition: FlowDeclaration = { throw new Error("Account address is required"); } - const collateral = ctx.contracts.collaterals[ctx.request.collIndex]; - if (!collateral) { - throw new Error(`Invalid collateral index: ${ctx.request.collIndex}`); - } - const { LeverageLSTZapper, CollToken } = collateral.contracts; + const branch = getBranch(ctx.request.branchId); - // ETH collateral doesn't need approval - if (collateral.symbol === "ETH") { + // ETH doesn't need approval + if (branch.symbol === "ETH") { return ["openTroveEth"]; } // Check if approval is needed const allowance = await readContract(ctx.wagmiConfig, { - ...CollToken, + ...branch.contracts.CollToken, functionName: "allowance", - args: [ctx.account, LeverageLSTZapper.address], + args: [ctx.account, branch.contracts.LeverageLSTZapper.address], }); const steps: string[] = []; diff --git a/frontend/app/src/tx-flows/openLeveragePosition.tsx b/frontend/app/src/tx-flows/openLeveragePosition.tsx index e981c2d4d..9bebeb10c 100644 --- a/frontend/app/src/tx-flows/openLeveragePosition.tsx +++ b/frontend/app/src/tx-flows/openLeveragePosition.tsx @@ -6,6 +6,7 @@ import { dnum18 } from "@/src/dnum-utils"; import { fmtnum } from "@/src/formatting"; import { getOpenLeveragedTroveParams } from "@/src/liquity-leverage"; import { + getBranch, getCollToken, getPrefixedTroveId, getTroveOperationHints, @@ -55,14 +56,14 @@ export const openLeveragePosition: FlowDeclaration Details({ request }) { const { loan } = request; - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { - throw new Error(`Invalid collateral index: ${loan.collIndex}`); + throw new Error(`Invalid branch: ${loan.branchId}`); } const collPrice = usePrice(collToken.symbol); const upfrontFee = usePredictOpenTroveUpfrontFee( - loan.collIndex, + loan.branchId, loan.borrowed, loan.interestRate, ); @@ -152,7 +153,7 @@ export const openLeveragePosition: FlowDeclaration steps: { approveLst: { name: ({ request }) => { - const collToken = getCollToken(request.loan.collIndex); + const collToken = getCollToken(request.loan.branchId); return `Approve ${collToken?.name ?? ""}`; }, Status: (props) => ( @@ -164,12 +165,8 @@ export const openLeveragePosition: FlowDeclaration async commit(ctx) { const { loan } = ctx.request; const initialDeposit = dn.div(loan.deposit, ctx.request.leverageFactor); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - const { LeverageLSTZapper, CollToken } = collateral.contracts; - + const branch = getBranch(loan.branchId); + const { LeverageLSTZapper, CollToken } = branch.contracts; return ctx.writeContract({ ...CollToken, functionName: "approve", @@ -193,14 +190,11 @@ export const openLeveragePosition: FlowDeclaration async commit(ctx) { const { loan } = ctx.request; const initialDeposit = dn.div(loan.deposit, ctx.request.leverageFactor); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - const { LeverageLSTZapper, LeverageWETHZapper } = collateral.contracts; + const branch = getBranch(loan.branchId); + const { LeverageLSTZapper, LeverageWETHZapper } = branch.contracts; const openLeveragedParams = await getOpenLeveragedTroveParams( - loan.collIndex, + loan.branchId, initialDeposit[0], ctx.request.leverageFactor, ctx.wagmiConfig, @@ -209,7 +203,7 @@ export const openLeveragePosition: FlowDeclaration const { upperHint, lowerHint } = await getTroveOperationHints({ wagmiConfig: ctx.wagmiConfig, contracts: ctx.contracts, - collIndex: loan.collIndex, + branchId: loan.branchId, interestRate: loan.interestRate[0], }); @@ -230,7 +224,7 @@ export const openLeveragePosition: FlowDeclaration }; // ETH collateral case - if (collateral.symbol === "ETH") { + if (branch.symbol === "ETH") { return ctx.writeContract({ ...LeverageWETHZapper, functionName: "openLeveragedTroveWithRawETH", @@ -252,16 +246,12 @@ export const openLeveragePosition: FlowDeclaration const receipt = await verifyTransaction(ctx.wagmiConfig, hash, ctx.isSafe); // Extract trove ID from logs - const collToken = getCollToken(ctx.request.loan.collIndex); - if (!collToken) throw new Error("Invalid collateral index"); - - const collateral = ctx.contracts.collaterals[ctx.request.loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + ctx.request.loan.collIndex); - } + const collToken = getCollToken(ctx.request.loan.branchId); + if (!collToken) throw new Error("Invalid branch"); + const branch = getBranch(ctx.request.loan.branchId); const [troveOperation] = parseEventLogs({ - abi: collateral.contracts.TroveManager.abi, + abi: branch.contracts.TroveManager.abi, logs: receipt.logs, eventName: "TroveOperation", }); @@ -272,7 +262,7 @@ export const openLeveragePosition: FlowDeclaration // Wait for trove to appear in subgraph const prefixedTroveId = getPrefixedTroveId( - ctx.request.loan.collIndex, + ctx.request.loan.branchId, `0x${troveOperation.args._troveId.toString(16)}`, ); @@ -293,9 +283,9 @@ export const openLeveragePosition: FlowDeclaration } const { loan } = ctx.request; - const collToken = getCollToken(loan.collIndex); + const collToken = getCollToken(loan.branchId); if (!collToken) { - throw new Error("Invalid collateral index: " + loan.collIndex); + throw new Error("Invalid branch: " + loan.branchId); } // ETH doesn't need approval @@ -303,12 +293,8 @@ export const openLeveragePosition: FlowDeclaration return ["openLeveragedTrove"]; } - const { collaterals } = ctx.contracts; - const collateral = collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - const { LeverageLSTZapper, CollToken } = collateral.contracts; + const branch = getBranch(loan.branchId); + const { LeverageLSTZapper, CollToken } = branch.contracts; const allowance = dnum18( await readContract(ctx.wagmiConfig, { diff --git a/frontend/app/src/tx-flows/shared.ts b/frontend/app/src/tx-flows/shared.ts index 861e2dedd..1041c0333 100644 --- a/frontend/app/src/tx-flows/shared.ts +++ b/frontend/app/src/tx-flows/shared.ts @@ -1,4 +1,4 @@ -import type { CollIndex, TroveId } from "@/src/types"; +import type { BranchId, TroveId } from "@/src/types"; import type { Config as WagmiConfig } from "wagmi"; import { getPrefixedTroveId } from "@/src/liquity-utils"; @@ -55,7 +55,7 @@ export async function verifyTroveUpdate( wagmiConfig: WagmiConfig, hash: string, loan: { - collIndex: CollIndex; + branchId: BranchId; troveId: TroveId; updatedAt: number; }, @@ -63,7 +63,7 @@ export async function verifyTroveUpdate( await waitForTransactionReceipt(wagmiConfig, { hash: hash as `0x${string}`, }); - const prefixedTroveId = getPrefixedTroveId(loan.collIndex, loan.troveId); + const prefixedTroveId = getPrefixedTroveId(loan.branchId, loan.troveId); while (true) { // wait for the trove to be updated in the subgraph const { trove } = await graphQuery( diff --git a/frontend/app/src/tx-flows/updateBorrowPosition.tsx b/frontend/app/src/tx-flows/updateBorrowPosition.tsx index 65f8668ba..8c1b199f2 100644 --- a/frontend/app/src/tx-flows/updateBorrowPosition.tsx +++ b/frontend/app/src/tx-flows/updateBorrowPosition.tsx @@ -3,7 +3,7 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; import { Amount } from "@/src/comps/Amount/Amount"; import { fmtnum } from "@/src/formatting"; -import { getCollToken, usePredictAdjustTroveUpfrontFee } from "@/src/liquity-utils"; +import { getBranch, getCollToken, usePredictAdjustTroveUpfrontFee } from "@/src/liquity-utils"; import { LoanCard } from "@/src/screens/TransactionsScreen/LoanCard"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; @@ -31,9 +31,9 @@ export const updateBorrowPosition: FlowDeclaration Summary({ request }) { const { loan, prevLoan } = request; - const collateral = getCollToken(loan.collIndex); + const collateral = getCollToken(loan.branchId); if (!collateral) { - throw new Error(`Invalid collateral index: ${loan.collIndex}`); + throw new Error(`Invalid branch: ${loan.branchId}`); } const upfrontFeeData = useUpfrontFeeData(loan, prevLoan); @@ -67,11 +67,7 @@ export const updateBorrowPosition: FlowDeclaration const { loan, prevLoan } = request; const collChange = getCollChange(loan, prevLoan); - - const collateral = getCollToken(loan.collIndex); - if (!collateral) { - throw new Error(`Invalid collateral index: ${loan.collIndex}`); - } + const collateral = getCollToken(loan.branchId); const collPrice = usePrice(collateral.symbol); const upfrontFeeData = useUpfrontFeeData(loan, prevLoan); @@ -142,13 +138,12 @@ export const updateBorrowPosition: FlowDeclaration ), async commit(ctx) { const debtChange = getDebtChange(ctx.request.loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[ctx.request.loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + ctx.request.loan.collIndex); - } - const Controller = collateral.symbol === "ETH" - ? collateral.contracts.LeverageWETHZapper - : collateral.contracts.LeverageLSTZapper; + + const branch = getBranch(ctx.request.loan.branchId); + + const Controller = branch.symbol === "ETH" + ? branch.contracts.LeverageWETHZapper + : branch.contracts.LeverageLSTZapper; return ctx.writeContract({ ...ctx.contracts.BoldToken, @@ -167,12 +162,9 @@ export const updateBorrowPosition: FlowDeclaration }, approveColl: { - name: ({ contracts, request }) => { - const coll = contracts.collaterals[request.loan.collIndex]; - if (!coll) { - throw new Error("Invalid collateral index: " + request.loan.collIndex); - } - return `Approve ${coll.symbol}`; + name: ({ request }) => { + const branch = getBranch(request.loan.branchId); + return `Approve ${branch.symbol}`; }, Status: (props) => ( async commit(ctx) { const collChange = getCollChange(ctx.request.loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[ctx.request.loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + ctx.request.loan.collIndex); - } + const branch = getBranch(ctx.request.loan.branchId); - const Controller = collateral.contracts.LeverageLSTZapper; + const Controller = branch.contracts.LeverageLSTZapper; return ctx.writeContract({ - ...collateral.contracts.CollToken, + ...branch.contracts.CollToken, functionName: "approve", args: [ Controller.address, @@ -215,16 +204,15 @@ export const updateBorrowPosition: FlowDeclaration const { loan, maxUpfrontFee } = ctx.request; const collChange = getCollChange(loan, ctx.request.prevLoan); const debtChange = getDebtChange(loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - if (collateral.symbol === "ETH") { + + const branch = getBranch(loan.branchId); + + if (branch.symbol === "ETH") { throw new Error("ETH collateral not supported for adjustTrove"); } return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "adjustTrove", args: [ BigInt(loan.troveId), @@ -249,21 +237,19 @@ export const updateBorrowPosition: FlowDeclaration async commit(ctx) { const { loan } = ctx.request; const debtChange = getDebtChange(loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - if (collateral.symbol === "ETH") { + const branch = getBranch(loan.branchId); + + if (branch.symbol === "ETH") { return ctx.writeContract({ - ...collateral.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "repayBold", args: [BigInt(loan.troveId), dn.abs(debtChange)[0]], }); } return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "repayBold", args: [BigInt(loan.troveId), dn.abs(debtChange)[0]], }); @@ -281,14 +267,12 @@ export const updateBorrowPosition: FlowDeclaration async commit(ctx) { const { loan } = ctx.request; const collChange = getCollChange(loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } - if (collateral.symbol === "ETH") { + const branch = getBranch(loan.branchId); + + if (branch.symbol === "ETH") { return ctx.writeContract({ - ...collateral.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "addCollWithRawETH", args: [BigInt(loan.troveId)], value: dn.abs(collChange)[0], @@ -296,7 +280,7 @@ export const updateBorrowPosition: FlowDeclaration } return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "addColl", args: [BigInt(loan.troveId), dn.abs(collChange)[0]], }); @@ -314,21 +298,18 @@ export const updateBorrowPosition: FlowDeclaration async commit(ctx) { const { loan, maxUpfrontFee } = ctx.request; const debtChange = getDebtChange(loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } + const branch = getBranch(loan.branchId); - if (collateral.symbol === "ETH") { + if (branch.symbol === "ETH") { return ctx.writeContract({ - ...collateral.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "withdrawBold", args: [BigInt(loan.troveId), dn.abs(debtChange)[0], maxUpfrontFee[0]], }); } return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "withdrawBold", args: [BigInt(loan.troveId), dn.abs(debtChange)[0], maxUpfrontFee[0]], }); @@ -346,21 +327,18 @@ export const updateBorrowPosition: FlowDeclaration async commit(ctx) { const { loan } = ctx.request; const collChange = getCollChange(loan, ctx.request.prevLoan); - const collateral = ctx.contracts.collaterals[loan.collIndex]; - if (!collateral) { - throw new Error("Invalid collateral index: " + loan.collIndex); - } + const branch = getBranch(loan.branchId); - if (collateral.symbol === "ETH") { + if (branch.symbol === "ETH") { return ctx.writeContract({ - ...collateral.contracts.LeverageWETHZapper, + ...branch.contracts.LeverageWETHZapper, functionName: "withdrawCollToRawETH", args: [BigInt(loan.troveId), dn.abs(collChange)[0]], }); } return ctx.writeContract({ - ...collateral.contracts.LeverageLSTZapper, + ...branch.contracts.LeverageLSTZapper, functionName: "withdrawColl", args: [BigInt(loan.troveId), dn.abs(collChange)[0]], }); @@ -379,14 +357,12 @@ export const updateBorrowPosition: FlowDeclaration const debtChange = getDebtChange(ctx.request.loan, ctx.request.prevLoan); const collChange = getCollChange(ctx.request.loan, ctx.request.prevLoan); - const coll = ctx.contracts.collaterals[ctx.request.loan.collIndex]; - if (!coll) { - throw new Error("Invalid collateral index: " + ctx.request.loan.collIndex); - } - const Controller = coll.symbol === "ETH" - ? coll.contracts.LeverageWETHZapper - : coll.contracts.LeverageLSTZapper; + const branch = getBranch(ctx.request.loan.branchId); + + const Controller = branch.symbol === "ETH" + ? branch.contracts.LeverageWETHZapper + : branch.contracts.LeverageLSTZapper; const isBoldApproved = !dn.lt(debtChange, 0) || !dn.gt( dn.abs(debtChange), @@ -401,9 +377,9 @@ export const updateBorrowPosition: FlowDeclaration ); // Collateral token needs to be approved if collChange > 0 and collToken != "ETH" (no LeverageWETHZapper) - const isCollApproved = coll.symbol === "ETH" || !dn.gt(collChange, 0) || !dn.gt(collChange, [ + const isCollApproved = branch.symbol === "ETH" || !dn.gt(collChange, 0) || !dn.gt(collChange, [ await ctx.readContract({ - ...coll.contracts.CollToken, + ...branch.contracts.CollToken, functionName: "allowance", args: [ctx.account, Controller.address], }) ?? 0n, @@ -415,7 +391,7 @@ export const updateBorrowPosition: FlowDeclaration if (!isBoldApproved) steps.push("approveBold"); if (!isCollApproved) steps.push("approveColl"); - return steps.concat(getFinalSteps(ctx.request, coll.symbol)); + return steps.concat(getFinalSteps(ctx.request, branch.symbol)); }, parseRequest(request) { @@ -477,7 +453,7 @@ function useUpfrontFeeData( const isBorrowing = dn.gt(debtChange, 0); const upfrontFee = usePredictAdjustTroveUpfrontFee( - loan.collIndex, + loan.branchId, loan.troveId, isBorrowing ? debtChange : [0n, 18], ); diff --git a/frontend/app/src/tx-flows/updateLeveragePosition.tsx b/frontend/app/src/tx-flows/updateLeveragePosition.tsx index 20634144b..a304fa88b 100644 --- a/frontend/app/src/tx-flows/updateLeveragePosition.tsx +++ b/frontend/app/src/tx-flows/updateLeveragePosition.tsx @@ -6,7 +6,7 @@ import { MAX_UPFRONT_FEE } from "@/src/constants"; import { dnum18 } from "@/src/dnum-utils"; import { fmtnum } from "@/src/formatting"; import { getLeverDownTroveParams, getLeverUpTroveParams } from "@/src/liquity-leverage"; -import { getCollToken, usePredictAdjustTroveUpfrontFee } from "@/src/liquity-utils"; +import { getBranch, getCollToken, usePredictAdjustTroveUpfrontFee } from "@/src/liquity-utils"; import { LoanCard } from "@/src/screens/TransactionsScreen/LoanCard"; import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; @@ -46,7 +46,7 @@ function useUpfrontFeeData( const isBorrowing = dn.gt(debtChange, 0); const upfrontFee = usePredictAdjustTroveUpfrontFee( - loan.collIndex, + loan.branchId, loan.troveId, isBorrowing ? debtChange : [0n, 18], ); @@ -68,10 +68,6 @@ export const updateLeveragePosition: FlowDeclaration { - const token = getCollToken(request.loan.collIndex); + const token = getCollToken(request.loan.branchId); return `Approve ${token?.name ?? ""}`; }, Status: (props) => ( @@ -196,14 +190,11 @@ export const updateLeveragePosition: FlowDeclaration= 0 && value <= 9; } @@ -47,7 +56,7 @@ export type PositionLoanBase = { batchManager: null | Address; borrowed: Dnum; borrower: Address; - collIndex: CollIndex; + branchId: BranchId; deposit: Dnum; interestRate: Dnum; status: @@ -86,7 +95,7 @@ export function isPositionLoanUncommitted( export type PositionEarn = { type: "earn"; owner: Address; - collIndex: CollIndex; + branchId: BranchId; deposit: Dnum; rewards: { bold: Dnum; diff --git a/frontend/app/src/valibot-utils.ts b/frontend/app/src/valibot-utils.ts index db05c58d7..9da1aca50 100644 --- a/frontend/app/src/valibot-utils.ts +++ b/frontend/app/src/valibot-utils.ts @@ -28,7 +28,7 @@ export function vDnum() { return v.custom(isDnum, "not a Dnum"); } -export function vCollIndex() { +export function vBranchId() { return v.union([ v.literal(0), v.literal(1), @@ -184,7 +184,7 @@ const VPositionLoanBase = v.object({ batchManager: v.union([v.null(), vAddress()]), borrowed: vDnum(), borrower: vAddress(), - collIndex: vCollIndex(), + branchId: vBranchId(), deposit: vDnum(), interestRate: vDnum(), status: v.union([ @@ -226,7 +226,7 @@ export function vPositionEarn() { return v.object({ type: v.literal("earn"), owner: vAddress(), - collIndex: vCollIndex(), + branchId: vBranchId(), deposit: vDnum(), rewards: v.object({ bold: vDnum(), From 561ccab09b0e3c0c38572e128f8ccc971ae1aa3a Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Fri, 7 Feb 2025 10:25:23 +0000 Subject: [PATCH 3/6] getCollateralContract(s) => getBranchContract(s) --- .../InterestRateField/IcStrategiesModal.tsx | 6 +-- frontend/app/src/contracts.ts | 35 ++++++++++++----- frontend/app/src/liquity-leverage.ts | 38 +++---------------- frontend/app/src/liquity-stability-pool.ts | 6 +-- frontend/app/src/liquity-utils.ts | 10 ++--- .../screens/AccountScreen/AccountScreen.tsx | 4 +- .../app/src/screens/LoanScreen/LoanScreen.tsx | 13 +------ frontend/app/src/services/Prices.tsx | 4 +- .../src/tx-flows/claimCollateralSurplus.tsx | 11 ------ 9 files changed, 47 insertions(+), 80 deletions(-) diff --git a/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx index 948a82eb7..7b477a820 100644 --- a/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx +++ b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx @@ -1,7 +1,7 @@ import type { BranchId, Delegate } from "@/src/types"; import content from "@/src/content"; -import { getCollateralContracts } from "@/src/contracts"; +import { getBranchContracts } from "@/src/contracts"; import { IC_STRATEGIES } from "@/src/demo-mode"; import { css } from "@/styled-system/css"; import { Modal, TextButton } from "@liquity2/uikit"; @@ -23,11 +23,11 @@ export function IcStrategiesModal({ onSelectDelegate: (delegate: Delegate) => void; visible: boolean; }) { - const collateral = getCollateralContracts(branchId); + const branchContracts = getBranchContracts(branchId); const [displayedDelegates, setDisplayedDelegates] = useState(5); - console.log(collateral); + console.log(branchContracts); return ( ( return CONTRACTS[name]; } -export function getCollateralContracts(branchIdOrSymbol: null): null; -export function getCollateralContracts(branchIdOrSymbol: CollateralSymbol | BranchId): BranchContracts; -export function getCollateralContracts( +export function getBranchContracts(branchIdOrSymbol: null): null; +export function getBranchContracts(branchIdOrSymbol: CollateralSymbol | BranchId): BranchContracts; +export function getBranchContracts( branchIdOrSymbol: CollateralSymbol | BranchId | null, ): BranchContracts | null; -export function getCollateralContracts( +export function getBranchContracts( branchIdOrSymbol: CollateralSymbol | BranchId | null, ): BranchContracts | null { if (branchIdOrSymbol === null) { return null; } const { branches } = getContracts(); - const collateral = typeof branchIdOrSymbol === "number" + const branch = typeof branchIdOrSymbol === "number" ? branches[branchIdOrSymbol] : branches.find((c) => c.symbol === branchIdOrSymbol); - if (collateral?.contracts) { - return collateral.contracts; + if (branch?.contracts) { + return branch.contracts; } throw new Error(`No collateral for index or symbol ${branchIdOrSymbol}`); } -export function getCollateralContract( +export function getBranchContract(branchIdOrSymbol: null, contractName: CollateralContractName): null; +export function getBranchContract( + branchIdOrSymbol: CollateralSymbol | BranchId, + contractName: CN, +): Contract; +export function getBranchContract( + branchIdOrSymbol: CollateralSymbol | BranchId | null, + contractName: CN, +): Contract | null; +export function getBranchContract( branchIdOrSymbol: CollateralSymbol | BranchId | null, contractName: CN, ): Contract | null { - const contracts = getCollateralContracts(branchIdOrSymbol); - return contracts?.[contractName] ?? null; + if (branchIdOrSymbol === null) { + return null; + } + const contracts = getBranchContracts(branchIdOrSymbol); + if (contracts[contractName]) { + return contracts[contractName]; + } + throw new Error(`No contract ${contractName} for branch ${branchIdOrSymbol}`); } diff --git a/frontend/app/src/liquity-leverage.ts b/frontend/app/src/liquity-leverage.ts index 31f9d0f57..26444f12f 100644 --- a/frontend/app/src/liquity-leverage.ts +++ b/frontend/app/src/liquity-leverage.ts @@ -3,7 +3,7 @@ import type { Config as WagmiConfig } from "wagmi"; import { CLOSE_FROM_COLLATERAL_SLIPPAGE, DATA_REFRESH_INTERVAL } from "@/src/constants"; import { getProtocolContract } from "@/src/contracts"; -import { getCollateralContracts } from "@/src/contracts"; +import { getBranchContracts } from "@/src/contracts"; import { dnum18 } from "@/src/dnum-utils"; import { useDebouncedQueryKey } from "@/src/react-utils"; import { useWagmiConfig } from "@/src/services/Ethereum"; @@ -19,14 +19,7 @@ export async function getLeverUpTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(branchId); - - if (!collContracts) { - throw new Error("Invalid branch: " + branchId); - } - - const { PriceFeed, TroveManager } = collContracts; - + const { PriceFeed, TroveManager } = getBranchContracts(branchId); const [priceResult, troveDataResult] = await readContracts(wagmiConfig, { contracts: [{ abi: PriceFeed.abi, @@ -78,14 +71,7 @@ export async function getLeverDownTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(branchId); - - if (!collContracts) { - throw new Error("Invalid branch: " + branchId); - } - - const { PriceFeed, TroveManager } = collContracts; - + const { PriceFeed, TroveManager } = getBranchContracts(branchId); const [priceResult, troveDataResult] = await readContracts(wagmiConfig, { contracts: [{ abi: PriceFeed.abi, @@ -138,14 +124,7 @@ export async function getOpenLeveragedTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const collContracts = getCollateralContracts(branchId); - - if (!collContracts) { - throw new Error("Invalid branch: " + branchId); - } - - const { PriceFeed } = collContracts; - + const { PriceFeed } = getBranchContracts(branchId); const [price] = await readContract(wagmiConfig, { abi: PriceFeed.abi, address: PriceFeed.address, @@ -172,14 +151,7 @@ export async function getCloseFlashLoanAmount( troveId: TroveId, wagmiConfig: WagmiConfig, ): Promise { - const collContracts = getCollateralContracts(branchId); - - if (!collContracts) { - throw new Error("Invalid branch: " + branchId); - } - - const { PriceFeed, TroveManager } = collContracts; - + const { PriceFeed, TroveManager } = getBranchContracts(branchId); const [priceResult, latestTroveDataResult] = await readContracts(wagmiConfig, { contracts: [ { diff --git a/frontend/app/src/liquity-stability-pool.ts b/frontend/app/src/liquity-stability-pool.ts index 190145e71..c018b1fe5 100644 --- a/frontend/app/src/liquity-stability-pool.ts +++ b/frontend/app/src/liquity-stability-pool.ts @@ -3,7 +3,7 @@ import type { UseQueryResult } from "@tanstack/react-query"; import type { Dnum } from "dnum"; import { SP_YIELD_SPLIT } from "@/src/constants"; -import { getCollateralContract } from "@/src/contracts"; +import { getBranchContract } from "@/src/contracts"; import { dnum18 } from "@/src/dnum-utils"; import { CHAIN_CONTRACT_MULTICALL } from "@/src/env"; import { getCollToken } from "@/src/liquity-utils"; @@ -97,8 +97,8 @@ type DepositParameters = { }; function useSpYieldGainParameters(symbol: CollateralSymbol | null) { - const ActivePool = getCollateralContract(symbol, "ActivePool"); - const StabilityPool = getCollateralContract(symbol, "StabilityPool"); + const ActivePool = getBranchContract(symbol, "ActivePool"); + const StabilityPool = getBranchContract(symbol, "StabilityPool"); const AP = ActivePool as NonNullable; const SP = StabilityPool as NonNullable; diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index f7e2aaf9a..0c58edd4f 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -15,7 +15,7 @@ import type { UseQueryResult } from "@tanstack/react-query"; import type { Config as WagmiConfig } from "wagmi"; import { DATA_REFRESH_INTERVAL, INTEREST_RATE_INCREMENT, INTEREST_RATE_MAX, INTEREST_RATE_MIN } from "@/src/constants"; -import { getCollateralContract, getContracts, getProtocolContract } from "@/src/contracts"; +import { getBranchContract, getContracts, getProtocolContract } from "@/src/contracts"; import { dnum18, DNUM_0, dnumOrNull, jsonStringifyWithDnum } from "@/src/dnum-utils"; import { CHAIN_BLOCK_EXPLORER, LIQUITY_STATS_URL } from "@/src/env"; import { useContinuousBoldGains } from "@/src/liquity-stability-pool"; @@ -160,7 +160,7 @@ export function useEarnPosition( enabled: getBoldGains.status === "success", }); - const StabilityPool = getCollateralContract(branchId, "StabilityPool"); + const StabilityPool = getBranchContract(branchId, "StabilityPool"); if (!StabilityPool) { throw new Error(`Invalid branch: ${branchId}`); } @@ -325,7 +325,7 @@ export function useStakePosition(address: null | Address) { } export function useTroveNftUrl(branchId: null | BranchId, troveId: null | TroveId) { - const TroveNft = getCollateralContract(branchId, "TroveNFT"); + const TroveNft = getBranchContract(branchId, "TroveNFT"); return TroveNft && troveId && `${CHAIN_BLOCK_EXPLORER?.url}nft/${TroveNft.address}/${BigInt(troveId)}`; } @@ -610,7 +610,7 @@ export function useLiquityStats() { } export function useLatestTroveData(branchId: BranchId, troveId: TroveId) { - const TroveManager = getCollateralContract(branchId, "TroveManager"); + const TroveManager = getBranchContract(branchId, "TroveManager"); if (!TroveManager) { throw new Error(`Invalid branch: ${branchId}`); } @@ -669,7 +669,7 @@ export function useInterestBatchDelegate( branchId: null | BranchId, address: null | Address, ): UseQueryResult { - const BorrowerOperations = getCollateralContract(branchId, "BorrowerOperations"); + const BorrowerOperations = getBranchContract(branchId, "BorrowerOperations"); if (!BorrowerOperations) { throw new Error(`Invalid branch: ${branchId}`); } diff --git a/frontend/app/src/screens/AccountScreen/AccountScreen.tsx b/frontend/app/src/screens/AccountScreen/AccountScreen.tsx index 81f9ade62..d87f343b6 100644 --- a/frontend/app/src/screens/AccountScreen/AccountScreen.tsx +++ b/frontend/app/src/screens/AccountScreen/AccountScreen.tsx @@ -6,7 +6,7 @@ import type { ReactNode } from "react"; import { ERC20Faucet } from "@/src/abi/ERC20Faucet"; import { Positions } from "@/src/comps/Positions/Positions"; import { Screen } from "@/src/comps/Screen/Screen"; -import { getCollateralContract, getProtocolContract } from "@/src/contracts"; +import { getBranchContract, getProtocolContract } from "@/src/contracts"; import { CHAIN_ID } from "@/src/env"; import { fmtnum } from "@/src/formatting"; import { getBranches } from "@/src/liquity-utils"; @@ -166,7 +166,7 @@ function Balance({ const balance = useBalance(address, tokenSymbol); const LqtyToken = getProtocolContract("LqtyToken"); - const CollToken = getCollateralContract( + const CollToken = getBranchContract( isCollateralSymbol(tokenSymbol) ? tokenSymbol : null, "CollToken", ); diff --git a/frontend/app/src/screens/LoanScreen/LoanScreen.tsx b/frontend/app/src/screens/LoanScreen/LoanScreen.tsx index 0110b6d88..78423c552 100644 --- a/frontend/app/src/screens/LoanScreen/LoanScreen.tsx +++ b/frontend/app/src/screens/LoanScreen/LoanScreen.tsx @@ -5,7 +5,7 @@ import type { PositionLoanCommitted } from "@/src/types"; import { Field } from "@/src/comps/Field/Field"; import { Screen } from "@/src/comps/Screen/Screen"; import content from "@/src/content"; -import { getCollateralContract } from "@/src/contracts"; +import { getBranchContract } from "@/src/contracts"; import { dnum18 } from "@/src/dnum-utils"; import { fmtnum } from "@/src/formatting"; import { getCollToken, getPrefixedTroveId, parsePrefixedTroveId, useLoan } from "@/src/liquity-utils"; @@ -225,19 +225,10 @@ function ClaimCollateralSurplus({ const account = useAccount(); const txFlow = useTransactionFlow(); const collToken = getCollToken(loan.branchId); - if (!collToken) { - throw new Error(`collToken not found for index ${loan.branchId}`); - } - - const csp = getCollateralContract(loan.branchId, "CollSurplusPool"); - if (!csp) { - throw new Error("Collateral surplus pool not found for branch: " + loan.branchId); - } - const collPriceUsd = usePrice(collToken.symbol); const collSurplus = useReadContract({ - ...csp, + ...getBranchContract(loan.branchId, "CollSurplusPool"), functionName: "getCollateral", args: [loan.borrower], query: { diff --git a/frontend/app/src/services/Prices.tsx b/frontend/app/src/services/Prices.tsx index e9957aa71..1f63337f6 100644 --- a/frontend/app/src/services/Prices.tsx +++ b/frontend/app/src/services/Prices.tsx @@ -5,7 +5,7 @@ import type { UseQueryResult } from "@tanstack/react-query"; import type { Dnum } from "dnum"; import { PRICE_REFRESH_INTERVAL } from "@/src/constants"; -import { getCollateralContract } from "@/src/contracts"; +import { getBranchContract } from "@/src/contracts"; import { dnum18 } from "@/src/dnum-utils"; import { COINGECKO_API_KEY } from "@/src/env"; import { isCollateralSymbol } from "@liquity2/uikit"; @@ -19,7 +19,7 @@ type PriceToken = "LQTY" | "BOLD" | "LUSD" | CollateralSymbol; function useCollateralPrice(symbol: null | CollateralSymbol): UseQueryResult { // "ETH" is a fallback when null is passed, so we can return a standard // query object from the PriceFeed ABI, while the query stays disabled - const PriceFeed = getCollateralContract(symbol ?? "ETH", "PriceFeed"); + const PriceFeed = getBranchContract(symbol ?? "ETH", "PriceFeed"); if (!PriceFeed) { throw new Error(`Price feed contract not found for ${symbol}`); diff --git a/frontend/app/src/tx-flows/claimCollateralSurplus.tsx b/frontend/app/src/tx-flows/claimCollateralSurplus.tsx index 3f1b057c7..ac0f346f4 100644 --- a/frontend/app/src/tx-flows/claimCollateralSurplus.tsx +++ b/frontend/app/src/tx-flows/claimCollateralSurplus.tsx @@ -1,7 +1,6 @@ import type { FlowDeclaration } from "@/src/services/TransactionFlow"; import type { ReactNode } from "react"; -import { getCollateralContract } from "@/src/contracts"; import { fmtnum } from "@/src/formatting"; import { getBranch, getCollToken } from "@/src/liquity-utils"; import { TransactionStatus } from "@/src/screens/TransactionsScreen/TransactionStatus"; @@ -29,17 +28,7 @@ export const claimCollateralSurplus: FlowDeclaration Date: Fri, 7 Feb 2025 10:26:15 +0000 Subject: [PATCH 4/6] HomeScreen: show debt by collateral --- frontend/app/src/liquity-utils.ts | 12 ++++++++++ .../app/src/screens/HomeScreen/HomeScreen.tsx | 24 ++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index 0c58edd4f..626f7ce6a 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -595,6 +595,18 @@ const StatsSchema = v.pipe( })), ); +export function useBranchDebt(branchId: BranchId) { + const BorrowerOperations = getBranchContract(branchId, "BorrowerOperations"); + return useReadContract({ + ...BorrowerOperations, + functionName: "getEntireSystemDebt", + query: { + refetchInterval: DATA_REFRESH_INTERVAL, + select: dnum18, + }, + }); +} + export function useLiquityStats() { return useQuery({ queryKey: ["liquity-stats"], diff --git a/frontend/app/src/screens/HomeScreen/HomeScreen.tsx b/frontend/app/src/screens/HomeScreen/HomeScreen.tsx index 747e214ea..245858f20 100644 --- a/frontend/app/src/screens/HomeScreen/HomeScreen.tsx +++ b/frontend/app/src/screens/HomeScreen/HomeScreen.tsx @@ -5,7 +5,14 @@ import type { CollateralSymbol } from "@/src/types"; import { Amount } from "@/src/comps/Amount/Amount"; import { Positions } from "@/src/comps/Positions/Positions"; import { DNUM_1 } from "@/src/dnum-utils"; -import { getBranch, getBranches, getCollToken, useAverageInterestRate, useEarnPool } from "@/src/liquity-utils"; +import { + getBranch, + getBranches, + getCollToken, + useAverageInterestRate, + useBranchDebt, + useEarnPool, +} from "@/src/liquity-utils"; import { useAccount } from "@/src/services/Ethereum"; import { css } from "@/styled-system/css"; import { AnchorTextButton, IconBorrow, IconEarn, TokenIcon } from "@liquity2/uikit"; @@ -46,6 +53,7 @@ export function HomeScreen() { Max LTV , + "Total debt", null, ] as const} rows={branches.map(({ symbol }) => ( @@ -88,6 +96,7 @@ function BorrowingRow({ const branch = getBranch(symbol); const collateral = getCollToken(branch.id); const avgInterestRate = useAverageInterestRate(branch.id); + const branchDebt = useBranchDebt(branch.id); const maxLtv = collateral?.collateralRatio && dn.gt(collateral.collateralRatio, 0) ? dn.div(DNUM_1, collateral.collateralRatio) @@ -120,6 +129,13 @@ function BorrowingRow({ percentage /> + + +
- - + */ + }
From 1b423bd5fdcd8173f80287efca27708f6b665fbe Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Fri, 7 Feb 2025 10:26:41 +0000 Subject: [PATCH 5/6] Fix subgraph query property (id => collIndex) --- frontend/app/src/subgraph-hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/src/subgraph-hooks.ts b/frontend/app/src/subgraph-hooks.ts index a157f819c..ce5998d2a 100644 --- a/frontend/app/src/subgraph-hooks.ts +++ b/frontend/app/src/subgraph-hooks.ts @@ -455,7 +455,7 @@ function subgraphTroveToLoan( throw new Error(`Invalid trove ID: ${trove.id} / ${trove.troveId}`); } - const branchId = trove.collateral.id; + const branchId = trove.collateral.collIndex; if (!isBranchId(branchId)) { throw new Error(`Invalid branch: ${branchId}`); } From ac05fe3d07782ffb552af93c912cd927be31fb50 Mon Sep 17 00:00:00 2001 From: Pierre Bertet Date: Sat, 8 Feb 2025 01:20:28 +0000 Subject: [PATCH 6/6] Typing fixes + remove getBranchContracts() & getContracts() --- frontend/app/src/comps/About/About.tsx | 14 +-- .../InterestRateField/IcStrategiesModal.tsx | 6 +- frontend/app/src/contracts.ts | 100 ++++++++++-------- frontend/app/src/env.ts | 19 ++-- frontend/app/src/liquity-leverage.ts | 33 +++--- frontend/app/src/liquity-utils.ts | 20 ++-- frontend/app/src/services/TransactionFlow.tsx | 8 +- frontend/app/src/types.ts | 3 + 8 files changed, 104 insertions(+), 99 deletions(-) diff --git a/frontend/app/src/comps/About/About.tsx b/frontend/app/src/comps/About/About.tsx index 7f7f2339c..30472b1c1 100644 --- a/frontend/app/src/comps/About/About.tsx +++ b/frontend/app/src/comps/About/About.tsx @@ -33,14 +33,14 @@ const ENV_EXCLUDE: Set = new Set([ // - contracts: main contracts (CONTRACT_*) // - branches: branches contracts (in COLLATERAL_CONTRACTS) function getEnvGroups() { - const config = { ...env }; + const envConfig = { ...env }; const contracts: Record = {}; for (const [key, value] of Object.entries(env) as Entries) { if (key.startsWith("CONTRACT_")) { contracts[key.replace("CONTRACT_", "")] = String(value); - delete config[key]; + delete envConfig[key]; continue; } } @@ -51,7 +51,7 @@ function getEnvGroups() { contracts: [string, string][]; }[] = []; - for (const { branchId, symbol, contracts } of config.COLLATERAL_CONTRACTS) { + for (const { branchId, symbol, contracts } of envConfig.BRANCHES) { branches.push({ branchId, symbol, @@ -61,10 +61,10 @@ function getEnvGroups() { }); } - delete config["COLLATERAL_CONTRACTS" as keyof typeof config]; + delete envConfig["COLLATERAL_CONTRACTS" as keyof typeof envConfig]; - const configFinal = Object.fromEntries( - Object.entries(config) + const envConfigFinal = Object.fromEntries( + Object.entries(envConfig) .filter(([key]) => !ENV_EXCLUDE.has(key as keyof typeof env)) .map(([key, value]) => { if (key === "CHAIN_BLOCK_EXPLORER") { @@ -79,7 +79,7 @@ function getEnvGroups() { }), ); - return { config: configFinal, contracts, branches }; + return { config: envConfigFinal, contracts, branches }; } const AboutContext = createContext<{ diff --git a/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx index 7b477a820..4842c588d 100644 --- a/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx +++ b/frontend/app/src/comps/InterestRateField/IcStrategiesModal.tsx @@ -1,8 +1,8 @@ import type { BranchId, Delegate } from "@/src/types"; import content from "@/src/content"; -import { getBranchContracts } from "@/src/contracts"; import { IC_STRATEGIES } from "@/src/demo-mode"; +import { getBranch } from "@/src/liquity-utils"; import { css } from "@/styled-system/css"; import { Modal, TextButton } from "@liquity2/uikit"; import Image from "next/image"; @@ -23,11 +23,11 @@ export function IcStrategiesModal({ onSelectDelegate: (delegate: Delegate) => void; visible: boolean; }) { - const branchContracts = getBranchContracts(branchId); + const branch = getBranch(branchId); const [displayedDelegates, setDisplayedDelegates] = useState(5); - console.log(branchContracts); + console.log(branch); return ( ; }; -const CONTRACTS: Contracts = { +export const CONTRACTS: Contracts = { BoldToken: { abi: abis.BoldToken, address: CONTRACT_BOLD_TOKEN }, - CollateralRegistry: { abi: abis.CollateralRegistry, address: CONTRACT_COLLATERAL_REGISTRY }, + CollateralRegistry: { + abi: abis.CollateralRegistry, + address: CONTRACT_COLLATERAL_REGISTRY, + }, Governance: { abi: abis.Governance, address: CONTRACT_GOVERNANCE }, - ExchangeHelpers: { abi: abis.ExchangeHelpers, address: CONTRACT_EXCHANGE_HELPERS }, + ExchangeHelpers: { + abi: abis.ExchangeHelpers, + address: CONTRACT_EXCHANGE_HELPERS, + }, HintHelpers: { abi: abis.HintHelpers, address: CONTRACT_HINT_HELPERS }, LqtyStaking: { abi: abis.LqtyStaking, address: CONTRACT_LQTY_STAKING }, LqtyToken: { abi: abis.LqtyToken, address: CONTRACT_LQTY_TOKEN }, LusdToken: { abi: abis.LusdToken, address: CONTRACT_LUSD_TOKEN }, - MultiTroveGetter: { abi: abis.MultiTroveGetter, address: CONTRACT_MULTI_TROVE_GETTER }, + MultiTroveGetter: { + abi: abis.MultiTroveGetter, + address: CONTRACT_MULTI_TROVE_GETTER, + }, WETH: { abi: abis.WETH, address: CONTRACT_WETH }, - - branches: COLLATERAL_CONTRACTS.map(({ branchId, symbol, contracts }) => ({ + branches: BRANCHES.map(({ branchId, symbol, contracts }) => ({ id: branchId, branchId, symbol, contracts: { ActivePool: { address: contracts.ACTIVE_POOL, abi: abis.ActivePool }, - BorrowerOperations: { address: contracts.BORROWER_OPERATIONS, abi: abis.BorrowerOperations }, - CollSurplusPool: { address: contracts.COLL_SURPLUS_POOL, abi: abis.CollSurplusPool }, + BorrowerOperations: { + address: contracts.BORROWER_OPERATIONS, + abi: abis.BorrowerOperations, + }, + CollSurplusPool: { + address: contracts.COLL_SURPLUS_POOL, + abi: abis.CollSurplusPool, + }, CollToken: { address: contracts.COLL_TOKEN, abi: abis.CollToken }, DefaultPool: { address: contracts.DEFAULT_POOL, abi: abis.DefaultPool }, LeverageLSTZapper: { @@ -131,45 +150,26 @@ const CONTRACTS: Contracts = { }, PriceFeed: { address: contracts.PRICE_FEED, abi: abis.PriceFeed }, SortedTroves: { address: contracts.SORTED_TROVES, abi: abis.SortedTroves }, - StabilityPool: { address: contracts.STABILITY_POOL, abi: abis.StabilityPool }, + StabilityPool: { + address: contracts.STABILITY_POOL, + abi: abis.StabilityPool, + }, TroveManager: { address: contracts.TROVE_MANAGER, abi: abis.TroveManager }, TroveNFT: { address: contracts.TROVE_NFT, abi: abis.TroveNFT }, }, })), }; -export function getContracts(): Contracts { - return CONTRACTS; -} - -export function getProtocolContract( - name: CN, -): ProtocolContractMap[CN] { +export function getProtocolContract< + CN extends ProtocolContractName, +>(name: CN): ProtocolContractMap[CN] { return CONTRACTS[name]; } -export function getBranchContracts(branchIdOrSymbol: null): null; -export function getBranchContracts(branchIdOrSymbol: CollateralSymbol | BranchId): BranchContracts; -export function getBranchContracts( - branchIdOrSymbol: CollateralSymbol | BranchId | null, -): BranchContracts | null; -export function getBranchContracts( - branchIdOrSymbol: CollateralSymbol | BranchId | null, -): BranchContracts | null { - if (branchIdOrSymbol === null) { - return null; - } - const { branches } = getContracts(); - const branch = typeof branchIdOrSymbol === "number" - ? branches[branchIdOrSymbol] - : branches.find((c) => c.symbol === branchIdOrSymbol); - if (branch?.contracts) { - return branch.contracts; - } - throw new Error(`No collateral for index or symbol ${branchIdOrSymbol}`); -} - -export function getBranchContract(branchIdOrSymbol: null, contractName: CollateralContractName): null; +export function getBranchContract( + branchIdOrSymbol: null, + contractName: CollateralContractName, +): null; export function getBranchContract( branchIdOrSymbol: CollateralSymbol | BranchId, contractName: CN, @@ -185,9 +185,19 @@ export function getBranchContract( if (branchIdOrSymbol === null) { return null; } - const contracts = getBranchContracts(branchIdOrSymbol); - if (contracts[contractName]) { - return contracts[contractName]; + const { branches } = CONTRACTS; + + const branch = typeof branchIdOrSymbol === "number" + ? branches[branchIdOrSymbol] + : branches.find((c) => c.symbol === branchIdOrSymbol); + if (!branch) { + throw new Error(`No branch for index or symbol ${branchIdOrSymbol}`); + } + + const contract = branch.contracts[contractName]; + if (!contract) { + throw new Error(`No contract ${contractName} for branch ${branchIdOrSymbol}`); } - throw new Error(`No contract ${contractName} for branch ${branchIdOrSymbol}`); + + return contract; } diff --git a/frontend/app/src/env.ts b/frontend/app/src/env.ts index 1c2716523..75bfba145 100644 --- a/frontend/app/src/env.ts +++ b/frontend/app/src/env.ts @@ -1,4 +1,4 @@ -import type { Address, BranchId } from "@/src/types"; +import type { Address, Branch, BranchId } from "@/src/types"; import { isBranchId } from "@/src/types"; import { vAddress, vEnvAddressAndBlock, vEnvCurrency, vEnvFlag, vEnvLink, vEnvUrlOrDefault } from "@/src/valibot-utils"; @@ -195,13 +195,11 @@ export const EnvSchema = v.pipe( ] as const; type ContractEnvName = typeof contractsEnvNames[number]; - - const collateralContracts: Array<{ - branchId: BranchId; + type BranchEnv = Omit & { contracts: Record; - strategies: Array<{ address: Address; name: string }>; - symbol: v.InferOutput; - }> = []; + }; + + const branches: BranchEnv[] = []; for (const index of Array(10).keys()) { const collEnvName = `COLL_${index}` as const; @@ -227,7 +225,8 @@ export const EnvSchema = v.pipe( throw new Error(`Invalid branch: ${index}`); } - collateralContracts[index] = { + branches[index] = { + id: index, branchId: index, contracts: contracts as Record, strategies: (env[`${collEnvName}_IC_STRATEGIES` as keyof typeof env] ?? []) as Array< @@ -239,7 +238,7 @@ export const EnvSchema = v.pipe( return { ...env, - COLLATERAL_CONTRACTS: collateralContracts, + BRANCHES: branches, }; }), ); @@ -379,7 +378,7 @@ export const { CHAIN_NAME, CHAIN_RPC_URL, COINGECKO_API_KEY, - COLLATERAL_CONTRACTS, + BRANCHES, CONTRACTS_COMMIT_HASH, CONTRACTS_COMMIT_URL, CONTRACT_BOLD_TOKEN, diff --git a/frontend/app/src/liquity-leverage.ts b/frontend/app/src/liquity-leverage.ts index 26444f12f..d4ed6664c 100644 --- a/frontend/app/src/liquity-leverage.ts +++ b/frontend/app/src/liquity-leverage.ts @@ -3,8 +3,8 @@ import type { Config as WagmiConfig } from "wagmi"; import { CLOSE_FROM_COLLATERAL_SLIPPAGE, DATA_REFRESH_INTERVAL } from "@/src/constants"; import { getProtocolContract } from "@/src/contracts"; -import { getBranchContracts } from "@/src/contracts"; import { dnum18 } from "@/src/dnum-utils"; +import { getBranch } from "@/src/liquity-utils"; import { useDebouncedQueryKey } from "@/src/react-utils"; import { useWagmiConfig } from "@/src/services/Ethereum"; import { useQuery } from "@tanstack/react-query"; @@ -19,7 +19,7 @@ export async function getLeverUpTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const { PriceFeed, TroveManager } = getBranchContracts(branchId); + const { PriceFeed, TroveManager } = getBranch(branchId).contracts; const [priceResult, troveDataResult] = await readContracts(wagmiConfig, { contracts: [{ abi: PriceFeed.abi, @@ -71,7 +71,7 @@ export async function getLeverDownTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const { PriceFeed, TroveManager } = getBranchContracts(branchId); + const { PriceFeed, TroveManager } = getBranch(branchId).contracts; const [priceResult, troveDataResult] = await readContracts(wagmiConfig, { contracts: [{ abi: PriceFeed.abi, @@ -124,7 +124,7 @@ export async function getOpenLeveragedTroveParams( leverageFactor: number, wagmiConfig: WagmiConfig, ) { - const { PriceFeed } = getBranchContracts(branchId); + const { PriceFeed } = getBranch(branchId).contracts; const [price] = await readContract(wagmiConfig, { abi: PriceFeed.abi, address: PriceFeed.address, @@ -151,21 +151,18 @@ export async function getCloseFlashLoanAmount( troveId: TroveId, wagmiConfig: WagmiConfig, ): Promise { - const { PriceFeed, TroveManager } = getBranchContracts(branchId); + const { PriceFeed, TroveManager } = getBranch(branchId).contracts; const [priceResult, latestTroveDataResult] = await readContracts(wagmiConfig, { - contracts: [ - { - abi: PriceFeed.abi, - address: PriceFeed.address, - functionName: "fetchPrice", - }, - { - abi: TroveManager.abi, - address: TroveManager.address, - functionName: "getLatestTroveData", - args: [BigInt(troveId)], - }, - ], + contracts: [{ + abi: PriceFeed.abi, + address: PriceFeed.address, + functionName: "fetchPrice", + }, { + abi: TroveManager.abi, + address: TroveManager.address, + functionName: "getLatestTroveData", + args: [BigInt(troveId)], + }], }); const [price] = priceResult.result ?? []; diff --git a/frontend/app/src/liquity-utils.ts b/frontend/app/src/liquity-utils.ts index 626f7ce6a..5cc333297 100644 --- a/frontend/app/src/liquity-utils.ts +++ b/frontend/app/src/liquity-utils.ts @@ -15,7 +15,7 @@ import type { UseQueryResult } from "@tanstack/react-query"; import type { Config as WagmiConfig } from "wagmi"; import { DATA_REFRESH_INTERVAL, INTEREST_RATE_INCREMENT, INTEREST_RATE_MAX, INTEREST_RATE_MIN } from "@/src/constants"; -import { getBranchContract, getContracts, getProtocolContract } from "@/src/contracts"; +import { CONTRACTS, getBranchContract, getProtocolContract } from "@/src/contracts"; import { dnum18, DNUM_0, dnumOrNull, jsonStringifyWithDnum } from "@/src/dnum-utils"; import { CHAIN_BLOCK_EXPLORER, LIQUITY_STATS_URL } from "@/src/env"; import { useContinuousBoldGains } from "@/src/liquity-stability-pool"; @@ -85,18 +85,14 @@ export function getCollToken(branchId: BranchId | null): CollateralToken | null return token; } -export function getBranchIdFromSymbol(symbol: null): null; -export function getBranchIdFromSymbol(symbol: CollateralSymbol): BranchId; -export function getBranchIdFromSymbol(symbol: CollateralSymbol | null): BranchId | null { - if (symbol === null) { - return null; - } - const branch = getBranch(symbol); - return branch.id; -} - export function getBranches(): Branch[] { - return getContracts().branches; + return CONTRACTS.branches.map((branchContracts) => ({ + id: branchContracts.branchId, + branchId: branchContracts.branchId, + contracts: branchContracts.contracts, + symbol: branchContracts.symbol, + strategies: [], + })); } export function getBranch(idOrSymbol: null): null; diff --git a/frontend/app/src/services/TransactionFlow.tsx b/frontend/app/src/services/TransactionFlow.tsx index 43d0a5caf..76fc3548d 100644 --- a/frontend/app/src/services/TransactionFlow.tsx +++ b/frontend/app/src/services/TransactionFlow.tsx @@ -8,7 +8,7 @@ import type { Config as WagmiConfig } from "wagmi"; import type { ReadContractOptions } from "wagmi/query"; import { GAS_MIN_HEADROOM, GAS_RELATIVE_HEADROOM, LOCAL_STORAGE_PREFIX } from "@/src/constants"; -import { getContracts } from "@/src/contracts"; +import { CONTRACTS } from "@/src/contracts"; import { jsonParseWithDnum, jsonStringifyWithDnum } from "@/src/dnum-utils"; import { useAccount } from "@/src/services/Ethereum"; import { useStoredState } from "@/src/services/StoredState"; @@ -326,7 +326,7 @@ export function TransactionFlow({ ? { ...flow, account: account.address, - contracts: getContracts(), + contracts: CONTRACTS, isSafe: account.safeStatus !== null, preferredApproveMethod: storedState.preferredApproveMethod, readContract: getReadContract(wagmiConfig), @@ -370,7 +370,7 @@ function useSteps( return flowDeclaration.getSteps({ account: account.address, - contracts: getContracts(), + contracts: CONTRACTS, isSafe: account.safeStatus !== null, preferredApproveMethod: storedState.preferredApproveMethod, readContract: getReadContract(wagmiConfig), @@ -423,7 +423,7 @@ function useFlowManager(account: Address | null, isSafe: boolean = false) { const params: FlowParams = { readContract: getReadContract(wagmiConfig), account, - contracts: getContracts(), + contracts: CONTRACTS, isSafe, preferredApproveMethod: storedState.preferredApproveMethod, request: flow.request, diff --git a/frontend/app/src/types.ts b/frontend/app/src/types.ts index 872cd49b5..ed0fa60b1 100644 --- a/frontend/app/src/types.ts +++ b/frontend/app/src/types.ts @@ -17,8 +17,11 @@ export type Branch = { contracts: BranchContracts; branchId: BranchId; // to be removed, use `id` instead symbol: CollateralSymbol; + strategies: Array<{ address: Address; name: string }>; }; +export type EnvBranch = Omit; + export function isBranchId(value: unknown): value is BranchId { return typeof value === "number" && value >= 0 && value <= 9; }