diff --git a/apps/main/src/lend/lib/apiLending.ts b/apps/main/src/lend/lib/apiLending.ts index 48928fd016..6eafcf73fe 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/llamalend/features/manage-loan/components/AddCollateralForm.tsx b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx index 1860433730..a1d442f479 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralForm.tsx @@ -1,24 +1,20 @@ -import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' 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 { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' -import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' 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 { Form } from '@ui-kit/widgets/DetailPageLayout/Form' import { InputDivider } from '../../../widgets/InputDivider' import { useAddCollateralForm } from '../hooks/useAddCollateralForm' +import { AddCollateralInfoAccordion } from './AddCollateralInfoAccordion' export const AddCollateralForm = ({ market, networks, chainId, - enabled, onAdded, }: { market: LlamaMarketTemplate | undefined @@ -28,19 +24,14 @@ export const AddCollateralForm = ({ onAdded?: NonNullable }) => { const network = networks[chainId] - const [isOpen, , , toggle] = useSwitch(false) const { form, + params, isPending, onSubmit, action, - params, values, - bands, - health, - prices, - gas, isApproved, formErrors, collateralToken, @@ -49,41 +40,26 @@ export const AddCollateralForm = ({ } = useAddCollateralForm({ market, network, - networks, - enabled, onAdded, }) - const marketRates = useMarketRates(params, isOpen) - return (
} > }> ({ /> - - ({ handledErrors={['userCollateral']} successTitle={t`Collateral added`} /> + + ) } diff --git a/apps/main/src/llamalend/features/manage-loan/components/AddCollateralInfoAccordion.tsx b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralInfoAccordion.tsx new file mode 100644 index 0000000000..036ede09f9 --- /dev/null +++ b/apps/main/src/llamalend/features/manage-loan/components/AddCollateralInfoAccordion.tsx @@ -0,0 +1,86 @@ +import BigNumber from 'bignumber.js' +import { useMemo } from 'react' +import type { Token } from '@/llamalend/features/borrow/types' +import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' +import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' +import type { NetworkDict } from '@/llamalend/llamalend.types' +import { useAddCollateralEstimateGas } from '@/llamalend/queries/add-collateral/add-collateral-gas-estimate.query' +import { getAddCollateralHealthOptions } from '@/llamalend/queries/add-collateral/add-collateral-health.query' +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 } from '@/llamalend/queries/utils' +import { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' +import type { CollateralForm } from '@/llamalend/queries/validation/manage-loan.validation' +import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' +import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import { useSwitch } from '@ui-kit/hooks/useSwitch' +import { q } from '@ui-kit/types/util' +import { decimal, Decimal } from '@ui-kit/utils' + +export function AddCollateralInfoAccordion({ + params, + values: { userCollateral }, + collateralToken, + borrowToken, + networks, +}: { + params: CollateralParams + values: CollateralForm + collateralToken: Token | undefined + borrowToken: Token | undefined + networks: NetworkDict +}) { + const [isOpen, , , toggle] = useSwitch(false) + const userState = q(useUserState(params, isOpen)) + + const expectedCollateral = useMemo( + () => + mapQuery( + userState, + (state) => + userCollateral && + state.collateral && { + value: decimal(new BigNumber(userCollateral).plus(state.collateral)) as Decimal, + tokenSymbol: collateralToken?.symbol, + }, + ), + [collateralToken?.symbol, userState, userCollateral], + ) + + return ( + getAddCollateralHealthOptions({ ...params, isFull }))} + prevHealth={useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }))} + rates={q(useMarketRates(params, isOpen))} + prevLoanToValue={useLoanToValueFromUserState({ + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen, + expectedBorrowed: userState.data?.debt, + })} + loanToValue={useLoanToValueFromUserState({ + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen && !!userCollateral, + collateralDelta: userCollateral, + expectedBorrowed: userState.data?.debt, + })} + userState={{ + ...userState, + borrowTokenSymbol: borrowToken?.symbol, + collateralTokenSymbol: collateralToken?.symbol, + }} + collateral={expectedCollateral} + /> + ) +} 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 362d5775e5..513c5eb786 100644 --- a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralForm.tsx @@ -1,20 +1,15 @@ -import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' 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 { LoanFormAlerts } from '@/llamalend/widgets/manage-loan/LoanFormAlerts' import { LoanFormTokenInput } from '@/llamalend/widgets/manage-loan/LoanFormTokenInput' -import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' 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 { Form } from '@ui-kit/widgets/DetailPageLayout/Form' import { InputDivider } from '../../../widgets/InputDivider' -import { setValueOptions } from '../../borrow/react-form.utils' import { useRemoveCollateralForm } from '../hooks/useRemoveCollateralForm' +import { RemoveCollateralInfoAccordion } from './RemoveCollateralInfoAccordion' export const RemoveCollateralForm = ({ market, @@ -30,20 +25,15 @@ export const RemoveCollateralForm = ({ onRemoved?: NonNullable }) => { const network = networks[chainId] - const [isOpen, , , toggle] = useSwitch(false) const { form, + params, isPending, onSubmit, action, - maxRemovable, - params, values, - bands, - health, - prices, - gas, + maxRemovable, formErrors, collateralToken, borrowToken, @@ -51,44 +41,27 @@ export const RemoveCollateralForm = ({ } = useRemoveCollateralForm({ market, network, - networks, enabled, onRemoved, }) - const marketRates = useMarketRates(params, isOpen) - return (
} > }> ({ max={maxRemovable} testId="remove-collateral-input" network={network} - message={ - form.setValue('userCollateral', maxRemovable.data, setValueOptions)} - /> - } + positionBalance={{ + position: maxRemovable, + tooltip: t`Max Removable Collateral`, + }} /> - - ({ handledErrors={['userCollateral']} successTitle={t`Collateral removed`} /> + + ) } diff --git a/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralInfoAccordion.tsx b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralInfoAccordion.tsx new file mode 100644 index 0000000000..70aa24b0e4 --- /dev/null +++ b/apps/main/src/llamalend/features/manage-loan/components/RemoveCollateralInfoAccordion.tsx @@ -0,0 +1,88 @@ +import BigNumber from 'bignumber.js' +import { useMemo } from 'react' +import type { Token } from '@/llamalend/features/borrow/types' +import { useLoanToValueFromUserState } from '@/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState' +import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' +import type { NetworkDict } from '@/llamalend/llamalend.types' +import { useMarketRates } from '@/llamalend/queries/market-rates' +import { useRemoveCollateralEstimateGas } from '@/llamalend/queries/remove-collateral/remove-collateral-gas-estimate.query' +import { getRemoveCollateralHealthOptions } from '@/llamalend/queries/remove-collateral/remove-collateral-health.query' +import { getUserHealthOptions } from '@/llamalend/queries/user-health.query' +import { useUserState } from '@/llamalend/queries/user-state.query' +import { mapQuery } from '@/llamalend/queries/utils' +import { CollateralParams } from '@/llamalend/queries/validation/manage-loan.types' +import type { CollateralForm } from '@/llamalend/queries/validation/manage-loan.validation' +import { LoanInfoAccordion } from '@/llamalend/widgets/manage-loan/LoanInfoAccordion' +import type { IChainId } from '@curvefi/llamalend-api/lib/interfaces' +import { useSwitch } from '@ui-kit/hooks/useSwitch' +import { q } from '@ui-kit/types/util' +import { decimal, Decimal } from '@ui-kit/utils' + +export function RemoveCollateralInfoAccordion({ + params, + values: { userCollateral }, + collateralToken, + borrowToken, + networks, +}: { + params: CollateralParams + values: CollateralForm + collateralToken: Token | undefined + borrowToken: Token | undefined + networks: NetworkDict +}) { + const [isOpen, , , toggle] = useSwitch(false) + const userState = q(useUserState(params, isOpen)) + const expectedCollateral = useMemo( + () => + // 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 && + userCollateral && { + value: decimal( + BigNumber.max(0, new BigNumber(state.collateral).minus(new BigNumber(userCollateral))), + ) as Decimal, + tokenSymbol: collateralToken?.symbol, + }, + ), + [collateralToken?.symbol, userState, userCollateral], + ) + + return ( + getRemoveCollateralHealthOptions({ ...params, isFull }))} + prevHealth={useHealthQueries((isFull) => getUserHealthOptions({ ...params, isFull }))} + rates={q(useMarketRates(params, isOpen))} + prevLoanToValue={useLoanToValueFromUserState({ + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen, + expectedBorrowed: userState.data?.debt, + })} + loanToValue={useLoanToValueFromUserState({ + chainId: params.chainId, + marketId: params.marketId, + userAddress: params.userAddress, + collateralToken, + borrowToken, + enabled: isOpen && !!userCollateral, + collateralDelta: userCollateral && (`-${userCollateral}` as Decimal), + expectedBorrowed: userState.data?.debt, + })} + userState={{ + ...userState, + borrowTokenSymbol: borrowToken?.symbol, + collateralTokenSymbol: collateralToken?.symbol, + }} + 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 512efddd4c..68907d167d 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useAddCollateralForm.ts @@ -2,23 +2,19 @@ import { useEffect, useMemo } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useForm } from 'react-hook-form' import { useConnection } from 'wagmi' -import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import { getTokens } from '@/llamalend/llama.utils' -import type { LlamaMarketTemplate, LlamaNetwork, NetworkDict } from '@/llamalend/llamalend.types' +import type { LlamaMarketTemplate, LlamaNetwork } from '@/llamalend/llamalend.types' import { type AddCollateralOptions, useAddCollateralMutation } from '@/llamalend/mutations/add-collateral.mutation' import { useAddCollateralIsApproved } from '@/llamalend/queries/add-collateral/add-collateral-approved.query' -import { useAddCollateralBands } from '@/llamalend/queries/add-collateral/add-collateral-bands.query' -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 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' 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' @@ -28,14 +24,10 @@ const useCallbackAfterFormUpdate = (form: UseFormReturn, callbac export const useAddCollateralForm = ({ market, network, - networks, - enabled, onAdded, }: { market: LlamaMarketTemplate | undefined network: LlamaNetwork - networks: NetworkDict - enabled?: boolean onAdded?: NonNullable }) => { const { address: userAddress } = useConnection() @@ -45,12 +37,14 @@ export const useAddCollateralForm = ({ const tokens = market && getTokens(market) const collateralToken = tokens?.collateralToken const borrowToken = tokens?.borrowToken + const { data: maxCollateral } = useTokenBalance({ chainId, userAddress, tokenAddress: collateralToken?.address }) const form = useForm({ ...formDefaultOptions, - resolver: vestResolver(collateralFormValidationSuite), + resolver: vestResolver(addCollateralFormValidationSuite), defaultValues: { userCollateral: undefined, + maxCollateral: undefined, }, }) @@ -58,19 +52,16 @@ export const useAddCollateralForm = ({ const params = useDebouncedValue( useMemo( - () => - ({ - chainId, - marketId, - userAddress, - userCollateral: values.userCollateral, - }) as CollateralParams, - [chainId, marketId, userAddress, values.userCollateral], + (): CollateralParams => ({ + chainId, + marketId, + userAddress, + ...values, + }), + [chainId, marketId, userAddress, values], ), ) - const isApproved = useAddCollateralIsApproved(params) - const { onSubmit, ...action } = useAddCollateralMutation({ marketId, network, @@ -79,14 +70,13 @@ export const useAddCollateralForm = ({ userAddress, }) + const formErrors = useFormErrors(form.formState) + useCallbackAfterFormUpdate(form, action.reset) - const bands = useAddCollateralBands(params, enabled) - const health = useHealthQueries((isFull) => getAddCollateralHealthOptions({ ...params, isFull }, enabled)) - const prices = useAddCollateralPrices(params, enabled) - const gas = useAddCollateralEstimateGas(networks, params, enabled) - - const formErrors = useFormErrors(form.formState) + useEffect(() => { + form.setValue('maxCollateral', maxCollateral, { shouldValidate: true }) + }, [form, maxCollateral]) return { form, @@ -95,14 +85,10 @@ export const useAddCollateralForm = ({ isPending: form.formState.isSubmitting || action.isPending, onSubmit: form.handleSubmit(onSubmit), action, - bands, - health, - prices, - gas, - isApproved, - formErrors, collateralToken, borrowToken, txHash: action.data?.hash, + isApproved: useAddCollateralIsApproved(params), + formErrors, } } diff --git a/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts b/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts index 7a76f607dd..18ea8e9bfa 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useLoanToValueFromUserState.ts @@ -7,7 +7,7 @@ import type { Decimal } from '@ui-kit/utils' import type { Token } from '../../borrow/types' type Params = { - chainId: ChainId + chainId: ChainId | null | undefined marketId: string | null | undefined userAddress: Address | null | undefined collateralToken: Token | undefined 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 75c4409a5a..8de79c2b03 100644 --- a/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts +++ b/apps/main/src/llamalend/features/manage-loan/hooks/useRemoveCollateralForm.ts @@ -2,21 +2,16 @@ import { useEffect, useMemo } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useForm } from 'react-hook-form' import { useConnection } from 'wagmi' -import { useHealthQueries } from '@/llamalend/hooks/useHealthQueries' import { getTokens } from '@/llamalend/llama.utils' -import type { LlamaMarketTemplate, NetworkDict } from '@/llamalend/llamalend.types' +import type { LlamaMarketTemplate } from '@/llamalend/llamalend.types' import { type RemoveCollateralOptions, useRemoveCollateralMutation, } from '@/llamalend/mutations/remove-collateral.mutation' -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 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' @@ -35,13 +30,11 @@ export const useRemoveCollateralForm = < >({ market, network, - networks, enabled, onRemoved, }: { market: LlamaMarketTemplate | undefined network: BaseConfig - networks: NetworkDict enabled?: boolean onRemoved?: NonNullable }) => { @@ -55,9 +48,10 @@ export const useRemoveCollateralForm = < const form = useForm({ ...formDefaultOptions, - resolver: vestResolver(collateralFormValidationSuite), + resolver: vestResolver(removeCollateralFormValidationSuite), defaultValues: { userCollateral: undefined, + maxCollateral: undefined, }, }) @@ -65,14 +59,13 @@ export const useRemoveCollateralForm = < const params = useDebouncedValue( useMemo( - () => - ({ - chainId, - marketId, - userAddress, - userCollateral: values.userCollateral, - }) as CollateralParams, - [chainId, marketId, userAddress, values.userCollateral], + (): CollateralParams => ({ + chainId, + marketId, + userAddress, + ...values, + }), + [chainId, marketId, userAddress, values], ), ) @@ -84,16 +77,15 @@ export const useRemoveCollateralForm = < userAddress, }) - useCallbackAfterFormUpdate(form, action.reset) - 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 formErrors = useFormErrors(form.formState) + useCallbackAfterFormUpdate(form, action.reset) + + useEffect(() => { + form.setValue('maxCollateral', maxRemovable.data, { shouldValidate: true }) + }, [form, maxRemovable.data]) + return { form, values, @@ -102,13 +94,9 @@ export const useRemoveCollateralForm = < onSubmit: form.handleSubmit(onSubmit), action, maxRemovable, - bands, - health, - prices, - gas, - txHash: action.data?.hash, collateralToken, borrowToken, + txHash: action.data?.hash, formErrors, } } diff --git a/apps/main/src/llamalend/helpers.ts b/apps/main/src/llamalend/helpers.ts new file mode 100644 index 0000000000..929f09f0d2 --- /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/llamalend/mutations/useLlammaMutation.ts b/apps/main/src/llamalend/mutations/useLlammaMutation.ts index a2a32f3766..d120fda1aa 100644 --- a/apps/main/src/llamalend/mutations/useLlammaMutation.ts +++ b/apps/main/src/llamalend/mutations/useLlammaMutation.ts @@ -5,6 +5,7 @@ import type { IChainId as LlamaChainId, INetworkName as LlamaNetworkId } from '@ import { useMutation } from '@tanstack/react-query' import { notify, useCurve } from '@ui-kit/features/connect-wallet' import { assertValidity, logError, logMutation, logSuccess, type ValidationSuite } 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' @@ -131,7 +132,7 @@ export function useLlammaMutation { logError(mutationKey, { error, variables, marketId: context?.market.id }) - notify(error.message, '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/queries/add-collateral/add-collateral-gas-estimate.query.ts b/apps/main/src/llamalend/queries/add-collateral/add-collateral-gas-estimate.query.ts index c9787f0fd8..add3fe9d71 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' @@ -19,7 +19,17 @@ 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) + + 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, }) diff --git a/apps/main/src/llamalend/queries/user-state.query.ts b/apps/main/src/llamalend/queries/user-state.query.ts index e747922f09..acb6e12d47 100644 --- a/apps/main/src/llamalend/queries/user-state.query.ts +++ b/apps/main/src/llamalend/queries/user-state.query.ts @@ -1,6 +1,7 @@ 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 const { useQuery: useUserState, invalidate: invalidateUserState } = queryFactory({ @@ -29,3 +30,5 @@ export const { useQuery: useUserState, invalidate: invalidateUserState } = query }, validationSuite: userMarketValidationSuite, }) + +export type UserState = QueryData diff --git a/apps/main/src/llamalend/queries/utils.ts b/apps/main/src/llamalend/queries/utils.ts new file mode 100644 index 0000000000..ee41c3ddac --- /dev/null +++ b/apps/main/src/llamalend/queries/utils.ts @@ -0,0 +1,18 @@ +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 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 + +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/queries/validation/borrow-fields.validation.ts b/apps/main/src/llamalend/queries/validation/borrow-fields.validation.ts index 55348eba37..e34aa29318 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) + } }) } @@ -63,9 +65,10 @@ export const validateLeverageEnabled = (leverageEnabled: boolean | undefined | n export const validateMaxCollateral = ( userCollateral: Decimal | undefined | null, maxCollateral: Decimal | undefined | null, + errorMessage?: string, ) => { skipWhen(userCollateral == null || maxCollateral == null, () => { - 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', () => { enforce(userCollateral).lte(maxCollateral) }) }) diff --git a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts index ca7a7b613e..dd9eda41ed 100644 --- a/apps/main/src/llamalend/queries/validation/manage-loan.types.ts +++ b/apps/main/src/llamalend/queries/validation/manage-loan.types.ts @@ -3,7 +3,10 @@ import { type FieldsOf } from '@ui-kit/lib' import type { UserMarketQuery } from '@ui-kit/lib/model' import type { Decimal } from '@ui-kit/utils' -export type CollateralQuery = UserMarketQuery & { userCollateral: Decimal } +export type CollateralQuery = UserMarketQuery & { + userCollateral: Decimal + maxCollateral?: Decimal +} type HealthQuery = { isFull: boolean } 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 d148b514ab..3ddde39fbb 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 @@ -26,21 +27,37 @@ export type RepayForm = FieldsOf<{ isFull: boolean }> -export const collateralValidationGroup = ({ chainId, userCollateral, marketId, userAddress }: CollateralParams) => +export const collateralValidationGroup = ({ + chainId, + userCollateral, + maxCollateral, + marketId, + userAddress, +}: CollateralParams) => group('chainValidation', () => { marketIdValidationSuite({ chainId, marketId }) userAddressValidationGroup({ userAddress }) validateUserCollateral(userCollateral) + validateMaxCollateral(userCollateral, maxCollateral) }) export const collateralValidationSuite = createValidationSuite((params: CollateralParams) => collateralValidationGroup(params), ) -export const collateralFormValidationSuite = createValidationSuite((params: CollateralForm) => { - validateUserCollateral(params.userCollateral) +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) diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormAlerts.tsx index bf8d16f073..06b952d18b 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' @@ -56,7 +57,7 @@ export const LoanFormAlerts = ({ data-testid={'loan-form-error'} > {t`An error occurred`} - {error.message} + {getErrorMessage(error)} )} diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx index d72b2e3c6a..4d5760c57d 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanFormTokenInput.tsx @@ -7,10 +7,15 @@ import type { LlamaNetwork } from '@/llamalend/llamalend.types' import type { INetworkName } from '@curvefi/llamalend-api/lib/interfaces' import type { PartialRecord } from '@curvefi/prices-api/objects.util' import { useTokenBalance } from '@ui-kit/hooks/useTokenBalance' +import { useTokenUsdRate } from '@ui-kit/lib/model/entities/token-usd-rate' +import { LlamaIcon } from '@ui-kit/shared/icons/LlamaIcon' import { HelperMessage, LargeTokenInput } from '@ui-kit/shared/ui/LargeTokenInput' +import type { LargeTokenInputProps } from '@ui-kit/shared/ui/LargeTokenInput' import { TokenLabel } from '@ui-kit/shared/ui/TokenLabel' import type { Query } from '@ui-kit/types/util' -import { Decimal } from '@ui-kit/utils' +import { decimal, Decimal } from '@ui-kit/utils' + +type WalletBalanceProps = NonNullable /** * A large token input field for loan forms, with balance and max handling. @@ -29,6 +34,7 @@ export const LoanFormTokenInput = < testId, message, network, + positionBalance, }: { label: string token: { address: Address; symbol?: string } | undefined @@ -42,6 +48,16 @@ export const LoanFormTokenInput = < form: UseFormReturn // the form, used to set the value and get errors testId: string message?: ReactNode + /** + * Optional, displays the position balance instead of the wallet balance. + */ + positionBalance?: { + position: Query + tooltip?: WalletBalanceProps['tooltip'] + } + /** + * The network of the token. + */ network: LlamaNetwork }) => { const { address: userAddress } = useConnection() @@ -54,11 +70,29 @@ export const LoanFormTokenInput = < userAddress, tokenAddress: token?.address, }) + const { data: usdRate } = useTokenUsdRate({ + chainId: network?.chainId, + tokenAddress: token?.address, + }) + + const { position, tooltip } = positionBalance ?? {} + const walletBalance = useMemo( + // todo: support separate isLoading for balance and for maxBalance in LargeTokenInput + () => ({ + balance: position?.data ?? balance, + symbol: token?.symbol, + loading: position?.isLoading ?? isBalanceLoading, + usdRate, + tooltip: tooltip, + prefix: position && LlamaIcon, + }), + [balance, isBalanceLoading, token?.symbol, usdRate, tooltip, position], + ) const errors = form.formState.errors as PartialRecord, Error> const relatedMaxFieldError = max?.data && 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) @@ -82,12 +116,9 @@ export const LoanFormTokenInput = < )} isError={!!error} message={error?.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(usdRate && usdRate * +(value ?? 0))} > {message && } diff --git a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx index 46edfe8fd9..0843996ef1 100644 --- a/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx +++ b/apps/main/src/llamalend/widgets/manage-loan/LoanInfoAccordion.tsx @@ -1,14 +1,20 @@ +import { UserState } from '@/llamalend/queries/user-state.query' +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' 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 type { Query } from '@ui-kit/types/util' import { Decimal, formatNumber, formatPercent, formatUsd } from '@ui-kit/utils' import { getHealthValueColor } from '../../features/market-position-details/utils' import { LoanLeverageActionInfo, type LoanLeverageActionInfoProps } from './LoanLeverageActionInfo' +const { Spacing } = SizesAndSpaces + export type LoanInfoGasData = { estGasCostUsd?: number | Decimal | `${number}` tooltip?: string @@ -30,15 +36,17 @@ 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<{ 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 } } @@ -56,83 +64,110 @@ export const LoanInfoAccordion = ({ prevLoanToValue, gas, debt, - prevDebt, + collateral, leverage, -}: LoanInfoAccordionProps) => ( - // error tooltip isn't displayed correctly because accordion takes the mouse focus. Use title for now. - - - } - expanded={isOpen} - toggle={toggle} - > - - {leverage?.enabled && } - {debt && ( - - )} - - formatNumber(p, { abbreviate: false })).join(' - ') ?? '-'} - error={prices.error} - loading={prices.isLoading} - testId="borrow-price-range" - /> - {range != null && ( - - )} - - {loanToValue && ( + 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(prevHealth, (v) => formatNumber(v, { abbreviate: false }))} + emptyValue="∞" + {...combineQueryState([health, prevHealth])} + valueColor={getHealthValueColor(Number(health.data ?? prevHealth?.data ?? 100), useTheme())} + testId="borrow-health" /> - )} - - - - -) + } + expanded={isOpen} + toggle={toggle} + > + + + {(debt || prevDebt) && ( + formatNumber(v.value, { abbreviate: false }))} + prevValue={prevDebt && formatNumber(prevDebt, { abbreviate: false })} + {...combineQueryState([debt, userState])} + valueRight={debt?.data?.tokenSymbol ?? userState?.borrowTokenSymbol} + testId="borrow-debt" + /> + )} + {(collateral || prevCollateral) && ( + formatNumber(v.value, { abbreviate: false }))} + prevValue={prevCollateral && formatNumber(prevCollateral, { abbreviate: false })} + {...combineQueryState([collateral, userState])} + valueRight={collateral?.data?.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 && ( + + )} + + {(loanToValue || prevLoanToValue) && ( + + )} + + {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 89596e2163..28ced8cde5 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 ( - <> + - + ) } diff --git a/apps/main/src/loan/lib/apiCrvusd.ts b/apps/main/src/loan/lib/apiCrvusd.ts index 0c5fc63bfe..9f1f394e77 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 { FormDetailInfo as FormDetailInfoDeleverage } from '@/loan/components/PageMintMarket/LoanDeleverage/types' import type { MaxRecvLeverage as MaxRecvLeverageForm } from '@/loan/components/PageMintMarket/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 a9ebedbef6..9ea5ddca0f 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 diff --git a/packages/curve-ui-kit/src/lib/queries/types.ts b/packages/curve-ui-kit/src/lib/queries/types.ts index da402a78b4..267b648512 100644 --- a/packages/curve-ui-kit/src/lib/queries/types.ts +++ b/packages/curve-ui-kit/src/lib/queries/types.ts @@ -12,3 +12,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']> diff --git a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx index 8e24303b17..6f81dc91f0 100644 --- a/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx +++ b/packages/curve-ui-kit/src/shared/ui/ActionInfo.tsx @@ -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). */ @@ -86,12 +88,15 @@ const valueSize = { large: 'headingSBold', } as const satisfies Record +const isSet = (v: ReactNode) => v || v === 0 + const ActionInfo = ({ label, labelColor, prevValue, prevValueColor, value, + emptyValue = '-', valueColor, valueLeft, valueRight, @@ -114,6 +119,9 @@ const ActionInfo = ({ }, [copyValue, openSnackbar]) const errorMessage = (typeof error === 'object' && error?.message) || (typeof error === 'string' && error) + const showPrevValue = isSet(value) && isSet(prevValue) + value ??= prevValue ?? emptyValue + return ( - {prevValue && ( + {showPrevValue && ( )} - {prevValue && ( + {showPrevValue && ( 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 86cad9c3f4..310e31603d 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', diff --git a/packages/curve-ui-kit/src/types/util.ts b/packages/curve-ui-kit/src/types/util.ts index fc076bc670..1c991f78dc 100644 --- a/packages/curve-ui-kit/src/types/util.ts +++ b/packages/curve-ui-kit/src/types/util.ts @@ -1,3 +1,5 @@ +import { UseQueryResult } from '@tanstack/react-query' + /** * Creates a deep partial type that makes all properties optional recursively, * while preserving function types as-is @@ -38,3 +40,13 @@ export type MakeOptional = Omit & Partial * @template T - The type of the data returned by the query. */ export type Query = { data: T | undefined; isLoading: boolean; error: Error | null | undefined } +/** + * Helper to extract only the relevant fields from a UseQueryResult into the Query type. + * This is necessary because passing UseQueryResult to any react component will crash the rendering due to + * react trying to serialize the react-query proxy object. + */ +export const q = ({ data, isLoading, error }: UseQueryResult): Query => ({ + data, + isLoading, + error, +}) diff --git a/packages/curve-ui-kit/src/utils/decimal.ts b/packages/curve-ui-kit/src/utils/decimal.ts index 53a13d0bfd..b7beed8802 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() } diff --git a/packages/curve-ui-kit/src/widgets/DetailPageLayout/FormContent.tsx b/packages/curve-ui-kit/src/widgets/DetailPageLayout/FormContent.tsx index 6b8ca607ee..e357127461 100644 --- a/packages/curve-ui-kit/src/widgets/DetailPageLayout/FormContent.tsx +++ b/packages/curve-ui-kit/src/widgets/DetailPageLayout/FormContent.tsx @@ -20,7 +20,7 @@ export const FormContent = ({ footer?: ReactNode header?: ReactNode }) => ( - + t.design.Layer[1].Fill }}> {header} t.design.Layer[1].Fill }} gap={Spacing.md} padding={Spacing.md}>