diff --git a/frontend/app/src/screens/LoanScreen/LoanCard.tsx b/frontend/app/src/screens/LoanScreen/LoanCard.tsx
index 8fb7f72e..1727bfd5 100644
--- a/frontend/app/src/screens/LoanScreen/LoanCard.tsx
+++ b/frontend/app/src/screens/LoanScreen/LoanCard.tsx
@@ -5,7 +5,6 @@ import type { LoanLoadingState } from "./LoanScreen";
import { INFINITY } from "@/src/characters";
import { Spinner } from "@/src/comps/Spinner/Spinner";
import { Value } from "@/src/comps/Value/Value";
-import { dnum18 } from "@/src/dnum-utils";
import { formatRisk } from "@/src/formatting";
import { fmtnum } from "@/src/formatting";
import { getLoanDetails } from "@/src/liquity-math";
@@ -190,7 +189,7 @@ export function LoanCard({
- {fmtnum(dn.mul(loan.interestRate, 100))}%
+ {fmtnum(loan.interestRate, 2, 100)}%
- {ltv && fmtnum(dn.mul(ltv, 100))}%
+ {fmtnum(ltv, "2z", 100)}%
@@ -455,7 +454,6 @@ function GridItem({
}) {
return (
@@ -189,7 +193,21 @@ export function PanelClosePosition({ loan }: { loan: PositionLoan }) {
size="large"
wide
onClick={() => {
- router.push("/transactions/close-loan");
+ if (account.address) {
+ txFlow.start({
+ flowId: "closeLoanPosition",
+ backLink: [
+ `/loan/close?id=${loan.collIndex}:${loan.troveId}`,
+ "Back to editing",
+ ],
+ successLink: ["/", "Go to the dashboard"],
+ successMessage: "The loan position has been closed successfully.",
+
+ collIndex: loan.collIndex,
+ prefixedTroveId: getPrefixedTroveId(loan.collIndex, loan.troveId),
+ });
+ router.push("/transactions");
+ }
}}
/>
diff --git a/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx b/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx
index 4347f9f3..77317a31 100644
--- a/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx
+++ b/frontend/app/src/screens/LoanScreen/PanelUpdateBorrowPosition.tsx
@@ -46,7 +46,6 @@ export function PanelUpdateBorrowPosition({
}) {
const router = useRouter();
const account = useAccount();
-
const txFlow = useTransactionFlow();
const collateral = TOKENS_BY_SYMBOL[loan.collateral];
diff --git a/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx b/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx
index 6b324e22..4e6872b1 100644
--- a/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx
+++ b/frontend/app/src/screens/LoanScreen/PanelUpdateRate.tsx
@@ -5,21 +5,26 @@ import { Field } from "@/src/comps/Field/Field";
import { InfoBox } from "@/src/comps/InfoBox/InfoBox";
import { InterestRateField } from "@/src/comps/InterestRateField/InterestRateField";
import { ValueUpdate } from "@/src/comps/ValueUpdate/ValueUpdate";
+import { dnum18 } from "@/src/dnum-utils";
import { useInputFieldValue } from "@/src/form-utils";
import { fmtnum, formatRisk } from "@/src/formatting";
import { getLoanDetails } from "@/src/liquity-math";
+import { getPrefixedTroveId } from "@/src/liquity-utils";
import { useAccount } from "@/src/services/Ethereum";
import { usePrice } from "@/src/services/Prices";
+import { useTransactionFlow } from "@/src/services/TransactionFlow";
import { riskLevelToStatusMode } from "@/src/uikit-utils";
import { css } from "@/styled-system/css";
import { Button, HFlex, InfoTooltip, StatusDot, TOKENS_BY_SYMBOL } from "@liquity2/uikit";
import * as dn from "dnum";
import { useRouter } from "next/navigation";
import { useState } from "react";
+import { maxUint256 } from "viem";
export function PanelUpdateRate({ loan }: { loan: PositionLoan }) {
const router = useRouter();
const account = useAccount();
+ const txFlow = useTransactionFlow();
const collateral = TOKENS_BY_SYMBOL[loan.collateral];
const collPrice = usePrice(collateral.symbol);
@@ -145,7 +150,26 @@ export function PanelUpdateRate({ loan }: { loan: PositionLoan }) {
size="large"
wide
onClick={() => {
- router.push("/transactions/update-loan");
+ if (account.address) {
+ txFlow.start({
+ flowId: "updateLoanInterestRate",
+ backLink: [
+ `/loan/rate?id=${loan.collIndex}:${loan.troveId}`,
+ "Back to editing",
+ ],
+ successLink: ["/", "Go to the dashboard"],
+ successMessage: "The position interest rate has been updated successfully.",
+
+ collIndex: loan.collIndex,
+ interestRate,
+ lowerHint: dnum18(0),
+ maxUpfrontFee: dnum18(maxUint256),
+ owner: account.address,
+ prefixedTroveId: getPrefixedTroveId(loan.collIndex, loan.troveId),
+ upperHint: dnum18(0),
+ });
+ router.push("/transactions");
+ }
}}
/>
diff --git a/frontend/app/src/screens/TransactionsScreen/LoanCard.tsx b/frontend/app/src/screens/TransactionsScreen/LoanCard.tsx
index 86b23468..dab9fce2 100644
--- a/frontend/app/src/screens/TransactionsScreen/LoanCard.tsx
+++ b/frontend/app/src/screens/TransactionsScreen/LoanCard.tsx
@@ -19,6 +19,7 @@ import * as dn from "dnum";
import { match, P } from "ts-pattern";
const LOAN_CARD_HEIGHT = 246 - 16;
+const LOAN_CARD_HEIGHT_REDUCED = 176;
export function LoanCard({
leverageMode,
@@ -33,9 +34,15 @@ export function LoanCard({
prevLoan?: PositionLoan | null;
onRetry: () => void;
}) {
- const collateral = loan && TOKENS_BY_SYMBOL[loan.collateral];
+ const collateral = (
+ loan && TOKENS_BY_SYMBOL[loan.collateral]
+ ) || (
+ prevLoan && TOKENS_BY_SYMBOL[prevLoan.collateral]
+ );
const collPriceUsd = usePrice(collateral ? collateral.symbol : null);
+ const isLoanClosing = prevLoan && !loan;
+
const loanDetails = loan && collateral && getLoanDetails(
loan.deposit,
loan.borrowed,
@@ -67,266 +74,413 @@ export function LoanCard({
return (
- {loan
- && loanDetails
- && collateral
- && typeof leverageFactor === "number"
- && depositPreLeverage
- && maxLtv
- && liquidationRisk
- && (
+ {isLoanClosing
+ ? (
<>
+
+ {collateral && (
+
+
+
+ {fmtnum(0)} {collateral.name}
+
+ {prevLoan && (
+
+ {fmtnum(prevLoan.deposit)} {collateral.name}
+
+ )}
+
+
+ )}
+
+ >
+ )
+ : loan
+ && loanDetails
+ && collateral
+ && typeof leverageFactor === "number"
+ && depositPreLeverage
+ && maxLtv
+ && liquidationRisk
+ && (
+ <>
+ {leverageMode
+ ? (
+
+ )
+ : (
+
+ )}
{leverageMode
? (
-
-
{fmtnum(loan.deposit)}
-
+
+
+ {fmtnum(depositPreLeverage)} {collateral.name}
+
+
+ )
+ : (
+
-
-
+ {fmtnum(loan.deposit)} {collateral.name}
+
+ {prevLoan && !dn.eq(prevLoan.deposit, loan.deposit) && (
+
- {loanDetails.status === "underwater" || leverageFactor === null
- ? INFINITY
- : `${roundToDecimal(leverageFactor, 1)}x`}
-
-
+ {fmtnum(prevLoan.deposit)} {collateral.name}
+
+ )}
-
- )
- : (
-
- {fmtnum(loan.borrowed)}
-
- {prevLoan && !dn.eq(prevLoan.borrowed, loan.borrowed) && (
-
- {fmtnum(prevLoan.borrowed)}
-
- )}
-
+
)}
-
-
-
- {leverageMode
- ? (
-
-
- {fmtnum(depositPreLeverage)} {collateral.name}
+
+
+
+ ${fmtnum(loanDetails.liquidationPrice)}
-
- )
- : (
-
-
-
- {fmtnum(loan.deposit)} {collateral.name}
-
- {prevLoan && !dn.eq(prevLoan.deposit, loan.deposit) && (
+ {loanDetails?.liquidationPrice
+ && prevLoanDetails?.liquidationPrice
+ && !dn.eq(
+ prevLoanDetails.liquidationPrice,
+ loanDetails.liquidationPrice,
+ )
+ && (
- {fmtnum(prevLoan.deposit)} {collateral.name}
+ ${fmtnum(prevLoanDetails.liquidationPrice)}
)}
-
-
- )}
-
-
-
- ${fmtnum(loanDetails.liquidationPrice)}
-
- {loanDetails?.liquidationPrice
- && prevLoanDetails?.liquidationPrice
- && !dn.eq(
- prevLoanDetails.liquidationPrice,
- loanDetails.liquidationPrice,
- )
- && (
+
+
+
+
+ {fmtnum(dn.mul(loan.interestRate, 100))}%
+ {prevLoan && !dn.eq(prevLoan.interestRate, loan.interestRate) && (
- ${fmtnum(prevLoanDetails.liquidationPrice)}
+ {fmtnum(dn.mul(prevLoan.interestRate, 100))}%
)}
-
-
-
- {fmtnum(dn.mul(loan.interestRate, 100))}%
-
-
-
+
+
+
- {ltv && fmtnum(dn.mul(ltv, 100))}%
+
+ {ltv && fmtnum(dn.mul(ltv, 100))}%
+
+ {ltv
+ && prevLoanDetails?.ltv
+ && !dn.eq(prevLoanDetails.ltv, ltv)
+ && (
+
+ {prevLoanDetails.ltv && fmtnum(dn.mul(prevLoanDetails.ltv, 100))}%
+
+ )}
- {ltv
- && prevLoanDetails?.ltv
- && !dn.eq(prevLoanDetails.ltv, ltv)
- && (
-
- {prevLoanDetails.ltv && fmtnum(dn.mul(prevLoanDetails.ltv, 100))}%
-
- )}
-
-
-
-
-
- {formatRisk(liquidationRisk)}
- {prevLoanDetails && liquidationRisk !== prevLoanDetails.liquidationRisk && (
- <>
-
-
- {formatRisk(prevLoanDetails.liquidationRisk)}
-
- >
- )}
-
-
- {redemptionRisk && (
-
+
+
- {formatRisk(redemptionRisk)}
+ {formatRisk(liquidationRisk)}
+ {prevLoanDetails && liquidationRisk !== prevLoanDetails.liquidationRisk && (
+ <>
+
+
+ {formatRisk(prevLoanDetails.liquidationRisk)}
+
+ >
+ )}
- )}
-
- >
- )}
+ {redemptionRisk && (
+
+
+
+ {formatRisk(redemptionRisk)}
+ {prevLoanDetails && redemptionRisk !== prevLoanDetails.redemptionRisk && (
+ <>
+
+
+ {formatRisk(prevLoanDetails.redemptionRisk)}
+
+ >
+ )}
+
+
+ )}
+
+ >
+ )}
);
}
+function TotalDebt({
+ positive,
+ loan,
+ prevLoan,
+}: {
+ positive?: boolean;
+ loan: PositionLoan;
+ prevLoan?: PositionLoan | null;
+}) {
+ return (
+
+
+
+
+ {fmtnum(loan.borrowed)}
+
+
+ {prevLoan && !dn.eq(prevLoan.borrowed, loan.borrowed) && (
+
+ {fmtnum(prevLoan.borrowed)}
+
+ )}
+
+
+
+ );
+}
+
+function TotalExposure({
+ loan,
+ loanDetails,
+}: {
+ loan: PositionLoan;
+ loanDetails: ReturnType;
+}) {
+ const collateral = loan && TOKENS_BY_SYMBOL[loan.collateral];
+ return (
+
+
+
+
{fmtnum(loan.deposit)}
+
+
+
+
+ {loanDetails.status === "underwater" || loanDetails.leverageFactor === null
+ ? INFINITY
+ : `${roundToDecimal(loanDetails.leverageFactor, 1)}x`}
+
+
+
+
+
+
+ );
+}
+
function LoadingCard({
children,
+ height,
leverage,
loadingState,
onRetry,
}: {
children: ReactNode;
+ height: number;
leverage: boolean;
loadingState: LoadingState;
onRetry: () => void;
@@ -358,8 +512,8 @@ function LoadingCard({
)
.otherwise(() => ({
cardtransform: "scale3d(1, 1, 1)",
- containerHeight: LOAN_CARD_HEIGHT,
- cardHeight: LOAN_CARD_HEIGHT,
+ containerHeight: height,
+ cardHeight: height,
cardBackground: token("colors.blue:950"),
cardColor: token("colors.white"),
})),
@@ -390,7 +544,7 @@ function LoadingCard({
userSelect: "none",
})}
style={{
- height: loadingState === "success" ? LOAN_CARD_HEIGHT : spring.cardHeight,
+ height: loadingState === "success" ? height : spring.cardHeight,
color: spring.cardColor,
background: spring.cardBackground,
transform: spring.cardtransform,
diff --git a/frontend/app/src/services/TransactionFlow.tsx b/frontend/app/src/services/TransactionFlow.tsx
index 4dc067df..f8f69afb 100644
--- a/frontend/app/src/services/TransactionFlow.tsx
+++ b/frontend/app/src/services/TransactionFlow.tsx
@@ -10,8 +10,9 @@
// - Flow declaration: Contains the logic for a specific flow (get steps, parse request, tx params).
// - Flow context: a transaction flow as stored in local storage (steps + request).
+import type { Request as CloseLoanPositionRequest } from "@/src/tx-flows/closeLoanPosition";
import type { Request as OpenLoanPositionRequest } from "@/src/tx-flows/openLoanPosition";
-import type { Request as RepayAndCloseLoanPositionRequest } from "@/src/tx-flows/repayAndCloseLoanPosition";
+import type { Request as UpdateLoanInterestRateRequest } from "@/src/tx-flows/updateLoanInterestRate";
import type { Request as UpdateLoanPositionRequest } from "@/src/tx-flows/updateLoanPosition";
import type { Address } from "@/src/types";
import type { WriteContractParameters } from "@wagmi/core";
@@ -21,8 +22,9 @@ import { LOCAL_STORAGE_PREFIX } from "@/src/constants";
import { useContracts } from "@/src/contracts";
import { jsonParseWithDnum, jsonStringifyWithDnum } from "@/src/dnum-utils";
import { useAccount, useWagmiConfig } from "@/src/services/Ethereum";
+import { closeLoanPosition } from "@/src/tx-flows/closeLoanPosition";
import { openLoanPosition } from "@/src/tx-flows/openLoanPosition";
-import { repayAndCloseLoanPosition } from "@/src/tx-flows/repayAndCloseLoanPosition";
+import { updateLoanInterestRate } from "@/src/tx-flows/updateLoanInterestRate";
import { updateLoanPosition } from "@/src/tx-flows/updateLoanPosition";
import { noop } from "@/src/utils";
import { vAddress } from "@/src/valibot-utils";
@@ -35,8 +37,9 @@ const TRANSACTION_FLOW_KEY = `${LOCAL_STORAGE_PREFIX}transaction_flow`;
export type FlowRequest =
| OpenLoanPositionRequest
- | RepayAndCloseLoanPositionRequest
- | UpdateLoanPositionRequest;
+ | UpdateLoanPositionRequest
+ | UpdateLoanInterestRateRequest
+ | CloseLoanPositionRequest;
const flowDeclarations: {
[K in FlowIdFromFlowRequest]: FlowDeclaration<
@@ -44,15 +47,17 @@ const flowDeclarations: {
any // Use 'any' here to allow any StepId type
>;
} = {
+ closeLoanPosition,
openLoanPosition,
- repayAndCloseLoanPosition,
+ updateLoanInterestRate,
updateLoanPosition,
};
const FlowIdSchema = v.union([
+ v.literal("closeLoanPosition"),
v.literal("openLoanPosition"),
- v.literal("repayAndCloseLoanPosition"),
v.literal("updateLoanPosition"),
+ v.literal("updateLoanInterestRate"),
]);
type ExtractStepId = T extends FlowDeclaration ? S : never;
@@ -161,8 +166,8 @@ export type FlowDeclaration<
FR extends FlowRequest,
StepId extends string = string,
> = {
- title: string;
- subtitle: string;
+ title: ReactNode;
+ subtitle: ReactNode;
Summary: ComponentType<{ flow: FlowContext }>;
Details: ComponentType<{ flow: FlowContext }>;
getSteps: GetStepsFn;
diff --git a/frontend/app/src/subgraph-queries.graphql b/frontend/app/src/subgraph-queries.graphql
index ad1f6f1d..0b6f79f8 100644
--- a/frontend/app/src/subgraph-queries.graphql
+++ b/frontend/app/src/subgraph-queries.graphql
@@ -1,5 +1,5 @@
query TrovesByAccount($account: Bytes!) {
- troves(where: { borrower: $account }) {
+ troves(where: { borrower: $account, closedAt: null }) {
id
troveId
borrower
diff --git a/frontend/app/src/tx-flows/closeLoanPosition.tsx b/frontend/app/src/tx-flows/closeLoanPosition.tsx
new file mode 100644
index 00000000..9d4ee463
--- /dev/null
+++ b/frontend/app/src/tx-flows/closeLoanPosition.tsx
@@ -0,0 +1,133 @@
+import type { LoadingState } from "@/src/screens/TransactionsScreen/TransactionsScreen";
+import type { FlowDeclaration } from "@/src/services/TransactionFlow";
+
+import { fmtnum } from "@/src/formatting";
+import { useCollateral } from "@/src/liquity-utils";
+import { parsePrefixedTroveId } from "@/src/liquity-utils";
+import { LoanCard } from "@/src/screens/TransactionsScreen/LoanCard";
+import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen";
+import { useLoanById } from "@/src/subgraph-hooks";
+import { vCollIndex, vPrefixedTroveId } from "@/src/valibot-utils";
+import { match, P } from "ts-pattern";
+import * as v from "valibot";
+
+const FlowIdSchema = v.literal("closeLoanPosition");
+
+const RequestSchema = v.object({
+ flowId: FlowIdSchema,
+
+ backLink: v.union([
+ v.null(),
+ v.tuple([
+ v.string(), // path
+ v.string(), // label
+ ]),
+ ]),
+ successLink: v.tuple([
+ v.string(), // path
+ v.string(), // label
+ ]),
+ successMessage: v.string(),
+
+ collIndex: vCollIndex(),
+ prefixedTroveId: vPrefixedTroveId(),
+});
+
+export type Request = v.InferOutput;
+
+type Step = "closeLoanPosition";
+
+const stepNames: Record = {
+ closeLoanPosition: "Close Position",
+};
+
+export const closeLoanPosition: FlowDeclaration = {
+ title: "Review & Send Transaction",
+ subtitle: (
+
+ You are repaying your debt and closing this position.
+ The deposit will be returned to your wallet
+
+ ),
+
+ Summary({ flow }) {
+ const loan = useLoanById(flow.request.prefixedTroveId);
+
+ const loadingState = match(loan)
+ .returnType()
+ .with({ status: "error" }, () => "error")
+ .with({ status: "pending" }, () => "loading")
+ .with({ data: null }, () => "not-found")
+ .with({ data: P.nonNullable }, () => "success")
+ .otherwise(() => "error");
+
+ return (
+ {}}
+ />
+ );
+ },
+
+ Details({ flow }) {
+ const { request } = flow;
+ const collateral = useCollateral(request.collIndex);
+ const loan = useLoanById(request.prefixedTroveId);
+
+ return loan.data && (
+ <>
+
+ {fmtnum(loan.data.borrowed, 4)} BOLD
+ ,
+ ]}
+ />
+
+ {fmtnum(loan.data.deposit, "2z")} {collateral.symbol}
+ ,
+ ]}
+ />
+ >
+ );
+ },
+ getStepName(stepid) {
+ return stepNames[stepid];
+ },
+ async getSteps() {
+ return ["closeLoanPosition"];
+ },
+ parseRequest(request) {
+ return v.parse(RequestSchema, request);
+ },
+ async writeContractParams({ contracts, request, stepId }) {
+ const collateral = contracts.collaterals[request.collIndex];
+ const { BorrowerOperations } = collateral.contracts;
+
+ if (!BorrowerOperations) {
+ throw new Error(`Collateral ${collateral.symbol} not supported`);
+ }
+
+ if (stepId === "closeLoanPosition") {
+ const { troveId } = parsePrefixedTroveId(request.prefixedTroveId);
+
+ return {
+ ...BorrowerOperations,
+ functionName: "closeTrove" as const,
+ args: [troveId],
+ };
+ }
+ return null;
+ },
+};
diff --git a/frontend/app/src/tx-flows/openLoanPosition.tsx b/frontend/app/src/tx-flows/openLoanPosition.tsx
index cafaf4cc..48479805 100644
--- a/frontend/app/src/tx-flows/openLoanPosition.tsx
+++ b/frontend/app/src/tx-flows/openLoanPosition.tsx
@@ -52,7 +52,7 @@ const stepNames: Record = {
};
export const openLoanPosition: FlowDeclaration = {
- title: "Review & Confirm",
+ title: "Review & Send Transaction",
subtitle: "Please review your borrow position before confirming",
Summary({ flow }) {
diff --git a/frontend/app/src/tx-flows/repayAndCloseLoanPosition.tsx b/frontend/app/src/tx-flows/repayAndCloseLoanPosition.tsx
deleted file mode 100644
index 7f98d936..00000000
--- a/frontend/app/src/tx-flows/repayAndCloseLoanPosition.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { FlowDeclaration } from "@/src/services/TransactionFlow";
-
-import { getTroveId } from "@/src/liquity-utils";
-import { vAddress } from "@/src/valibot-utils";
-import * as v from "valibot";
-
-const FlowIdSchema = v.literal("repayAndCloseLoanPosition");
-
-const RequestSchema = v.object({
- flowId: FlowIdSchema,
- backLink: v.union([
- v.null(),
- v.tuple([
- v.string(), // path
- v.string(), // label
- ]),
- ]),
- successLink: v.tuple([
- v.string(), // path
- v.string(), // label
- ]),
- successMessage: v.string(),
-
- collIndex: v.number(),
- owner: vAddress(),
- ownerIndex: v.number(),
-});
-
-export type Request = v.InferOutput;
-
-export const repayAndCloseLoanPosition: FlowDeclaration = {
- title: "",
- subtitle: "",
- Summary: () => null,
- Details: () => null,
- getStepName: () => "",
-
- getSteps: async function getSteps() {
- return ["closeTrove"];
- },
- parseRequest: (request): Request => {
- return v.parse(RequestSchema, request);
- },
- writeContractParams: async ({ contracts, request, stepId }) => {
- const collateral = contracts.collaterals[request.collIndex];
- const { BorrowerOperations } = collateral.contracts;
-
- if (!BorrowerOperations) {
- throw new Error(`Collateral ${collateral.symbol} not supported`);
- }
-
- if (stepId === "closeTrove") {
- const troveId = getTroveId(request.owner, request.ownerIndex);
- return {
- ...BorrowerOperations,
- functionName: "closeTrove" as const,
- args: [troveId],
- };
- }
- return null;
- },
-};
diff --git a/frontend/app/src/tx-flows/updateLoanInterestRate.tsx b/frontend/app/src/tx-flows/updateLoanInterestRate.tsx
new file mode 100644
index 00000000..cba6b21b
--- /dev/null
+++ b/frontend/app/src/tx-flows/updateLoanInterestRate.tsx
@@ -0,0 +1,129 @@
+import type { LoadingState } from "@/src/screens/TransactionsScreen/TransactionsScreen";
+import type { FlowDeclaration } from "@/src/services/TransactionFlow";
+
+import { fmtnum } from "@/src/formatting";
+import { parsePrefixedTroveId } from "@/src/liquity-utils";
+import { LoanCard } from "@/src/screens/TransactionsScreen/LoanCard";
+import { TransactionDetailsRow } from "@/src/screens/TransactionsScreen/TransactionsScreen";
+import { useLoanById } from "@/src/subgraph-hooks";
+import { vAddress, vCollIndex, vDnum, vPrefixedTroveId } from "@/src/valibot-utils";
+import * as dn from "dnum";
+import { match, P } from "ts-pattern";
+import * as v from "valibot";
+
+const FlowIdSchema = v.literal("updateLoanInterestRate");
+
+const RequestSchema = v.object({
+ flowId: FlowIdSchema,
+
+ backLink: v.union([
+ v.null(),
+ v.tuple([
+ v.string(), // path
+ v.string(), // label
+ ]),
+ ]),
+ successLink: v.tuple([
+ v.string(), // path
+ v.string(), // label
+ ]),
+ successMessage: v.string(),
+
+ collIndex: vCollIndex(),
+ interestRate: vDnum(),
+ lowerHint: vDnum(),
+ maxUpfrontFee: vDnum(),
+ owner: vAddress(),
+ prefixedTroveId: vPrefixedTroveId(),
+ upperHint: vDnum(),
+});
+
+export type Request = v.InferOutput;
+
+type Step = "adjustInterestRate";
+
+const stepNames: Record = {
+ adjustInterestRate: "Update Interest Rate",
+};
+
+export const updateLoanInterestRate: FlowDeclaration = {
+ title: "Review & Confirm",
+ subtitle: "Please review the changes of your borrow position before confirming",
+ Summary({ flow }) {
+ const loan = useLoanById(flow.request.prefixedTroveId);
+
+ const loadingState = match(loan)
+ .returnType()
+ .with({ status: "error" }, () => "error")
+ .with({ status: "pending" }, () => "loading")
+ .with({ data: null }, () => "not-found")
+ .with({ data: P.nonNullable }, () => "success")
+ .otherwise(() => "error");
+
+ return (
+ {}}
+ />
+ );
+ },
+ Details({ flow }) {
+ const { request } = flow;
+
+ const loan = useLoanById(flow.request.prefixedTroveId);
+ const boldPerYear = dn.mul(loan.data?.borrowed ?? 0n, request.interestRate);
+
+ return (
+
+ {fmtnum(request.interestRate, "full", 100)}%
+ ,
+
+ ~{fmtnum(boldPerYear, 4)} BOLD per year
+
,
+ ]}
+ />
+ );
+ },
+ getStepName(stepid) {
+ return stepNames[stepid];
+ },
+ async getSteps() {
+ return ["adjustInterestRate"];
+ },
+ parseRequest(request) {
+ return v.parse(RequestSchema, request);
+ },
+ async writeContractParams({ contracts, request, stepId }) {
+ const collateral = contracts.collaterals[request.collIndex];
+ const { BorrowerOperations } = collateral.contracts;
+
+ if (!BorrowerOperations) {
+ throw new Error(`Collateral ${collateral.symbol} not supported`);
+ }
+
+ if (stepId === "adjustInterestRate") {
+ const { troveId } = parsePrefixedTroveId(request.prefixedTroveId);
+ return {
+ ...BorrowerOperations,
+ functionName: "adjustTroveInterestRate" as const,
+ args: [
+ troveId,
+ request.interestRate[0],
+ request.upperHint[0],
+ request.lowerHint[0],
+ request.maxUpfrontFee[0],
+ ],
+ };
+ }
+ return null;
+ },
+};
diff --git a/frontend/app/src/tx-flows/updateLoanPosition.tsx b/frontend/app/src/tx-flows/updateLoanPosition.tsx
index bfed7455..8f46cc77 100644
--- a/frontend/app/src/tx-flows/updateLoanPosition.tsx
+++ b/frontend/app/src/tx-flows/updateLoanPosition.tsx
@@ -47,11 +47,11 @@ type Step = "approve" | "adjustTrove";
const stepNames: Record = {
approve: "Approve",
- adjustTrove: "Adjust Trove",
+ adjustTrove: "Update Position",
};
export const updateLoanPosition: FlowDeclaration = {
- title: "Review & Confirm",
+ title: "Review & Send Transaction",
subtitle: "Please review the changes of your borrow position before confirming",
Summary({ flow }) {
const { symbol } = useCollateral(flow.request.collIndex);
@@ -189,7 +189,7 @@ export const updateLoanPosition: FlowDeclaration = {
return isApproved ? ["adjustTrove"] : ["approve", "adjustTrove"];
},
- parseRequest(request): Request {
+ parseRequest(request) {
return v.parse(RequestSchema, request);
},
async writeContractParams({ contracts, request, stepId }) {