From b6f07b297448eb863c842854eec476e12a628b49 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 1 Dec 2025 16:58:44 +0100 Subject: [PATCH 1/4] wip: repay form --- .../lend/components/PageLoanManage/Page.tsx | 2 +- .../lend/components/PageLoanManage/index.tsx | 75 +++- .../lend/components/PageLoanManage/types.ts | 7 +- .../manage-loan/components/RepayForm.tsx | 352 ++++++++++++++++-- .../manage-loan/hooks/useRepayForm.ts | 11 +- .../src/llamalend/mutations/repay.mutation.ts | 11 +- .../validation/manage-loan.validation.ts | 40 +- .../widgets/manage-loan/LoanFormWrapper.tsx | 24 +- .../loan/components/PageLoanManage/index.tsx | 37 +- .../loan/components/PageLoanManage/types.ts | 7 +- 10 files changed, 486 insertions(+), 80 deletions(-) diff --git a/apps/main/src/lend/components/PageLoanManage/Page.tsx b/apps/main/src/lend/components/PageLoanManage/Page.tsx index 33e06b16c3..5e91177662 100644 --- a/apps/main/src/lend/components/PageLoanManage/Page.tsx +++ b/apps/main/src/lend/components/PageLoanManage/Page.tsx @@ -187,7 +187,7 @@ const Page = () => { (isManageSoftLiq ? ( ) : ( - + ))} diff --git a/apps/main/src/lend/components/PageLoanManage/index.tsx b/apps/main/src/lend/components/PageLoanManage/index.tsx index 6fbdbfb48c..50cded31b5 100644 --- a/apps/main/src/lend/components/PageLoanManage/index.tsx +++ b/apps/main/src/lend/components/PageLoanManage/index.tsx @@ -11,6 +11,7 @@ import { getLoanManagePathname } from '@/lend/utils/utilsRouter' import { AddCollateralForm } from '@/llamalend/features/manage-loan/components/AddCollateralForm' import { RemoveCollateralForm } from '@/llamalend/features/manage-loan/components/RemoveCollateralForm' import { RepayForm } from '@/llamalend/features/manage-loan/components/RepayForm' +import type { BorrowPositionDetailsProps } from '@/llamalend/features/market-position-details' import Stack from '@mui/material/Stack' import { AppFormContentWrapper } from '@ui/AppForm' import { useNavigate } from '@ui-kit/hooks/router' @@ -21,30 +22,61 @@ import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' const { MaxWidth } = SizesAndSpaces -const tabsLoan: TabOption[] = [ - { value: 'loan-increase', label: t`Borrow more` }, - { value: 'loan-decrease', label: t`Repay` }, - { value: 'loan-liquidate', label: t`Self-liquidate` }, -] +type OldTab = 'loan' | 'collateral' | 'leverage' +type NewTab = 'loan' | 'repay' | 'collateral' -const tabsCollateral: TabOption[] = [ - { value: 'collateral-increase', label: t`Add collateral` }, - { value: 'collateral-decrease', label: t`Remove collateral` }, -] +const oldMenu = { + loan: {value: 'loan' as const, label: t`Loan`, + subtabs: [ + { value: 'loan-increase', label: t`Borrow more` }, + { value: 'loan-decrease', label: t`Repay` }, + { value: 'loan-liquidate', label: t`Self-liquidate` }, + ] satisfies TabOption[], + }, + collateral: { + subtabs: [value: 'collateral' as const, label: t`Collateral`, + { value: 'collateral-increase', label: t`Add collateral` }, + { value: 'collateral-decrease', label: t`Remove collateral` }, + ] satisfies TabOption[], + ...(market?.leverage?.hasLeverage() ? [{ value: 'leverage' as const, label: t`Leverage` }] : []), + }, +} satisfies Record -const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) => { - const { rChainId, rOwmId, rFormType, market, params, isLoaded } = pageProps +type ManageLoanProps = PageContentProps & { params: MarketUrlParams } & { + borrowPositionDetails: BorrowPositionDetailsProps +} + +const ManageLoan = (pageProps: ManageLoanProps) => { + const { rChainId, rOwmId, rFormType, market, params, isLoaded, borrowPositionDetails } = pageProps const push = useNavigate() const shouldUseManageLoanMuiForm = useManageLoanMuiForm() const useMuiForm = shouldUseManageLoanMuiForm && !!market + const isLeveragedPosition = borrowPositionDetails.leverage?.value && borrowPositionDetails.leverage.value > 1 - type Tab = 'loan' | 'collateral' | 'leverage' - const tabs: TabOption[] = useMemo( + const tabsLoanMui: TabOption[] = useMemo( () => [ - { value: 'loan' as const, label: t`Loan` }, - { value: 'collateral' as const, label: t`Collateral` }, - ...(market?.leverage?.hasLeverage() ? [{ value: 'leverage' as const, label: t`Leverage` }] : []), + { value: 'loan-increase', label: t`Borrow` }, + { value: 'loan-repay-wallet', label: t`Repay from wallet` }, + { value: 'loan-repay-collateral', label: t`Repay from collateral` }, + { value: 'loan-decrease', label: t`Repay` }, + { value: 'loan-liquidate', label: t`Self-liquidate` }, ], + [leverageEnabled], + ) + + const tabs: TabOption[] = useMemo( + () => + useMuiForm + ? [ + { value: 'loan' as const, label: t`Loan` }, + { value: 'repay' as const, label: isLeveragedPosition ? t`Delever` : t`Repay` }, + { value: 'collateral' as const, label: t`Collateral` }, + ] + : [ + { value: 'loan' as const, label: t`Loan` }, + { value: 'collateral' as const, label: t`Collateral` }, + ...(market?.leverage?.hasLeverage() ? [{ value: 'leverage' as const, label: t`Leverage` }] : []), + ], [market?.leverage], ) @@ -52,8 +84,15 @@ const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) = const [subTab, setSubTab] = useState('loan-increase') const subTabs = useMemo( - () => (!rFormType || rFormType === 'loan' ? tabsLoan : rFormType === 'collateral' ? tabsCollateral : []), - [rFormType], + () => + !rFormType || rFormType === 'loan' + ? useMuiForm + ? tabsLoanMui + : tabsLoan + : rFormType === 'collateral' + ? tabsCollateral + : [], + [rFormType, tabsLoanMui, useMuiForm], ) useEffect(() => setSubTab(subTabs[0]?.value), [subTabs]) diff --git a/apps/main/src/lend/components/PageLoanManage/types.ts b/apps/main/src/lend/components/PageLoanManage/types.ts index b57bb5ee69..7cf16fb666 100644 --- a/apps/main/src/lend/components/PageLoanManage/types.ts +++ b/apps/main/src/lend/components/PageLoanManage/types.ts @@ -2,7 +2,12 @@ import { FutureRates } from '@/lend/types/lend.types' export type DetailInfoTypes = 'user' | 'market' export type FormType = 'loan' | 'collateral' | 'leverage' -export type LoanFormType = 'loan-increase' | 'loan-decrease' | 'loan-liquidate' +export type LoanFormType = + | 'loan-increase' + | 'loan-decrease' + | 'loan-liquidate' + | 'loan-repay-wallet' + | 'loan-repay-collateral' export type CollateralFormType = 'collateral-increase' | 'collateral-decrease' export type LeverageFormType = 'leverage-borrow-more' diff --git a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx index 3bad68091c..615a543de8 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx @@ -1,35 +1,139 @@ +import BigNumber from 'bignumber.js' +import { useEffect, useMemo } from 'react' import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { RepayOptions } from '@/llamalend/mutations/repay.mutation' +import { useMarketFutureRates } from '@/llamalend/queries/market-future-rates.query' import { useMarketRates } from '@/llamalend/queries/market-rates' +import { useUserHealth } from '@/llamalend/queries/user-health.query' +import { useUserState } from '@/llamalend/queries/user-state.query' +import type { RepayFromCollateralParams } from '@/llamalend/queries/validation/manage-loan.types' +import type { RepayForm as RepayFormValues } from '@/llamalend/queries/validation/manage-loan.validation' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import Button from '@mui/material/Button' +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' import type { Decimal } from '@ui-kit/utils' import { InputDivider } from '../../../widgets/InputDivider' +import { setValueOptions } from '../../borrow/react-form.utils' import { useRepayForm } from '../hooks/useRepayForm' -export const RepayForm = ({ - market, - networks, - chainId, - enabled, - onRepaid, -}: { +type RepayFormProps = { market: LlamaMarketTemplate | undefined networks: NetworkDict chainId: ChainId enabled?: boolean onRepaid: NonNullable + leverageEnabled: boolean +} + +const useRepayInfo = ({ + params, + collateralToken, + expectedBorrowed, + values, + isInfoOpen, + collateralDelta, +}: { + params: RepayFromCollateralParams + collateralToken: { address: string; symbol?: string } | undefined + expectedBorrowed: ReturnType['expectedBorrowed'] + values: RepayFormValues + isInfoOpen: boolean + collateralDelta?: Decimal }) => { + const repayAmount = expectedBorrowed.data?.totalBorrowed ?? values.userBorrowed ?? values.userCollateral + const debtDelta = repayAmount ? (`-${repayAmount}` as Decimal) : undefined + const enabled = isInfoOpen && !!params.marketId + + const prevLoanToValue = useLoanToValueFromUserState({ + ...params, + collateralToken, + borrowToken, + enabled, + }) + const loanToValue = useLoanToValueFromUserState({ + ...params, + collateralToken, + borrowToken, + enabled, + debtDelta, + collateralDelta, + }) + + const userHealth = useUserHealth( + { chainId: params.chainId, marketId: params.marketId, userAddress: params.userAddress }, + enabled, + ) + const prevHealth = useMemo( + () => ({ + data: userHealth.data?.health, + isLoading: userHealth.isLoading, + error: userHealth.error, + }), + [userHealth.data?.health, userHealth.error, userHealth.isLoading], + ) + + const userState = useUserState( + { chainId: params.chainId, marketId: params.marketId, userAddress: params.userAddress }, + enabled, + ) + + const futureDebt = useMemo(() => { + if (!userState.data?.debt || repayAmount == null) return undefined + const debt = new BigNumber(userState.data.debt).minus(repayAmount) + return (debt.isNegative() ? new BigNumber(0) : debt).toString() as Decimal + }, [repayAmount, userState.data?.debt]) + + const debt = useMemo( + () => ({ + data: futureDebt, + isLoading: userState.isLoading || expectedBorrowed.isLoading, + error: userState.error ?? expectedBorrowed.error, + }), + [expectedBorrowed.error, expectedBorrowed.isLoading, futureDebt, userState.error, userState.isLoading], + ) + + const prevDebt = useMemo( + () => ({ + data: userState.data?.debt, + isLoading: userState.isLoading, + error: userState.error, + }), + [userState.data?.debt, userState.error, userState.isLoading], + ) + + const rates = useMarketRates(params, enabled) + const futureRates = useMarketFutureRates( + { + chainId: params.chainId, + marketId: params.marketId ?? '', + debt: (futureDebt ?? userState.data?.debt ?? '0') as Decimal, + }, + enabled && !!(futureDebt ?? userState.data?.debt) && !!params.marketId, + ) + + return { repayAmount, loanToValue, prevLoanToValue, prevHealth, debt, prevDebt, rates, futureRates } +} + +export const RepayFromWallet = ({ + market, + networks, + chainId, + enabled, + onRepaid, + leverageEnabled, +}: RepayFormProps) => { const network = networks[chainId] const [isOpen, , , toggle] = useSwitch(false) + const [withdrawChecked, enableWithdraw, disableWithdraw] = useSwitch(false) const { form, @@ -47,6 +151,7 @@ export const RepayForm = ({ borrowToken, params, values, + expectedBorrowed, txHash, } = useRepayForm({ market, @@ -54,11 +159,41 @@ export const RepayForm = ({ networks, enabled, onRepaid, + leverageEnabled, }) - const marketRates = useMarketRates(params, isOpen) + useEffect(() => { + form.setValue('userCollateral', '0', setValueOptions) + }, [form]) - const isDisabled = formErrors.length > 0 || isAvailable.data === false + useEffect(() => { + if (!withdrawChecked) { + form.setValue('stateCollateral', '0', setValueOptions) + } + }, [form, withdrawChecked]) + + const collateralDelta = + withdrawChecked && values.stateCollateral ? (`-${values.stateCollateral}` as Decimal) : undefined + + const info = useRepayInfo({ + params, + collateralToken, + expectedBorrowed, + values, + isInfoOpen: isOpen, + collateralDelta, + }) + + const hasRepayAmount = useMemo( + () => + new BigNumber(values.stateCollateral ?? 0) + .plus(values.userCollateral ?? 0) + .plus(values.userBorrowed ?? 0) + .gt(0), + [values.stateCollateral, values.userBorrowed, values.userCollateral], + ) + + const isDisabled = formErrors.length > 0 || isAvailable.data === false || !hasRepayAmount return ( ({ isOpen={isOpen} toggle={toggle} health={health} + prevHealth={info.prevHealth} bands={bands} prices={prices} - rates={marketRates} - loanToValue={useLoanToValueFromUserState({ - chainId, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: isOpen, - debtDelta: values.userBorrowed == null ? undefined : (`-${values.userBorrowed}` as Decimal), - })} + prevRates={info.rates} + rates={info.futureRates} + loanToValue={info.loanToValue} + prevLoanToValue={info.prevLoanToValue} gas={gas} + debt={info.debt} + prevDebt={info.prevDebt} + debtTokenSymbol={borrowToken?.symbol} /> } > - }> + : undefined}> - + )} + + + (e.target.checked ? enableWithdraw() : disableWithdraw())} + /> + } + label={t`Repay & Withdraw`} + /> + + + + + + ) +} + +export const RepayFromCollateral = ({ + market, + networks, + chainId, + enabled, + onRepaid, + leverageEnabled, +}: RepayFormProps) => { + const network = networks[chainId] + const [isOpen, , , toggle] = useSwitch(false) + const [useWalletCollateral, enableWalletCollateral, disableWalletCollateral] = useSwitch(!leverageEnabled) + + const { + form, + isPending, + onSubmit, + action, + bands, + healthFull, + prices, + gas, + isAvailable, + isFull, + formErrors, + collateralToken, + borrowToken, + params, + values, + expectedBorrowed, + txHash, + } = useRepayForm({ + market, + network, + networks, + enabled, + onRepaid, + leverageEnabled, + }) + + useEffect(() => { + if (!useWalletCollateral) { + form.setValue('userCollateral', '0', setValueOptions) + } + form.setValue('userBorrowed', '0', setValueOptions) + }, [form, useWalletCollateral]) + + const collateralDelta = + values.stateCollateral && values.stateCollateral !== '0' ? (`-${values.stateCollateral}` as Decimal) : undefined + + const info = useRepayInfo({ + params, + collateralToken, + expectedBorrowed, + values, + isInfoOpen: isOpen, + collateralDelta, + }) + + const hasRepayAmount = useMemo( + () => + new BigNumber(values.stateCollateral ?? 0) + .plus(values.userCollateral ?? 0) + .plus(values.userBorrowed ?? 0) + .gt(0), + [values.stateCollateral, values.userBorrowed, values.userCollateral], + ) + + const isDisabled = formErrors.length > 0 || isAvailable.data === false || !hasRepayAmount + + return ( + + } + > + : undefined}> + {useWalletCollateral && ( + + )} + (e.target.checked ? enableWalletCollateral() : disableWalletCollateral())} + /> + } + label={t`Add collateral from wallet`} + /> + @@ -125,7 +403,7 @@ export const RepayForm = ({ txHash={txHash} formErrors={formErrors} network={network} - handledErrors={['stateCollateral', 'userCollateral', 'userBorrowed']} + handledErrors={['stateCollateral', 'userCollateral']} successTitle={t`Loan repaid`} /> diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts index af011733b8..21f9d04e4d 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts @@ -32,12 +32,14 @@ export const useRepayForm = enabled?: boolean onRepaid: NonNullable + leverageEnabled: boolean }) => { const { address: userAddress } = useAccount() const { chainId } = network @@ -74,7 +76,14 @@ export const useRepayForm = ['onSuccess'] onReset?: () => void userAddress: Address | undefined + leverageEnabled: boolean } export const useRepayMutation = ({ @@ -29,18 +30,20 @@ export const useRepayMutation = ({ onRepaid, onReset, userAddress, + leverageEnabled, }: RepayOptions) => { const { mutate, mutateAsync, error, data, isPending, isSuccess, reset } = useLlammaMutation({ network, marketId, - mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repay'] as const, + mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repay', { leverageEnabled }] as const, mutationFn: async ({ userCollateral, userBorrowed, stateCollateral }, { market }) => ({ - hash: - market instanceof LendMarketTemplate + hash: leverageEnabled + ? market instanceof LendMarketTemplate ? ((await market.leverage.repay(stateCollateral, userCollateral, userBorrowed)) as Hex) : market.leverageV2.hasLeverage() ? ((await market.leverageV2.repay(stateCollateral, userCollateral, userBorrowed)) as Hex) - : ((await market.deleverage.repay(userCollateral)) as Hex), + : ((await market.deleverage.repay(userCollateral)) as Hex) + : ((await market.repay(userCollateral)) as Hex), }), validationSuite: repayFromCollateralValidationSuite, pendingMessage: (mutation, { market }) => t`Repaying loan... ${formatTokenAmounts(market, mutation)}`, diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts index 648af9da25..21e12ea7e0 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -1,4 +1,5 @@ -import { group } from 'vest' +import BigNumber from 'bignumber.js' +import { enforce, group, test } from 'vest' import { validateIsFull, validateUserBorrowed, @@ -25,6 +26,29 @@ export type RepayForm = FieldsOf<{ userBorrowed: Decimal }> +const validateRepayCollateralField = (field: 'stateCollateral' | 'userCollateral', value: Decimal | null | undefined) => + test(field, `Collateral amount must be a non-negative number`, () => { + if (value == null) return + enforce(value).isNumeric().gte(0) + }) + +const validateRepayBorrowedField = (userBorrowed: Decimal | null | undefined) => + test('userBorrowed', 'Borrow amount must be a non-negative number', () => { + if (userBorrowed == null) return + enforce(userBorrowed).isNumeric().gte(0) + }) + +const validateRepayHasValue = ( + stateCollateral: Decimal | null | undefined, + userCollateral: Decimal | null | undefined, + userBorrowed: Decimal | null | undefined, +) => + test('root', 'Enter an amount to repay', () => { + const total = new BigNumber(stateCollateral ?? 0).plus(userCollateral ?? 0).plus(userBorrowed ?? 0) + + enforce(total.gt(0)).isTruthy() + }) + export const collateralValidationGroup = ({ chainId, userCollateral, marketId, userAddress }: CollateralParams) => group('chainValidation', () => { marketIdValidationSuite({ chainId, marketId }) @@ -55,9 +79,10 @@ export const repayFromCollateralValidationGroup = ({ chainValidationGroup({ chainId }) llamaApiValidationGroup({ chainId }) userAddressValidationGroup({ userAddress }) - validateUserCollateral(userCollateral) - validateUserCollateral(stateCollateral) - validateUserBorrowed(userBorrowed) + validateRepayCollateralField('userCollateral', userCollateral) + validateRepayCollateralField('stateCollateral', stateCollateral) + validateRepayBorrowedField(userBorrowed) + validateRepayHasValue(stateCollateral, userCollateral, userBorrowed) } export const repayFromCollateralValidationSuite = createValidationSuite((params: RepayFromCollateralParams) => @@ -65,9 +90,10 @@ export const repayFromCollateralValidationSuite = createValidationSuite((params: ) export const repayFormValidationSuite = createValidationSuite((params: RepayForm) => { - validateUserCollateral(params.userCollateral) - validateUserCollateral(params.stateCollateral) - validateUserBorrowed(params.userBorrowed) + validateRepayCollateralField('userCollateral', params.userCollateral) + validateRepayCollateralField('stateCollateral', params.stateCollateral) + validateRepayBorrowedField(params.userBorrowed) + validateRepayHasValue(params.stateCollateral, params.userCollateral, params.userBorrowed) }) export const repayFromCollateralIsFullValidationSuite = createValidationSuite( diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx index ea9637f8b2..5dfb01d1d0 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx @@ -2,7 +2,6 @@ import type { FormEventHandler, ReactNode } from 'react' import { FormProvider } from 'react-hook-form' import type { FieldValues, FormProviderProps } from 'react-hook-form' import Stack from '@mui/material/Stack' -import { AppFormContentWrapper } from '@ui/AppForm' import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' const { Spacing } = SizesAndSpaces @@ -24,10 +23,25 @@ export const LoanFormWrapper =
- t.design.Layer[1].Fill }}> - - {children} - + t.design.Layer[1].Fill, + // + // align-items: flex-start; + // display: grid; + // grid-row-gap: var(--spacing-3); + // padding: var(--spacing-3); + // min-height: 17.125rem; + // width: ${MaxWidth.actionCard}; + // max-width: ${MaxWidth.actionCard}; + // // let the action card take the full width below the tablet breakpoint + // @media (max-width: ${basicMuiTheme.breakpoints.values.tablet}px) { + // width: 100%; + // max-width: 100%; + // } + }} + > + {children} {infoAccordion} diff --git a/apps/main/src/loan/components/PageLoanManage/index.tsx b/apps/main/src/loan/components/PageLoanManage/index.tsx index 991b5dd60e..b1acc01896 100644 --- a/apps/main/src/loan/components/PageLoanManage/index.tsx +++ b/apps/main/src/loan/components/PageLoanManage/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react' import { AddCollateralForm } from '@/llamalend/features/manage-loan/components/AddCollateralForm' import { RemoveCollateralForm } from '@/llamalend/features/manage-loan/components/RemoveCollateralForm' -import { RepayForm } from '@/llamalend/features/manage-loan/components/RepayForm' +import { RepayFromCollateral, RepayFromWallet } from '@/llamalend/features/manage-loan/components/RepayForm' import CollateralDecrease from '@/loan/components/PageLoanManage/CollateralDecrease' import CollateralIncrease from '@/loan/components/PageLoanManage/CollateralIncrease' import LoanDecrease from '@/loan/components/PageLoanManage/LoanDecrease' @@ -44,6 +44,21 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla const push = useNavigate() const shouldUseManageLoanMuiForm = useManageLoanMuiForm() const useMuiForm = shouldUseManageLoanMuiForm && !!llamma + const leverageEnabled = !!llamma?.leverageV2?.hasLeverage?.() + + const tabsLoanMui: TabOption[] = useMemo( + () => [ + { value: 'loan-increase', label: t`Borrow more` }, + ...(leverageEnabled + ? [ + { value: 'loan-repay-collateral', label: t`Repay from collateral` }, + { value: 'loan-repay-wallet', label: t`Repay from wallet` }, + ] + : [{ value: 'loan-decrease', label: t`Repay` }]), + { value: 'loan-liquidate', label: t`Self-liquidate` }, + ], + [leverageEnabled], + ) type Tab = 'loan' | 'collateral' | 'deleverage' const tabs: TabOption[] = [ @@ -56,8 +71,9 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla const [subTab, setSubTab] = useState('loan-increase') const subTabs = useMemo( - () => (rFormType === 'loan' ? tabsLoan : rFormType === 'collateral' ? tabsCollateral : []), - [rFormType], + () => + rFormType === 'loan' ? (useMuiForm ? tabsLoanMui : tabsLoan) : rFormType === 'collateral' ? tabsCollateral : [], + [rFormType, tabsLoanMui, useMuiForm], ) useEffect(() => setSubTab(subTabs[0]?.value), [subTabs]) @@ -96,13 +112,24 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla )} - {subTab === 'loan-decrease' && llamma && ( - {}} + leverageEnabled={leverageEnabled} + /> + )} + {(subTab === 'loan-repay-collateral' || subTab === 'loan-decrease') && llamma && ( + {}} + leverageEnabled={leverageEnabled} /> )} {subTab === 'loan-liquidate' && } diff --git a/apps/main/src/loan/components/PageLoanManage/types.ts b/apps/main/src/loan/components/PageLoanManage/types.ts index 61ca3cec0d..dd5a0639a9 100644 --- a/apps/main/src/loan/components/PageLoanManage/types.ts +++ b/apps/main/src/loan/components/PageLoanManage/types.ts @@ -1,7 +1,12 @@ import { ChainId, type CollateralUrlParams, LlamaApi, Llamma } from '@/loan/types/loan.types' export type FormType = 'loan' | 'collateral' | 'swap' | 'deleverage' -export type LoanFormType = 'loan-increase' | 'loan-decrease' | 'loan-liquidate' +export type LoanFormType = + | 'loan-increase' + | 'loan-decrease' + | 'loan-liquidate' + | 'loan-repay-wallet' + | 'loan-repay-collateral' export type CollateralFormType = 'collateral-increase' | 'collateral-decrease' export type FormStatus = { From 5ab81707292ddf53a0a5da57944f0ae1918ed4e3 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 16 Dec 2025 11:45:18 +0100 Subject: [PATCH 2/4] feat: repay form missing queries and validation --- .../lend/components/PageLoanManage/Page.tsx | 8 +- .../borrow/components/CreateLoanForm.tsx | 10 +- .../components/RepayLoanInfoAccordion.tsx | 79 ++++++++++ .../components/AddCollateralForm.tsx | 20 +-- .../components/RemoveCollateralForm.tsx | 26 ++-- .../manage-loan/components/RepayForm.tsx | 87 ++++++----- .../hooks/useLoanToValueFromUserState.ts | 17 +-- .../manage-loan/hooks/useRepayForm.ts | 88 ++++------- .../hooks/useActionInfos.ts | 22 +-- .../hooks/useImproveHealthTab.ts | 1 + .../src/llamalend/mutations/repay.mutation.ts | 31 +--- .../create-loan-approve-estimate-gas.query.ts | 1 + .../queries/repay/repay-bands.query.ts | 18 ++- .../queries/repay/repay-estimate-gas.query.ts | 139 ++++++++++++++++++ .../repay/repay-expected-borrowed.query.ts | 14 +- .../queries/repay/repay-gas-estimate.query.ts | 6 +- .../queries/repay/repay-health.query.ts | 17 +-- .../queries/repay/repay-is-approved.query.ts | 22 +-- .../queries/repay/repay-is-available.query.ts | 17 ++- .../queries/repay/repay-is-full.query.ts | 19 ++- .../queries/repay/repay-price-impact.query.ts | 19 +-- .../queries/repay/repay-prices.query.ts | 15 +- .../queries/repay/repay-route-image.query.ts | 10 +- .../llamalend/queries/user-health.query.ts | 4 +- .../validation/borrow-fields.validation.ts | 2 +- .../queries/validation/manage-loan.types.ts | 14 +- .../validation/manage-loan.validation.ts | 93 +++++++----- .../widgets/manage-loan/LoanInfoAccordion.tsx | 4 +- .../manage-loan/LoanLeverageActionInfo.tsx | 69 +++++---- .../loan/components/PageLoanManage/index.tsx | 22 +-- packages/curve-ui-kit/src/types/util.ts | 13 ++ 31 files changed, 555 insertions(+), 352 deletions(-) create mode 100644 apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx create mode 100644 apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts diff --git a/apps/main/src/lend/components/PageLoanManage/Page.tsx b/apps/main/src/lend/components/PageLoanManage/Page.tsx index f5c206c53f..57b9f5eb9f 100644 --- a/apps/main/src/lend/components/PageLoanManage/Page.tsx +++ b/apps/main/src/lend/components/PageLoanManage/Page.tsx @@ -130,13 +130,7 @@ const Page = () => { <> - {rChainId && rOwmId && ( - - )} + {rChainId && rOwmId && } ({ const network = networks[chainId] const [preset, setPreset] = useBorrowPreset(BorrowPreset.Safe) const { + form, values, - onSubmit, + params, isPending, + onSubmit, maxTokenValues, - params, - form, - collateralToken, borrowToken, + collateralToken, isCreated, creationError, txHash, - formErrors, isApproved, + formErrors, } = useCreateLoanForm({ market, network, preset, onCreated }) const setRange = useCallback( (range: number) => { diff --git a/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx b/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx new file mode 100644 index 0000000000..f32b1d3c35 --- /dev/null +++ b/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx @@ -0,0 +1,79 @@ +import type { Token } from '@/llamalend/features/borrow/types' +import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' +import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' +import type { NetworkDict } from '@/llamalend/llamalend.types' +import { useMarketFutureRates } from '@/llamalend/queries/market-future-rates.query' +import { useMarketRates } from '@/llamalend/queries/market-rates' +import { useRepayBands } from '@/llamalend/queries/repay/repay-bands.query' +import { useRepayExpectedBorrowed } from '@/llamalend/queries/repay/repay-expected-borrowed.query' +import { useRepayEstimateGas } from '@/llamalend/queries/repay/repay-gas-estimate.query' +import { useRepayHealth } from '@/llamalend/queries/repay/repay-health.query' +import { useRepayPriceImpact } from '@/llamalend/queries/repay/repay-price-impact.query' +import { useRepayPrices } from '@/llamalend/queries/repay/repay-prices.query' +import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' +import { useUserState } from '@/llamalend/queries/user-state.query' +import type { RepayParams } from '@/llamalend/queries/validation/manage-loan.types' +import type { RepayForm } from '@/llamalend/queries/validation/manage-loan.validation' +import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' +import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import { useSwitch } from '@ui-kit/hooks/useSwitch' +import { q } from '@ui-kit/types/util' +import { decimal, Decimal } from '@ui-kit/utils' + +export function RepayLoanInfoAccordion({ + params, + values: { slippage, leverageEnabled, userCollateral, userBorrowed }, + collateralToken, + borrowToken, + networks, + onSlippageChange, +}: { + params: RepayParams + values: RepayForm + collateralToken: Token | undefined + borrowToken: Token | undefined + networks: NetworkDict + onSlippageChange: (newSlippage: Decimal) => void +}) { + const [isOpen, , , toggle] = useSwitch(false) + const userState = q(useUserState(params, isOpen)) + return ( + getUserHealthOptions({ ...params, isFull }))} + prevRates={q(useMarketRates(params, isOpen))} + rates={q(useMarketFutureRates(params, isOpen))} + debt={{ + ...userState, + data: userState?.data?.debt && decimal(+userState.data.debt - +(userBorrowed ?? 0)), + tokenSymbol: borrowToken?.symbol, + }} + prevDebt={{ ...userState, data: userState?.data?.debt }} + prices={q(useRepayPrices(params, isOpen))} + // routeImage={q(useRepayRouteImage(params, isOpen))} + loanToValue={useLoanToValueFromUserState( + { + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + collateralDelta: userCollateral && `${-+userCollateral}`, + }, + isOpen, + )} + leverage={{ + enabled: leverageEnabled, + expectedBorrowed: useRepayExpectedBorrowed(params, isOpen), + priceImpact: useRepayPriceImpact(params, isOpen), + slippage, + onSlippageChange, + collateralSymbol: collateralToken?.symbol, + }} + /> + ) +} diff --git a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx index 2b7996d65b..46c4d4eb3d 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -68,15 +68,17 @@ export const AddCollateralForm = ({ bands={bands} prices={prices} rates={marketRates} - loanToValue={useLoanToValueFromUserState({ - chainId: params.chainId!, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: isOpen, - collateralDelta: values.userCollateral, - })} + loanToValue={useLoanToValueFromUserState( + { + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + collateralDelta: values.userCollateral, + }, + isOpen, + )} gas={gas} /> } diff --git a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx index 9041ad73ab..0937226458 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -70,18 +70,20 @@ export const RemoveCollateralForm = ({ bands={bands} prices={prices} rates={marketRates} - loanToValue={useLoanToValueFromUserState({ - chainId: params.chainId!, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: isOpen, - collateralDelta: - values.userCollateral != null - ? (`-${values.userCollateral}` as unknown as import('@ui-kit/utils').Decimal) - : undefined, - })} + loanToValue={useLoanToValueFromUserState( + { + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + collateralDelta: + values.userCollateral != null + ? (`-${values.userCollateral}` as unknown as import('@ui-kit/utils').Decimal) + : undefined, + }, + isOpen, + )} gas={gas} /> } diff --git a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx index 2a3eead762..832cc9248d 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx @@ -1,15 +1,16 @@ -import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' +import { RepayLoanInfoAccordion } from '@/llamalend/features/borrow/components/RepayLoanInfoAccordion' +import { setValueOptions } from '@/llamalend/features/borrow/react-form.utils' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { RepayOptions } from '@/llamalend/mutations/repay.mutation' -import { useMarketRates } from '@/llamalend/queries/market-rates' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' -import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import { notFalsy } from '@curvefi/prices-api/objects.util' import Button from '@mui/material/Button' +import Checkbox from '@mui/material/Checkbox' +import FormControlLabel from '@mui/material/FormControlLabel' import Stack from '@mui/material/Stack' -import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' import { InputDivider } from '../../../widgets/InputDivider' import { useRepayForm } from '../hooks/useRepayForm' @@ -34,28 +35,21 @@ export const RepayForm = ({ fromBorrowed?: boolean }) => { const network = networks[chainId] - const [isOpen, , , toggle] = useSwitch(false) - const { form, + values, + params, isPending, onSubmit, - action, - bands, - health, - prices, - gas, isDisabled, - isFull, - formErrors, - collateralToken, borrowToken, - params, - values, + collateralToken, + isRepaid, + repayError, txHash, - expectedBorrowed, - routeImage, - priceImpact, + isApproved, + formErrors, + isFull, } = useRepayForm({ market, network, @@ -63,35 +57,23 @@ export const RepayForm = ({ enabled, onRepaid, }) - - const marketRates = useMarketRates(params, isOpen) - + const { withdrawEnabled: withdrawEnabled } = values return ( - form.setValue('slippage', value, setValueOptions)} /> } > - }> + : undefined}> {fromCollateral && ( ({ )} + form.setValue('withdrawEnabled', e.target.checked, setValueOptions)} + /> + } + label={t`Repay & Withdraw`} + /> diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts index 7a76f607dd..9b715e9e22 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts @@ -7,12 +7,11 @@ import type { Decimal } from '@ui-kit/utils' import type { Token } from '../../borrow/types' type Params = { - chainId: ChainId + chainId: ChainId | null | undefined marketId: string | null | undefined userAddress: Address | null | undefined collateralToken: Token | undefined borrowToken: Token | undefined - enabled: boolean /** * Net change applied to on-chain collateral (positive = adding, negative = removing). * TODO: use expectedCollateral from llamalend-js, currently being implemented by @0xPearce @@ -29,16 +28,10 @@ type Params = { * It uses the generic userState query so it can be reused across * add-collateral, remove-collateral and repay flows. */ -export const useLoanToValueFromUserState = ({ - chainId, - marketId, - userAddress, - collateralToken, - borrowToken, - enabled, - collateralDelta, - expectedBorrowed, -}: Params) => { +export const useLoanToValueFromUserState = ( + { chainId, marketId, userAddress, collateralToken, borrowToken, collateralDelta, expectedBorrowed }: Params, + enabled: boolean, +) => { const { data: userState, isLoading: isUserLoading, diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts index 3ebbff89f3..ad1c91ceb8 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRepayForm.ts @@ -2,26 +2,20 @@ import { useEffect, useMemo } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useForm } from 'react-hook-form' import { useConnection } from 'wagmi' -import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' -import { getTokens } from '@/llamalend/llama.utils' +import { getTokens, hasLeverage } from '@/llamalend/llama.utils' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import { type RepayOptions, useRepayMutation } from '@/llamalend/mutations/repay.mutation' -import { useRepayBands } from '@/llamalend/queries/repay/repay-bands.query' -import { useRepayExpectedBorrowed } from '@/llamalend/queries/repay/repay-expected-borrowed.query' -import { useRepayEstimateGas } from '@/llamalend/queries/repay/repay-gas-estimate.query' -import { getRepayHealthOptions } from '@/llamalend/queries/repay/repay-health.query' +import { useBorrowCreateLoanIsApproved } from '@/llamalend/queries/create-loan/borrow-create-loan-approved.query' import { useRepayIsAvailable } from '@/llamalend/queries/repay/repay-is-available.query' import { useRepayIsFull } from '@/llamalend/queries/repay/repay-is-full.query' -import { useRepayPriceImpact } from '@/llamalend/queries/repay/repay-price-impact.query' -import { useRepayPrices } from '@/llamalend/queries/repay/repay-prices.query' -import { useRepayRouteImage } from '@/llamalend/queries/repay/repay-route-image.query' -import type { RepayFromCollateralIsFullParams } from '@/llamalend/queries/validation/manage-loan.types' +import type { RepayIsFullParams } from '@/llamalend/queries/validation/manage-loan.types' import { type RepayForm, repayFormValidationSuite } from '@/llamalend/queries/validation/manage-loan.validation' import type { IChainId as LlamaChainId, INetworkName as LlamaNetworkId } from '@curvefi/llamalend-api/lib/interfaces' import { vestResolver } from '@hookform/resolvers/vest' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' import { formDefaultOptions } from '@ui-kit/lib/model' -import { useFormErrors } from '../../borrow/react-form.utils' +import { SLIPPAGE_PRESETS } from '@ui-kit/widgets/SlippageSettings/slippage.utils' +import { setValueOptions, useFormErrors } from '../../borrow/react-form.utils' const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) @@ -32,14 +26,12 @@ export const useRepayForm = enabled?: boolean onRepaid?: NonNullable - leverageEnabled: boolean }) => { const { address: userAddress } = useConnection() const { chainId } = network @@ -56,7 +48,10 @@ export const useRepayForm = => ({ - chainId, - marketId, - userAddress, - stateCollateral: values.stateCollateral, - userCollateral: values.userCollateral, - userBorrowed: values.userBorrowed, - isFull: values.isFull, - }), - [ - chainId, - marketId, - userAddress, - values.stateCollateral, - values.userCollateral, - values.userBorrowed, - values.isFull, - ], + (): RepayIsFullParams => ({ chainId, marketId, userAddress, ...values }), + [chainId, marketId, userAddress, values], ), ) - const { onSubmit, ...action } = useRepayMutation({ + const { + onSubmit, + isPending: isRepaying, + isSuccess: isRepaid, + error: repayError, + data, + reset: resetRepay, + } = useRepayMutation({ network, marketId, onRepaid, onReset: form.reset, userAddress, - leverageEnabled, }) - useCallbackAfterFormUpdate(form, action.reset) + useCallbackAfterFormUpdate(form, resetRepay) // reset mutation state on form change - const bands = useRepayBands(params, enabled) - const expectedBorrowed = useRepayExpectedBorrowed(params, enabled) - const health = useHealthQueries((isFull) => getRepayHealthOptions({ ...params, isFull }, enabled)) const isAvailable = useRepayIsAvailable(params, enabled) const isFull = useRepayIsFull(params, enabled) - const priceImpact = useRepayPriceImpact(params, enabled) - const prices = useRepayPrices(params, enabled) - const routeImage = useRepayRouteImage(params, enabled) - const gas = useRepayEstimateGas(networks, params, enabled) const formErrors = useFormErrors(form.formState) - useEffect(() => form.setValue('isFull', isFull.data, { shouldValidate: true }), [form, isFull.data]) + useEffect(() => form.setValue('isFull', isFull.data, setValueOptions), [form, isFull.data]) + + // todo: remove from form, move this to queries directly as they depend on market only + useEffect(() => market && form.setValue('leverageEnabled', hasLeverage(market), setValueOptions), [market, form]) return { form, values, params, - isPending: form.formState.isSubmitting || action.isPending, + isPending: form.formState.isSubmitting || isRepaying, onSubmit: form.handleSubmit(onSubmit), - action, - bands, - expectedBorrowed, - health, isDisabled: !isAvailable.data || formErrors.length > 0, - isFull, - priceImpact, - prices, - routeImage, - gas, - txHash: action.data?.hash, - collateralToken, borrowToken, - formErrors, + collateralToken, + isRepaid, + repayError, + txHash: data?.hash, + isApproved: useBorrowCreateLoanIsApproved(params), + formErrors: useFormErrors(form.formState), + isFull, } } diff --git a/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useActionInfos.ts b/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useActionInfos.ts index da21a506ae..3e725c6d5c 100644 --- a/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useActionInfos.ts +++ b/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useActionInfos.ts @@ -12,25 +12,11 @@ import { useLoanInfo } from './useLoanInfo' */ export function useActionInfos(params: MarketParams): ActionInfosProps { const { address: userAddress } = useConnection() - const { data: userHealth } = useHealthQueries((isFull) => - getUserHealthOptions( - { - ...{ ...params, userAddress }, - isFull, - }, - undefined, - ), - ) - - const health = { current: Number(userHealth ?? 0) } - - const loanInfo = useLoanInfo(params) - const collateral = useCollateralInfo(params) - + const { data: userHealth } = useHealthQueries((isFull) => getUserHealthOptions({ ...params, userAddress, isFull })) return { - health, - loanInfo, - collateral, + health: { current: Number(userHealth ?? 0) }, + loanInfo: useLoanInfo(params), + collateral: useCollateralInfo(params), transaction: { estimatedTxCost: { eth: 0.0024, gwei: 0.72, dollars: 0.48 }, }, diff --git a/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useImproveHealthTab.ts b/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useImproveHealthTab.ts index 449a42daa6..4842018cc1 100644 --- a/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useImproveHealthTab.ts +++ b/apps/main/src/llamalend/features/manage-soft-liquidation/hooks/useImproveHealthTab.ts @@ -21,6 +21,7 @@ export function useImproveHealthTab(params: MarketParams): ImproveHealthProps { userCollateral: '0' as Decimal, userBorrowed: debt, isFull: false, // todo: implement full repays + leverageEnabled: false, // todo: implement leverage }) }, [mutate], diff --git a/apps/main/src/llamalend/mutations/repay.mutation.ts b/apps/main/src/llamalend/mutations/repay.mutation.ts index be8fc60b59..9b449022cf 100644 --- a/apps/main/src/llamalend/mutations/repay.mutation.ts +++ b/apps/main/src/llamalend/mutations/repay.mutation.ts @@ -29,7 +29,6 @@ export type RepayOptions = { onRepaid?: LlammaMutationOptions['onSuccess'] onReset?: () => void userAddress: Address | undefined - leverageEnabled: boolean } const approveRepay = async ( @@ -53,8 +52,10 @@ const approveRepay = async ( return (await market.repayApprove(userBorrowed)) as Hex[] } -const repay = async (market: LlamaMarketTemplate, mutation: RepayMutation): Promise => { - const { stateCollateral, userCollateral, userBorrowed, isFull, leverageEnabled } = mutation +const repay = async ( + market: LlamaMarketTemplate, + { stateCollateral, userCollateral, userBorrowed, isFull, leverageEnabled }: RepayMutation, +): Promise => { if (isFull) { return (await market.fullRepay()) as Hex } @@ -63,7 +64,6 @@ const repay = async (market: LlamaMarketTemplate, mutation: RepayMutation): Prom return (await market.repay(userBorrowed)) as Hex } if (market instanceof LendMarketTemplate) { - // note: MintMarketTemplate.leverage(v1) does not have repay methods await market.leverage.repayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed) return (await market.leverage.repay(stateCollateral, userCollateral, userBorrowed)) as Hex } @@ -83,36 +83,19 @@ export const useRepayMutation = ({ onRepaid, onReset, userAddress, - leverageEnabled, }: RepayOptions) => { const config = useConfig() const { mutate, mutateAsync, error, data, isPending, isSuccess, reset } = useLlammaMutation({ network, marketId, - mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repay', { leverageEnabled }] as const, + mutationKey: [...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repay'] as const, mutationFn: async (mutation, { market }) => { - const { stateCollateral, userBorrowed, userCollateral, isFull } = mutation - await waitForApproval({ - isApproved: () => - fetchRepayIsApproved( - { - chainId, - marketId, - userAddress, - stateCollateral, - userCollateral, - userBorrowed, - isFull, - leverageEnabled, - }, - { staleTime: 0 }, - ), - onApprove: () => approveRepay(market, { ...mutation, leverageEnabled }), + isApproved: async () => await fetchRepayIsApproved(mutation, { staleTime: 0 }), + onApprove: async () => await approveRepay(market, mutation), message: t`Approved repayment`, config, }) - return { hash: await repay(market, mutation) } }, validationSuite: repayFromCollateralIsFullValidationSuite, diff --git a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts index 546a35c5e5..10bf85b39a 100644 --- a/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/create-loan/create-loan-approve-estimate-gas.query.ts @@ -44,6 +44,7 @@ const { useQuery: useCreateLoanApproveEstimateGas } = queryFactory({ dependencies: (params) => [createLoanMaxReceiveKey(params)], }) +// todo: expand this to consider estimation after approval, see `useRepayEstimateGas` export const useCreateLoanEstimateGas = ( networks: NetworkDict, query: GasEstimateParams, diff --git a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts index e4556e4b41..fc972c8b5a 100644 --- a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts @@ -1,8 +1,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' export const { useQuery: useRepayBands } = queryFactory({ queryKey: ({ @@ -12,26 +12,34 @@ export const { useQuery: useRepayBands } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayBands', { stateCollateral }, { userCollateral }, { userBorrowed }, + { leverageEnabled }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, - }: RepayFromCollateralQuery): Promise<[number, number]> => { + leverageEnabled, + }: RepayQuery): Promise<[number, number]> => { const market = getLlamaMarket(marketId) + if (!leverageEnabled) { + console.assert(!+userCollateral, 'userCollateral should be 0 when leverage is disabled') + console.assert(!+stateCollateral, 'stateCollateral should be 0 when leverage is disabled') + return market.repayBands(userBorrowed) + } return market instanceof LendMarketTemplate ? await market.leverage.repayBands(stateCollateral, userCollateral, userBorrowed) : market.leverageV2.hasLeverage() ? await market.leverageV2.repayBands(stateCollateral, userCollateral, userBorrowed) : await market.deleverage.repayBands(userCollateral) }, - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts new file mode 100644 index 0000000000..f26f5a8a2a --- /dev/null +++ b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts @@ -0,0 +1,139 @@ +import { useEstimateGas } from '@/llamalend/hooks/useEstimateGas' +import { getLlamaMarket } from '@/llamalend/llama.utils' +import type { NetworkDict } from '@/llamalend/llamalend.types' +import { type RepayIsApprovedParams, useRepayIsApproved } from '@/llamalend/queries/repay/repay-is-approved.query' +import type { IChainId, TGas } from '@curvefi/llamalend-api/lib/interfaces' +import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' +import { queryFactory, rootKeys } from '@ui-kit/lib/model' +import { type RepayIsFullQuery } from '../validation/manage-loan.types' +import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' + +const { useQuery: useRepayLoanEstimateGas } = queryFactory({ + queryKey: ({ + chainId, + marketId, + stateCollateral = '0', + userCollateral = '0', + userBorrowed = '0', + userAddress, + isFull, + slippage, + leverageEnabled, + }: RepayIsApprovedParams) => + [ + ...rootKeys.userMarket({ chainId, marketId, userAddress }), + 'estimateGas.repay', + { stateCollateral }, + { userCollateral }, + { userBorrowed }, + { isFull }, + { slippage }, + { leverageEnabled }, + ] as const, + queryFn: async ({ + marketId, + stateCollateral, + userCollateral, + userBorrowed, + isFull, + userAddress, + slippage, + leverageEnabled, + }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { + const market = getLlamaMarket(marketId) + if (isFull) { + return await market.estimateGas.fullRepay(userAddress) + } + if (leverageEnabled) { + if (market instanceof LendMarketTemplate) { + return await market.leverage.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) + } + if (market.leverageV2.hasLeverage()) { + return await market.leverageV2.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) + } + } + console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) + console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) + return await market.estimateGas.repay(userCollateral) + }, + staleTime: '1m', + validationSuite: repayFromCollateralIsFullValidationSuite, +}) + +const { useQuery: useRepayLoanApproveEstimateGas } = queryFactory({ + queryKey: ({ + chainId, + marketId, + stateCollateral = '0', + userCollateral = '0', + userBorrowed = '0', + userAddress, + isFull, + leverageEnabled, + }: RepayIsApprovedParams) => + [ + ...rootKeys.userMarket({ chainId, marketId, userAddress }), + 'estimateGas.repayApprove', + { stateCollateral }, + { userCollateral }, + { userBorrowed }, + { isFull }, + { leverageEnabled }, + ] as const, + queryFn: async ({ + marketId, + stateCollateral, + userCollateral, + userBorrowed, + isFull, + userAddress, + leverageEnabled, + }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { + const market = getLlamaMarket(marketId) + if (isFull) { + return await market.estimateGas.fullRepayApprove(userAddress) + } + if (leverageEnabled) { + if (market instanceof LendMarketTemplate) { + return await market.leverage.estimateGas.repayApprove(userCollateral, userBorrowed) + } + if (market.leverageV2.hasLeverage()) { + return await market.leverageV2.estimateGas.repayApprove(userCollateral, userBorrowed) + } + } + console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) + console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) + return await market.estimateGas.repayApprove(userCollateral) + }, + staleTime: '1m', + validationSuite: repayFromCollateralIsFullValidationSuite, +}) + +export const useRepayEstimateGas = ( + networks: NetworkDict, + query: RepayIsApprovedParams, + enabled?: boolean, +) => { + const { chainId } = query + const { data: isApproved, isLoading: isApprovedLoading, error: isApprovedError } = useRepayIsApproved(query, enabled) + const { + data: approveEstimate, + isLoading: approveLoading, + error: approveError, + } = useRepayLoanApproveEstimateGas(query, enabled && !isApproved) + const { + data: repayEstimate, + isLoading: repayLoading, + error: repayError, + } = useRepayLoanEstimateGas(query, enabled && isApproved) + const { + data, + isLoading: conversionLoading, + error: estimateError, + } = useEstimateGas(networks, chainId, isApproved ? approveEstimate : repayEstimate, enabled) + return { + data, + isLoading: [isApprovedLoading, approveLoading, repayLoading, conversionLoading].some(Boolean), + error: [isApprovedError, approveError, repayError, estimateError].find(Boolean), + } +} diff --git a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts index 8b957233e4..e82dc53fa2 100644 --- a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts @@ -2,10 +2,10 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type Decimal } from '@ui-kit/utils' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' -type RepayExpectedBorrowedResult = { +export type RepayExpectedBorrowedResult = { totalBorrowed: Decimal borrowedFromStateCollateral?: Decimal borrowedFromUserCollateral?: Decimal @@ -21,15 +21,17 @@ export const { useQuery: useRepayExpectedBorrowed } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayExpectedBorrowed', { stateCollateral }, { userCollateral }, { userBorrowed }, + { leverageEnabled }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayFromCollateralQuery) => { + queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { // todo: investigate if this is OK when the user's position is not leveraged const market = getLlamaMarket(marketId) if (market instanceof LendMarketTemplate) { @@ -47,5 +49,5 @@ export const { useQuery: useRepayExpectedBorrowed } = queryFactory({ return { totalBorrowed: stablecoins[routeIdx] as Decimal } }, staleTime: '1m', - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: true }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts b/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts index cd4c62be54..5ba313c186 100644 --- a/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts @@ -5,11 +5,11 @@ import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { type FieldsOf } from '@ui-kit/lib' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import type { RepayFromCollateralHealthQuery } from '../validation/manage-loan.types' +import type { RepayHealthQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' import { repayIsFullQueryKey } from './repay-is-full.query' -type RepayFromCollateralGasQuery = RepayFromCollateralHealthQuery +type RepayFromCollateralGasQuery = RepayHealthQuery type RepayFromCollateralGasParams = FieldsOf> const { useQuery: useRepayGasEstimate } = queryFactory({ @@ -71,7 +71,7 @@ export const useRepayEstimateGas = ( const { chainId } = query const { data: estimate, isLoading: estimateLoading, error: estimateError } = useRepayGasEstimate(query, enabled) const { - data, + data = null, isLoading: conversionLoading, error: conversionError, } = useEstimateGas(networks, chainId, estimate, enabled) diff --git a/apps/main/src/llamalend/queries/repay/repay-health.query.ts b/apps/main/src/llamalend/queries/repay/repay-health.query.ts index e531003b5b..fe4f05ee53 100644 --- a/apps/main/src/llamalend/queries/repay/repay-health.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-health.query.ts @@ -2,13 +2,10 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' -import { - type RepayFromCollateralHealthQuery, - type RepayFromCollateralHealthParams, -} from '../validation/manage-loan.types' +import { type RepayHealthQuery, type RepayHealthParams } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' -export const { getQueryOptions: getRepayHealthOptions } = queryFactory({ +export const { getQueryOptions: getRepayHealthOptions, useQuery: useRepayHealth } = queryFactory({ queryKey: ({ chainId, marketId, @@ -17,7 +14,7 @@ export const { getQueryOptions: getRepayHealthOptions } = queryFactory({ userBorrowed = '0', userAddress, isFull, - }: RepayFromCollateralHealthParams) => + }: RepayHealthParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayHealth', @@ -26,13 +23,7 @@ export const { getQueryOptions: getRepayHealthOptions } = queryFactory({ { userBorrowed }, { isFull }, ] as const, - queryFn: async ({ - marketId, - stateCollateral, - userCollateral, - userBorrowed, - isFull, - }: RepayFromCollateralHealthQuery) => { + queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, isFull }: RepayHealthQuery) => { const market = getLlamaMarket(marketId) return ( market instanceof LendMarketTemplate diff --git a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts index ba2c5ae3a1..0606ede5b3 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts @@ -1,13 +1,13 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' +import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { - type RepayFromCollateralIsFullParams, - type RepayFromCollateralIsFullQuery, -} from '../validation/manage-loan.types' +import { type RepayIsFullParams, type RepayIsFullQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' -type RepayIsApprovedParams = RepayFromCollateralIsFullParams & { leverageEnabled?: boolean } +export type RepayIsApprovedParams = RepayIsFullParams & { + leverageEnabled?: boolean +} export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } = queryFactory({ queryKey: ({ @@ -37,12 +37,16 @@ export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } isFull, userAddress, leverageEnabled, - }: RepayFromCollateralIsFullQuery & { leverageEnabled?: boolean }): Promise => { + }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { const market = getLlamaMarket(marketId) if (isFull) return await market.fullRepayIsApproved(userAddress) - if (market instanceof LendMarketTemplate) return await market.leverage.repayIsApproved(userCollateral, userBorrowed) - if (leverageEnabled && market.leverageV2.hasLeverage()) { - return await market.leverageV2.repayIsApproved(userCollateral, userBorrowed) + if (leverageEnabled) { + if (market instanceof LendMarketTemplate) { + return await market.leverage.repayIsApproved(userCollateral, userBorrowed) + } + if (market.leverageV2.hasLeverage()) { + return await market.leverageV2.repayIsApproved(userCollateral, userBorrowed) + } } console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) diff --git a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts index 937661aa62..364a0da48f 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts @@ -1,8 +1,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' export const { useQuery: useRepayIsAvailable } = queryFactory({ queryKey: ({ @@ -12,13 +12,15 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayIsAvailable', { stateCollateral }, { userCollateral }, { userBorrowed }, + { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -26,8 +28,13 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ userCollateral, userBorrowed, userAddress, - }: RepayFromCollateralQuery): Promise => { + leverageEnabled, + }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) + if (!leverageEnabled) { + const debt = (await market.userState(userAddress))?.debt + return debt != null && +debt > 0 + } return market instanceof LendMarketTemplate ? await market.leverage.repayIsAvailable(stateCollateral, userCollateral, userBorrowed, userAddress) : market.leverageV2.hasLeverage() @@ -35,5 +42,5 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ : await market.deleverage.isAvailable(userCollateral, userAddress) }, staleTime: '1m', - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts index 03a680f24c..5104b94967 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts @@ -1,8 +1,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = queryFactory({ queryKey: ({ @@ -12,13 +12,15 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayIsFull', { stateCollateral }, { userCollateral }, { userBorrowed }, + { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -26,8 +28,15 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query userCollateral, userBorrowed, userAddress, - }: RepayFromCollateralQuery): Promise => { + leverageEnabled, + }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) + if (!leverageEnabled) { + console.assert(!+stateCollateral, 'State collateral should be zero when leverage is disabled') + console.assert(!+userCollateral, 'User collateral should be zero when leverage is disabled') + const { debt } = await market.userState(userAddress) + return debt === userBorrowed + } return market instanceof LendMarketTemplate ? await market.leverage.repayIsFull(stateCollateral, userCollateral, userBorrowed, userAddress) : market.leverageV2.hasLeverage() @@ -35,5 +44,5 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query : await market.deleverage.isFullRepayment(userCollateral, userAddress) }, staleTime: '1m', - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts index dc1dd23e4c..2fd045c353 100644 --- a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts @@ -1,8 +1,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' type RepayPriceImpactResult = number @@ -12,22 +12,17 @@ export const { useQuery: useRepayPriceImpact } = queryFactory({ marketId, stateCollateral = '0', userCollateral = '0', - userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayPriceImpact', { stateCollateral }, { userCollateral }, - { userBorrowed }, + { leverageEnabled }, ] as const, - queryFn: async ({ - marketId, - stateCollateral, - userCollateral, - userBorrowed, - }: RepayFromCollateralQuery): Promise => { + queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) return market instanceof LendMarketTemplate ? +(await market.leverage.repayPriceImpact(stateCollateral, userCollateral)) @@ -36,5 +31,5 @@ export const { useQuery: useRepayPriceImpact } = queryFactory({ : +(await market.deleverage.priceImpact(userCollateral)) }, staleTime: '1m', - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: true }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts index 98c4f14aa3..4717514368 100644 --- a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts @@ -2,8 +2,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' export const { useQuery: useRepayPrices } = queryFactory({ queryKey: ({ @@ -13,16 +13,21 @@ export const { useQuery: useRepayPrices } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + leverageEnabled, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayPrices', { stateCollateral }, { userCollateral }, { userBorrowed }, + { leverageEnabled }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayFromCollateralQuery) => { + queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, leverageEnabled }: RepayQuery) => { const market = getLlamaMarket(marketId) + if (!leverageEnabled) { + return (await market.repayPrices(userBorrowed)) as Decimal[] + } return ( market instanceof LendMarketTemplate ? await market.leverage.repayPrices(stateCollateral, userCollateral, userBorrowed) @@ -31,5 +36,5 @@ export const { useQuery: useRepayPrices } = queryFactory({ : await market.deleverage.repayPrices(userCollateral) ) as Decimal[] }, - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts b/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts index 9a35990c74..a765f8b209 100644 --- a/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts @@ -1,8 +1,8 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type RepayFromCollateralParams, type RepayFromCollateralQuery } from '../validation/manage-loan.types' -import { repayFromCollateralValidationSuite } from '../validation/manage-loan.validation' +import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' +import { repayValidationSuite } from '../validation/manage-loan.validation' export const { useQuery: useRepayRouteImage } = queryFactory({ queryKey: ({ @@ -12,7 +12,7 @@ export const { useQuery: useRepayRouteImage } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - }: RepayFromCollateralParams) => + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayRouteImage', @@ -20,7 +20,7 @@ export const { useQuery: useRepayRouteImage } = queryFactory({ { userCollateral }, { userBorrowed }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayFromCollateralQuery) => { + queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayQuery) => { const market = getLlamaMarket(marketId) if (market instanceof LendMarketTemplate) { return await market.leverage.repayRouteImage(stateCollateral, userCollateral) @@ -31,5 +31,5 @@ export const { useQuery: useRepayRouteImage } = queryFactory({ return null }, staleTime: '1m', - validationSuite: repayFromCollateralValidationSuite, + validationSuite: repayValidationSuite({ leverageRequired: true }), }) diff --git a/apps/main/src/llamalend/queries/user-health.query.ts b/apps/main/src/llamalend/queries/user-health.query.ts index 325437bcc2..fed3abc270 100644 --- a/apps/main/src/llamalend/queries/user-health.query.ts +++ b/apps/main/src/llamalend/queries/user-health.query.ts @@ -3,7 +3,7 @@ import { queryFactory, rootKeys, type UserMarketParams, type UserMarketQuery } f import { userMarketValidationSuite } from '@ui-kit/lib/model/query/user-market-validation' import { createValidationSuite } from '@ui-kit/lib/validation' import type { Decimal } from '@ui-kit/utils' -import { validateIsFull } from './validation/borrow-fields.validation' +import { validateBoolean } from './validation/borrow-fields.validation' type UserHealthParams = UserMarketParams & { isFull: boolean } type UserHealthQuery = UserMarketQuery & { isFull: boolean } @@ -19,6 +19,6 @@ export const { getQueryOptions: getUserHealthOptions, invalidate: invalidateUser (await getLlamaMarket(marketId).userHealth(isFull, userAddress)) as Decimal, validationSuite: createValidationSuite(({ userAddress, isFull, marketId, chainId }: UserHealthParams) => { userMarketValidationSuite({ userAddress, marketId, chainId }) - validateIsFull(isFull) + validateBoolean(isFull) }), }) diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 55348eba37..0f83ea2a23 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -71,7 +71,7 @@ export const validateMaxCollateral = ( }) } -export const validateIsFull = (value: boolean | undefined | null) => { +export const validateBoolean = (value: boolean | undefined | null) => { test('root', 'Form is not completely filled out', () => { enforce(value).isBoolean() }) diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts index ca7a7b613e..f0ec0c372d 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts @@ -9,16 +9,18 @@ type HealthQuery = { isFull: boolean } export type CollateralHealthQuery = CollateralQuery & HealthQuery -export type RepayFromCollateralQuery = CollateralQuery & { +export type RepayQuery = CollateralQuery & { stateCollateral: Decimal userBorrowed: Decimal + slippage: Decimal + leverageEnabled: boolean } -export type RepayFromCollateralHealthQuery = RepayFromCollateralQuery & HealthQuery -export type RepayFromCollateralIsFullQuery = RepayFromCollateralQuery & HealthQuery +export type RepayHealthQuery = RepayQuery & HealthQuery +export type RepayIsFullQuery = RepayQuery & HealthQuery -export type RepayFromCollateralParams = FieldsOf> -export type RepayFromCollateralHealthParams = FieldsOf> -export type RepayFromCollateralIsFullParams = FieldsOf> +export type RepayParams = FieldsOf> +export type RepayHealthParams = FieldsOf> +export type RepayIsFullParams = FieldsOf> export type CollateralParams = FieldsOf> export type CollateralHealthParams = FieldsOf> diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts index fbfa5c12bc..9f90e628de 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -1,11 +1,16 @@ import BigNumber from 'bignumber.js' import { enforce, group, test } from 'vest' -import { validateIsFull, validateUserCollateral } from '@/llamalend/queries/validation/borrow-fields.validation' +import { + validateBoolean, + validateLeverageEnabled, + validateSlippage, + validateUserCollateral, +} from '@/llamalend/queries/validation/borrow-fields.validation' import type { CollateralHealthParams, CollateralParams, - RepayFromCollateralIsFullParams, - RepayFromCollateralParams, + RepayIsFullParams, + RepayParams, } from '@/llamalend/queries/validation/manage-loan.types' import { createValidationSuite, type FieldsOf } from '@ui-kit/lib' import { chainValidationGroup } from '@ui-kit/lib/model/query/chain-validation' @@ -16,14 +21,17 @@ import type { Decimal } from '@ui-kit/utils' export type CollateralForm = FieldsOf<{ userCollateral: Decimal }> -export type RepayForm = FieldsOf<{ - stateCollateral: Decimal - userCollateral: Decimal - userBorrowed: Decimal - isFull: boolean -}> +export type RepayForm = { + stateCollateral: Decimal | undefined + userCollateral: Decimal | undefined + userBorrowed: Decimal | undefined + isFull: boolean | undefined + slippage: Decimal + withdrawEnabled: boolean + leverageEnabled: boolean +} -const validateRepayCollateralField = (field: 'stateCollateral' | 'userCollateral', value: Decimal | null | undefined) => +const validateRepayField = (field: 'stateCollateral' | 'userCollateral', value: Decimal | null | undefined) => test(field, `Collateral amount must be a non-negative number`, () => { if (value == null) return enforce(value).isNumeric().gte(0) @@ -63,40 +71,59 @@ export const collateralFormValidationSuite = createValidationSuite((params: Coll export const collateralHealthValidationSuite = createValidationSuite(({ isFull, ...rest }: CollateralHealthParams) => { collateralValidationGroup(rest) - validateIsFull(isFull) + validateBoolean(isFull) }) -export const repayFromCollateralValidationGroup = ({ - chainId, - stateCollateral, - userCollateral, - userBorrowed, - userAddress, -}: RepayFromCollateralParams) => { +export const repayValidationGroup = ( + { + chainId, + stateCollateral, + userCollateral, + userBorrowed, + userAddress, + leverageEnabled, + slippage, + }: RepayParams, + { leverageRequired = false }: { leverageRequired?: boolean } = {}, +) => { chainValidationGroup({ chainId }) llamaApiValidationGroup({ chainId }) userAddressValidationGroup({ userAddress }) - validateRepayCollateralField('userCollateral', userCollateral) - validateRepayCollateralField('stateCollateral', stateCollateral) + validateRepayField('userCollateral', userCollateral) + validateRepayField('stateCollateral', stateCollateral) validateRepayBorrowedField(userBorrowed) validateRepayHasValue(stateCollateral, userCollateral, userBorrowed) + validateSlippage(slippage) + validateLeverageEnabled(leverageEnabled, leverageRequired) } -export const repayFromCollateralValidationSuite = createValidationSuite((params: RepayFromCollateralParams) => - repayFromCollateralValidationGroup(params), -) +export const repayValidationSuite = ({ leverageRequired }: { leverageRequired: boolean }) => + createValidationSuite((params: RepayParams) => repayValidationGroup(params, { leverageRequired })) -export const repayFormValidationSuite = createValidationSuite((params: RepayForm) => { - validateRepayCollateralField('userCollateral', params.userCollateral) - validateRepayCollateralField('stateCollateral', params.stateCollateral) - validateRepayBorrowedField(params.userBorrowed) - validateRepayHasValue(params.stateCollateral, params.userCollateral, params.userBorrowed) - validateIsFull(params.isFull) -}) +export const repayFormValidationSuite = createValidationSuite( + ({ + isFull, + stateCollateral, + userCollateral, + userBorrowed, + withdrawEnabled, + leverageEnabled, + slippage, + }: RepayForm) => { + validateRepayField('userCollateral', userCollateral) + validateRepayField('stateCollateral', stateCollateral) + validateRepayBorrowedField(userBorrowed) + validateRepayHasValue(stateCollateral, userCollateral, userBorrowed) + validateBoolean(isFull) + validateBoolean(leverageEnabled) + validateBoolean(withdrawEnabled) + validateSlippage(slippage) + }, +) export const repayFromCollateralIsFullValidationSuite = createValidationSuite( - ({ isFull, ...params }: RepayFromCollateralIsFullParams) => { - repayFromCollateralValidationGroup(params) - group('isFull', () => validateIsFull(isFull)) + ({ isFull, ...params }: RepayIsFullParams) => { + repayValidationGroup(params) + group('isFull', () => validateBoolean(isFull)) }, ) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 46edfe8fd9..616ed67cd2 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -24,7 +24,7 @@ export type LoanLeverageMaxReceive = { maxLeverage?: Decimal } -type LoanInfoAccordionProps = { +export type LoanInfoAccordionProps = { isOpen: boolean toggle: () => void range?: number @@ -37,7 +37,7 @@ type LoanInfoAccordionProps = { loanToValue: Query prevLoanToValue?: Query gas: Query - debt?: Query & { tokenSymbol: string } + debt?: Query & { tokenSymbol: string | undefined } prevDebt?: Query leverage?: LoanLeverageActionInfoProps & { enabled: boolean } } diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx index c0e8c68012..52bc8ff763 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx @@ -1,4 +1,5 @@ import { notFalsy } from 'router-api/src/router.utils' +import type { RepayExpectedBorrowedResult } from '@/llamalend/queries/repay/repay-expected-borrowed.query' import { t } from '@ui-kit/lib/i18n' import ActionInfo from '@ui-kit/shared/ui/ActionInfo' import type { Query } from '@ui-kit/types/util' @@ -7,8 +8,9 @@ import { SlippageToleranceActionInfo } from '@ui-kit/widgets/SlippageSettings' import type { LoanLeverageExpectedCollateral, LoanLeverageMaxReceive } from './LoanInfoAccordion' export type LoanLeverageActionInfoProps = { - expectedCollateral: Query - maxReceive: Query + expectedCollateral?: Query + expectedBorrowed?: Query + maxReceive?: Query priceImpact: Query slippage: Decimal onSlippageChange: (newSlippage: Decimal) => void @@ -28,46 +30,41 @@ export const LoanLeverageActionInfo = ({ onSlippageChange, collateralSymbol, }: LoanLeverageActionInfoProps) => { - const { - data: expectedCollateralData, - isLoading: expectedCollateralLoading, - error: expectedCollateralError, - } = expectedCollateral - const { data: maxReceiveData, isLoading: maxReceiveLoading, error: maxReceiveError } = maxReceive - const { data: priceImpactPercent, isLoading: priceImpactPercentLoading, error: priceImpactPercentError } = priceImpact - - const { totalCollateral, leverage } = expectedCollateralData ?? {} - const { avgPrice, maxLeverage } = maxReceiveData ?? {} - - const isHighImpact = priceImpactPercent != null && priceImpactPercent > +slippage + const isHighImpact = priceImpact.data != null && priceImpact.data > +slippage return ( <> - - - + {expectedCollateral && maxReceive && ( + q.error)?.error} + loading={[expectedCollateral, maxReceive].some((q) => q.isLoading)} + /> + )} + {expectedCollateral && ( + + )} + {maxReceive && ( + + )} diff --git a/apps/main/src/loan/components/PageLoanManage/index.tsx b/apps/main/src/loan/components/PageLoanManage/index.tsx index b53e8f3511..b2cd35562b 100644 --- a/apps/main/src/loan/components/PageLoanManage/index.tsx +++ b/apps/main/src/loan/components/PageLoanManage/index.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react' import { AddCollateralForm } from '@/llamalend/features/manage-loan/components/AddCollateralForm' import { RemoveCollateralForm } from '@/llamalend/features/manage-loan/components/RemoveCollateralForm' -import { RepayFromCollateral, RepayFromWallet } from '@/llamalend/features/manage-loan/components/RepayForm' +import { RepayForm } from '@/llamalend/features/manage-loan/components/RepayForm' import CollateralDecrease from '@/loan/components/PageLoanManage/CollateralDecrease' import CollateralIncrease from '@/loan/components/PageLoanManage/CollateralIncrease' import LoanDecrease from '@/loan/components/PageLoanManage/LoanDecrease' @@ -50,11 +50,11 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla () => [ { value: 'loan-increase', label: t`Borrow more` }, ...(leverageEnabled - ? [ + ? ([ { value: 'loan-repay-collateral', label: t`Repay from collateral` }, { value: 'loan-repay-wallet', label: t`Repay from wallet` }, - ] - : [{ value: 'loan-decrease', label: t`Repay` }]), + ] as const) + : ([{ value: 'loan-decrease', label: t`Repay` }] as const)), { value: 'loan-liquidate', label: t`Self-liquidate` }, ], [leverageEnabled], @@ -113,27 +113,17 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla )} {subTab === 'loan-repay-wallet' && leverageEnabled && llamma && ( - {}} - leverageEnabled={leverageEnabled} fromBorrowed fromWallet /> )} {(subTab === 'loan-repay-collateral' || subTab === 'loan-decrease') && llamma && ( - {}} - leverageEnabled={leverageEnabled} - fromCollateral - /> + )} {subTab === 'loan-liquidate' && } {subTab === 'collateral-increase' && llamma && ( diff --git a/packages/curve-ui-kit/src/types/util.ts b/packages/curve-ui-kit/src/types/util.ts index 17eb674967..771c45222e 100644 --- a/packages/curve-ui-kit/src/types/util.ts +++ b/packages/curve-ui-kit/src/types/util.ts @@ -1,3 +1,5 @@ +import type { UseQueryResult } from '@tanstack/react-query' + /** * Creates a deep partial type that makes all properties optional recursively, * while preserving function types as-is @@ -38,3 +40,14 @@ export type MakeOptional = Omit & Partial * @template T - The type of the data returned by the query. */ export type Query = { data: T | undefined; isLoading: boolean; error: Error | null | undefined } + +/** + * Helper to extract only the relevant fields from a UseQueryResult into the Query type. + * This is necessary because passing UseQueryResult to any react component will crash the rendering due to + * react trying to serialize the react-query proxy object. + */ +export const q = ({ data, isLoading, error }: UseQueryResult): Query => ({ + data, + isLoading, + error, +}) From 3d3487934341ccddb6acfc0be1b662c43c2b5d91 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 16 Dec 2025 17:44:32 +0100 Subject: [PATCH 3/4] fix: remove leverageEnabled from RepayForm --- .../borrow/components/RepayLoanInfoAccordion.tsx | 6 ++++-- .../manage-loan/components/RepayForm.tsx | 2 ++ .../llamalend/queries/repay/repay-bands.query.ts | 7 ++----- .../queries/repay/repay-estimate-gas.query.ts | 16 +++++----------- .../repay/repay-expected-borrowed.query.ts | 6 +++--- .../queries/repay/repay-is-approved.query.ts | 13 ++++--------- .../queries/repay/repay-is-available.query.ts | 8 +++----- .../queries/repay/repay-is-full.query.ts | 9 +++------ .../queries/repay/repay-price-impact.query.ts | 10 +--------- .../queries/repay/repay-prices.query.ts | 10 +++++----- .../validation/borrow-fields.validation.ts | 10 ++++++++++ .../queries/validation/manage-loan.types.ts | 1 - .../queries/validation/manage-loan.validation.ts | 14 +++----------- 13 files changed, 45 insertions(+), 67 deletions(-) diff --git a/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx b/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx index f32b1d3c35..3e3e876186 100644 --- a/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/features/borrow/components/RepayLoanInfoAccordion.tsx @@ -22,11 +22,12 @@ import { decimal, Decimal } from '@ui-kit/utils' export function RepayLoanInfoAccordion({ params, - values: { slippage, leverageEnabled, userCollateral, userBorrowed }, + values: { slippage, userCollateral, userBorrowed }, collateralToken, borrowToken, networks, onSlippageChange, + hasLeverage, }: { params: RepayParams values: RepayForm @@ -34,6 +35,7 @@ export function RepayLoanInfoAccordion({ borrowToken: Token | undefined networks: NetworkDict onSlippageChange: (newSlippage: Decimal) => void + hasLeverage: boolean | undefined }) { const [isOpen, , , toggle] = useSwitch(false) const userState = q(useUserState(params, isOpen)) @@ -67,7 +69,7 @@ export function RepayLoanInfoAccordion({ isOpen, )} leverage={{ - enabled: leverageEnabled, + enabled: !!hasLeverage, expectedBorrowed: useRepayExpectedBorrowed(params, isOpen), priceImpact: useRepayPriceImpact(params, isOpen), slippage, diff --git a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx index 832cc9248d..039aaadbe5 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx @@ -1,5 +1,6 @@ import { RepayLoanInfoAccordion } from '@/llamalend/features/borrow/components/RepayLoanInfoAccordion' import { setValueOptions } from '@/llamalend/features/borrow/react-form.utils' +import { hasLeverage } from '@/llamalend/llama.utils' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { RepayOptions } from '@/llamalend/mutations/repay.mutation' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' @@ -70,6 +71,7 @@ export const RepayForm = ({ borrowToken={borrowToken} networks={networks} onSlippageChange={(value) => form.setValue('slippage', value, setValueOptions)} + hasLeverage={market && hasLeverage(market)} /> } > diff --git a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts index fc972c8b5a..7c8f5d3a68 100644 --- a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts @@ -1,4 +1,4 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' @@ -12,7 +12,6 @@ export const { useQuery: useRepayBands } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - leverageEnabled, }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -20,17 +19,15 @@ export const { useQuery: useRepayBands } = queryFactory({ { stateCollateral }, { userCollateral }, { userBorrowed }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, - leverageEnabled, }: RepayQuery): Promise<[number, number]> => { const market = getLlamaMarket(marketId) - if (!leverageEnabled) { + if (!hasLeverage(market)) { console.assert(!+userCollateral, 'userCollateral should be 0 when leverage is disabled') console.assert(!+stateCollateral, 'stateCollateral should be 0 when leverage is disabled') return market.repayBands(userBorrowed) diff --git a/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts index f26f5a8a2a..1c1b5b1ff1 100644 --- a/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts @@ -1,5 +1,5 @@ import { useEstimateGas } from '@/llamalend/hooks/useEstimateGas' -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import type { NetworkDict } from '@/llamalend/llamalend.types' import { type RepayIsApprovedParams, useRepayIsApproved } from '@/llamalend/queries/repay/repay-is-approved.query' import type { IChainId, TGas } from '@curvefi/llamalend-api/lib/interfaces' @@ -18,7 +18,6 @@ const { useQuery: useRepayLoanEstimateGas } = queryFactory({ userAddress, isFull, slippage, - leverageEnabled, }: RepayIsApprovedParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -28,7 +27,6 @@ const { useQuery: useRepayLoanEstimateGas } = queryFactory({ { userBorrowed }, { isFull }, { slippage }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -38,13 +36,12 @@ const { useQuery: useRepayLoanEstimateGas } = queryFactory({ isFull, userAddress, slippage, - leverageEnabled, - }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { + }: RepayIsFullQuery): Promise => { const market = getLlamaMarket(marketId) if (isFull) { return await market.estimateGas.fullRepay(userAddress) } - if (leverageEnabled) { + if (hasLeverage(market)) { if (market instanceof LendMarketTemplate) { return await market.leverage.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) } @@ -69,7 +66,6 @@ const { useQuery: useRepayLoanApproveEstimateGas } = queryFactory({ userBorrowed = '0', userAddress, isFull, - leverageEnabled, }: RepayIsApprovedParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -78,7 +74,6 @@ const { useQuery: useRepayLoanApproveEstimateGas } = queryFactory({ { userCollateral }, { userBorrowed }, { isFull }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -87,13 +82,12 @@ const { useQuery: useRepayLoanApproveEstimateGas } = queryFactory({ userBorrowed, isFull, userAddress, - leverageEnabled, - }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { + }: RepayIsFullQuery): Promise => { const market = getLlamaMarket(marketId) if (isFull) { return await market.estimateGas.fullRepayApprove(userAddress) } - if (leverageEnabled) { + if (hasLeverage(market)) { if (market instanceof LendMarketTemplate) { return await market.leverage.estimateGas.repayApprove(userCollateral, userBorrowed) } diff --git a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts index e82dc53fa2..dabbd2c222 100644 --- a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts @@ -1,4 +1,4 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type Decimal } from '@ui-kit/utils' @@ -21,7 +21,6 @@ export const { useQuery: useRepayExpectedBorrowed } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - leverageEnabled, }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -29,11 +28,12 @@ export const { useQuery: useRepayExpectedBorrowed } = queryFactory({ { stateCollateral }, { userCollateral }, { userBorrowed }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { // todo: investigate if this is OK when the user's position is not leveraged const market = getLlamaMarket(marketId) + console.assert(hasLeverage(market), `Expected leverage to be enabled for market ${marketId}`) + if (market instanceof LendMarketTemplate) { const result = await market.leverage.repayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed) return result as RepayExpectedBorrowedResult diff --git a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts index 0606ede5b3..779b2d046e 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts @@ -1,13 +1,11 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayIsFullParams, type RepayIsFullQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' -export type RepayIsApprovedParams = RepayIsFullParams & { - leverageEnabled?: boolean -} +export type RepayIsApprovedParams = RepayIsFullParams export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } = queryFactory({ queryKey: ({ @@ -18,7 +16,6 @@ export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } userBorrowed = '0', userAddress, isFull, - leverageEnabled, }: RepayIsApprovedParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -27,7 +24,6 @@ export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } { userCollateral }, { userBorrowed }, { isFull }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -36,11 +32,10 @@ export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } userBorrowed, isFull, userAddress, - leverageEnabled, - }: RepayIsFullQuery & { leverageEnabled?: boolean }): Promise => { + }: RepayIsFullQuery): Promise => { const market = getLlamaMarket(marketId) if (isFull) return await market.fullRepayIsApproved(userAddress) - if (leverageEnabled) { + if (hasLeverage(market)) { if (market instanceof LendMarketTemplate) { return await market.leverage.repayIsApproved(userCollateral, userBorrowed) } diff --git a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts index 364a0da48f..0642af6381 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts @@ -1,4 +1,4 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' @@ -12,7 +12,6 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - leverageEnabled, }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -20,7 +19,6 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ { stateCollateral }, { userCollateral }, { userBorrowed }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -28,10 +26,10 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ userCollateral, userBorrowed, userAddress, - leverageEnabled, }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) - if (!leverageEnabled) { + if (!hasLeverage(market)) { + // for simple markets, repay is available if the user has any debt const debt = (await market.userState(userAddress))?.debt return debt != null && +debt > 0 } diff --git a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts index 5104b94967..765f777b33 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts @@ -1,4 +1,4 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' @@ -12,7 +12,6 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query userCollateral = '0', userBorrowed = '0', userAddress, - leverageEnabled, }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -20,7 +19,6 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query { stateCollateral }, { userCollateral }, { userBorrowed }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, @@ -28,14 +26,13 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query userCollateral, userBorrowed, userAddress, - leverageEnabled, }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) - if (!leverageEnabled) { + if (!hasLeverage(market)) { console.assert(!+stateCollateral, 'State collateral should be zero when leverage is disabled') console.assert(!+userCollateral, 'User collateral should be zero when leverage is disabled') const { debt } = await market.userState(userAddress) - return debt === userBorrowed + return userBorrowed >= debt } return market instanceof LendMarketTemplate ? await market.leverage.repayIsFull(stateCollateral, userCollateral, userBorrowed, userAddress) diff --git a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts index 2fd045c353..5b1e2a7292 100644 --- a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts @@ -7,20 +7,12 @@ import { repayValidationSuite } from '../validation/manage-loan.validation' type RepayPriceImpactResult = number export const { useQuery: useRepayPriceImpact } = queryFactory({ - queryKey: ({ - chainId, - marketId, - stateCollateral = '0', - userCollateral = '0', - userAddress, - leverageEnabled, - }: RepayParams) => + queryKey: ({ chainId, marketId, stateCollateral = '0', userCollateral = '0', userAddress }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayPriceImpact', { stateCollateral }, { userCollateral }, - { leverageEnabled }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayQuery): Promise => { const market = getLlamaMarket(marketId) diff --git a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts index 4717514368..cd43ff7c37 100644 --- a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts @@ -1,4 +1,4 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' @@ -13,7 +13,6 @@ export const { useQuery: useRepayPrices } = queryFactory({ userCollateral = '0', userBorrowed = '0', userAddress, - leverageEnabled, }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), @@ -21,11 +20,12 @@ export const { useQuery: useRepayPrices } = queryFactory({ { stateCollateral }, { userCollateral }, { userBorrowed }, - { leverageEnabled }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, leverageEnabled }: RepayQuery) => { + queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { const market = getLlamaMarket(marketId) - if (!leverageEnabled) { + if (!hasLeverage(market)) { + console.assert(!+userCollateral, 'userCollateral should be 0 when leverage is disabled') + console.assert(!+stateCollateral, 'stateCollateral should be 0 when leverage is disabled') return (await market.repayPrices(userBorrowed)) as Decimal[] } return ( diff --git a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 0f83ea2a23..a3b1668e3d 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -1,5 +1,6 @@ import { enforce, test, skipWhen } from 'vest' import { BORROW_PRESET_RANGES } from '@/llamalend/constants' +import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' import { Decimal } from '@ui-kit/utils' export const validateUserBorrowed = (userBorrowed: Decimal | null | undefined) => { @@ -60,6 +61,15 @@ export const validateLeverageEnabled = (leverageEnabled: boolean | undefined | n }) } +export const validateLeverageSupported = (marketId: string | null | undefined, leverageRequired: boolean) => { + skipWhen(!leverageRequired || !marketId, () => { + test('marketId', 'Market does not support leverage', () => { + const market = getLlamaMarket(marketId!) + enforce(hasLeverage(market)).isTruthy() + }) + }) +} + export const validateMaxCollateral = ( userCollateral: Decimal | undefined | null, maxCollateral: Decimal | undefined | null, diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts index f0ec0c372d..7d86767d7c 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts @@ -13,7 +13,6 @@ export type RepayQuery = CollateralQuery & { stateCollateral: Decimal userBorrowed: Decimal slippage: Decimal - leverageEnabled: boolean } export type RepayHealthQuery = RepayQuery & HealthQuery diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts index 9f90e628de..436018a73b 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js' import { enforce, group, test } from 'vest' import { validateBoolean, - validateLeverageEnabled, + validateLeverageSupported, validateSlippage, validateUserCollateral, } from '@/llamalend/queries/validation/borrow-fields.validation' @@ -75,15 +75,7 @@ export const collateralHealthValidationSuite = createValidationSuite(({ isFull, }) export const repayValidationGroup = ( - { - chainId, - stateCollateral, - userCollateral, - userBorrowed, - userAddress, - leverageEnabled, - slippage, - }: RepayParams, + { chainId, marketId, stateCollateral, userCollateral, userBorrowed, userAddress, slippage }: RepayParams, { leverageRequired = false }: { leverageRequired?: boolean } = {}, ) => { chainValidationGroup({ chainId }) @@ -94,7 +86,7 @@ export const repayValidationGroup = ( validateRepayBorrowedField(userBorrowed) validateRepayHasValue(stateCollateral, userCollateral, userBorrowed) validateSlippage(slippage) - validateLeverageEnabled(leverageEnabled, leverageRequired) + validateLeverageSupported(marketId, leverageRequired) } export const repayValidationSuite = ({ leverageRequired }: { leverageRequired: boolean }) => From 7e6afd152647e0616b5229fc33294557ba816540 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 17 Dec 2025 17:01:52 +0100 Subject: [PATCH 4/4] refactor: simplify repay queries switches are so 90s, but I didn't manage to use objects while keeping type safety --- .../manage-loan/components/RepayForm.tsx | 18 ++++--- apps/main/src/llamalend/llama.utils.ts | 16 ++++-- .../queries/repay/repay-bands.query.ts | 22 ++++---- .../queries/repay/repay-estimate-gas.query.ts | 45 ++++++++-------- .../repay/repay-expected-borrowed.query.ts | 37 +++++++------- .../queries/repay/repay-gas-estimate.query.ts | 18 ++++--- .../queries/repay/repay-health.query.ts | 21 ++++---- .../queries/repay/repay-is-approved.query.ts | 27 +++++----- .../queries/repay/repay-is-available.query.ts | 24 ++++----- .../queries/repay/repay-is-full.query.ts | 25 +++++---- .../queries/repay/repay-price-impact.query.ts | 36 +++++++++---- .../queries/repay/repay-prices.query.ts | 24 ++++----- .../queries/repay/repay-query.helpers.ts | 51 +++++++++++++++++++ .../queries/repay/repay-route-image.query.ts | 20 ++++---- .../src/llamalend/queries/user-state.query.ts | 6 ++- .../validation/manage-loan.validation.ts | 20 +++++++- 16 files changed, 251 insertions(+), 159 deletions(-) create mode 100644 apps/main/src/llamalend/queries/repay/repay-query.helpers.ts diff --git a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx index 039aaadbe5..f937aeb1df 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RepayForm.tsx @@ -1,6 +1,6 @@ import { RepayLoanInfoAccordion } from '@/llamalend/features/borrow/components/RepayLoanInfoAccordion' import { setValueOptions } from '@/llamalend/features/borrow/react-form.utils' -import { hasLeverage } from '@/llamalend/llama.utils' +import { hasDeleverage, hasLeverage } from '@/llamalend/llama.utils' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { RepayOptions } from '@/llamalend/mutations/repay.mutation' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' @@ -24,7 +24,7 @@ export const RepayForm = ({ onRepaid, fromCollateral, fromWallet, - fromBorrowed, + fromBorrowed: showUserBorrowed, }: { market: LlamaMarketTemplate | undefined networks: NetworkDict @@ -59,6 +59,8 @@ export const RepayForm = ({ onRepaid, }) const { withdrawEnabled: withdrawEnabled } = values + const showStateCollateral = market && hasLeverage(market) && fromCollateral + const showUserCollateral = market && (hasLeverage(market) || hasDeleverage(market)) && fromWallet return ( ({ } > : undefined}> - {fromCollateral && ( + {showStateCollateral && ( ({ network={network} /> )} - {fromWallet && ( + {showUserCollateral && ( ({ network={network} /> )} - {fromBorrowed && ( + {showUserBorrowed && ( ({ formErrors={formErrors} network={network} handledErrors={notFalsy( - fromCollateral && 'stateCollateral', - fromWallet && 'userCollateral', - fromBorrowed && 'userBorrowed', + showStateCollateral && 'stateCollateral', + showUserCollateral && 'userCollateral', + showUserBorrowed && 'userBorrowed', )} successTitle={t`Loan repaid`} /> diff --git a/apps/main/src/llamalend/llama.utils.ts b/apps/main/src/llamalend/llama.utils.ts index f1a98c8d3a..3f109b77e7 100644 --- a/apps/main/src/llamalend/llama.utils.ts +++ b/apps/main/src/llamalend/llama.utils.ts @@ -24,9 +24,19 @@ export const getLlamaMarket = (id: string, lib = requireLib('llamaApi')): LlamaM * - Mint Market and either its `leverageZap` is not the zero address or its `leverageV2` property has leverage */ export const hasLeverage = (market: LlamaMarketTemplate) => - market instanceof LendMarketTemplate - ? market.leverage.hasLeverage() - : market.leverageZap !== zeroAddress || market.leverageV2.hasLeverage() + hasV1Leverage(market) || (market instanceof MintMarketTemplate && hasV2Leverage(market)) + +export const hasV1Leverage = (market: LlamaMarketTemplate) => + market instanceof LendMarketTemplate ? market.leverage.hasLeverage() : market?.leverageZap !== zeroAddress + +export const hasV2Leverage = (market: MintMarketTemplate) => !!market?.leverageV2.hasLeverage() + +export const hasV1Deleverage = (market: LlamaMarketTemplate) => + market instanceof LendMarketTemplate ? market.leverage.hasLeverage() : market?.deleverageZap !== zeroAddress + +// hasV2Leverage works for deleverage as well +export const hasDeleverage = (market: LlamaMarketTemplate) => + hasV1Deleverage(market) || (market instanceof MintMarketTemplate && hasV2Leverage(market)) const getBorrowSymbol = (market: LlamaMarketTemplate) => market instanceof MintMarketTemplate ? CRVUSD.symbol : market.borrowed_token.symbol diff --git a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts index 7c8f5d3a68..1cb4a51e2a 100644 --- a/apps/main/src/llamalend/queries/repay/repay-bands.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-bands.query.ts @@ -1,8 +1,7 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' export const { useQuery: useRepayBands } = queryFactory({ queryKey: ({ @@ -26,17 +25,16 @@ export const { useQuery: useRepayBands } = queryFactory({ userCollateral, userBorrowed, }: RepayQuery): Promise<[number, number]> => { - const market = getLlamaMarket(marketId) - if (!hasLeverage(market)) { - console.assert(!+userCollateral, 'userCollateral should be 0 when leverage is disabled') - console.assert(!+stateCollateral, 'stateCollateral should be 0 when leverage is disabled') - return market.repayBands(userBorrowed) + const [type, impl, args] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return impl.repayBands(...args) + case 'deleverage': + return impl.repayBands(...args) + case 'unleveraged': + return impl.repayBands(...args) } - return market instanceof LendMarketTemplate - ? await market.leverage.repayBands(stateCollateral, userCollateral, userBorrowed) - : market.leverageV2.hasLeverage() - ? await market.leverageV2.repayBands(stateCollateral, userCollateral, userBorrowed) - : await market.deleverage.repayBands(userCollateral) }, validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts index 1c1b5b1ff1..c73dca10e4 100644 --- a/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-estimate-gas.query.ts @@ -1,12 +1,12 @@ import { useEstimateGas } from '@/llamalend/hooks/useEstimateGas' -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' +import { getLlamaMarket } from '@/llamalend/llama.utils' import type { NetworkDict } from '@/llamalend/llamalend.types' import { type RepayIsApprovedParams, useRepayIsApproved } from '@/llamalend/queries/repay/repay-is-approved.query' import type { IChainId, TGas } from '@curvefi/llamalend-api/lib/interfaces' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayIsFullQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' const { useQuery: useRepayLoanEstimateGas } = queryFactory({ queryKey: ({ @@ -41,17 +41,16 @@ const { useQuery: useRepayLoanEstimateGas } = queryFactory({ if (isFull) { return await market.estimateGas.fullRepay(userAddress) } - if (hasLeverage(market)) { - if (market instanceof LendMarketTemplate) { - return await market.leverage.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) - } - if (market.leverageV2.hasLeverage()) { - return await market.leverageV2.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) - } + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.estimateGas.repay(stateCollateral, userCollateral, userBorrowed, +slippage) + case 'deleverage': + throw new Error('estimateGas.repay is not supported for deleverage repay') + case 'unleveraged': + return await impl.estimateGas.repay(userBorrowed) } - console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) - console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) - return await market.estimateGas.repay(userCollateral) }, staleTime: '1m', validationSuite: repayFromCollateralIsFullValidationSuite, @@ -83,21 +82,19 @@ const { useQuery: useRepayLoanApproveEstimateGas } = queryFactory({ isFull, userAddress, }: RepayIsFullQuery): Promise => { - const market = getLlamaMarket(marketId) if (isFull) { - return await market.estimateGas.fullRepayApprove(userAddress) + return await getLlamaMarket(marketId).estimateGas.fullRepayApprove(userAddress) } - if (hasLeverage(market)) { - if (market instanceof LendMarketTemplate) { - return await market.leverage.estimateGas.repayApprove(userCollateral, userBorrowed) - } - if (market.leverageV2.hasLeverage()) { - return await market.leverageV2.estimateGas.repayApprove(userCollateral, userBorrowed) - } + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.estimateGas.repayApprove(userCollateral, userBorrowed) + case 'deleverage': + throw new Error('estimateGas.repayApprove is not supported for deleverage repay') + case 'unleveraged': + return await impl.estimateGas.repayApprove(userBorrowed) } - console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) - console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) - return await market.estimateGas.repayApprove(userCollateral) }, staleTime: '1m', validationSuite: repayFromCollateralIsFullValidationSuite, diff --git a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts index dabbd2c222..1d67ed490d 100644 --- a/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-expected-borrowed.query.ts @@ -1,9 +1,8 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' -import { type Decimal } from '@ui-kit/utils' +import { decimal, type Decimal } from '@ui-kit/utils' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation, getUserDebt } from './repay-query.helpers' export type RepayExpectedBorrowedResult = { totalBorrowed: Decimal @@ -29,24 +28,22 @@ export const { useQuery: useRepayExpectedBorrowed } = queryFactory({ { userCollateral }, { userBorrowed }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { - // todo: investigate if this is OK when the user's position is not leveraged - const market = getLlamaMarket(marketId) - console.assert(hasLeverage(market), `Expected leverage to be enabled for market ${marketId}`) - - if (market instanceof LendMarketTemplate) { - const result = await market.leverage.repayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed) - return result as RepayExpectedBorrowedResult - } - if (market.leverageV2.hasLeverage()) { - const result = await market.leverageV2.repayExpectedBorrowed(stateCollateral, userCollateral, userBorrowed) - return result as RepayExpectedBorrowedResult + queryFn: async ({ chainId, marketId, userAddress, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { + const [type, impl, args] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return (await impl.repayExpectedBorrowed(...args)) as RepayExpectedBorrowedResult + case 'deleverage': { + const { stablecoins, routeIdx } = await impl.repayStablecoins(...args) + return { totalBorrowed: stablecoins[routeIdx] as Decimal } + } + case 'unleveraged': + return { + // todo: double if this is correct or if we should use the `debt` field from userState + totalBorrowed: decimal(getUserDebt({ chainId, marketId, userAddress }) - +userBorrowed)!, + } } - - console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) - console.assert(!+userBorrowed, `Expected 0 userBorrowed for non-leverage market, got ${userBorrowed}`) - const { stablecoins, routeIdx } = await market.deleverage.repayStablecoins(userCollateral) - return { totalBorrowed: stablecoins[routeIdx] as Decimal } }, staleTime: '1m', validationSuite: repayValidationSuite({ leverageRequired: true }), diff --git a/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts b/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts index 5ba313c186..4f7b8c9a81 100644 --- a/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-gas-estimate.query.ts @@ -8,6 +8,7 @@ import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { RepayHealthQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' import { repayIsFullQueryKey } from './repay-is-full.query' +import { getRepayImplementation } from './repay-query.helpers' type RepayFromCollateralGasQuery = RepayHealthQuery type RepayFromCollateralGasParams = FieldsOf> @@ -38,19 +39,22 @@ const { useQuery: useRepayGasEstimate } = queryFactory({ isFull, userAddress, }: RepayFromCollateralGasQuery) => { - const market = getLlamaMarket(marketId) if (isFull) { + const market = getLlamaMarket(marketId) return market instanceof LendMarketTemplate ? await market.estimateGas.fullRepay(userAddress) : await market.fullRepayEstimateGas(userAddress) } - if (market instanceof LendMarketTemplate) { - return await market.leverage.estimateGas.repay(stateCollateral, userCollateral, userBorrowed) + const [type, impl, args] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.estimateGas.repay(...args) + case 'deleverage': + return await impl.estimateGas.repay(...args) + case 'unleveraged': + return await impl.estimateGas.repay(...args) } - if (market.leverageV2.hasLeverage()) { - return await market.leverageV2.estimateGas.repay(stateCollateral, userCollateral, userBorrowed) - } - return await market.deleverage.estimateGas.repay(userCollateral) }, validationSuite: repayFromCollateralIsFullValidationSuite, dependencies: ({ diff --git a/apps/main/src/llamalend/queries/repay/repay-health.query.ts b/apps/main/src/llamalend/queries/repay/repay-health.query.ts index fe4f05ee53..213b664414 100644 --- a/apps/main/src/llamalend/queries/repay/repay-health.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-health.query.ts @@ -1,9 +1,8 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' import { type RepayHealthQuery, type RepayHealthParams } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' export const { getQueryOptions: getRepayHealthOptions, useQuery: useRepayHealth } = queryFactory({ queryKey: ({ @@ -24,14 +23,16 @@ export const { getQueryOptions: getRepayHealthOptions, useQuery: useRepayHealth { isFull }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed, isFull }: RepayHealthQuery) => { - const market = getLlamaMarket(marketId) - return ( - market instanceof LendMarketTemplate - ? await market.leverage.repayHealth(stateCollateral, userCollateral, userBorrowed, isFull) - : market.leverageV2.hasLeverage() - ? await market.leverageV2.repayHealth(stateCollateral, userCollateral, userBorrowed, isFull) - : await market.deleverage.repayHealth(userCollateral, isFull) - ) as Decimal + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return (await impl.repayHealth(stateCollateral, userCollateral, userBorrowed, isFull)) as Decimal + case 'deleverage': + return (await impl.repayHealth(userCollateral, isFull)) as Decimal + case 'unleveraged': + return '0' as Decimal + } }, validationSuite: repayFromCollateralIsFullValidationSuite, }) diff --git a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts index 779b2d046e..2c864a8991 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-approved.query.ts @@ -1,9 +1,9 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' +import { getLlamaMarket } from '@/llamalend/llama.utils' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayIsFullParams, type RepayIsFullQuery } from '../validation/manage-loan.types' import { repayFromCollateralIsFullValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' export type RepayIsApprovedParams = RepayIsFullParams @@ -33,19 +33,18 @@ export const { useQuery: useRepayIsApproved, fetchQuery: fetchRepayIsApproved } isFull, userAddress, }: RepayIsFullQuery): Promise => { - const market = getLlamaMarket(marketId) - if (isFull) return await market.fullRepayIsApproved(userAddress) - if (hasLeverage(market)) { - if (market instanceof LendMarketTemplate) { - return await market.leverage.repayIsApproved(userCollateral, userBorrowed) - } - if (market.leverageV2.hasLeverage()) { - return await market.leverageV2.repayIsApproved(userCollateral, userBorrowed) - } + if (isFull) return await getLlamaMarket(marketId).fullRepayIsApproved(userAddress) + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.repayIsApproved(userCollateral, userBorrowed) + case 'deleverage': + console.warn('repayIsApproved is not supported for deleverage repay') + return true // todo: figure out approval for deleverage repay + case 'unleveraged': + return await impl.repayIsApproved(userBorrowed) } - console.assert(!+stateCollateral, `Expected 0 stateCollateral for non-leverage market, got ${stateCollateral}`) - console.assert(!+userCollateral, `Expected 0 userCollateral for non-leverage market, got ${userCollateral}`) - return await market.repayIsApproved(userCollateral) }, staleTime: '1m', validationSuite: repayFromCollateralIsFullValidationSuite, diff --git a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts index 0642af6381..31d576c009 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-available.query.ts @@ -1,8 +1,7 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation, getUserDebt } from './repay-query.helpers' export const { useQuery: useRepayIsAvailable } = queryFactory({ queryKey: ({ @@ -21,23 +20,24 @@ export const { useQuery: useRepayIsAvailable } = queryFactory({ { userBorrowed }, ] as const, queryFn: async ({ + chainId, marketId, stateCollateral, userCollateral, userBorrowed, userAddress, }: RepayQuery): Promise => { - const market = getLlamaMarket(marketId) - if (!hasLeverage(market)) { - // for simple markets, repay is available if the user has any debt - const debt = (await market.userState(userAddress))?.debt - return debt != null && +debt > 0 + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.repayIsAvailable(stateCollateral, userCollateral, userBorrowed, userAddress) + case 'deleverage': + return await impl.isAvailable(userCollateral, userAddress) + case 'unleveraged': { + return !!getUserDebt({ chainId, marketId, userAddress }) + } } - return market instanceof LendMarketTemplate - ? await market.leverage.repayIsAvailable(stateCollateral, userCollateral, userBorrowed, userAddress) - : market.leverageV2.hasLeverage() - ? await market.leverageV2.repayIsAvailable(stateCollateral, userCollateral, userBorrowed, userAddress) - : await market.deleverage.isAvailable(userCollateral, userAddress) }, staleTime: '1m', validationSuite: repayValidationSuite({ leverageRequired: false }), diff --git a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts index 765f777b33..5e52b7724d 100644 --- a/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-is-full.query.ts @@ -1,8 +1,7 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation, getUserDebt } from './repay-query.helpers' export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = queryFactory({ queryKey: ({ @@ -21,24 +20,24 @@ export const { useQuery: useRepayIsFull, queryKey: repayIsFullQueryKey } = query { userBorrowed }, ] as const, queryFn: async ({ + chainId, marketId, stateCollateral, userCollateral, userBorrowed, userAddress, }: RepayQuery): Promise => { - const market = getLlamaMarket(marketId) - if (!hasLeverage(market)) { - console.assert(!+stateCollateral, 'State collateral should be zero when leverage is disabled') - console.assert(!+userCollateral, 'User collateral should be zero when leverage is disabled') - const { debt } = await market.userState(userAddress) - return userBorrowed >= debt + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.repayIsFull(stateCollateral, userCollateral, userBorrowed, userAddress) + case 'deleverage': + return await impl.isFullRepayment(userCollateral, userAddress) + case 'unleveraged': { + return +userBorrowed >= getUserDebt({ chainId, marketId, userAddress }) + } } - return market instanceof LendMarketTemplate - ? await market.leverage.repayIsFull(stateCollateral, userCollateral, userBorrowed, userAddress) - : market.leverageV2.hasLeverage() - ? await market.leverageV2.repayIsFull(stateCollateral, userCollateral, userBorrowed, userAddress) - : await market.deleverage.isFullRepayment(userCollateral, userAddress) }, staleTime: '1m', validationSuite: repayValidationSuite({ leverageRequired: false }), diff --git a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts index 5b1e2a7292..8bd12747c2 100644 --- a/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-price-impact.query.ts @@ -1,26 +1,42 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' type RepayPriceImpactResult = number export const { useQuery: useRepayPriceImpact } = queryFactory({ - queryKey: ({ chainId, marketId, stateCollateral = '0', userCollateral = '0', userAddress }: RepayParams) => + queryKey: ({ + chainId, + marketId, + stateCollateral = '0', + userCollateral = '0', + userBorrowed = '0', + userAddress, + }: RepayParams) => [ ...rootKeys.userMarket({ chainId, marketId, userAddress }), 'repayPriceImpact', { stateCollateral }, { userCollateral }, + { userBorrowed }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayQuery): Promise => { - const market = getLlamaMarket(marketId) - return market instanceof LendMarketTemplate - ? +(await market.leverage.repayPriceImpact(stateCollateral, userCollateral)) - : market.leverageV2.hasLeverage() - ? +(await market.leverageV2.repayPriceImpact(stateCollateral, userCollateral)) - : +(await market.deleverage.priceImpact(userCollateral)) + queryFn: async ({ + marketId, + stateCollateral, + userCollateral, + userBorrowed, + }: RepayQuery): Promise => { + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return +(await impl.repayPriceImpact(stateCollateral, userCollateral)) + case 'deleverage': + return +(await impl.priceImpact(userCollateral)) + case 'unleveraged': + return 0 + } }, staleTime: '1m', validationSuite: repayValidationSuite({ leverageRequired: true }), diff --git a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts index cd43ff7c37..7d1a2e436c 100644 --- a/apps/main/src/llamalend/queries/repay/repay-prices.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-prices.query.ts @@ -1,9 +1,8 @@ -import { getLlamaMarket, hasLeverage } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' export const { useQuery: useRepayPrices } = queryFactory({ queryKey: ({ @@ -22,19 +21,16 @@ export const { useQuery: useRepayPrices } = queryFactory({ { userBorrowed }, ] as const, queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { - const market = getLlamaMarket(marketId) - if (!hasLeverage(market)) { - console.assert(!+userCollateral, 'userCollateral should be 0 when leverage is disabled') - console.assert(!+stateCollateral, 'stateCollateral should be 0 when leverage is disabled') - return (await market.repayPrices(userBorrowed)) as Decimal[] + const [type, impl, args] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return (await impl.repayPrices(...args)) as Decimal[] + case 'deleverage': + return (await impl.repayPrices(...args)) as Decimal[] + case 'unleveraged': + return (await impl.repayPrices(...args)) as Decimal[] } - return ( - market instanceof LendMarketTemplate - ? await market.leverage.repayPrices(stateCollateral, userCollateral, userBorrowed) - : market.leverageV2.hasLeverage() - ? await market.leverageV2.repayPrices(stateCollateral, userCollateral, userBorrowed) - : await market.deleverage.repayPrices(userCollateral) - ) as Decimal[] }, validationSuite: repayValidationSuite({ leverageRequired: false }), }) diff --git a/apps/main/src/llamalend/queries/repay/repay-query.helpers.ts b/apps/main/src/llamalend/queries/repay/repay-query.helpers.ts new file mode 100644 index 0000000000..5273d607cc --- /dev/null +++ b/apps/main/src/llamalend/queries/repay/repay-query.helpers.ts @@ -0,0 +1,51 @@ +import { getLlamaMarket, hasDeleverage, hasLeverage, hasV2Leverage } from '@/llamalend/llama.utils' +import { getUserState } from '@/llamalend/queries/user-state.query' +import type { RepayQuery } from '@/llamalend/queries/validation/manage-loan.types' +import { MintMarketTemplate } from '@curvefi/llamalend-api/lib/mintMarkets' +import { type UserMarketQuery } from '@ui-kit/lib/model' + +/** + * Determines the appropriate repay implementation and its parameters based on the market type and leverage options. + * We will use V2 leverage if available, then leverage V1. Otherwise: + * - if deleverage is supported and userCollateral > 0, we use deleverage + * - otherwise, we use the unleveraged implementation. + */ +export function getRepayImplementation( + marketId: string, + { + stateCollateral, + userCollateral, + userBorrowed, + }: Pick, +) { + const market = getLlamaMarket(marketId) + + if (market instanceof MintMarketTemplate) { + if (hasV2Leverage(market)) { + return ['V2', market.leverageV2, [stateCollateral, userCollateral, userBorrowed]] as const + } + if (+userCollateral && hasDeleverage(market)) { + // use deleverage only if userCollateral > 0 & supported, otherwise fall back to unleveraged + if (+userBorrowed) throw new Error(`Invalid userBorrowed for deleverage: ${userBorrowed}`) + if (+stateCollateral) throw new Error(`Invalid stateCollateral for deleverage: ${stateCollateral}`) + return ['deleverage', market.deleverage, [userCollateral]] as const + } + } else if (hasLeverage(market)) { + // v1 leverage for mint markets is ignored, it doesn't support repay. Supported via deleverage above + return ['V1', market.leverage, [stateCollateral, userCollateral, userBorrowed]] as const + } + + if (+userCollateral) throw new Error(`Invalid userCollateral for deleverage: ${userCollateral}`) + if (+stateCollateral) throw new Error(`Invalid stateCollateral for deleverage: ${stateCollateral}`) + return ['unleveraged', market, [userBorrowed]] as const +} + +// todo: check if we should use `stablecoin` instead of `debt` +export const getUserDebt = ({ chainId, userAddress, marketId }: UserMarketQuery) => + +( + getUserState({ + chainId, + marketId, + userAddress, + })?.debt ?? 0 + ) diff --git a/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts b/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts index a765f8b209..aa18da4d1f 100644 --- a/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts +++ b/apps/main/src/llamalend/queries/repay/repay-route-image.query.ts @@ -1,8 +1,7 @@ -import { getLlamaMarket } from '@/llamalend/llama.utils' -import { LendMarketTemplate } from '@curvefi/llamalend-api/lib/lendMarkets' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import { type RepayParams, type RepayQuery } from '../validation/manage-loan.types' import { repayValidationSuite } from '../validation/manage-loan.validation' +import { getRepayImplementation } from './repay-query.helpers' export const { useQuery: useRepayRouteImage } = queryFactory({ queryKey: ({ @@ -20,15 +19,16 @@ export const { useQuery: useRepayRouteImage } = queryFactory({ { userCollateral }, { userBorrowed }, ] as const, - queryFn: async ({ marketId, stateCollateral, userCollateral }: RepayQuery) => { - const market = getLlamaMarket(marketId) - if (market instanceof LendMarketTemplate) { - return await market.leverage.repayRouteImage(stateCollateral, userCollateral) + queryFn: async ({ marketId, stateCollateral, userCollateral, userBorrowed }: RepayQuery) => { + const [type, impl] = getRepayImplementation(marketId, { userCollateral, stateCollateral, userBorrowed }) + switch (type) { + case 'V1': + case 'V2': + return await impl.repayRouteImage(stateCollateral, userCollateral) + case 'deleverage': + case 'unleveraged': + throw new Error('repayRouteImage is not supported for deleverage or unleveraged repay') } - if (market.leverageV2.hasLeverage()) { - return await market.leverageV2.repayRouteImage(stateCollateral, userCollateral) - } - return null }, staleTime: '1m', validationSuite: repayValidationSuite({ leverageRequired: true }), diff --git a/apps/main/src/llamalend/queries/user-state.query.ts b/apps/main/src/llamalend/queries/user-state.query.ts index e747922f09..45c47ab0a7 100644 --- a/apps/main/src/llamalend/queries/user-state.query.ts +++ b/apps/main/src/llamalend/queries/user-state.query.ts @@ -3,7 +3,11 @@ import { queryFactory, rootKeys, type UserMarketParams, type UserMarketQuery } f import { userMarketValidationSuite } from '@ui-kit/lib/model/query/user-market-validation' import type { Decimal } from '@ui-kit/utils' -export const { useQuery: useUserState, invalidate: invalidateUserState } = queryFactory({ +export const { + useQuery: useUserState, + invalidate: invalidateUserState, + getQueryData: getUserState, +} = queryFactory({ queryKey: (params: UserMarketParams) => [...rootKeys.userMarket(params), 'market-user-state'] as const, queryFn: async ({ marketId, userAddress }: UserMarketQuery) => { const market = getLlamaMarket(marketId) diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts index 436018a73b..acbc9f7792 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -1,5 +1,6 @@ import BigNumber from 'bignumber.js' -import { enforce, group, test } from 'vest' +import { enforce, group, skipWhen, test } from 'vest' +import { getRepayImplementation } from '@/llamalend/queries/repay/repay-query.helpers' import { validateBoolean, validateLeverageSupported, @@ -54,6 +55,22 @@ const validateRepayHasValue = ( enforce(total.gt(0)).isTruthy() }) +const validateRepayFieldsForMarket = ( + marketId: string | null | undefined, + stateCollateral: Decimal | null | undefined, + userCollateral: Decimal | null | undefined, + userBorrowed: Decimal | null | undefined, +) => { + skipWhen(!marketId, () => { + // Get the implementation to validate fields according to market capabilities. Default to 0 just like the queries + getRepayImplementation(marketId!, { + stateCollateral: stateCollateral ?? '0', + userCollateral: userCollateral ?? '0', + userBorrowed: userBorrowed ?? '0', + }) + }) +} + export const collateralValidationGroup = ({ chainId, userCollateral, marketId, userAddress }: CollateralParams) => group('chainValidation', () => { marketIdValidationSuite({ chainId, marketId }) @@ -85,6 +102,7 @@ export const repayValidationGroup = ( validateRepayField('stateCollateral', stateCollateral) validateRepayBorrowedField(userBorrowed) validateRepayHasValue(stateCollateral, userCollateral, userBorrowed) + validateRepayFieldsForMarket(marketId, stateCollateral, userCollateral, userBorrowed) validateSlippage(slippage) validateLeverageSupported(marketId, leverageRequired) }