From 5120611ccdd2b6976bc690654810b022f0106e83 Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 27 Nov 2025 14:27:56 +0100 Subject: [PATCH 01/52] feat: layer 1 fill to collateral sub tabs --- apps/main/src/loan/components/PageLoanManage/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/src/loan/components/PageLoanManage/index.tsx b/apps/main/src/loan/components/PageLoanManage/index.tsx index 991b5dd60..f333f900f 100644 --- a/apps/main/src/loan/components/PageLoanManage/index.tsx +++ b/apps/main/src/loan/components/PageLoanManage/index.tsx @@ -87,6 +87,7 @@ const LoanManage = ({ curve, isReady, llamma, llammaId, params, rChainId, rColla onChange={setSubTab} options={subTabs} fullWidth + sx={{ backgroundColor: (t) => t.design.Layer[1].Fill }} /> {subTab === 'loan-increase' && ( From c2f1d4f98498948a50043ad0777df02e99ac231f Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 28 Nov 2025 12:08:07 +0100 Subject: [PATCH 02/52] feat: add collateral usd rate for input and token balance --- .../components/AddCollateralForm.tsx | 2 +- .../manage-loan/LoanFormTokenInput.tsx | 26 ++++++++++++++----- .../widgets/manage-loan/LoanFormWrapper.tsx | 2 +- .../src/themes/stories/Tabs.stories.tsx | 2 +- 4 files changed, 23 insertions(+), 9 deletions(-) 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 e220cdcd3..648bc8eaf 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -83,7 +83,7 @@ export const AddCollateralForm = ({ > }> ({ + balance, + symbol: token?.symbol, + loading: isBalanceLoading, + usdRate, + }), + [balance, isBalanceLoading, token?.symbol, usdRate], + ) + const errors = form.formState.errors as PartialRecord, Error> const relatedMaxFieldError = max?.fieldName && errors[max.fieldName] const error = errors[name] || max?.error || balanceError || relatedMaxFieldError @@ -74,12 +91,9 @@ export const LoanFormTokenInput = < )} isError={!!error} message={error?.message ?? message} - walletBalance={useMemo( - // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput - () => ({ balance, symbol: token?.symbol, loading: isBalanceLoading }), - [balance, isBalanceLoading, token?.symbol], - )} + walletBalance={walletBalance} maxBalance={useMemo(() => max && { balance: max.data, chips: 'max' }, [max])} + inputBalanceUsd={decimal(form.getValues(name) && usdRate && usdRate * +form.getValues(name))} /> ) } diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx index ea9637f8b..1c74e0f1e 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormWrapper.tsx @@ -23,7 +23,7 @@ export const LoanFormWrapper = ) => (
- + t.design.Layer[1].Fill }}> {children} diff --git a/packages/curve-ui-kit/src/themes/stories/Tabs.stories.tsx b/packages/curve-ui-kit/src/themes/stories/Tabs.stories.tsx index a68caa461..28f2dc5de 100644 --- a/packages/curve-ui-kit/src/themes/stories/Tabs.stories.tsx +++ b/packages/curve-ui-kit/src/themes/stories/Tabs.stories.tsx @@ -1,7 +1,7 @@ import { ComponentProps, useState } from 'react' +import { Stack } from '@mui/material' import type { Meta, StoryObj } from '@storybook/react-vite' import { TabsSwitcher, type TabOption } from '../../shared/ui/TabsSwitcher' -import { Stack } from '@mui/material' type Tab = `${number}` const tabs: TabOption[] = [ From 49c357136c91f86640dc60ecb6361754e068165c Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 28 Nov 2025 19:02:27 +0100 Subject: [PATCH 03/52] feat: input balance usd --- .../src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index 3299b02fd..c1d7677bc 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -71,6 +71,7 @@ export const LoanFormTokenInput = < const errors = form.formState.errors as PartialRecord, Error> const relatedMaxFieldError = max?.fieldName && errors[max.fieldName] const error = errors[name] || max?.error || balanceError || relatedMaxFieldError + const value = form.getValues(name) return ( } - balance={form.getValues(name)} + balance={value} onBalance={useCallback( (v?: Decimal) => form.setValue(name, v as FieldPathValue, setValueOptions), [form, name], @@ -93,7 +94,7 @@ export const LoanFormTokenInput = < message={error?.message ?? message} walletBalance={walletBalance} maxBalance={useMemo(() => max && { balance: max.data, chips: 'max' }, [max])} - inputBalanceUsd={decimal(form.getValues(name) && usdRate && usdRate * +form.getValues(name))} + inputBalanceUsd={decimal(value && usdRate && usdRate * +value)} /> ) } From 1fb765606ef77d61fdaf176825d3797635687fdc Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 28 Nov 2025 19:03:42 +0100 Subject: [PATCH 04/52] refactor: loan info accordion with prev and current helper --- .../widgets/manage-loan/LoanInfoAccordion.tsx | 108 ++++++++++++------ 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 8c350253b..d6435b676 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -30,18 +30,48 @@ type LoanInfoAccordionProps = { range?: number health: Query prevHealth?: Query - bands: Query<[number, number]> - prices: Query + bands?: Query<[number, number]> + prices?: Query rates: Query<{ borrowApr?: Decimal } | null> prevRates?: Query<{ borrowApr?: Decimal } | null> loanToValue: Query prevLoanToValue?: Query gas: Query - debt?: Query & { tokenSymbol: string } - prevDebt?: Query + debt?: Query & { tokenSymbol?: string } + prevDebt?: Query & { tokenSymbol?: string } + collateral?: Query & { tokenSymbol?: string } + prevCollateral?: Query & { tokenSymbol?: string } leverage?: LoanLeverageActionInfoProps & { enabled: boolean } } +/** + * Builds props for ActionInfo component that handles prev/current value transitions. + * - Displays "previous -> current" when both values are available + * - Displays "current" when only current value is available + * - Displays "previous" when only previous value is available + * - Displays empty value when neither are available + */ +const buildPrevCurrentValues = ( + current: Query | undefined, + previous: Query | undefined, + format: (value: NonNullable) => string, + emptyValue = '-', +) => { + const hasCurrent = current?.data != null + const hasPrevious = previous?.data != null + + return { + ...(hasCurrent && hasPrevious && { prevValue: format(previous!.data as NonNullable) }), + value: hasCurrent + ? format(current!.data as NonNullable) + : hasPrevious + ? format(previous!.data as NonNullable) + : emptyValue, + error: current?.error ?? previous?.error, + loading: current?.isLoading || previous?.isLoading, + } +} + export const LoanInfoAccordion = ({ isOpen, toggle, @@ -55,23 +85,22 @@ export const LoanInfoAccordion = ({ loanToValue, prevLoanToValue, gas, - debt, prevDebt, + debt, + prevCollateral, + collateral, leverage, }: LoanInfoAccordionProps) => ( // error tooltip isn't displayed correctly because accordion takes the mouse focus. Use title for now. - + formatNumber(v, { abbreviate: false }), '∞')} + valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} testId="borrow-health" /> } @@ -80,31 +109,41 @@ export const LoanInfoAccordion = ({ > {leverage?.enabled && } - {debt && ( + {(debt || prevDebt) && ( formatNumber(v, { abbreviate: false }))} + valueRight={debt?.tokenSymbol ?? prevDebt?.tokenSymbol} testId="borrow-debt" /> )} - - formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} - error={prices.error} - loading={prices.isLoading} - testId="borrow-price-range" - /> + {(collateral || prevCollateral) && ( + formatNumber(v, { abbreviate: false }))} + valueRight={collateral?.tokenSymbol ?? prevCollateral?.tokenSymbol} + testId="borrow-collateral" + /> + )} + + {bands && ( + + )} + {prices && ( + formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} + error={prices.error} + loading={prices.isLoading} + testId="borrow-price-range" + /> + )} {range != null && ( )} @@ -116,14 +155,11 @@ export const LoanInfoAccordion = ({ loading={rates.isLoading || prevRates?.isLoading} testId="borrow-apr" /> - {loanToValue && ( + {(loanToValue || prevLoanToValue) && ( )} Date: Fri, 28 Nov 2025 19:30:26 +0100 Subject: [PATCH 05/52] feat: previous collateral, debt, ltv and health to form --- .../components/AddCollateralForm.tsx | 84 +++++++++++++++---- .../manage-loan/hooks/useAddCollateralForm.ts | 3 + apps/main/src/llamalend/queries/utils.ts | 16 ++++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 apps/main/src/llamalend/queries/utils.ts 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 648bc8eaf..9e2b690cd 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -1,7 +1,12 @@ +import { useMemo } from 'react' import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' +import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { AddCollateralOptions } from '@/llamalend/mutations/add-collateral.mutation' import { useMarketRates } from '@/llamalend/queries/market-rates' +import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' +import { mapQuery } from '@/llamalend/queries/utils' +import type { Query } from '@/llamalend/widgets/manage-loan/loan.types' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' @@ -11,9 +16,15 @@ import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' +import { decimal } from '@ui-kit/utils' import { InputDivider } from '../../../widgets/InputDivider' import { useAddCollateralForm } from '../hooks/useAddCollateralForm' +const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ + ...query, + tokenSymbol, +}) + export const AddCollateralForm = ({ market, networks, @@ -37,15 +48,14 @@ export const AddCollateralForm = ({ action, params, values, - bands, health, - prices, gas, isApproved, formErrors, collateralToken, borrowToken, txHash, + userState, } = useAddCollateralForm({ market, network, @@ -54,30 +64,74 @@ export const AddCollateralForm = ({ onAdded, }) - const marketRates = useMarketRates(params, isOpen) + const prevCollateral = useMemo( + () => + withTokenSymbol( + mapQuery(userState, (state) => state?.collateral), + collateralToken?.symbol, + ), + [collateralToken?.symbol, userState], + ) + const prevDebt = useMemo( + () => + withTokenSymbol( + mapQuery(userState, (state) => state?.debt), + borrowToken?.symbol, + ), + [borrowToken?.symbol, userState], + ) + const prevLoanToValue = useLoanToValueFromUserState({ + chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen, + }) + const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) + const collateral = useMemo( + () => + withTokenSymbol( + { + ...mapQuery(userState, (state) => state?.collateral), + data: decimal( + values.userCollateral + ? +values.userCollateral + (userState.data?.collateral ? +userState.data?.collateral : 0) + : null, + ), + }, + collateralToken?.symbol, + ), + [collateralToken?.symbol, userState, values.userCollateral], + ) + const marketRates = useMarketRates(params, isOpen) + const loanToValue = useLoanToValueFromUserState({ + chainId: params.chainId!, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: !!enabled && !!values.userCollateral, + collateralDelta: values.userCollateral, + }) return ( } > diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 2aebb351f..9d8d4277d 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -11,6 +11,7 @@ import { useAddCollateralBands } from '@/llamalend/queries/add-collateral/add-co import { useAddCollateralEstimateGas } from '@/llamalend/queries/add-collateral/add-collateral-gas-estimate.query' import { getAddCollateralHealthOptions } from '@/llamalend/queries/add-collateral/add-collateral-health.query' import { useAddCollateralPrices } from '@/llamalend/queries/add-collateral/add-collateral-prices.query' +import { useUserState } from '@/llamalend/queries/user-state.query' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { collateralFormValidationSuite, @@ -81,6 +82,7 @@ export const useAddCollateralForm = ({ useCallbackAfterFormUpdate(form, action.reset) + const userState = useUserState(params, enabled) const bands = useAddCollateralBands(params, enabled) const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) const prices = useAddCollateralPrices(params, enabled) @@ -104,5 +106,6 @@ export const useAddCollateralForm = ({ collateralToken, borrowToken, txHash: action.data?.hash, + userState, } } diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts new file mode 100644 index 000000000..fbf39a158 --- /dev/null +++ b/apps/main/src/llamalend/queries/utils.ts @@ -0,0 +1,16 @@ +import type { Query } from '../widgets/manage-loan/loan.types' + +/** + * Maps a Query type to extract partial data from it. + * Preserves error and loading states while transforming the data. + */ +export function mapQuery( + query: Query, + selector: (data: TSource) => TResult, +): Query { + return { + data: query.data === undefined ? undefined : selector(query.data), + isLoading: query.isLoading, + error: query.error, + } +} From 2b8fffbcc92db050bc373c6e05b6753eb63fa4be Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 1 Dec 2025 12:31:23 +0100 Subject: [PATCH 06/52] refactor: handle prev and current values in the action info --- .../widgets/manage-loan/LoanInfoAccordion.tsx | 56 ++++++++----------- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 18 ++++-- .../shared/ui/stories/ActionInfo.stories.tsx | 4 ++ 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index d6435b676..6f5d570ce 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -44,33 +44,13 @@ type LoanInfoAccordionProps = { leverage?: LoanLeverageActionInfoProps & { enabled: boolean } } -/** - * Builds props for ActionInfo component that handles prev/current value transitions. - * - Displays "previous -> current" when both values are available - * - Displays "current" when only current value is available - * - Displays "previous" when only previous value is available - * - Displays empty value when neither are available - */ -const buildPrevCurrentValues = ( - current: Query | undefined, - previous: Query | undefined, - format: (value: NonNullable) => string, - emptyValue = '-', -) => { - const hasCurrent = current?.data != null - const hasPrevious = previous?.data != null +const formatQueryValue = (query: Query | undefined, format: (value: NonNullable) => string) => + query?.data != null ? format(query.data as NonNullable) : undefined - return { - ...(hasCurrent && hasPrevious && { prevValue: format(previous!.data as NonNullable) }), - value: hasCurrent - ? format(current!.data as NonNullable) - : hasPrevious - ? format(previous!.data as NonNullable) - : emptyValue, - error: current?.error ?? previous?.error, - loading: current?.isLoading || previous?.isLoading, - } -} +const getQueryState = (current: Query | undefined, previous: Query | undefined) => ({ + error: current?.error ?? previous?.error, + loading: current?.isLoading || previous?.isLoading, +}) export const LoanInfoAccordion = ({ isOpen, @@ -99,7 +79,10 @@ export const LoanInfoAccordion = ({ info={ formatNumber(v, { abbreviate: false }), '∞')} + value={formatQueryValue(health, (v) => formatNumber(v, { abbreviate: false }))} + prevValue={formatQueryValue(prevHealth, (v) => formatNumber(v, { abbreviate: false }))} + emptyValue="∞" + {...getQueryState(health, prevHealth)} valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} testId="borrow-health" /> @@ -112,7 +95,9 @@ export const LoanInfoAccordion = ({ {(debt || prevDebt) && ( formatNumber(v, { abbreviate: false }))} + value={formatQueryValue(debt, (v) => formatNumber(v, { abbreviate: false }))} + prevValue={formatQueryValue(prevDebt, (v) => formatNumber(v, { abbreviate: false }))} + {...getQueryState(debt, prevDebt)} valueRight={debt?.tokenSymbol ?? prevDebt?.tokenSymbol} testId="borrow-debt" /> @@ -120,7 +105,9 @@ export const LoanInfoAccordion = ({ {(collateral || prevCollateral) && ( formatNumber(v, { abbreviate: false }))} + value={formatQueryValue(collateral, (v) => formatNumber(v, { abbreviate: false }))} + prevValue={formatQueryValue(prevCollateral, (v) => formatNumber(v, { abbreviate: false }))} + {...getQueryState(collateral, prevCollateral)} valueRight={collateral?.tokenSymbol ?? prevCollateral?.tokenSymbol} testId="borrow-collateral" /> @@ -149,16 +136,17 @@ export const LoanInfoAccordion = ({ )} {(loanToValue || prevLoanToValue) && ( )} diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index a8ea60ba8..9c5b9491c 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -31,7 +31,7 @@ export type ActionInfoProps = { /** Custom color for the label text */ labelColor?: TypographyProps['color'] /** Primary value to display and copy */ - value: ReactNode + value?: ReactNode /** Custom color for the value text */ valueColor?: TypographyProps['color'] /** Optional content to display to the left of the value */ @@ -41,9 +41,11 @@ export type ActionInfoProps = { /** Tooltip text to display when hovering over the value */ valueTooltip?: ReactNode /** Previous value (if needed for comparison) */ - prevValue?: string + prevValue?: ReactNode /** Custom color for the previous value text */ prevValueColor?: TypographyProps['color'] + /** Placeholder when no value or previous value is provided */ + emptyValue?: ReactNode /** URL to navigate to when clicking the external link button */ link?: string /** Value to be copied (will display a copy button). */ @@ -92,6 +94,7 @@ const ActionInfo = ({ prevValue, prevValueColor, value, + emptyValue = '-', valueColor, valueLeft, valueRight, @@ -114,6 +117,11 @@ const ActionInfo = ({ }, [copyValue, openSnackbar]) const errorMessage = (typeof error === 'object' && error?.message) || (typeof error === 'string' && error) + const hasValue = value != null + const hasPrevValue = prevValue != null + const showPrevValue = hasValue && hasPrevValue + const displayValue = hasValue ? value : hasPrevValue ? prevValue : emptyValue + return ( - {prevValue && ( + {showPrevValue && ( )} - {prevValue && ( + {showPrevValue && ( - {loading ? (typeof loading === 'string' ? loading : MOCK_SKELETON) : value} + {loading ? (typeof loading === 'string' ? loading : MOCK_SKELETON) : displayValue} diff --git a/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx b/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx index 86cad9c3f..310e31603 100644 --- a/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx +++ b/packages/curve-ui-kit/src/shared/ui/stories/ActionInfo.stories.tsx @@ -41,6 +41,10 @@ const meta: Meta = { control: 'color', description: 'Custom color for the previous value text', }, + emptyValue: { + control: 'text', + description: 'Placeholder rendered when neither current nor previous value is provided', + }, link: { control: 'text', description: 'The URL to navigate to when clicking the external link button', From 6c2bc4612f1cfd6df56e910d682871dc1a409d4e Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 1 Dec 2025 14:10:04 +0100 Subject: [PATCH 07/52] fix: token address already handled by validation suite --- .../llamalend/widgets/manage-loan/LoanFormTokenInput.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index c1d7677bc..30dbedafa 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -52,10 +52,10 @@ export const LoanFormTokenInput = < error: balanceError, } = useTokenBalance({ chainId: network?.chainId, userAddress }, token) - const { data: usdRate } = useTokenUsdRate( - { chainId: network?.chainId, tokenAddress: token?.address }, - !!token?.address, - ) + const { data: usdRate } = useTokenUsdRate({ + chainId: network?.chainId, + tokenAddress: token?.address, + }) const walletBalance = useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput From cebd7b701959771d1c2a09121482d2d9028759f2 Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 1 Dec 2025 16:45:55 +0100 Subject: [PATCH 08/52] feat: added collateral greater than wallet balance error to validation suite --- .../features/manage-loan/hooks/useAddCollateralForm.ts | 7 +++++++ .../features/manage-loan/hooks/useRemoveCollateralForm.ts | 1 + .../llamalend/queries/validation/manage-loan.validation.ts | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 9d8d4277d..52fbd545d 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -20,6 +20,7 @@ import { import type { IChainId as LlamaChainId } from '@curvefi/llamalend-api/lib/interfaces' import { vestResolver } from '@hookform/resolvers/vest' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' +import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' import { formDefaultOptions } from '@ui-kit/lib/model' import { useFormErrors } from '../../borrow/react-form.utils' @@ -46,12 +47,14 @@ export const useAddCollateralForm = ({ const tokens = market && getTokens(market) const collateralToken = tokens?.collateralToken const borrowToken = tokens?.borrowToken + const { data: maxCollateral } = useTokenBalance({ chainId, userAddress }, collateralToken) const form = useForm({ ...formDefaultOptions, resolver: vestResolver(collateralFormValidationSuite), defaultValues: { userCollateral: undefined, + maxCollateral: undefined, }, }) @@ -82,6 +85,10 @@ export const useAddCollateralForm = ({ useCallbackAfterFormUpdate(form, action.reset) + useEffect(() => { + form.setValue('maxCollateral', maxCollateral, { shouldValidate: true }) + }, [form, maxCollateral]) + const userState = useUserState(params, enabled) const bands = useAddCollateralBands(params, enabled) const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 2328dd7aa..9edaaf215 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -58,6 +58,7 @@ export const useRemoveCollateralForm = < resolver: vestResolver(collateralFormValidationSuite), defaultValues: { userCollateral: undefined, + maxCollateral: undefined, }, }) 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 648af9da2..72b3da053 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -1,6 +1,7 @@ import { group } from 'vest' import { validateIsFull, + validateMaxCollateral, validateUserBorrowed, validateUserCollateral, } from '@/llamalend/queries/validation/borrow-fields.validation' @@ -17,7 +18,7 @@ import { marketIdValidationSuite } from '@ui-kit/lib/model/query/market-id-valid import { userAddressValidationGroup } from '@ui-kit/lib/model/query/user-address-validation' import type { Decimal } from '@ui-kit/utils' -export type CollateralForm = FieldsOf<{ userCollateral: Decimal }> +export type CollateralForm = FieldsOf<{ userCollateral: Decimal; maxCollateral: Decimal }> export type RepayForm = FieldsOf<{ stateCollateral: Decimal @@ -38,6 +39,7 @@ export const collateralValidationSuite = createValidationSuite((params: Collater export const collateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { validateUserCollateral(params.userCollateral) + validateMaxCollateral(params.userCollateral, params.maxCollateral) }) export const collateralHealthValidationSuite = createValidationSuite(({ isFull, ...rest }: CollateralHealthParams) => { From 9dcfe7335a1e69f1a30edd6ecf4644b232e80cb7 Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 1 Dec 2025 16:47:19 +0100 Subject: [PATCH 09/52] feat: improve add collateral button label --- .../features/manage-loan/components/AddCollateralForm.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 9e2b690cd..6f5a7b643 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -153,7 +153,11 @@ export const AddCollateralForm = ({ disabled={formErrors.length > 0} data-testid="add-collateral-submit-button" > - {isPending ? t`Processing...` : isApproved.data ? t`Add collateral` : t`Approve & Add collateral`} + {isPending + ? t`Processing...` + : !isApproved.data && !isApproved.isPending && values.userCollateral + ? t`Approve & Add collateral` + : t`Add collateral`} Date: Mon, 1 Dec 2025 17:32:03 +0100 Subject: [PATCH 10/52] fix: add collateral approve estimate gas query --- .../add-collateral/add-collateral-gas-estimate.query.ts | 6 +++++- .../src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts b/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts index c9787f0fd..c670f5b34 100644 --- a/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts +++ b/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts @@ -19,7 +19,11 @@ const { useQuery: useAddCollateralGasEstimate } = queryFactory({ ] as const, queryFn: async ({ marketId, userCollateral }: AddCollateralGasQuery) => { const market = getLlamaMarket(marketId) - return await market.estimateGas.addCollateralApprove(userCollateral) + const isApproved = await market.addCollateralIsApproved(userCollateral) + const result = isApproved + ? await market.estimateGas.addCollateral(userCollateral) + : await market.estimateGas.addCollateralApprove(userCollateral) + return result }, validationSuite: collateralValidationSuite, }) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 6f5d570ce..eaeb1ffc6 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -150,8 +150,9 @@ export const LoanInfoAccordion = ({ testId="borrow-ltv" /> )} + {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} Date: Tue, 2 Dec 2025 09:49:27 +0100 Subject: [PATCH 11/52] feat: remove min height from AppFormContentWrapper --- packages/ui/src/AppForm/styles.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ui/src/AppForm/styles.ts b/packages/ui/src/AppForm/styles.ts index 79535a6df..49f9a947a 100644 --- a/packages/ui/src/AppForm/styles.ts +++ b/packages/ui/src/AppForm/styles.ts @@ -10,7 +10,6 @@ export const AppFormContentWrapper = styled(Box)` 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 From d098bf68188257dd956fe676e20662fd5b716e96 Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 2 Dec 2025 10:37:43 +0100 Subject: [PATCH 12/52] feat: fire icon to transaction action info --- .../src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index eaeb1ffc6..40a81916d 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -8,6 +8,8 @@ import ActionInfo from '@ui-kit/shared/ui/ActionInfo' import { Decimal, formatNumber, formatPercent, formatUsd } from '@ui-kit/utils' import { getHealthValueColor } from '../../features/market-position-details/utils' import { LoanLeverageActionInfo, type LoanLeverageActionInfoProps } from './LoanLeverageActionInfo' +import { FireIcon } from '@ui-kit/shared/icons/FireIcon' + export type LoanInfoGasData = { estGasCostUsd?: number | Decimal | `${number}` @@ -156,6 +158,7 @@ export const LoanInfoAccordion = ({ value={gas.data?.estGasCostUsd == null ? '-' : formatUsd(gas.data.estGasCostUsd)} valueTooltip={gas.data?.tooltip} loading={gas.isLoading} + valueLeft={} /> From ec65aa4dee17c166b4df3498ebacd3a9783076dd Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 2 Dec 2025 12:20:46 +0100 Subject: [PATCH 13/52] feat: layer 1 fill subtabs for lend markets --- apps/main/src/lend/components/PageLoanManage/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/src/lend/components/PageLoanManage/index.tsx b/apps/main/src/lend/components/PageLoanManage/index.tsx index 6fbdbfb48..665f5358c 100644 --- a/apps/main/src/lend/components/PageLoanManage/index.tsx +++ b/apps/main/src/lend/components/PageLoanManage/index.tsx @@ -81,6 +81,7 @@ const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) = onChange={setSubTab} options={subTabs} fullWidth + sx={{ backgroundColor: (t) => t.design.Layer[1].Fill }} /> {subTab === 'loan-increase' && ( From 0486409c8200b577e6237ae4a1a91ae61dc16717 Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 2 Dec 2025 12:53:01 +0100 Subject: [PATCH 14/52] fix: review minor changes and optimizations --- .../manage-loan/components/AddCollateralForm.tsx | 6 +++--- packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) 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 6f5a7b643..bbb75fc83 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -155,9 +155,9 @@ export const AddCollateralForm = ({ > {isPending ? t`Processing...` - : !isApproved.data && !isApproved.isPending && values.userCollateral - ? t`Approve & Add collateral` - : t`Add collateral`} + : isApproved.data || isApproved.isPending || !values.userCollateral + ? t`Add collateral` + : t`Approve & Add collateral`} @@ -176,7 +174,7 @@ const ActionInfo = ({ color={error ? 'error' : (valueColor ?? 'textPrimary')} component="div" > - {loading ? (typeof loading === 'string' ? loading : MOCK_SKELETON) : displayValue} + {loading ? (typeof loading === 'string' ? loading : MOCK_SKELETON) : value} From 257b2d40691f7d3524a83372b0b6c1bcd29e1e0a Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 14:48:15 +0100 Subject: [PATCH 15/52] fix: sub tabs labels --- apps/main/src/lend/components/PageLoanManage/index.tsx | 4 ++-- .../features/manage-loan/components/AddCollateralForm.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/main/src/lend/components/PageLoanManage/index.tsx b/apps/main/src/lend/components/PageLoanManage/index.tsx index 665f5358c..83a6a659b 100644 --- a/apps/main/src/lend/components/PageLoanManage/index.tsx +++ b/apps/main/src/lend/components/PageLoanManage/index.tsx @@ -28,8 +28,8 @@ const tabsLoan: TabOption[] = [ ] const tabsCollateral: TabOption[] = [ - { value: 'collateral-increase', label: t`Add collateral` }, - { value: 'collateral-decrease', label: t`Remove collateral` }, + { value: 'collateral-increase', label: t`Add` }, + { value: 'collateral-decrease', label: t`Remove` }, ] const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) => { 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 bbb75fc83..c55c55c63 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -137,7 +137,7 @@ export const AddCollateralForm = ({ > }> Date: Wed, 3 Dec 2025 15:05:25 +0100 Subject: [PATCH 16/52] fix: validation user collateral triggered when user collateral is 0 --- .../queries/validation/borrow-fields.validation.ts | 6 ++++-- .../llamalend/queries/validation/manage-loan.validation.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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 e81f90261..fc6375e9e 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -8,9 +8,11 @@ export const validateUserBorrowed = (userBorrowed: Decimal | null | undefined) = }) } -export const validateUserCollateral = (userCollateral: Decimal | undefined | null) => { +export const validateUserCollateral = (userCollateral: Decimal | undefined | null, required: boolean = true) => { test('userCollateral', `Collateral amount must be a positive number`, () => { - enforce(userCollateral).isNumeric().gt(0) + if (required || userCollateral != null) { + enforce(userCollateral).isNumeric().gt(0) + } }) } 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 72b3da053..7105999c7 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -38,7 +38,7 @@ export const collateralValidationSuite = createValidationSuite((params: Collater ) export const collateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { - validateUserCollateral(params.userCollateral) + validateUserCollateral(params.userCollateral, false) validateMaxCollateral(params.userCollateral, params.maxCollateral) }) From 374a0e70dd682b48c337480b472f5d021bd681f2 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 15:21:11 +0100 Subject: [PATCH 17/52] feat: loan accordion spacing --- .../widgets/manage-loan/LoanInfoAccordion.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 40a81916d..8be3c42be 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -3,13 +3,15 @@ import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' import { useTheme } from '@mui/material/styles' import { t } from '@ui-kit/lib/i18n' +import { FireIcon } from '@ui-kit/shared/icons/FireIcon' import { Accordion } from '@ui-kit/shared/ui/Accordion' import ActionInfo from '@ui-kit/shared/ui/ActionInfo' +import { SizesAndSpaces } from '@ui-kit/themes/design/1_sizes_spaces' import { Decimal, formatNumber, formatPercent, formatUsd } from '@ui-kit/utils' import { getHealthValueColor } from '../../features/market-position-details/utils' import { LoanLeverageActionInfo, type LoanLeverageActionInfoProps } from './LoanLeverageActionInfo' -import { FireIcon } from '@ui-kit/shared/icons/FireIcon' +const { Spacing } = SizesAndSpaces export type LoanInfoGasData = { estGasCostUsd?: number | Decimal | `${number}` @@ -54,6 +56,8 @@ const getQueryState = (current: Query | undefined, previous: Query + export const LoanInfoAccordion = ({ isOpen, toggle, @@ -93,7 +97,6 @@ export const LoanInfoAccordion = ({ toggle={toggle} > - {leverage?.enabled && } {(debt || prevDebt) && ( )} + {leverage?.enabled && ( + <> + + + + )} + {/* TODO: add router provider and slippage */} + + {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} } + valueLeft={} /> From 1fe14fa1d09170256a7ecaabf31b966a59340faa Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 15:35:12 +0100 Subject: [PATCH 18/52] feat: show usd rate when no input value --- .../src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index 30dbedafa..d45c911e9 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -94,7 +94,7 @@ export const LoanFormTokenInput = < message={error?.message ?? message} walletBalance={walletBalance} maxBalance={useMemo(() => max && { balance: max.data, chips: 'max' }, [max])} - inputBalanceUsd={decimal(value && usdRate && usdRate * +value)} + inputBalanceUsd={decimal(usdRate && usdRate * +(value ?? 0))} /> ) } From 6adf3ed4b4cc9d970adeca2925626f9f9c73972c Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 16:32:17 +0100 Subject: [PATCH 19/52] refactor: form alert position --- .../components/AddCollateralForm.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 c55c55c63..bb0540b86 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -147,6 +147,16 @@ export const AddCollateralForm = ({ /> + + - - ) } From c4949b0d8ea50c11aafd70a981384040feca7992 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 16:44:21 +0100 Subject: [PATCH 20/52] feat: resolved custom alert form errors --- .../widgets/manage-loan/LoanFormAlerts.tsx | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx index bf8d16f07..7a1f4ba4e 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx @@ -9,6 +9,11 @@ import { t } from '@ui-kit/lib/i18n' export type FormErrors = readonly (readonly [Field, string])[] +type ErrorDisplay = { + title: string + message?: string +} + export type LoanFormAlertProps = { network: BaseConfig isSuccess: boolean @@ -19,6 +24,12 @@ export type LoanFormAlertProps = { successTitle: string } +const resolveErrorDisplay = (err: any): ErrorDisplay => { + const code = err?.code as string | undefined + if (code === 'ACTION_REJECTED') return { title: t`User rejected action` } + return { title: t`An error occurred`, message: err.message } +} + export const LoanFormAlerts = ({ network, isSuccess, @@ -27,37 +38,41 @@ export const LoanFormAlerts = ({ formErrors, handledErrors, successTitle, -}: LoanFormAlertProps) => ( - <> - {isSuccess && ( - - {successTitle} - {txHash && ( - - {t`View on Explorer`} - - )} - - )} - {formErrors.some(([field]) => !handledErrors.includes(field)) && ( - - {t`Please correct the errors`} - {formErrors - .filter(([field]) => !handledErrors.includes(field)) - .map(([field, message]) => ( - {message} - ))} - - )} - {error && ( - - {t`An error occurred`} - {error.message} - - )} - -) +}: LoanFormAlertProps) => { + const resolvedError = error ? resolveErrorDisplay(error) : null + + return ( + <> + {isSuccess && ( + + {successTitle} + {txHash && ( + + {t`View on Explorer`} + + )} + + )} + {formErrors.some(([field]) => !handledErrors.includes(field)) && ( + + {t`Please correct the errors`} + {formErrors + .filter(([field]) => !handledErrors.includes(field)) + .map(([field, message]) => ( + {message} + ))} + + )} + {resolvedError && ( + + {resolvedError.title} + {resolvedError.message} + + )} + + ) +} From 91220630815740b43f2ecae4d0d35a327b93428f Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 3 Dec 2025 16:44:27 +0100 Subject: [PATCH 21/52] feat: removed error notification --- apps/main/src/llamalend/mutations/useLlammaMutation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/main/src/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index 261dbb133..7c6ae94c1 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -134,7 +134,6 @@ export function useLlammaMutation { logError(mutationKey, { error, variables, marketId: context?.market.id }) - notify(error.message, 'error') }, onSettled: (_data, _error, _variables, context) => context?.pendingNotification?.dismiss(), }) From 62afec5b7f0c990c736c7da1c4e8f03b3f56e93f Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 4 Dec 2025 14:51:05 +0100 Subject: [PATCH 22/52] refactor: expected collateral using bignumber --- .../features/manage-loan/components/AddCollateralForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 bb0540b86..03c9b0b2d 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { useMemo } from 'react' import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' @@ -97,7 +98,9 @@ export const AddCollateralForm = ({ ...mapQuery(userState, (state) => state?.collateral), data: decimal( values.userCollateral - ? +values.userCollateral + (userState.data?.collateral ? +userState.data?.collateral : 0) + ? new BigNumber(values.userCollateral) + .plus(userState.data?.collateral ? new BigNumber(userState.data?.collateral) : '0') + .toString() : null, ), }, From c0cffef523d19d2c205077af4de2f9f4073ad973 Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 4 Dec 2025 14:52:54 +0100 Subject: [PATCH 23/52] feat: general error message for toast --- apps/main/src/llamalend/mutations/useLlammaMutation.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/main/src/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index 7c6ae94c1..96e7e3245 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -6,6 +6,7 @@ import type { IChainId as LlamaChainId, INetworkName as LlamaNetworkId } from '@ import { useMutation } from '@tanstack/react-query' import { notify, useConnection } from '@ui-kit/features/connect-wallet' import { assertValidity, logError, logMutation, logSuccess } from '@ui-kit/lib' +import { t } from '@ui-kit/lib/i18n' import { waitForTransactionReceipt } from '@wagmi/core' import { getLlamaMarket, updateUserEventsApi } from '../llama.utils' import type { LlamaMarketTemplate } from '../llamalend.types' @@ -134,6 +135,7 @@ export function useLlammaMutation { logError(mutationKey, { error, variables, marketId: context?.market.id }) + notify(t`Transaction failed`, 'error') }, onSettled: (_data, _error, _variables, context) => context?.pendingNotification?.dismiss(), }) From 74f5cf4b44f96c8215d4ff5f1392d8e0dda5ae84 Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 4 Dec 2025 16:36:34 +0100 Subject: [PATCH 24/52] refactor: get message error helper for loan and lend into llamalend --- .../src/dex/components/MonadBannerAlert.tsx | 2 +- .../src/dex/components/PagePool/index.tsx | 2 +- apps/main/src/lend/lib/apiLending.ts | 3 ++- apps/main/src/lend/utils/helpers.ts | 20 ------------------ apps/main/src/llamalend/helpers.ts | 21 +++++++++++++++++++ apps/main/src/loan/lib/apiCrvusd.ts | 3 ++- apps/main/src/loan/utils/helpers.ts | 16 -------------- 7 files changed, 27 insertions(+), 40 deletions(-) create mode 100644 apps/main/src/llamalend/helpers.ts diff --git a/apps/main/src/dex/components/MonadBannerAlert.tsx b/apps/main/src/dex/components/MonadBannerAlert.tsx index 2052a63fb..bb069672c 100644 --- a/apps/main/src/dex/components/MonadBannerAlert.tsx +++ b/apps/main/src/dex/components/MonadBannerAlert.tsx @@ -1,6 +1,6 @@ -import { Banner } from '@ui-kit/shared/ui/Banner' import { Stack } from '@mui/material' import { t } from '@ui-kit/lib/i18n' +import { Banner } from '@ui-kit/shared/ui/Banner' import { PoolUrlParams } from '../types/main.types' const MonadBannerAlert = ({ params }: { params: PoolUrlParams }) => { diff --git a/apps/main/src/dex/components/PagePool/index.tsx b/apps/main/src/dex/components/PagePool/index.tsx index b27745e18..2fb5ad314 100644 --- a/apps/main/src/dex/components/PagePool/index.tsx +++ b/apps/main/src/dex/components/PagePool/index.tsx @@ -17,10 +17,10 @@ import usePoolAlert from '@/dex/hooks/usePoolAlert' import useTokensMapper from '@/dex/hooks/useTokensMapper' import { getUserPoolActiveKey } from '@/dex/store/createUserSlice' import useStore from '@/dex/store/useStore' +import type { PoolUrlParams } from '@/dex/types/main.types' import { getChainPoolIdActiveKey } from '@/dex/utils' import { getPath } from '@/dex/utils/utilsRouter' import { ManageGauge } from '@/dex/widgets/manage-gauge' -import type { PoolUrlParams } from '@/dex/types/main.types' import Stack from '@mui/material/Stack' import AlertBox from '@ui/AlertBox' import { AppFormContentWrapper } from '@ui/AppForm' diff --git a/apps/main/src/lend/lib/apiLending.ts b/apps/main/src/lend/lib/apiLending.ts index 48928fd01..6eafcf73f 100644 --- a/apps/main/src/lend/lib/apiLending.ts +++ b/apps/main/src/lend/lib/apiLending.ts @@ -28,7 +28,8 @@ import { UserMarketBalances, } from '@/lend/types/lend.types' import { OneWayMarketTemplate } from '@/lend/types/lend.types' -import { fulfilledValue, getErrorMessage, log } from '@/lend/utils/helpers' +import { fulfilledValue, log } from '@/lend/utils/helpers' +import { getErrorMessage } from '@/llamalend/helpers' import { getIsUserCloseToLiquidation, getLiquidationStatus, reverseBands, sortBandsLend } from '@/llamalend/llama.utils' import PromisePool from '@supercharge/promise-pool' import type { StepStatus } from '@ui/Stepper/types' diff --git a/apps/main/src/lend/utils/helpers.ts b/apps/main/src/lend/utils/helpers.ts index 72f14081f..0862dc8c1 100644 --- a/apps/main/src/lend/utils/helpers.ts +++ b/apps/main/src/lend/utils/helpers.ts @@ -1,12 +1,7 @@ import { Api, OneWayMarketTemplate } from '@/lend/types/lend.types' -import { t } from '@ui-kit/lib/i18n' export * from './utilsRouter' -interface CustomError extends Error { - data?: { message: string } -} - export const isDevelopment = process.env.NODE_ENV === 'development' export function log(fnName: string, ...args: unknown[]) { @@ -15,21 +10,6 @@ export function log(fnName: string, ...args: unknown[]) { } } -export function getErrorMessage(error: CustomError, defaultErrorMessage: string) { - let errorMessage = defaultErrorMessage - - if (error?.message) { - if (error.message.startsWith('user rejected transaction')) { - errorMessage = t`User rejected transaction` - } else if ('data' in error && typeof error.data?.message === 'string') { - errorMessage = error.data.message - } else { - errorMessage = error.message - } - } - return errorMessage -} - export function scrollToTop() { window.scroll({ top: 0, diff --git a/apps/main/src/llamalend/helpers.ts b/apps/main/src/llamalend/helpers.ts new file mode 100644 index 000000000..929f09f0d --- /dev/null +++ b/apps/main/src/llamalend/helpers.ts @@ -0,0 +1,21 @@ +import { t } from '@ui-kit/lib/i18n' + +interface CustomError extends Error { + data?: { message: string } + code?: string +} + +export function getErrorMessage(error: CustomError | null, defaultErrorMessage?: string): string { + let errorMessage = defaultErrorMessage ?? '' + + if (error?.message) { + if (error.message.startsWith('user rejected transaction') || error?.code === 'ACTION_REJECTED') { + errorMessage = t`User rejected transaction` + } else if ('data' in error && typeof error.data?.message === 'string') { + errorMessage = error.data.message + } else { + errorMessage = error.message + } + } + return errorMessage +} diff --git a/apps/main/src/loan/lib/apiCrvusd.ts b/apps/main/src/loan/lib/apiCrvusd.ts index d12aff2ab..8a4e2a56d 100644 --- a/apps/main/src/loan/lib/apiCrvusd.ts +++ b/apps/main/src/loan/lib/apiCrvusd.ts @@ -1,11 +1,12 @@ import { cloneDeep } from 'lodash' +import { getErrorMessage } from '@/llamalend/helpers' import { getIsUserCloseToLiquidation, getLiquidationStatus, reverseBands, sortBandsMint } from '@/llamalend/llama.utils' import type { MaxRecvLeverage as MaxRecvLeverageForm } from '@/loan/components/PageLoanCreate/types' import type { FormDetailInfo as FormDetailInfoDeleverage } from '@/loan/components/PageLoanManage/LoanDeleverage/types' import networks from '@/loan/networks' import type { LiqRange, MaxRecvLeverage, Provider } from '@/loan/store/types' import { ChainId, LlamaApi, Llamma, UserLoanDetails, type BandBalance } from '@/loan/types/loan.types' -import { fulfilledValue, getErrorMessage, log } from '@/loan/utils/helpers' +import { fulfilledValue, log } from '@/loan/utils/helpers' import { hasV2Leverage } from '@/loan/utils/leverage' import type { TGas } from '@curvefi/llamalend-api/lib/interfaces' import PromisePool from '@supercharge/promise-pool' diff --git a/apps/main/src/loan/utils/helpers.ts b/apps/main/src/loan/utils/helpers.ts index a9ebedbef..9ea5ddca0 100644 --- a/apps/main/src/loan/utils/helpers.ts +++ b/apps/main/src/loan/utils/helpers.ts @@ -1,6 +1,5 @@ import networks from '@/loan/networks' import { type ChainId, LlamaApi } from '@/loan/types/loan.types' -import { t } from '@ui-kit/lib/i18n' interface CustomError extends Error { data?: { message: string } @@ -14,21 +13,6 @@ export function log(fnName: string, ...args: unknown[]) { } } -export function getErrorMessage(error: CustomError, defaultErrorMessage: string) { - let errorMessage = defaultErrorMessage - - if (error?.message) { - if (error.message.startsWith('user rejected transaction')) { - errorMessage = t`User rejected transaction` - } else if ('data' in error && typeof error.data?.message === 'string') { - errorMessage = error.data.message - } else { - errorMessage = error.message - } - } - return errorMessage -} - export function fulfilledValue(result: PromiseSettledResult) { if (result.status === 'fulfilled') { return result.value From 8fabbbfaecfade8d714f192a6e3566ede068d875 Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 4 Dec 2025 16:36:50 +0100 Subject: [PATCH 25/52] refactor: use getErrorMessage from llamalend --- .../widgets/manage-loan/LoanFormAlerts.tsx | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx index 7a1f4ba4e..b9f24ff99 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx @@ -1,3 +1,4 @@ +import { getErrorMessage } from '@/llamalend/helpers' import type { INetworkName } from '@curvefi/llamalend-api/lib/interfaces' import type { Hex } from '@curvefi/prices-api' import Alert from '@mui/material/Alert' @@ -9,11 +10,6 @@ import { t } from '@ui-kit/lib/i18n' export type FormErrors = readonly (readonly [Field, string])[] -type ErrorDisplay = { - title: string - message?: string -} - export type LoanFormAlertProps = { network: BaseConfig isSuccess: boolean @@ -24,12 +20,6 @@ export type LoanFormAlertProps = { successTitle: string } -const resolveErrorDisplay = (err: any): ErrorDisplay => { - const code = err?.code as string | undefined - if (code === 'ACTION_REJECTED') return { title: t`User rejected action` } - return { title: t`An error occurred`, message: err.message } -} - export const LoanFormAlerts = ({ network, isSuccess, @@ -38,10 +28,7 @@ export const LoanFormAlerts = ({ formErrors, handledErrors, successTitle, -}: LoanFormAlertProps) => { - const resolvedError = error ? resolveErrorDisplay(error) : null - - return ( +}: LoanFormAlertProps) => ( <> {isSuccess && ( @@ -63,16 +50,15 @@ export const LoanFormAlerts = ({ ))} )} - {resolvedError && ( + {!!error && ( - {resolvedError.title} - {resolvedError.message} + {t`An error occurred`} + {getErrorMessage(error)} )} ) -} From 0710721cf9d5ec9c27389bbae6df4129a1fde3cb Mon Sep 17 00:00:00 2001 From: Pearce Date: Thu, 4 Dec 2025 16:49:06 +0100 Subject: [PATCH 26/52] chore: format --- .../widgets/manage-loan/LoanFormAlerts.tsx | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx index b9f24ff99..16c0daded 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx @@ -29,36 +29,36 @@ export const LoanFormAlerts = ({ handledErrors, successTitle, }: LoanFormAlertProps) => ( - <> - {isSuccess && ( - - {successTitle} - {txHash && ( - - {t`View on Explorer`} - - )} - - )} - {formErrors.some(([field]) => !handledErrors.includes(field)) && ( - - {t`Please correct the errors`} - {formErrors - .filter(([field]) => !handledErrors.includes(field)) - .map(([field, message]) => ( - {message} - ))} - - )} - {!!error && ( - - {t`An error occurred`} - {getErrorMessage(error)} - - )} - - ) + <> + {isSuccess && ( + + {successTitle} + {txHash && ( + + {t`View on Explorer`} + + )} + + )} + {formErrors.some(([field]) => !handledErrors.includes(field)) && ( + + {t`Please correct the errors`} + {formErrors + .filter(([field]) => !handledErrors.includes(field)) + .map(([field, message]) => ( + {message} + ))} + + )} + {!!error && ( + + {t`An error occurred`} + {getErrorMessage(error)} + + )} + +) From 737110d0e193257a4a1ea7283ae1a9df49b0155f Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 5 Dec 2025 17:44:14 +0100 Subject: [PATCH 27/52] refactor: pass user state to the loan info accordion --- .../components/AddCollateralForm.tsx | 53 +---- .../manage-loan/hooks/useAddCollateralForm.ts | 28 +++ .../src/llamalend/queries/user-state.query.ts | 8 +- apps/main/src/llamalend/queries/utils.ts | 8 + .../widgets/manage-loan/LoanInfoAccordion.tsx | 211 +++++++++--------- 5 files changed, 155 insertions(+), 153 deletions(-) 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 03c9b0b2d..7351ac60a 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -1,13 +1,9 @@ -import BigNumber from 'bignumber.js' -import { useMemo } from 'react' import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { AddCollateralOptions } from '@/llamalend/mutations/add-collateral.mutation' import { useMarketRates } from '@/llamalend/queries/market-rates' import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' -import { mapQuery } from '@/llamalend/queries/utils' -import type { Query } from '@/llamalend/widgets/manage-loan/loan.types' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' @@ -17,15 +13,9 @@ import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' -import { decimal } from '@ui-kit/utils' import { InputDivider } from '../../../widgets/InputDivider' import { useAddCollateralForm } from '../hooks/useAddCollateralForm' -const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ - ...query, - tokenSymbol, -}) - export const AddCollateralForm = ({ market, networks, @@ -57,6 +47,7 @@ export const AddCollateralForm = ({ borrowToken, txHash, userState, + expectedCollateral, } = useAddCollateralForm({ market, network, @@ -65,22 +56,6 @@ export const AddCollateralForm = ({ onAdded, }) - const prevCollateral = useMemo( - () => - withTokenSymbol( - mapQuery(userState, (state) => state?.collateral), - collateralToken?.symbol, - ), - [collateralToken?.symbol, userState], - ) - const prevDebt = useMemo( - () => - withTokenSymbol( - mapQuery(userState, (state) => state?.debt), - borrowToken?.symbol, - ), - [borrowToken?.symbol, userState], - ) const prevLoanToValue = useLoanToValueFromUserState({ chainId, marketId: params.marketId, @@ -91,23 +66,6 @@ export const AddCollateralForm = ({ }) const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) - const collateral = useMemo( - () => - withTokenSymbol( - { - ...mapQuery(userState, (state) => state?.collateral), - data: decimal( - values.userCollateral - ? new BigNumber(values.userCollateral) - .plus(userState.data?.collateral ? new BigNumber(userState.data?.collateral) : '0') - .toString() - : null, - ), - }, - collateralToken?.symbol, - ), - [collateralToken?.symbol, userState, values.userCollateral], - ) const marketRates = useMarketRates(params, isOpen) const loanToValue = useLoanToValueFromUserState({ chainId: params.chainId!, @@ -131,10 +89,13 @@ export const AddCollateralForm = ({ rates={marketRates} prevLoanToValue={prevLoanToValue} loanToValue={loanToValue} - prevDebt={prevDebt} + userState={{ + ...userState, + borrowTokenSymbol: borrowToken?.symbol, + collateralTokenSymbol: collateralToken?.symbol, + }} gas={gas} - prevCollateral={prevCollateral} - collateral={collateral} + collateral={expectedCollateral} /> } > diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 52fbd545d..5f7e2a121 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { useEffect, useMemo } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useForm } from 'react-hook-form' @@ -12,18 +13,26 @@ import { useAddCollateralEstimateGas } from '@/llamalend/queries/add-collateral/ import { getAddCollateralHealthOptions } from '@/llamalend/queries/add-collateral/add-collateral-health.query' import { useAddCollateralPrices } from '@/llamalend/queries/add-collateral/add-collateral-prices.query' import { useUserState } from '@/llamalend/queries/user-state.query' +import { mapQuery } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { collateralFormValidationSuite, type CollateralForm, } from '@/llamalend/queries/validation/manage-loan.validation' +import { Query } from '@/llamalend/widgets/manage-loan/loan.types' import type { IChainId as LlamaChainId } from '@curvefi/llamalend-api/lib/interfaces' import { vestResolver } from '@hookform/resolvers/vest' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' import { formDefaultOptions } from '@ui-kit/lib/model' +import { decimal } from '@ui-kit/utils/decimal' import { useFormErrors } from '../../borrow/react-form.utils' +const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ + ...query, + tokenSymbol, +}) + const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) @@ -95,6 +104,24 @@ export const useAddCollateralForm = ({ const prices = useAddCollateralPrices(params, enabled) const gas = useAddCollateralEstimateGas(networks, params, enabled) + const expectedCollateral = useMemo( + () => + withTokenSymbol( + { + ...mapQuery(userState, (state) => state?.collateral), + data: decimal( + values.userCollateral + ? new BigNumber(values.userCollateral) + .plus(userState.data?.collateral ? new BigNumber(userState.data?.collateral) : '0') + .toString() + : null, + ), + }, + collateralToken?.symbol, + ), + [collateralToken?.symbol, userState, values.userCollateral], + ) + const formErrors = useFormErrors(form.formState) return { @@ -114,5 +141,6 @@ export const useAddCollateralForm = ({ borrowToken, txHash: action.data?.hash, userState, + expectedCollateral, } } diff --git a/apps/main/src/llamalend/queries/user-state.query.ts b/apps/main/src/llamalend/queries/user-state.query.ts index e747922f0..3c097905e 100644 --- a/apps/main/src/llamalend/queries/user-state.query.ts +++ b/apps/main/src/llamalend/queries/user-state.query.ts @@ -3,9 +3,15 @@ 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 type UserState = { + collateral: Decimal + stablecoin: Decimal + debt: Decimal +} + export const { useQuery: useUserState, invalidate: invalidateUserState } = queryFactory({ queryKey: (params: UserMarketParams) => [...rootKeys.userMarket(params), 'market-user-state'] as const, - queryFn: async ({ marketId, userAddress }: UserMarketQuery) => { + queryFn: async ({ marketId, userAddress }: UserMarketQuery): Promise => { const market = getLlamaMarket(marketId) const userState = await market.userState(userAddress) diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts index fbf39a158..8e656d7c4 100644 --- a/apps/main/src/llamalend/queries/utils.ts +++ b/apps/main/src/llamalend/queries/utils.ts @@ -14,3 +14,11 @@ export function mapQuery( error: query.error, } } + +export const formatQueryValue = (query: Query | undefined, format: (value: NonNullable) => string) => + query?.data != null ? format(query.data as NonNullable) : undefined + +export const getQueryState = (current: Query | undefined, previous: Query | undefined) => ({ + error: current?.error ?? previous?.error, + loading: current?.isLoading || previous?.isLoading, +}) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 8be3c42be..ff488e995 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -1,3 +1,5 @@ +import { UserState } from '@/llamalend/queries/user-state.query' +import { formatQueryValue, getQueryState } from '@/llamalend/queries/utils' import { Query } from '@/llamalend/widgets/manage-loan/loan.types' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' @@ -42,20 +44,12 @@ type LoanInfoAccordionProps = { prevLoanToValue?: Query gas: Query debt?: Query & { tokenSymbol?: string } - prevDebt?: Query & { tokenSymbol?: string } collateral?: Query & { tokenSymbol?: string } - prevCollateral?: Query & { tokenSymbol?: string } + // userState values are used as prev values if collateral or debt are available + userState?: Query & { borrowTokenSymbol?: string; collateralTokenSymbol?: string } leverage?: LoanLeverageActionInfoProps & { enabled: boolean } } -const formatQueryValue = (query: Query | undefined, format: (value: NonNullable) => string) => - query?.data != null ? format(query.data as NonNullable) : undefined - -const getQueryState = (current: Query | undefined, previous: Query | undefined) => ({ - error: current?.error ?? previous?.error, - loading: current?.isLoading || previous?.isLoading, -}) - const AccordionSpacing = () => export const LoanInfoAccordion = ({ @@ -71,108 +65,113 @@ export const LoanInfoAccordion = ({ loanToValue, prevLoanToValue, gas, - prevDebt, debt, - prevCollateral, collateral, leverage, -}: LoanInfoAccordionProps) => ( - // error tooltip isn't displayed correctly because accordion takes the mouse focus. Use title for now. - - formatNumber(v, { abbreviate: false }))} - prevValue={formatQueryValue(prevHealth, (v) => formatNumber(v, { abbreviate: false }))} - emptyValue="∞" - {...getQueryState(health, prevHealth)} - valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} - testId="borrow-health" - /> - } - expanded={isOpen} - toggle={toggle} - > - - {(debt || prevDebt) && ( + userState, +}: LoanInfoAccordionProps) => { + const prevDebt = userState?.data?.debt + const prevCollateral = userState?.data?.collateral + return ( + // error tooltip isn't displayed correctly because accordion takes the mouse focus. Use title for now. + + formatNumber(v, { abbreviate: false }))} - prevValue={formatQueryValue(prevDebt, (v) => formatNumber(v, { abbreviate: false }))} - {...getQueryState(debt, prevDebt)} - valueRight={debt?.tokenSymbol ?? prevDebt?.tokenSymbol} - testId="borrow-debt" + label="" + value={formatQueryValue(health, (v) => formatNumber(v, { abbreviate: false }))} + prevValue={formatQueryValue(prevHealth, (v) => formatNumber(v, { abbreviate: false }))} + emptyValue="∞" + {...getQueryState(health, prevHealth)} + valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} + testId="borrow-health" /> - )} - {(collateral || prevCollateral) && ( - formatNumber(v, { abbreviate: false }))} - prevValue={formatQueryValue(prevCollateral, (v) => formatNumber(v, { abbreviate: false }))} - {...getQueryState(collateral, prevCollateral)} - valueRight={collateral?.tokenSymbol ?? prevCollateral?.tokenSymbol} - testId="borrow-collateral" - /> - )} - - {bands && ( + } + expanded={isOpen} + toggle={toggle} + > + + {(debt || prevDebt) && ( + formatNumber(v, { abbreviate: false }))} + prevValue={prevDebt ? formatNumber(prevDebt, { abbreviate: false }) : undefined} + {...getQueryState(debt, userState)} + valueRight={debt?.tokenSymbol ?? userState?.borrowTokenSymbol} + testId="borrow-debt" + /> + )} + {(collateral || prevCollateral) && ( + formatNumber(v, { abbreviate: false }))} + prevValue={prevCollateral ? formatNumber(prevCollateral, { abbreviate: false }) : undefined} + {...getQueryState(collateral, userState)} + valueRight={collateral?.tokenSymbol ?? userState?.collateralTokenSymbol} + testId="borrow-collateral" + /> + )} + {bands && ( + + )} + {prices && ( + formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} + error={prices.error} + loading={prices.isLoading} + testId="borrow-price-range" + /> + )} + {range != null && ( + + )} - )} - {prices && ( + {(loanToValue || prevLoanToValue) && ( + + )} + {leverage?.enabled && ( + <> + + + + )} + {/* TODO: add router provider and slippage */} + + {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} - error={prices.error} - loading={prices.isLoading} - testId="borrow-price-range" + label={t`Estimated tx cost`} + value={gas.data?.estGasCostUsd == null ? '-' : formatUsd(gas.data.estGasCostUsd)} + valueTooltip={gas.data?.tooltip} + loading={gas.isLoading} + valueLeft={} /> - )} - {range != null && ( - - )} - - {(loanToValue || prevLoanToValue) && ( - - )} - {leverage?.enabled && ( - <> - - - - )} - {/* TODO: add router provider and slippage */} - - - {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} - } - /> - - - -) + + + + ) +} From 6714195564f1dd17cb5d929a85d82c638fa2836c Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 5 Dec 2025 17:44:48 +0100 Subject: [PATCH 28/52] chore: docs and small optimization --- apps/main/src/llamalend/mutations/useLlammaMutation.ts | 2 +- apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/main/src/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index 96e7e3245..1f4452b7b 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -135,7 +135,7 @@ export function useLlammaMutation { logError(mutationKey, { error, variables, marketId: context?.market.id }) - notify(t`Transaction failed`, 'error') + notify(t`Transaction failed`, 'error') // hide the actual error message, it can be too long - display it in the form }, onSettled: (_data, _error, _variables, context) => context?.pendingNotification?.dismiss(), }) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx index 16c0daded..06b952d18 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx @@ -50,7 +50,7 @@ export const LoanFormAlerts = ({ ))} )} - {!!error && ( + {error && ( Date: Fri, 5 Dec 2025 18:20:11 +0100 Subject: [PATCH 29/52] refactor: moved withTokenSymbol to llamalend utils --- .../features/manage-loan/hooks/useAddCollateralForm.ts | 8 +------- apps/main/src/llamalend/queries/utils.ts | 5 +++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 8abf448f7..0f17acbae 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -13,13 +13,12 @@ import { useAddCollateralEstimateGas } from '@/llamalend/queries/add-collateral/ import { getAddCollateralHealthOptions } from '@/llamalend/queries/add-collateral/add-collateral-health.query' import { useAddCollateralPrices } from '@/llamalend/queries/add-collateral/add-collateral-prices.query' import { useUserState } from '@/llamalend/queries/user-state.query' -import { mapQuery } from '@/llamalend/queries/utils' +import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { collateralFormValidationSuite, type CollateralForm, } from '@/llamalend/queries/validation/manage-loan.validation' -import { Query } from '@/llamalend/widgets/manage-loan/loan.types' import type { IChainId as LlamaChainId } from '@curvefi/llamalend-api/lib/interfaces' import { vestResolver } from '@hookform/resolvers/vest' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' @@ -28,11 +27,6 @@ import { formDefaultOptions } from '@ui-kit/lib/model' import { decimal } from '@ui-kit/utils/decimal' import { useFormErrors } from '../../borrow/react-form.utils' -const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ - ...query, - tokenSymbol, -}) - const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts index 8e656d7c4..11a8a2fd0 100644 --- a/apps/main/src/llamalend/queries/utils.ts +++ b/apps/main/src/llamalend/queries/utils.ts @@ -22,3 +22,8 @@ export const getQueryState = (current: Query | undefined, previous: Que error: current?.error ?? previous?.error, loading: current?.isLoading || previous?.isLoading, }) + +export const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ + ...query, + tokenSymbol, +}) From ee3c1241b23a6bf004478ab87c4f9f02eb8ede00 Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 5 Dec 2025 19:02:40 +0100 Subject: [PATCH 30/52] fix: pass expectedBorrowed to loan to value calculation --- .../features/manage-loan/components/AddCollateralForm.tsx | 2 ++ 1 file changed, 2 insertions(+) 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 3cbfb1f88..92930294a 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -63,6 +63,7 @@ export const AddCollateralForm = ({ collateralToken, borrowToken, enabled: isOpen, + expectedBorrowed: userState.data?.debt, }) const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) @@ -75,6 +76,7 @@ export const AddCollateralForm = ({ borrowToken, enabled: !!enabled && !!values.userCollateral, collateralDelta: values.userCollateral, + expectedBorrowed: userState.data?.debt, }) return ( Date: Fri, 5 Dec 2025 19:03:07 +0100 Subject: [PATCH 31/52] refactor: expected collateral calculation --- .../features/manage-loan/hooks/useAddCollateralForm.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 0f17acbae..333d180e7 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -104,10 +104,8 @@ export const useAddCollateralForm = ({ { ...mapQuery(userState, (state) => state?.collateral), data: decimal( - values.userCollateral - ? new BigNumber(values.userCollateral) - .plus(userState.data?.collateral ? new BigNumber(userState.data?.collateral) : '0') - .toString() + values.userCollateral != null && userState.data?.collateral != null + ? new BigNumber(values.userCollateral).plus(new BigNumber(userState.data?.collateral)).toString() : null, ), }, From 40891a82aaf8894073941513416f20c5a9f6c657 Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 8 Dec 2025 12:06:37 +0100 Subject: [PATCH 32/52] feat: added prev LTV, LTV, expected collateral and prev health to Remove Collateral accordion --- .../components/AddCollateralForm.tsx | 2 +- .../components/RemoveCollateralForm.tsx | 74 ++++++++++++------- .../hooks/useRemoveCollateralForm.ts | 27 +++++++ 3 files changed, 75 insertions(+), 28 deletions(-) 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 92930294a..26066a493 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -66,7 +66,6 @@ export const AddCollateralForm = ({ expectedBorrowed: userState.data?.debt, }) const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) - const marketRates = useMarketRates(params, isOpen) const loanToValue = useLoanToValueFromUserState({ chainId: params.chainId!, @@ -78,6 +77,7 @@ export const AddCollateralForm = ({ collateralDelta: values.userCollateral, expectedBorrowed: userState.data?.debt, }) + return ( ({ maxRemovable, params, values, - bands, health, - prices, gas, formErrors, collateralToken, borrowToken, txHash, + userState, + expectedCollateral, } = useRemoveCollateralForm({ market, network, @@ -56,39 +58,57 @@ export const RemoveCollateralForm = ({ onRemoved, }) + const prevLoanToValue = useLoanToValueFromUserState({ + chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen, + expectedBorrowed: userState.data?.debt, + }) + const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) const marketRates = useMarketRates(params, isOpen) + const loanToValue = useLoanToValueFromUserState({ + chainId: params.chainId!, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: !!enabled && !!values.userCollateral, + collateralDelta: + values.userCollateral != null + ? (`-${values.userCollateral}` as unknown as import('@ui-kit/utils').Decimal) + : undefined, + expectedBorrowed: userState.data?.debt, + }) return ( } > }> ({ /> - - ({ handledErrors={['userCollateral']} successTitle={t`Collateral removed`} /> + + ) } diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 33ee05f51..3cf6c85fe 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { useEffect, useMemo } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useForm } from 'react-hook-form' @@ -14,6 +15,8 @@ import { useRemoveCollateralEstimateGas } from '@/llamalend/queries/remove-colla import { getRemoveCollateralHealthOptions } from '@/llamalend/queries/remove-collateral/remove-collateral-health.query' import { useMaxRemovableCollateral } from '@/llamalend/queries/remove-collateral/remove-collateral-max-removable.query' import { useRemoveCollateralPrices } from '@/llamalend/queries/remove-collateral/remove-collateral-prices.query' +import { useUserState } from '@/llamalend/queries/user-state.query' +import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { collateralFormValidationSuite, @@ -24,6 +27,7 @@ import { vestResolver } from '@hookform/resolvers/vest' import type { BaseConfig } from '@ui/utils' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' import { formDefaultOptions } from '@ui-kit/lib/model' +import { decimal } from '@ui-kit/utils/decimal' import { useFormErrors } from '../../borrow/react-form.utils' const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => @@ -87,12 +91,33 @@ export const useRemoveCollateralForm = < useCallbackAfterFormUpdate(form, action.reset) + const userState = useUserState(params, enabled) const maxRemovable = useMaxRemovableCollateral(params, enabled) const bands = useRemoveCollateralBands(params, enabled) const health = useHealthQueries((isFull) => getRemoveCollateralHealthOptions({ ...params, isFull }, enabled)) const prices = useRemoveCollateralPrices(params, enabled) const gas = useRemoveCollateralEstimateGas(networks, params, enabled) + const expectedCollateral = useMemo( + () => + withTokenSymbol( + { + ...mapQuery(userState, (state) => state?.collateral), + data: decimal( + userState.data?.collateral != null && values.userCollateral != null + ? // An error will be thrown by the validation suite if the user tries to remove more collateral than they have + BigNumber.max( + new BigNumber(userState.data?.collateral).minus(new BigNumber(values.userCollateral)), + '0', + ).toString() + : null, + ), + }, + collateralToken?.symbol, + ), + [collateralToken?.symbol, userState, values.userCollateral], + ) + const formErrors = useFormErrors(form.formState) return { @@ -111,5 +136,7 @@ export const useRemoveCollateralForm = < collateralToken, borrowToken, formErrors, + userState, + expectedCollateral, } } From 65b645ee063a332d94dfb54244a299e77b50662a Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 8 Dec 2025 12:08:59 +0100 Subject: [PATCH 33/52] feat: custom position balance to LoanFormTokenInput --- .../components/RemoveCollateralForm.tsx | 5 ++++ .../manage-loan/LoanFormTokenInput.tsx | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) 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 6d6b09346..21cd29e60 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -13,6 +13,7 @@ import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' +import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import { Balance } from '@ui-kit/shared/ui/Balance' import { InputDivider } from '../../../widgets/InputDivider' import { setValueOptions } from '../../borrow/react-form.utils' @@ -116,6 +117,10 @@ export const RemoveCollateralForm = ({ max={maxRemovable} testId="remove-collateral-input" network={network} + fromPosition={{ + tooltip: t`Collateral Balance`, + prefix: LlamaIcon, + }} message={ + /** * A large token input field for loan forms, with balance and max handling. */ @@ -30,6 +33,7 @@ export const LoanFormTokenInput = < testId, message, network, + fromPosition, }: { label: string token: { address: Address; symbol?: string } | undefined @@ -43,15 +47,26 @@ export const LoanFormTokenInput = < form: UseFormReturn // the form, used to set the value and get errors testId: string message?: ReactNode + /** + * Optional object, that display the position balance instead of the wallet balance. + * The data is taken from `max` prop, works only if `max` is not undefined. + */ + fromPosition?: { + tooltip: WalletBalanceProps['tooltip'] + prefix: WalletBalanceProps['prefix'] + } + /** + * The network of the token. + */ network: LlamaNetwork }) => { + fromPosition = max ? fromPosition : undefined const { address: userAddress } = useAccount() const { data: balance, isLoading: isBalanceLoading, error: balanceError, } = useTokenBalance({ chainId: network?.chainId, userAddress }, token) - const { data: usdRate } = useTokenUsdRate({ chainId: network?.chainId, tokenAddress: token?.address, @@ -60,12 +75,14 @@ export const LoanFormTokenInput = < const walletBalance = useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput () => ({ - balance, + balance: fromPosition ? max?.data : balance, symbol: token?.symbol, - loading: isBalanceLoading, + loading: fromPosition ? max?.isLoading : isBalanceLoading, usdRate, + tooltip: fromPosition?.tooltip, + prefix: fromPosition?.prefix, }), - [balance, isBalanceLoading, token?.symbol, usdRate], + [balance, isBalanceLoading, max, token?.symbol, usdRate, fromPosition], ) const errors = form.formState.errors as PartialRecord, Error> From 8661665607ce3c4ce1f9b6d0292e19a702cc4255 Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 8 Dec 2025 12:09:18 +0100 Subject: [PATCH 34/52] feat: custom position balance to LTI stories --- .../ui/stories/LargeTokenInput.stories.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx b/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx index e47564f88..46213767b 100644 --- a/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx +++ b/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx @@ -2,6 +2,7 @@ import { useRef, useState } from 'react' import { fn } from 'storybook/test' import { Select, MenuItem, Typography, Stack } from '@mui/material' import type { Meta, StoryObj } from '@storybook/react-vite' +import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import type { Decimal } from '@ui-kit/utils' import { LargeTokenInput, type LargeTokenInputRef, type LargeTokenInputProps } from '../LargeTokenInput' @@ -180,6 +181,26 @@ export const WithChipsCustom: Story = { }, } +export const WithMarketPosition: Story = { + args: { + walletBalance: { + symbol: TOKEN_OPTIONS[1].walletBalance.symbol, + balance: TOKEN_OPTIONS[1].walletBalance.balance, + notionalValueUsd: TOKEN_OPTIONS[1].walletBalance.notionalValueUsd, + prefix: LlamaIcon, + tooltip: 'Collateral Balance', + }, + }, + render: (args) => , + parameters: { + docs: { + description: { + story: 'Large token input with a custom input chip', + }, + }, + }, +} + export const WithoutMessage: Story = { args: { message: undefined, From cd580637c9bc5199b8222b65f02d39be5ba308b1 Mon Sep 17 00:00:00 2001 From: Pearce Date: Mon, 8 Dec 2025 12:33:36 +0100 Subject: [PATCH 35/52] refactor: position balance --- .../components/RemoveCollateralForm.tsx | 5 ++-- .../manage-loan/LoanFormTokenInput.tsx | 23 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) 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 21cd29e60..784667684 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -117,8 +117,9 @@ export const RemoveCollateralForm = ({ max={maxRemovable} testId="remove-collateral-input" network={network} - fromPosition={{ - tooltip: t`Collateral Balance`, + positionBalance={{ + position: maxRemovable, + tooltip: t`Max Removable Collateral`, prefix: LlamaIcon, }} message={ diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index b70adf99a..3bde8ab3f 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -33,7 +33,7 @@ export const LoanFormTokenInput = < testId, message, network, - fromPosition, + positionBalance, }: { label: string token: { address: Address; symbol?: string } | undefined @@ -48,19 +48,18 @@ export const LoanFormTokenInput = < testId: string message?: ReactNode /** - * Optional object, that display the position balance instead of the wallet balance. - * The data is taken from `max` prop, works only if `max` is not undefined. + * Optional, displays the position balance instead of the wallet balance. */ - fromPosition?: { - tooltip: WalletBalanceProps['tooltip'] - prefix: WalletBalanceProps['prefix'] + positionBalance?: { + position: Query + tooltip?: WalletBalanceProps['tooltip'] + prefix?: WalletBalanceProps['prefix'] } /** * The network of the token. */ network: LlamaNetwork }) => { - fromPosition = max ? fromPosition : undefined const { address: userAddress } = useAccount() const { data: balance, @@ -75,14 +74,14 @@ export const LoanFormTokenInput = < const walletBalance = useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput () => ({ - balance: fromPosition ? max?.data : balance, + balance: positionBalance ? positionBalance.position.data : balance, symbol: token?.symbol, - loading: fromPosition ? max?.isLoading : isBalanceLoading, + loading: positionBalance ? positionBalance.position.isLoading : isBalanceLoading, usdRate, - tooltip: fromPosition?.tooltip, - prefix: fromPosition?.prefix, + tooltip: positionBalance?.tooltip, + prefix: positionBalance?.prefix, }), - [balance, isBalanceLoading, max, token?.symbol, usdRate, fromPosition], + [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance], ) const errors = form.formState.errors as PartialRecord, Error> From 0aefe06b81191810075f8223effcb051d85a8244 Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 9 Dec 2025 15:42:45 +0100 Subject: [PATCH 36/52] fix: position balance array deps freezing loan form token input --- .../widgets/manage-loan/LoanFormTokenInput.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index 3bde8ab3f..3f54fec77 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -71,17 +71,27 @@ export const LoanFormTokenInput = < tokenAddress: token?.address, }) + const position = positionBalance?.position const walletBalance = useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput () => ({ - balance: positionBalance ? positionBalance.position.data : balance, + balance: position ? position.data : balance, symbol: token?.symbol, - loading: positionBalance ? positionBalance.position.isLoading : isBalanceLoading, + loading: position ? position.isLoading : isBalanceLoading, usdRate, tooltip: positionBalance?.tooltip, prefix: positionBalance?.prefix, }), - [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance], + [ + balance, + isBalanceLoading, + token?.symbol, + usdRate, + positionBalance?.tooltip, + positionBalance?.prefix, + position?.data, + position?.isLoading, + ], ) const errors = form.formState.errors as PartialRecord, Error> From 7832bd6e29391d30ee4887244f8d968811eb85da Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 9 Dec 2025 15:58:43 +0100 Subject: [PATCH 37/52] refactor: collateral form validation suite with custom position for add adn remove form --- .../manage-loan/hooks/useAddCollateralForm.ts | 12 ++++++------ .../manage-loan/hooks/useRemoveCollateralForm.ts | 4 ++-- .../queries/validation/borrow-fields.validation.ts | 3 ++- .../queries/validation/manage-loan.validation.ts | 10 +++++++++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 333d180e7..e722884d6 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -16,7 +16,7 @@ import { useUserState } from '@/llamalend/queries/user-state.query' import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { - collateralFormValidationSuite, + addCollateralFormValidationSuite, type CollateralForm, } from '@/llamalend/queries/validation/manage-loan.validation' import type { IChainId as LlamaChainId } from '@curvefi/llamalend-api/lib/interfaces' @@ -54,7 +54,7 @@ export const useAddCollateralForm = ({ const form = useForm({ ...formDefaultOptions, - resolver: vestResolver(collateralFormValidationSuite), + resolver: vestResolver(addCollateralFormValidationSuite), defaultValues: { userCollateral: undefined, maxCollateral: undefined, @@ -88,10 +88,6 @@ export const useAddCollateralForm = ({ useCallbackAfterFormUpdate(form, action.reset) - useEffect(() => { - form.setValue('maxCollateral', maxCollateral, { shouldValidate: true }) - }, [form, maxCollateral]) - const userState = useUserState(params, enabled) const bands = useAddCollateralBands(params, enabled) const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) @@ -116,6 +112,10 @@ export const useAddCollateralForm = ({ const formErrors = useFormErrors(form.formState) + useEffect(() => { + form.setValue('maxCollateral', maxCollateral, { shouldValidate: true }) + }, [form, maxCollateral]) + return { form, values, diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 3cf6c85fe..c54bae4b5 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -19,7 +19,7 @@ import { useUserState } from '@/llamalend/queries/user-state.query' import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { - collateralFormValidationSuite, + removeCollateralFormValidationSuite, type CollateralForm, } from '@/llamalend/queries/validation/manage-loan.validation' import type { IChainId as LlamaChainId, INetworkName as LlamaNetworkId } from '@curvefi/llamalend-api/lib/interfaces' @@ -59,7 +59,7 @@ export const useRemoveCollateralForm = < const form = useForm({ ...formDefaultOptions, - resolver: vestResolver(collateralFormValidationSuite), + resolver: vestResolver(removeCollateralFormValidationSuite), defaultValues: { userCollateral: undefined, maxCollateral: undefined, 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 fc6375e9e..4ba9e40a9 100644 --- a/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts +++ b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts @@ -47,8 +47,9 @@ export const validateMaxDebt = (debt: Decimal | undefined | null, maxDebt: Decim export const validateMaxCollateral = ( userCollateral: Decimal | undefined | null, maxCollateral: Decimal | undefined | null, + errorMessage?: string, ) => { - test('userCollateral', 'Collateral must be less than or equal to your wallet balance', () => { + test('userCollateral', errorMessage ?? 'Collateral must be less than or equal to your wallet balance', () => { if (userCollateral != null && maxCollateral != null) { enforce(userCollateral).lte(maxCollateral) } 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 5ef0b4bf4..3ccc049d2 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.validation.ts @@ -38,11 +38,19 @@ export const collateralValidationSuite = createValidationSuite((params: Collater collateralValidationGroup(params), ) -export const collateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { +export const addCollateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { validateUserCollateral(params.userCollateral, false) validateMaxCollateral(params.userCollateral, params.maxCollateral) }) +export const removeCollateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { + validateUserCollateral(params.userCollateral, false) + validateMaxCollateral( + params.userCollateral, + params.maxCollateral, + 'Collateral must be less than or equal to your position balance', + ) +}) export const collateralHealthValidationSuite = createValidationSuite(({ isFull, ...rest }: CollateralHealthParams) => { collateralValidationGroup(rest) validateIsFull(isFull) From cecc4930458d3c585da843bcc66aeeed42f75c8d Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 10 Dec 2025 12:20:46 +0100 Subject: [PATCH 38/52] fix: collateral sub tabs naming and color --- apps/main/src/lend/components/PageLoanManage/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/main/src/lend/components/PageLoanManage/index.tsx b/apps/main/src/lend/components/PageLoanManage/index.tsx index 6fbdbfb48..83a6a659b 100644 --- a/apps/main/src/lend/components/PageLoanManage/index.tsx +++ b/apps/main/src/lend/components/PageLoanManage/index.tsx @@ -28,8 +28,8 @@ const tabsLoan: TabOption[] = [ ] const tabsCollateral: TabOption[] = [ - { value: 'collateral-increase', label: t`Add collateral` }, - { value: 'collateral-decrease', label: t`Remove collateral` }, + { value: 'collateral-increase', label: t`Add` }, + { value: 'collateral-decrease', label: t`Remove` }, ] const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) => { @@ -81,6 +81,7 @@ const ManageLoan = (pageProps: PageContentProps & { params: MarketUrlParams }) = onChange={setSubTab} options={subTabs} fullWidth + sx={{ backgroundColor: (t) => t.design.Layer[1].Fill }} /> {subTab === 'loan-increase' && ( From a23fcabd1bd7104fec7e3ae888688bd11ae9e396 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 10 Dec 2025 15:44:33 +0100 Subject: [PATCH 39/52] fix: token input freeze by enabling queries with form.isValid --- .../components/RemoveCollateralForm.tsx | 8 ++--- .../hooks/useRemoveCollateralForm.ts | 30 ++++++++++++------- .../manage-loan/LoanFormTokenInput.tsx | 11 +------ 3 files changed, 24 insertions(+), 25 deletions(-) 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 956581a0a..f61399fd8 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -15,6 +15,7 @@ import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import { Balance } from '@ui-kit/shared/ui/Balance' +import { Decimal } from '@ui-kit/utils/decimal' import { InputDivider } from '../../../widgets/InputDivider' import { setValueOptions } from '../../borrow/react-form.utils' import { useRemoveCollateralForm } from '../hooks/useRemoveCollateralForm' @@ -76,11 +77,8 @@ export const RemoveCollateralForm = ({ userAddress: params.userAddress, collateralToken, borrowToken, - enabled: !!enabled && !!values.userCollateral, - collateralDelta: - values.userCollateral != null - ? (`-${values.userCollateral}` as unknown as import('@ui-kit/utils').Decimal) - : undefined, + enabled: !!enabled && form.formState.isValid, + collateralDelta: values.userCollateral != null ? (`-${values.userCollateral}` as Decimal) : undefined, expectedBorrowed: userState.data?.debt, }) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 25a9c112c..0a5b937f1 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -67,17 +67,20 @@ export const useRemoveCollateralForm = < }) const values = form.watch() + const { isValid } = form.formState - const params = useDebouncedValue( + const { params, isValid: debouncedIsValid } = useDebouncedValue( useMemo( - () => - ({ + () => ({ + params: { chainId, marketId, userAddress, userCollateral: values.userCollateral, - }) as CollateralParams, - [chainId, marketId, userAddress, values.userCollateral], + } as CollateralParams, + isValid, + }), + [chainId, marketId, userAddress, values.userCollateral, isValid], ), ) @@ -93,10 +96,13 @@ export const useRemoveCollateralForm = < const userState = useUserState(params, enabled) const maxRemovable = useMaxRemovableCollateral(params, enabled) - const bands = useRemoveCollateralBands(params, enabled) - const health = useHealthQueries((isFull) => getRemoveCollateralHealthOptions({ ...params, isFull }, enabled)) - const prices = useRemoveCollateralPrices(params, enabled) - const gas = useRemoveCollateralEstimateGas(networks, params, enabled) + + const bands = useRemoveCollateralBands(params, enabled && debouncedIsValid) + const health = useHealthQueries((isFull) => + getRemoveCollateralHealthOptions({ ...params, isFull }, enabled && debouncedIsValid), + ) + const prices = useRemoveCollateralPrices(params, enabled && debouncedIsValid) + const gas = useRemoveCollateralEstimateGas(networks, params, enabled && debouncedIsValid) const expectedCollateral = useMemo( () => @@ -105,7 +111,7 @@ export const useRemoveCollateralForm = < ...mapQuery(userState, (state) => state?.collateral), data: decimal( userState.data?.collateral != null && values.userCollateral != null - ? // An error will be thrown by the validation suite if the user tries to remove more collateral than they have + ? // An error will be thrown by the validation suite, this is just for preventing negative collateral in the UI BigNumber.max( new BigNumber(userState.data?.collateral).minus(new BigNumber(values.userCollateral)), '0', @@ -120,6 +126,10 @@ export const useRemoveCollateralForm = < const formErrors = useFormErrors(form.formState) + useEffect(() => { + form.setValue('maxCollateral', maxRemovable.data, { shouldValidate: true }) + }, [form, maxRemovable.data]) + return { form, values, diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index 1df410843..78fd80055 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -82,16 +82,7 @@ export const LoanFormTokenInput = < tooltip: positionBalance?.tooltip, prefix: positionBalance?.prefix, }), - [ - balance, - isBalanceLoading, - token?.symbol, - usdRate, - positionBalance?.tooltip, - positionBalance?.prefix, - position?.data, - position?.isLoading, - ], + [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance?.tooltip, positionBalance?.prefix, position], ) const errors = form.formState.errors as PartialRecord, Error> From e8052eb5c2c0ca8875449deafb61fd26d20b9794 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 10 Dec 2025 16:34:23 +0100 Subject: [PATCH 40/52] fix: enable new LTV query when user collateral value exist --- .../features/manage-loan/components/RemoveCollateralForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f61399fd8..6567885e9 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -77,7 +77,7 @@ export const RemoveCollateralForm = ({ userAddress: params.userAddress, collateralToken, borrowToken, - enabled: !!enabled && form.formState.isValid, + enabled: !!enabled && !!values.userCollateral && form.formState.isValid, collateralDelta: values.userCollateral != null ? (`-${values.userCollateral}` as Decimal) : undefined, expectedBorrowed: userState.data?.debt, }) From 0a4a7339698d1750c8e9f91c8aa195e35d8a4b16 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 10 Dec 2025 18:43:39 +0100 Subject: [PATCH 41/52] refactor: hooks moved into useAddCollateralForm --- apps/main/src/lend/utils/helpers.ts | 8 ---- .../components/AddCollateralForm.tsx | 32 +++------------- .../manage-loan/hooks/useAddCollateralForm.ts | 37 +++++++++++++++++-- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/apps/main/src/lend/utils/helpers.ts b/apps/main/src/lend/utils/helpers.ts index 72f14081f..141a72dd6 100644 --- a/apps/main/src/lend/utils/helpers.ts +++ b/apps/main/src/lend/utils/helpers.ts @@ -30,14 +30,6 @@ export function getErrorMessage(error: CustomError, defaultErrorMessage: string) return errorMessage } -export function scrollToTop() { - window.scroll({ - top: 0, - left: 0, - behavior: 'smooth', - }) -} - export function fulfilledValue(result: PromiseSettledResult) { if (result.status === 'fulfilled') { return result.value 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 5203ab159..bc4596000 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -1,9 +1,5 @@ -import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' -import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { AddCollateralOptions } from '@/llamalend/mutations/add-collateral.mutation' -import { useMarketRates } from '@/llamalend/queries/market-rates' -import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' @@ -37,7 +33,6 @@ export const AddCollateralForm = ({ isPending, onSubmit, action, - params, values, health, gas, @@ -48,36 +43,19 @@ export const AddCollateralForm = ({ txHash, userState, expectedCollateral, + prevHealth, + marketRates, + prevLoanToValue, + loanToValue, } = useAddCollateralForm({ market, network, networks, enabled, onAdded, + isAccordionOpen: isOpen, }) - const prevLoanToValue = useLoanToValueFromUserState({ - chainId, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: isOpen, - expectedBorrowed: userState.data?.debt, - }) - const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) - - const marketRates = useMarketRates(params, isOpen) - const loanToValue = useLoanToValueFromUserState({ - chainId: params.chainId!, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: !!enabled && !!values.userCollateral, - collateralDelta: values.userCollateral, - expectedBorrowed: userState.data?.debt, - }) return ( , callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) @@ -36,12 +39,14 @@ export const useAddCollateralForm = ({ networks, enabled, onAdded, + isAccordionOpen, }: { market: LlamaMarketTemplate | undefined network: LlamaNetwork networks: NetworkDict enabled?: boolean onAdded: NonNullable + isAccordionOpen: boolean }) => { const { address: userAddress } = useConnection() const { chainId } = network @@ -93,10 +98,31 @@ export const useAddCollateralForm = ({ }, [form, maxCollateral]) const userState = useUserState(params, enabled) - const bands = useAddCollateralBands(params, enabled) - const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) const prices = useAddCollateralPrices(params, enabled) + const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) const gas = useAddCollateralEstimateGas(networks, params, enabled) + const bands = useAddCollateralBands(params, enabled && isAccordionOpen) + const prevLoanToValue = useLoanToValueFromUserState({ + chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isAccordionOpen, + expectedBorrowed: userState.data?.debt, + }) + const loanToValue = useLoanToValueFromUserState({ + chainId: params.chainId!, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isAccordionOpen && !!values.userCollateral, + collateralDelta: values.userCollateral, + expectedBorrowed: userState.data?.debt, + }) + const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, enabled)) + const marketRates = useMarketRates(params, isAccordionOpen) const expectedCollateral = useMemo( () => @@ -119,20 +145,23 @@ export const useAddCollateralForm = ({ return { form, values, - params, isPending: form.formState.isSubmitting || action.isPending, onSubmit: form.handleSubmit(onSubmit), action, bands, + prevHealth, health, + prevLoanToValue, + loanToValue, + marketRates, prices, gas, isApproved, formErrors, collateralToken, + expectedCollateral, borrowToken, txHash: action.data?.hash, userState, - expectedCollateral, } } From 89fc65b0e2434e490bf09db886c117097a08f7e6 Mon Sep 17 00:00:00 2001 From: Pearce Date: Wed, 10 Dec 2025 19:16:16 +0100 Subject: [PATCH 42/52] refactor: hooks moved into useRemoveCollateralForm --- .../components/RemoveCollateralForm.tsx | 34 +++------------- .../manage-loan/hooks/useAddCollateralForm.ts | 2 +- .../hooks/useRemoveCollateralForm.ts | 40 +++++++++++++++++-- 3 files changed, 42 insertions(+), 34 deletions(-) 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 6567885e9..c0bb8ec41 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -1,9 +1,5 @@ -import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' -import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' import type { RemoveCollateralOptions } from '@/llamalend/mutations/remove-collateral.mutation' -import { useMarketRates } from '@/llamalend/queries/market-rates' -import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' import { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' import { LoanFormWrapper } from '@/llamalend/widgets/manage-loan/LoanFormWrapper' @@ -15,7 +11,6 @@ import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import { Balance } from '@ui-kit/shared/ui/Balance' -import { Decimal } from '@ui-kit/utils/decimal' import { InputDivider } from '../../../widgets/InputDivider' import { setValueOptions } from '../../borrow/react-form.utils' import { useRemoveCollateralForm } from '../hooks/useRemoveCollateralForm' @@ -42,8 +37,6 @@ export const RemoveCollateralForm = ({ onSubmit, action, maxRemovable, - params, - values, health, gas, formErrors, @@ -52,34 +45,17 @@ export const RemoveCollateralForm = ({ txHash, userState, expectedCollateral, + prevHealth, + marketRates, + prevLoanToValue, + loanToValue, } = useRemoveCollateralForm({ market, network, networks, enabled, onRemoved, - }) - - const prevLoanToValue = useLoanToValueFromUserState({ - chainId, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: isOpen, - expectedBorrowed: userState.data?.debt, - }) - const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, undefined)) - const marketRates = useMarketRates(params, isOpen) - const loanToValue = useLoanToValueFromUserState({ - chainId: params.chainId!, - marketId: params.marketId, - userAddress: params.userAddress, - collateralToken, - borrowToken, - enabled: !!enabled && !!values.userCollateral && form.formState.isValid, - collateralDelta: values.userCollateral != null ? (`-${values.userCollateral}` as Decimal) : undefined, - expectedBorrowed: userState.data?.debt, + isAccordionOpen: isOpen, }) return ( diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 8c9c03968..53a793a54 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -97,6 +97,7 @@ export const useAddCollateralForm = ({ const prices = useAddCollateralPrices(params, enabled) const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) const gas = useAddCollateralEstimateGas(networks, params, enabled) + const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, enabled)) const bands = useAddCollateralBands(params, enabled && isAccordionOpen) const prevLoanToValue = useLoanToValueFromUserState({ chainId, @@ -117,7 +118,6 @@ export const useAddCollateralForm = ({ collateralDelta: values.userCollateral, expectedBorrowed: userState.data?.debt, }) - const prevHealth = useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }, enabled)) const marketRates = useMarketRates(params, isAccordionOpen) const expectedCollateral = useMemo( diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 0a5b937f1..24cebfbb3 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -10,11 +10,13 @@ import { type RemoveCollateralOptions, useRemoveCollateralMutation, } from '@/llamalend/mutations/remove-collateral.mutation' +import { useMarketRates } from '@/llamalend/queries/market-rates' import { useRemoveCollateralBands } from '@/llamalend/queries/remove-collateral/remove-collateral-bands.query' import { useRemoveCollateralEstimateGas } from '@/llamalend/queries/remove-collateral/remove-collateral-gas-estimate.query' import { getRemoveCollateralHealthOptions } from '@/llamalend/queries/remove-collateral/remove-collateral-health.query' import { useMaxRemovableCollateral } from '@/llamalend/queries/remove-collateral/remove-collateral-max-removable.query' import { useRemoveCollateralPrices } from '@/llamalend/queries/remove-collateral/remove-collateral-prices.query' +import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' import { useUserState } from '@/llamalend/queries/user-state.query' import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' @@ -27,8 +29,10 @@ import { vestResolver } from '@hookform/resolvers/vest' import type { BaseConfig } from '@ui/utils' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' import { formDefaultOptions } from '@ui-kit/lib/model' -import { decimal } from '@ui-kit/utils/decimal' +import { Decimal, decimal } from '@ui-kit/utils/decimal' import { useFormErrors } from '../../borrow/react-form.utils' +import { useLoanToValueFromUserState } from './useLoanToValueFromUserState' + const useCallbackAfterFormUpdate = (form: UseFormReturn, callback: () => void) => useEffect(() => form.subscribe({ formState: { values: true }, callback }), [form, callback]) @@ -42,12 +46,14 @@ export const useRemoveCollateralForm = < networks, enabled, onRemoved, + isAccordionOpen, }: { market: LlamaMarketTemplate | undefined network: BaseConfig networks: NetworkDict enabled?: boolean onRemoved: NonNullable + isAccordionOpen: boolean }) => { const { address: userAddress } = useConnection() const { chainId } = network @@ -95,14 +101,36 @@ export const useRemoveCollateralForm = < useCallbackAfterFormUpdate(form, action.reset) const userState = useUserState(params, enabled) + const prices = useRemoveCollateralPrices(params, enabled && debouncedIsValid) const maxRemovable = useMaxRemovableCollateral(params, enabled) - - const bands = useRemoveCollateralBands(params, enabled && debouncedIsValid) const health = useHealthQueries((isFull) => getRemoveCollateralHealthOptions({ ...params, isFull }, enabled && debouncedIsValid), ) - const prices = useRemoveCollateralPrices(params, enabled && debouncedIsValid) const gas = useRemoveCollateralEstimateGas(networks, params, enabled && debouncedIsValid) + const prevHealth = useHealthQueries((isFull) => + getUserHealthOptions({ ...params, isFull }, enabled && debouncedIsValid), + ) + const bands = useRemoveCollateralBands(params, enabled && isAccordionOpen && debouncedIsValid) + const prevLoanToValue = useLoanToValueFromUserState({ + chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isAccordionOpen && debouncedIsValid, + expectedBorrowed: userState.data?.debt, + }) + const loanToValue = useLoanToValueFromUserState({ + chainId: params.chainId!, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isAccordionOpen && !!values.userCollateral && debouncedIsValid, + collateralDelta: values.userCollateral != null ? (`-${values.userCollateral}` as Decimal) : undefined, + expectedBorrowed: userState.data?.debt, + }) + const marketRates = useMarketRates(params, isAccordionOpen) const expectedCollateral = useMemo( () => @@ -148,5 +176,9 @@ export const useRemoveCollateralForm = < formErrors, userState, expectedCollateral, + prevHealth, + marketRates, + prevLoanToValue, + loanToValue, } } From c88be3b2f39e7d49b7766b0079b69aed370a262a Mon Sep 17 00:00:00 2001 From: Pearce Date: Fri, 12 Dec 2025 16:21:22 +0100 Subject: [PATCH 43/52] refactor: LoanInfoAccordion ternary and gaps --- .../widgets/manage-loan/LoanInfoAccordion.tsx | 148 +++++++++--------- .../manage-loan/LoanLeverageActionInfo.tsx | 5 +- 2 files changed, 75 insertions(+), 78 deletions(-) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 5352bab20..a7b3c7e79 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -50,8 +50,6 @@ type LoanInfoAccordionProps = { leverage?: LoanLeverageActionInfoProps & { enabled: boolean } } -const AccordionSpacing = () => - export const LoanInfoAccordion = ({ isOpen, toggle, @@ -92,84 +90,82 @@ export const LoanInfoAccordion = ({ expanded={isOpen} toggle={toggle} > - - {(debt || prevDebt) && ( - formatNumber(v, { abbreviate: false }))} - prevValue={prevDebt ? formatNumber(prevDebt, { abbreviate: false }) : undefined} - {...getQueryState(debt, userState)} - valueRight={debt?.tokenSymbol ?? userState?.borrowTokenSymbol} - testId="borrow-debt" - /> - )} - {(collateral || prevCollateral) && ( - formatNumber(v, { abbreviate: false }))} - prevValue={prevCollateral ? formatNumber(prevCollateral, { abbreviate: false }) : undefined} - {...getQueryState(collateral, userState)} - valueRight={collateral?.tokenSymbol ?? userState?.collateralTokenSymbol} - testId="borrow-collateral" - /> - )} - {bands && ( - - )} - {prices && ( + + + {(debt || prevDebt) && ( + formatNumber(v, { abbreviate: false }))} + prevValue={prevDebt && formatNumber(prevDebt, { abbreviate: false })} + {...getQueryState(debt, userState)} + valueRight={debt?.tokenSymbol ?? userState?.borrowTokenSymbol} + testId="borrow-debt" + /> + )} + {(collateral || prevCollateral) && ( + formatNumber(v, { abbreviate: false }))} + prevValue={prevCollateral && formatNumber(prevCollateral, { abbreviate: false })} + {...getQueryState(collateral, userState)} + valueRight={collateral?.tokenSymbol ?? userState?.collateralTokenSymbol} + testId="borrow-collateral" + /> + )} + {bands && ( + + )} + {prices && ( + formatNumber(p, { abbreviate: false })).join(' - ')} + error={prices.error} + loading={prices.isLoading} + testId="borrow-price-range" + /> + )} + {range != null && ( + + )} formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} - error={prices.error} - loading={prices.isLoading} - testId="borrow-price-range" + label={t`Borrow APR`} + value={rates.data?.borrowApr && formatPercent(rates.data.borrowApr)} + prevValue={prevRates?.data?.borrowApr && formatPercent(prevRates.data.borrowApr)} + {...getQueryState(rates, prevRates)} + testId="borrow-apr" /> - )} - {range != null && ( - - )} - - {(loanToValue || prevLoanToValue) && ( + {(loanToValue || prevLoanToValue) && ( + + )} + + {leverage?.enabled && } + {/* TODO: add router provider and slippage */} + + {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} } /> - )} - {leverage?.enabled && ( - <> - - - - )} - {/* TODO: add router provider and slippage */} - - {/* TODO: add gas estimate steps (1. approve, 2. add collateral) */} - } - /> + diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanLeverageActionInfo.tsx index c0e8c6801..6b3760258 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 Stack from '@mui/material/Stack' import { t } from '@ui-kit/lib/i18n' import ActionInfo from '@ui-kit/shared/ui/ActionInfo' import type { Query } from '@ui-kit/types/util' @@ -42,7 +43,7 @@ export const LoanLeverageActionInfo = ({ const isHighImpact = priceImpactPercent != null && priceImpactPercent > +slippage return ( - <> + - + ) } From c390786ef63a4cbc9b21902c96c81762b1355fb8 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 12:13:53 +0100 Subject: [PATCH 44/52] feat: combine gas estimation for approval and add collateral if not approved --- .../add-collateral-gas-estimate.query.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts b/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts index c670f5b34..add3fe9d7 100644 --- a/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts +++ b/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts @@ -1,7 +1,7 @@ import { useEstimateGas } from '@/llamalend/hooks/useEstimateGas' import { getLlamaMarket } from '@/llamalend/llama.utils' import { type NetworkDict } from '@/llamalend/llamalend.types' -import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import type { IChainId, TGas } from '@curvefi/llamalend-api/lib/interfaces' import { type FieldsOf } from '@ui-kit/lib' import { queryFactory, rootKeys } from '@ui-kit/lib/model' import type { CollateralQuery } from '../validation/manage-loan.types' @@ -20,10 +20,16 @@ const { useQuery: useAddCollateralGasEstimate } = queryFactory({ queryFn: async ({ marketId, userCollateral }: AddCollateralGasQuery) => { const market = getLlamaMarket(marketId) const isApproved = await market.addCollateralIsApproved(userCollateral) - const result = isApproved - ? await market.estimateGas.addCollateral(userCollateral) - : await market.estimateGas.addCollateralApprove(userCollateral) - return result + + if (isApproved) { + return market.estimateGas.addCollateral(userCollateral) + } + // When not approved, sum both approval gas and addCollateral gas + const [approveGas, addCollateralGas] = await Promise.all([ + market.estimateGas.addCollateralApprove(userCollateral), + market.estimateGas.addCollateral(userCollateral), + ]) + return (Number(approveGas) + Number(addCollateralGas)) as TGas }, validationSuite: collateralValidationSuite, }) From fff1f4e0dc9c1d1e13ff712c6238c512f9601d52 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 12:23:48 +0100 Subject: [PATCH 45/52] refactor: deduce userState type from query --- apps/main/src/llamalend/queries/user-state.query.ts | 11 ++++------- packages/curve-ui-kit/src/lib/queries/types.ts | 3 +++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/main/src/llamalend/queries/user-state.query.ts b/apps/main/src/llamalend/queries/user-state.query.ts index 3c097905e..acb6e12d4 100644 --- a/apps/main/src/llamalend/queries/user-state.query.ts +++ b/apps/main/src/llamalend/queries/user-state.query.ts @@ -1,17 +1,12 @@ import { getLlamaMarket } from '@/llamalend/llama.utils' import { queryFactory, rootKeys, type UserMarketParams, type UserMarketQuery } from '@ui-kit/lib/model' import { userMarketValidationSuite } from '@ui-kit/lib/model/query/user-market-validation' +import type { QueryData } from '@ui-kit/lib/queries' import type { Decimal } from '@ui-kit/utils' -export type UserState = { - collateral: Decimal - stablecoin: Decimal - debt: Decimal -} - export const { useQuery: useUserState, invalidate: invalidateUserState } = queryFactory({ queryKey: (params: UserMarketParams) => [...rootKeys.userMarket(params), 'market-user-state'] as const, - queryFn: async ({ marketId, userAddress }: UserMarketQuery): Promise => { + queryFn: async ({ marketId, userAddress }: UserMarketQuery) => { const market = getLlamaMarket(marketId) const userState = await market.userState(userAddress) @@ -35,3 +30,5 @@ export const { useQuery: useUserState, invalidate: invalidateUserState } = query }, validationSuite: userMarketValidationSuite, }) + +export type UserState = QueryData diff --git a/packages/curve-ui-kit/src/lib/queries/types.ts b/packages/curve-ui-kit/src/lib/queries/types.ts index 70e5a3dd3..9b872e38d 100644 --- a/packages/curve-ui-kit/src/lib/queries/types.ts +++ b/packages/curve-ui-kit/src/lib/queries/types.ts @@ -10,3 +10,6 @@ export type PartialQueryResult = Pick< UseQueryResult, 'data' | 'isLoading' | 'isPending' | 'isError' | 'isFetching' > + +/** Extracts the data type from a useQuery hook */ +export type QueryData any> = NonNullable['data']> From 1cddcf3202dd06da89c3d233313e01ad849a4da8 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 12:25:16 +0100 Subject: [PATCH 46/52] fix: check for empty string in Action Info --- packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index 41d1d16ca..b3cbe784b 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -88,6 +88,8 @@ const valueSize = { large: 'headingSBold', } as const satisfies Record +const isSet = (v: ReactNode) => v != null && v !== '' + const ActionInfo = ({ label, labelColor, @@ -117,7 +119,7 @@ const ActionInfo = ({ }, [copyValue, openSnackbar]) const errorMessage = (typeof error === 'object' && error?.message) || (typeof error === 'string' && error) - const showPrevValue = value != null && prevValue != null + const showPrevValue = isSet(value) && isSet(prevValue) value ??= prevValue ?? emptyValue return ( From 7b9be3ac47d91ae983251356e1f3433542fdf4a1 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 15:08:47 +0100 Subject: [PATCH 47/52] refactor: tokenSymbol part of query's data --- .../manage-loan/hooks/useAddCollateralForm.ts | 20 ++++++++----------- apps/main/src/llamalend/queries/utils.ts | 19 ++++-------------- .../widgets/manage-loan/LoanInfoAccordion.tsx | 12 +++++------ 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 8b5f12f36..de518bacc 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -15,7 +15,7 @@ import { useAddCollateralPrices } from '@/llamalend/queries/add-collateral/add-c import { useMarketRates } from '@/llamalend/queries/market-rates' import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' import { useUserState } from '@/llamalend/queries/user-state.query' -import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' +import { mapQuery } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { collateralFormValidationSuite, @@ -126,17 +126,13 @@ export const useAddCollateralForm = ({ const expectedCollateral = useMemo( () => - withTokenSymbol( - { - ...mapQuery(userState, (state) => state?.collateral), - data: decimal( - values.userCollateral != null && userState.data?.collateral != null - ? new BigNumber(values.userCollateral).plus(new BigNumber(userState.data?.collateral)).toString() - : null, - ), - }, - collateralToken?.symbol, - ), + mapQuery(userState, (state) => { + const value = + values.userCollateral != null && + state?.collateral != null && + decimal(new BigNumber(values.userCollateral).plus(state.collateral).toString()) + return value ? { value, tokenSymbol: collateralToken?.symbol } : null + }), [collateralToken?.symbol, userState, values.userCollateral], ) diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts index 17e083951..9ae0e82ec 100644 --- a/apps/main/src/llamalend/queries/utils.ts +++ b/apps/main/src/llamalend/queries/utils.ts @@ -4,16 +4,10 @@ import type { Query } from '@ui-kit/types/util' * Maps a Query type to extract partial data from it. * Preserves error and loading states while transforming the data. */ -export function mapQuery( - query: Query, - selector: (data: TSource) => TResult, -): Query { - return { - data: query.data === undefined ? undefined : selector(query.data), - isLoading: query.isLoading, - error: query.error, - } -} +export const mapQuery = (query: Query, selector: (data: TSource) => TResult) => ({ + ...query, + data: query.data && selector(query.data), +}) export const formatQueryValue = (query: Query | undefined, format: (value: NonNullable) => string) => query?.data != null ? format(query.data as NonNullable) : undefined @@ -22,8 +16,3 @@ export const getQueryState = (current: Query | undefined, previous: Que error: current?.error ?? previous?.error, loading: current?.isLoading || previous?.isLoading, }) - -export const withTokenSymbol = (query: Query, tokenSymbol?: string) => ({ - ...query, - tokenSymbol, -}) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index a7b3c7e79..42ca089a9 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -43,8 +43,8 @@ type LoanInfoAccordionProps = { loanToValue: Query prevLoanToValue?: Query gas: Query - debt?: Query & { tokenSymbol?: string } - collateral?: Query & { tokenSymbol?: string } + debt?: Query<{ value: Decimal; tokenSymbol?: string } | null> + collateral?: Query<{ value: Decimal; tokenSymbol?: string } | null> // userState values are used as prev values if collateral or debt are available userState?: Query & { borrowTokenSymbol?: string; collateralTokenSymbol?: string } leverage?: LoanLeverageActionInfoProps & { enabled: boolean } @@ -95,20 +95,20 @@ export const LoanInfoAccordion = ({ {(debt || prevDebt) && ( formatNumber(v, { abbreviate: false }))} + value={formatQueryValue(debt, (v) => formatNumber(v.value, { abbreviate: false }))} prevValue={prevDebt && formatNumber(prevDebt, { abbreviate: false })} {...getQueryState(debt, userState)} - valueRight={debt?.tokenSymbol ?? userState?.borrowTokenSymbol} + valueRight={debt?.data?.tokenSymbol ?? userState?.borrowTokenSymbol} testId="borrow-debt" /> )} {(collateral || prevCollateral) && ( formatNumber(v, { abbreviate: false }))} + value={formatQueryValue(collateral, (v) => formatNumber(v.value, { abbreviate: false }))} prevValue={prevCollateral && formatNumber(prevCollateral, { abbreviate: false })} {...getQueryState(collateral, userState)} - valueRight={collateral?.tokenSymbol ?? userState?.collateralTokenSymbol} + valueRight={collateral?.data?.tokenSymbol ?? userState?.collateralTokenSymbol} testId="borrow-collateral" /> )} From 4ba0d569f0ff6306cdf1a7f76c036473f5e6b264 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 15:09:53 +0100 Subject: [PATCH 48/52] refactor: combine query state --- apps/main/src/llamalend/queries/utils.ts | 6 +++--- .../widgets/manage-loan/LoanInfoAccordion.tsx | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts index 9ae0e82ec..ee41c3dda 100644 --- a/apps/main/src/llamalend/queries/utils.ts +++ b/apps/main/src/llamalend/queries/utils.ts @@ -12,7 +12,7 @@ export const mapQuery = (query: Query, selector: (dat export const formatQueryValue = (query: Query | undefined, format: (value: NonNullable) => string) => query?.data != null ? format(query.data as NonNullable) : undefined -export const getQueryState = (current: Query | undefined, previous: Query | undefined) => ({ - error: current?.error ?? previous?.error, - loading: current?.isLoading || previous?.isLoading, +export const combineQueryState = (queries: (Query | undefined)[]) => ({ + error: queries && queries.reduce['error']>((acc, x) => acc ?? x?.error, undefined), + loading: queries && queries.reduce['isLoading']>((acc, x) => acc || !!x?.isLoading, false), }) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 42ca089a9..0843996ef 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -1,5 +1,5 @@ import { UserState } from '@/llamalend/queries/user-state.query' -import { formatQueryValue, getQueryState } from '@/llamalend/queries/utils' +import { combineQueryState, formatQueryValue } from '@/llamalend/queries/utils' import Box from '@mui/material/Box' import Stack from '@mui/material/Stack' import { useTheme } from '@mui/material/styles' @@ -82,7 +82,7 @@ export const LoanInfoAccordion = ({ value={formatQueryValue(health, (v) => formatNumber(v, { abbreviate: false }))} prevValue={formatQueryValue(prevHealth, (v) => formatNumber(v, { abbreviate: false }))} emptyValue="∞" - {...getQueryState(health, prevHealth)} + {...combineQueryState([health, prevHealth])} valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} testId="borrow-health" /> @@ -97,7 +97,7 @@ export const LoanInfoAccordion = ({ label={t`Debt`} value={formatQueryValue(debt, (v) => formatNumber(v.value, { abbreviate: false }))} prevValue={prevDebt && formatNumber(prevDebt, { abbreviate: false })} - {...getQueryState(debt, userState)} + {...combineQueryState([debt, userState])} valueRight={debt?.data?.tokenSymbol ?? userState?.borrowTokenSymbol} testId="borrow-debt" /> @@ -107,7 +107,7 @@ export const LoanInfoAccordion = ({ label={t`Collateral`} value={formatQueryValue(collateral, (v) => formatNumber(v.value, { abbreviate: false }))} prevValue={prevCollateral && formatNumber(prevCollateral, { abbreviate: false })} - {...getQueryState(collateral, userState)} + {...combineQueryState([collateral, userState])} valueRight={collateral?.data?.tokenSymbol ?? userState?.collateralTokenSymbol} testId="borrow-collateral" /> @@ -141,7 +141,7 @@ export const LoanInfoAccordion = ({ label={t`Borrow APR`} value={rates.data?.borrowApr && formatPercent(rates.data.borrowApr)} prevValue={prevRates?.data?.borrowApr && formatPercent(prevRates.data.borrowApr)} - {...getQueryState(rates, prevRates)} + {...combineQueryState([rates, prevRates])} testId="borrow-apr" /> {(loanToValue || prevLoanToValue) && ( @@ -149,7 +149,7 @@ export const LoanInfoAccordion = ({ label={t`Loan to value ratio`} value={formatQueryValue(loanToValue, formatPercent)} prevValue={formatQueryValue(prevLoanToValue, formatPercent)} - {...getQueryState(loanToValue, prevLoanToValue)} + {...combineQueryState([loanToValue, prevLoanToValue])} testId="borrow-ltv" /> )} From 64c86d0e59209f36806670c8f1befb09da413250 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 15:35:27 +0100 Subject: [PATCH 49/52] fix: tokenSymbol part of query's data --- .../hooks/useRemoveCollateralForm.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 0265f20da..3108ad410 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -18,7 +18,7 @@ import { useMaxRemovableCollateral } from '@/llamalend/queries/remove-collateral import { useRemoveCollateralPrices } from '@/llamalend/queries/remove-collateral/remove-collateral-prices.query' import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' import { useUserState } from '@/llamalend/queries/user-state.query' -import { mapQuery, withTokenSymbol } from '@/llamalend/queries/utils' +import { mapQuery } from '@/llamalend/queries/utils' import type { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' import { removeCollateralFormValidationSuite, @@ -133,21 +133,16 @@ export const useRemoveCollateralForm = < const expectedCollateral = useMemo( () => - withTokenSymbol( - { - ...mapQuery(userState, (state) => state?.collateral), - data: decimal( - userState.data?.collateral != null && values.userCollateral != null - ? // An error will be thrown by the validation suite, this is just for preventing negative collateral in the UI - BigNumber.max( - new BigNumber(userState.data?.collateral).minus(new BigNumber(values.userCollateral)), - '0', - ).toString() - : null, - ), - }, - collateralToken?.symbol, - ), + mapQuery(userState, (state) => { + // An error will be thrown by the validation suite, this is just for preventing negative collateral in the UI + const value = + state?.collateral != null && + values.userCollateral != null && + decimal( + BigNumber.max(new BigNumber(state.collateral).minus(new BigNumber(values.userCollateral)), '0').toString(), + ) + return value ? { value, tokenSymbol: collateralToken?.symbol } : null + }), [collateralToken?.symbol, userState, values.userCollateral], ) From 88f912a05adaaf4fcf5decd951c5f166552c9fd3 Mon Sep 17 00:00:00 2001 From: Pearce Date: Sat, 13 Dec 2025 16:10:19 +0100 Subject: [PATCH 50/52] refactor: hardcode LlamaIcon for position balance and improve code readability --- .../components/RemoveCollateralForm.tsx | 2 -- .../hooks/useRemoveCollateralForm.ts | 2 +- .../manage-loan/LoanFormTokenInput.tsx | 6 +++--- .../src/shared/ui/LargeTokenInput.tsx | 1 + .../ui/stories/LargeTokenInput.stories.tsx | 21 ------------------- 5 files changed, 5 insertions(+), 27 deletions(-) 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 c0bb8ec41..ba806d757 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -9,7 +9,6 @@ import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' -import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import { Balance } from '@ui-kit/shared/ui/Balance' import { InputDivider } from '../../../widgets/InputDivider' import { setValueOptions } from '../../borrow/react-form.utils' @@ -94,7 +93,6 @@ export const RemoveCollateralForm = ({ positionBalance={{ position: maxRemovable, tooltip: t`Max Removable Collateral`, - prefix: LlamaIcon, }} message={ tooltip?: WalletBalanceProps['tooltip'] - prefix?: WalletBalanceProps['prefix'] } /** * The network of the token. @@ -80,9 +80,9 @@ export const LoanFormTokenInput = < loading: position ? position.isLoading : isBalanceLoading, usdRate, tooltip: positionBalance?.tooltip, - prefix: positionBalance?.prefix, + prefix: position && LlamaIcon, }), - [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance?.tooltip, positionBalance?.prefix, position], + [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance?.tooltip, position], ) const errors = form.formState.errors as PartialRecord, Error> diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index c516047e2..991cc5db8 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -149,6 +149,7 @@ export type LargeTokenInputProps = { */ tokenSelector?: ReactNode + // TODO: rename to just "balance" because multipurpose now (walletBalance, positionBalance ...) /** Optional wallet balance configuration. */ walletBalance?: BalanceProps diff --git a/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx b/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx index 46213767b..e47564f88 100644 --- a/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx +++ b/packages/curve-ui-kit/src/shared/ui/stories/LargeTokenInput.stories.tsx @@ -2,7 +2,6 @@ import { useRef, useState } from 'react' import { fn } from 'storybook/test' import { Select, MenuItem, Typography, Stack } from '@mui/material' import type { Meta, StoryObj } from '@storybook/react-vite' -import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import type { Decimal } from '@ui-kit/utils' import { LargeTokenInput, type LargeTokenInputRef, type LargeTokenInputProps } from '../LargeTokenInput' @@ -181,26 +180,6 @@ export const WithChipsCustom: Story = { }, } -export const WithMarketPosition: Story = { - args: { - walletBalance: { - symbol: TOKEN_OPTIONS[1].walletBalance.symbol, - balance: TOKEN_OPTIONS[1].walletBalance.balance, - notionalValueUsd: TOKEN_OPTIONS[1].walletBalance.notionalValueUsd, - prefix: LlamaIcon, - tooltip: 'Collateral Balance', - }, - }, - render: (args) => , - parameters: { - docs: { - description: { - story: 'Large token input with a custom input chip', - }, - }, - }, -} - export const WithoutMessage: Story = { args: { message: undefined, From 7e6d82c01fe84e3d683559806cfc35180ce99977 Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 16 Dec 2025 21:02:31 +0100 Subject: [PATCH 51/52] refactor: improve code readibility for collateral form hooks + docs --- .../manage-loan/hooks/useAddCollateralForm.ts | 19 +++++++++------- .../hooks/useRemoveCollateralForm.ts | 22 ++++++++++--------- .../manage-loan/LoanFormTokenInput.tsx | 10 ++++----- .../curve-ui-kit/src/shared/ui/ActionInfo.tsx | 2 +- .../src/shared/ui/LargeTokenInput.tsx | 2 +- packages/curve-ui-kit/src/utils/decimal.ts | 4 ++-- 6 files changed, 32 insertions(+), 27 deletions(-) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts index 138b41b7b..f5e45bbf4 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -26,7 +26,7 @@ import { vestResolver } from '@hookform/resolvers/vest' import { useDebouncedValue } from '@ui-kit/hooks/useDebounce' import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' import { formDefaultOptions } from '@ui-kit/lib/model' -import { decimal } from '@ui-kit/utils/decimal' +import { Decimal, decimal } from '@ui-kit/utils/decimal' import { useFormErrors } from '../../borrow/react-form.utils' import { useLoanToValueFromUserState } from './useLoanToValueFromUserState' @@ -122,13 +122,16 @@ export const useAddCollateralForm = ({ const expectedCollateral = useMemo( () => - mapQuery(userState, (state) => { - const value = - values.userCollateral != null && - state?.collateral != null && - decimal(new BigNumber(values.userCollateral).plus(state.collateral).toString()) - return value ? { value, tokenSymbol: collateralToken?.symbol } : null - }), + mapQuery( + userState, + (state) => + (values.userCollateral && + state.collateral && { + value: decimal(new BigNumber(values.userCollateral).plus(state.collateral)) as Decimal, + tokenSymbol: collateralToken?.symbol, + }) ?? + null, + ), [collateralToken?.symbol, userState, values.userCollateral], ) diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts index 66e39cf09..20a4565d7 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -133,16 +133,18 @@ export const useRemoveCollateralForm = < const expectedCollateral = useMemo( () => - mapQuery(userState, (state) => { - // An error will be thrown by the validation suite, this is just for preventing negative collateral in the UI - const value = - state?.collateral != null && - values.userCollateral != null && - decimal( - BigNumber.max(new BigNumber(state.collateral).minus(new BigNumber(values.userCollateral)), '0').toString(), - ) - return value ? { value, tokenSymbol: collateralToken?.symbol } : null - }), + // An error will be thrown by the validation suite, the "max" is just for preventing negative collateral in the UI + mapQuery( + userState, + (state) => + state.collateral && + values.userCollateral && { + value: decimal( + BigNumber.max('0', new BigNumber(state.collateral).minus(new BigNumber(values.userCollateral))), + ) as Decimal, + tokenSymbol: collateralToken?.symbol, + }, + ), [collateralToken?.symbol, userState, values.userCollateral], ) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index 0afb75846..b6ed19e50 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -71,18 +71,18 @@ export const LoanFormTokenInput = < tokenAddress: token?.address, }) - const position = positionBalance?.position + const { position, tooltip } = positionBalance ?? {} const walletBalance = useMemo( // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput () => ({ - balance: position ? position.data : balance, + balance: position?.data ?? balance, symbol: token?.symbol, - loading: position ? position.isLoading : isBalanceLoading, + loading: position?.isLoading ?? isBalanceLoading, usdRate, - tooltip: positionBalance?.tooltip, + tooltip: tooltip, prefix: position && LlamaIcon, }), - [balance, isBalanceLoading, token?.symbol, usdRate, positionBalance?.tooltip, position], + [balance, isBalanceLoading, token?.symbol, usdRate, tooltip, position], ) const errors = form.formState.errors as PartialRecord, Error> diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index b453d6bc3..6f81dc91f 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -88,7 +88,7 @@ const valueSize = { large: 'headingSBold', } as const satisfies Record -const isSet = (v: ReactNode) => v != null && v !== '' +const isSet = (v: ReactNode) => v || v === 0 const ActionInfo = ({ label, diff --git a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx index 991cc5db8..6d3bd9754 100644 --- a/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx +++ b/packages/curve-ui-kit/src/shared/ui/LargeTokenInput.tsx @@ -149,7 +149,7 @@ export type LargeTokenInputProps = { */ tokenSelector?: ReactNode - // TODO: rename to just "balance" because multipurpose now (walletBalance, positionBalance ...) + // TODO: receive a `maxBalance` ReactNode to allow anything to be injected /** Optional wallet balance configuration. */ walletBalance?: BalanceProps diff --git a/packages/curve-ui-kit/src/utils/decimal.ts b/packages/curve-ui-kit/src/utils/decimal.ts index 53a13d0bf..b7beed880 100644 --- a/packages/curve-ui-kit/src/utils/decimal.ts +++ b/packages/curve-ui-kit/src/utils/decimal.ts @@ -21,8 +21,8 @@ export type Decimal = `${number}` export type Amount = number | Decimal /** Converts a string to a Decimal typed string, returning undefined for null, undefined, empty strings, or non-finite values. */ -export const decimal = (value: number | string | undefined | null): Decimal | undefined => { - if (typeof value === 'number') { +export const decimal = (value: number | string | undefined | null | BigNumber): Decimal | undefined => { + if (typeof value === 'number' || value instanceof BigNumber) { value = value.toString() } From 4a56ff71a3c4c1d33f198e533182d42c79c51ec8 Mon Sep 17 00:00:00 2001 From: Pearce Date: Tue, 16 Dec 2025 21:13:05 +0100 Subject: [PATCH 52/52] feat: remove duplicate max collateral LTI message --- .../manage-loan/components/RemoveCollateralForm.tsx | 12 ------------ 1 file changed, 12 deletions(-) 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 ba806d757..82721afdc 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -9,9 +9,7 @@ import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import { useSwitch } from '@ui-kit/hooks/useSwitch' import { t } from '@ui-kit/lib/i18n' -import { Balance } from '@ui-kit/shared/ui/Balance' import { InputDivider } from '../../../widgets/InputDivider' -import { setValueOptions } from '../../borrow/react-form.utils' import { useRemoveCollateralForm } from '../hooks/useRemoveCollateralForm' export const RemoveCollateralForm = ({ @@ -94,16 +92,6 @@ export const RemoveCollateralForm = ({ position: maxRemovable, tooltip: t`Max Removable Collateral`, }} - message={ - form.setValue('userCollateral', maxRemovable.data, setValueOptions)} - /> - } />