diff --git a/.changeset/twenty-sites-smile.md b/.changeset/twenty-sites-smile.md new file mode 100644 index 0000000000..164b1d8092 --- /dev/null +++ b/.changeset/twenty-sites-smile.md @@ -0,0 +1,5 @@ +--- +"@venusprotocol/evm": minor +--- + +add base Boost tab diff --git a/apps/evm/src/components/Accordion/index.tsx b/apps/evm/src/components/Accordion/index.tsx index ed69ccf18a..3e3abdb52e 100644 --- a/apps/evm/src/components/Accordion/index.tsx +++ b/apps/evm/src/components/Accordion/index.tsx @@ -26,7 +26,7 @@ export const Accordion: React.FC = ({ > {!!title && {title}} -
+
{!!rightLabel && {rightLabel}} diff --git a/apps/evm/src/components/Icon/icons/arrowRightFull.tsx b/apps/evm/src/components/Icon/icons/arrowRightFull.tsx new file mode 100644 index 0000000000..a30cbc240e --- /dev/null +++ b/apps/evm/src/components/Icon/icons/arrowRightFull.tsx @@ -0,0 +1,29 @@ +import type { SVGProps } from 'react'; + +const SvgArrowRightFull = (props: SVGProps) => ( + + + + +); + +export default SvgArrowRightFull; diff --git a/apps/evm/src/components/Icon/icons/index.ts b/apps/evm/src/components/Icon/icons/index.ts index c7f81928ab..b45526743a 100644 --- a/apps/evm/src/components/Icon/icons/index.ts +++ b/apps/evm/src/components/Icon/icons/index.ts @@ -73,3 +73,4 @@ export { default as transactionCollateral } from './transactionCollateral'; export { default as transactionLink } from './transactionLink'; export { default as eMode } from './eMode'; export { default as gear } from './gear'; +export { default as arrowRightFull } from './arrowRightFull'; diff --git a/apps/evm/src/components/LabeledInlineContent/index.tsx b/apps/evm/src/components/LabeledInlineContent/index.tsx index ce867cc982..e904d2d240 100644 --- a/apps/evm/src/components/LabeledInlineContent/index.tsx +++ b/apps/evm/src/components/LabeledInlineContent/index.tsx @@ -28,7 +28,7 @@ export const LabeledInlineContent = ({ className={cn('flex w-full items-center justify-between space-x-4', className)} {...otherContainerProps} > -
+
{typeof iconSrc === 'string' && ( )} @@ -37,18 +37,13 @@ export const LabeledInlineContent = ({ )} -

- {label} -

+

{label}

{!!tooltip && }
{children}
diff --git a/apps/evm/src/components/Notice/index.tsx b/apps/evm/src/components/Notice/index.tsx index 319997a0ba..650e8791a5 100644 --- a/apps/evm/src/components/Notice/index.tsx +++ b/apps/evm/src/components/Notice/index.tsx @@ -21,6 +21,7 @@ export const Notice = ({ description, variant = 'info', onClose, + size = 'md', ...otherProps }: NoticeProps) => (
{title &&

{title}

} - {!!description &&

{description}

} + {!!description && ( +

{description}

+ )}
diff --git a/apps/evm/src/components/Notice/types.ts b/apps/evm/src/components/Notice/types.ts index 5e96d638e3..5f2aa5e660 100644 --- a/apps/evm/src/components/Notice/types.ts +++ b/apps/evm/src/components/Notice/types.ts @@ -6,5 +6,6 @@ export interface NoticeProps extends Omit, description?: string | ReactElement; title?: string | ReactElement; variant?: NoticeVariant; + size?: 'sm' | 'md'; onClose?: () => void; } diff --git a/apps/evm/src/components/RiskAcknowledgementToggle/index.tsx b/apps/evm/src/components/RiskAcknowledgementToggle/index.tsx index e6d797da66..c54de5019b 100644 --- a/apps/evm/src/components/RiskAcknowledgementToggle/index.tsx +++ b/apps/evm/src/components/RiskAcknowledgementToggle/index.tsx @@ -12,12 +12,12 @@ export const RiskAcknowledgementToggle: React.FC return (
- +
-

{t('operationForm.riskyOperation.toggleLabel')}

+

{t('operationForm.riskyOperation.toggleLabel')}

); diff --git a/apps/evm/src/components/SelectTokenTextField/TokenList/index.tsx b/apps/evm/src/components/SelectTokenTextField/TokenList/index.tsx deleted file mode 100644 index cd520757d5..0000000000 --- a/apps/evm/src/components/SelectTokenTextField/TokenList/index.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { Typography } from '@mui/material'; -import { type InputHTMLAttributes, useMemo, useState } from 'react'; - -import { TokenIconWithSymbol } from 'components/TokenIconWithSymbol'; -import { useTranslation } from 'libs/translations'; -import type { Token, TokenBalance } from 'types'; -import { areTokensEqual, convertMantissaToTokens } from 'utilities'; - -import { SenaryButton } from '@venusprotocol/ui'; -import { Icon } from 'components'; -import { TextField } from '../../TextField'; -import { useStyles as useParentStyles } from '../styles'; -import { getTokenListItemTestId } from '../testIdGetters'; -import type { OptionalTokenBalance } from '../types'; -import { useStyles } from './styles'; - -export interface TokenListProps { - tokenBalances: OptionalTokenBalance[]; - onTokenClick: (token: Token) => void; - displayCommonTokenButtons: boolean; - selectedToken: Token; - 'data-testid'?: string; -} - -const commonTokenSymbols = ['XVS', 'BNB', 'USDT', 'BTCB']; - -export const TokenList: React.FC = ({ - tokenBalances, - onTokenClick, - displayCommonTokenButtons, - selectedToken, - 'data-testid': testId, -}) => { - const { t } = useTranslation(); - const parentStyles = useParentStyles(); - const styles = useStyles(); - - const commonTokenBalances = displayCommonTokenButtons - ? tokenBalances.filter(tokenBalance => commonTokenSymbols.includes(tokenBalance.token.symbol)) - : []; - - const [searchValue, setSearchValue] = useState(''); - - const handleSearchInputChange: InputHTMLAttributes['onChange'] = event => - setSearchValue(event.currentTarget.value); - - // Sort tokens alphabetically, placing tokens with a non-zero balance at the - // top of the list - const sortedTokenBalances = useMemo( - () => - [...tokenBalances].sort((a, b) => { - const aIsNonNegative = !!a.balanceMantissa?.isGreaterThan(0); - const bIsNonNegative = !!b.balanceMantissa?.isGreaterThan(0); - - // Both are non-negative or both are negative - if (aIsNonNegative === bIsNonNegative) { - return a.token.symbol.localeCompare(b.token.symbol); - } - - // If a is non-negative and b is negative, a comes first - if (aIsNonNegative) { - return -1; - } - - // If b is non-negative and a is negative, b comes first - return 1; - }) as TokenBalance[], - [tokenBalances], - ); - - // Filter tokens based on search - const filteredTokenBalances = useMemo(() => { - if (!searchValue) { - return sortedTokenBalances; - } - - const formattedSearchValue = searchValue.toLowerCase(); - - // Enable user to search by symbol or address - return sortedTokenBalances.filter( - tokenBalance => - tokenBalance.token.symbol.toLowerCase().includes(formattedSearchValue) || - tokenBalance.token.address.toLowerCase().includes(formattedSearchValue), - ); - }, [sortedTokenBalances, searchValue]); - - return ( -
-
- - - {commonTokenBalances.length > 2 && ( -
- {commonTokenBalances.map(commonTokenBalance => ( - onTokenClick(commonTokenBalance.token)} - css={styles.commonTokenButton} - key={`select-token-text-field-common-token-${commonTokenBalance.token.symbol}`} - > - - - ))} -
- )} -
- -
- {filteredTokenBalances.map(tokenBalance => ( -
onTokenClick(tokenBalance.token)} - key={`select-token-text-field-item-${tokenBalance.token.address}`} - data-testid={ - !!testId && - getTokenListItemTestId({ - parentTestId: testId, - tokenAddress: tokenBalance.token.address, - }) - } - > - - - {tokenBalance.balanceMantissa && ( - - {convertMantissaToTokens({ - value: tokenBalance.balanceMantissa, - token: tokenBalance.token, - returnInReadableFormat: true, - - addSymbol: false, - })} - - )} - - {!tokenBalance.balanceMantissa && areTokensEqual(tokenBalance.token, selectedToken) && ( - - )} -
- ))} -
-
- ); -}; - -export default TokenList; diff --git a/apps/evm/src/components/SelectTokenTextField/TokenList/styles.ts b/apps/evm/src/components/SelectTokenTextField/TokenList/styles.ts deleted file mode 100644 index 15f654c3a5..0000000000 --- a/apps/evm/src/components/SelectTokenTextField/TokenList/styles.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { css } from '@emotion/react'; -import { useTheme } from '@mui/material'; - -const ITEM_HEIGHT_RATIO = 10; - -export const useStyles = () => { - const theme = useTheme(); - - return { - container: css` - position: absolute; - z-index: 2; - left: 0; - right: 0; - top: ${theme.spacing(2)}; - border-radius: ${theme.spacing(3)}; - background-color: ${theme.palette.background.default}; - box-shadow: 0 4px 15px 0 #0d1017; - overflow: hidden; - `, - searchField: css` - margin-bottom: ${theme.spacing(4)}; - - > div { - background-color: ${theme.palette.secondary.main}; - } - `, - commonTokenList: css` - display: flex; - overflow-y: auto; - -ms-overflow-style: none; - scrollbar-width: none; - - ::-webkit-scrollbar { - display: none; - } - `, - commonTokenButton: css` - :not(:last-of-type) { - margin-right: ${theme.spacing(2)}; - } - `, - list: css` - max-height: ${theme.spacing(ITEM_HEIGHT_RATIO * 6)}; - overflow-y: auto; - `, - item: css` - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - height: ${theme.spacing(ITEM_HEIGHT_RATIO)}; - padding: ${theme.spacing(0, 3)}; - - :hover { - background-color: ${theme.palette.secondary.light}; - } - `, - }; -}; diff --git a/apps/evm/src/components/SelectTokenTextField/__testUtils__/testUtils.ts b/apps/evm/src/components/SelectTokenTextField/__testUtils__/testUtils.ts index 487f63fae3..8ebeab6dee 100644 --- a/apps/evm/src/components/SelectTokenTextField/__testUtils__/testUtils.ts +++ b/apps/evm/src/components/SelectTokenTextField/__testUtils__/testUtils.ts @@ -1,8 +1,8 @@ import { fireEvent, getByTestId } from '@testing-library/react'; import type { Token } from 'types'; - -import { getTokenListItemTestId, getTokenSelectButtonTestId } from '../testIdGetters'; +import { getTokenListItemTestId } from '../../TokenListWrapper/testIdGetters'; +import { getTokenSelectButtonTestId } from '../testIdGetters'; export const selectToken = ({ token, diff --git a/apps/evm/src/components/SelectTokenTextField/index.tsx b/apps/evm/src/components/SelectTokenTextField/index.tsx index cf06f64366..072166b115 100644 --- a/apps/evm/src/components/SelectTokenTextField/index.tsx +++ b/apps/evm/src/components/SelectTokenTextField/index.tsx @@ -2,22 +2,18 @@ import { Typography } from '@mui/material'; import { useState } from 'react'; -import type { Token } from 'types'; - import { TertiaryButton } from '@venusprotocol/ui'; +import type { Token } from 'types'; import { Icon } from '../Icon'; import { TokenIcon } from '../TokenIcon'; +import { type OptionalTokenBalance, TokenListWrapper } from '../TokenListWrapper'; import { TokenTextField, type TokenTextFieldProps } from '../TokenTextField'; -import TokenList from './TokenList'; import { useStyles } from './styles'; import { getTokenMaxButtonTestId, getTokenSelectButtonTestId, getTokenTextFieldTestId, } from './testIdGetters'; -import type { OptionalTokenBalance } from './types'; - -export * from './types'; export interface SelectTokenTextFieldProps extends Omit { tokenBalances: OptionalTokenBalance[]; @@ -54,70 +50,62 @@ export const SelectTokenTextField: React.FC = ({ return (
- - -
- - -
{selectedToken.symbol}
-
- - -
- - {rightMaxButton && ( + setIsTokenListShown(false)} + isListShown={isTokenListShown} + selectedToken={selectedToken} + data-testid={testId} + > + - {rightMaxButton.label} +
+ + +
{selectedToken.symbol}
+
+ +
- )} - - } - data-testid={!!testId && getTokenTextFieldTestId({ parentTestId: testId })} - {...otherTokenTextFieldProps} - /> -
- {isTokenListShown && ( - - )} -
+ {rightMaxButton && ( + + {rightMaxButton.label} + + )} + + } + data-testid={!!testId && getTokenTextFieldTestId({ parentTestId: testId })} + {...otherTokenTextFieldProps} + /> +
{description} - -
setIsTokenListShown(false)} - />
); }; diff --git a/apps/evm/src/components/SelectTokenTextField/styles.ts b/apps/evm/src/components/SelectTokenTextField/styles.ts index bb9d3d4f72..68a8a0b6e8 100644 --- a/apps/evm/src/components/SelectTokenTextField/styles.ts +++ b/apps/evm/src/components/SelectTokenTextField/styles.ts @@ -10,22 +10,6 @@ export const useStyles = () => { color: ${theme.palette.text.secondary}; margin-top: ${theme.spacing(1)}; `, - tokenListContainer: css` - position: relative; - `, - getBackdrop: ({ isTokenListShown }: { isTokenListShown: boolean }) => css` - display: none; - position: fixed; - z-index: 1; - inset: 0; - - ${ - isTokenListShown && - css` - display: block; - ` - } - `, getButton: ({ isTokenListShown }: { isTokenListShown: boolean }) => css` > span { display: flex; @@ -44,17 +28,6 @@ export const useStyles = () => { ` } `, - token: css` - > img { - width: ${theme.shape.iconSize.large}px; - height: ${theme.shape.iconSize.large}px; - } - - > span { - font-size: ${theme.typography.small1.fontSize}; - font-weight: ${theme.typography.small1.fontWeight}; - } - `, getArrowIcon: ({ isTokenListShown }: { isTokenListShown: boolean }) => css` transform: rotate(${isTokenListShown ? '0' : '180deg'}); color: inherit; diff --git a/apps/evm/src/components/SelectTokenTextField/testIdGetters.ts b/apps/evm/src/components/SelectTokenTextField/testIdGetters.ts index 35c960b30f..71ceb7532d 100644 --- a/apps/evm/src/components/SelectTokenTextField/testIdGetters.ts +++ b/apps/evm/src/components/SelectTokenTextField/testIdGetters.ts @@ -6,11 +6,3 @@ export const getTokenSelectButtonTestId = ({ parentTestId }: { parentTestId: str export const getTokenMaxButtonTestId = ({ parentTestId }: { parentTestId: string }) => `${parentTestId}-token-max-button`; - -export const getTokenListItemTestId = ({ - parentTestId, - tokenAddress, -}: { - parentTestId: string; - tokenAddress: string; -}) => `${parentTestId}-token-select-button-${tokenAddress}`; diff --git a/apps/evm/src/components/SelectTokenTextField/types.ts b/apps/evm/src/components/SelectTokenTextField/types.ts deleted file mode 100644 index 6006f4d886..0000000000 --- a/apps/evm/src/components/SelectTokenTextField/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { TokenBalance } from 'types'; - -export interface OptionalTokenBalance extends Omit { - balanceMantissa?: BigNumber; -} diff --git a/apps/evm/src/components/Slider/index.tsx b/apps/evm/src/components/Slider/index.tsx index fc222c99d8..680d25963e 100644 --- a/apps/evm/src/components/Slider/index.tsx +++ b/apps/evm/src/components/Slider/index.tsx @@ -9,6 +9,7 @@ export interface SliderProps { min?: number; disabled?: boolean; className?: string; + rangeClassName?: string; } export const Slider: React.FC = ({ @@ -19,6 +20,7 @@ export const Slider: React.FC = ({ step, disabled = false, className, + rangeClassName, }) => ( = ({ data-slot="slider-track" className="bg-lightGrey relative grow overflow-hidden rounded-full h-2 w-full" > - + {tabs.map((tab, index) => ( - + + + + + + + {/* TODO: add step to allow Comptroller contract as delegate */} + + + + {shouldAskUserRiskAcknowledgement && ( + handleToggleAcknowledgeRisk(checked)} + /> + )} +
+ +
+ + + +
+ + + ); +}; + +export default BoostForm; diff --git a/apps/evm/src/pages/Market/OperationForm/BoostForm/testIds.ts b/apps/evm/src/pages/Market/OperationForm/BoostForm/testIds.ts new file mode 100644 index 0000000000..881c9c8832 --- /dev/null +++ b/apps/evm/src/pages/Market/OperationForm/BoostForm/testIds.ts @@ -0,0 +1,6 @@ +const TEST_IDS = { + selectTokenTextField: 'select-token-text-field', + availableAmount: 'available-amount', +}; + +export default TEST_IDS; diff --git a/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/index.tsx new file mode 100644 index 0000000000..65fdbb858c --- /dev/null +++ b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/index.tsx @@ -0,0 +1,73 @@ +import { handleError } from 'libs/errors'; +import type { Asset, Pool, Token } from 'types'; + +import type BigNumber from 'bignumber.js'; +import type { FormError } from '../../types'; +import type { FormErrorCode, FormValues } from './types'; +import useFormValidation from './useFormValidation'; + +export * from './types'; + +export interface UseFormInput { + asset: Asset; + pool: Pool; + limitTokens: BigNumber; + onSubmit: (input: { fromToken: Token; fromTokenAmountTokens: string }) => Promise; + formValues: FormValues; + setFormValues: (setter: (currentFormValues: FormValues) => FormValues | FormValues) => void; + initialFormValues: FormValues; + onSubmitSuccess?: () => void; + simulatedPool?: Pool; +} + +interface UseFormOutput { + handleSubmit: (e?: React.SyntheticEvent) => Promise; + isFormValid: boolean; + formError?: FormError; +} + +const useForm = ({ + asset, + pool, + simulatedPool, + limitTokens, + onSubmitSuccess, + formValues, + setFormValues, + initialFormValues, +}: UseFormInput): UseFormOutput => { + const { isFormValid, formError } = useFormValidation({ + asset, + pool, + limitTokens, + simulatedPool, + formValues, + }); + + const handleSubmit = async (e?: React.SyntheticEvent) => { + e?.preventDefault(); + + if (!isFormValid) { + return; + } + + try { + // TODO: submit form + + // Reset form and close modal on success only + setFormValues(() => initialFormValues); + + onSubmitSuccess?.(); + } catch (error) { + handleError({ error }); + } + }; + + return { + handleSubmit, + isFormValid, + formError, + }; +}; + +export default useForm; diff --git a/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/types.ts b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/types.ts new file mode 100644 index 0000000000..bfed1566eb --- /dev/null +++ b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/types.ts @@ -0,0 +1,17 @@ +import type { Token } from 'types'; + +export interface FormValues { + suppliedToken: Token; + amountTokens: string; + acknowledgeRisk: boolean; +} + +export type FormErrorCode = + | 'EMPTY_TOKEN_AMOUNT' + | 'NO_COLLATERALS' + | 'BORROW_CAP_ALREADY_REACHED' + | 'HIGHER_THAN_BORROW_CAP' + | 'HIGHER_THAN_LIQUIDITY' + | 'HIGHER_THAN_AVAILABLE_AMOUNT' + | 'TOO_RISKY' + | 'REQUIRES_RISK_ACKNOWLEDGEMENT'; diff --git a/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/useFormValidation.ts b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/useFormValidation.ts new file mode 100644 index 0000000000..0acc072637 --- /dev/null +++ b/apps/evm/src/pages/Market/OperationForm/BoostForm/useForm/useFormValidation.ts @@ -0,0 +1,152 @@ +import BigNumber from 'bignumber.js'; +import { useMemo } from 'react'; + +import type { Asset, Pool } from 'types'; + +import { + HEALTH_FACTOR_LIQUIDATION_THRESHOLD, + HEALTH_FACTOR_MODERATE_THRESHOLD, +} from 'constants/healthFactor'; +import { useTranslation } from 'libs/translations'; +import { formatTokensToReadableValue } from 'utilities'; +import type { FormError } from '../../types'; +import type { FormErrorCode, FormValues } from './types'; + +interface UseFormValidationInput { + asset: Asset; + pool: Pool; + formValues: FormValues; + limitTokens: BigNumber; + simulatedPool?: Pool; +} + +interface UseFormValidationOutput { + isFormValid: boolean; + formError?: FormError; +} + +const useFormValidation = ({ + asset, + pool, + limitTokens, + simulatedPool, + formValues, +}: UseFormValidationInput): UseFormValidationOutput => { + const { t } = useTranslation(); + + const formError = useMemo | undefined>(() => { + if (!pool?.userBorrowLimitCents || pool.userBorrowLimitCents.isEqualTo(0)) { + return { + code: 'NO_COLLATERALS', + message: t('operationForm.error.noCollateral', { + tokenSymbol: asset.vToken.underlyingToken.symbol, + }), + }; + } + + if ( + asset.borrowCapTokens && + asset.borrowBalanceTokens.isGreaterThanOrEqualTo(asset.borrowCapTokens) + ) { + return { + code: 'BORROW_CAP_ALREADY_REACHED', + message: t('operationForm.error.borrowCapReached', { + assetBorrowCap: formatTokensToReadableValue({ + value: asset.borrowCapTokens, + token: asset.vToken.underlyingToken, + }), + }), + }; + } + + const borrowedTokenAmountTokens = formValues.amountTokens + ? new BigNumber(formValues.amountTokens) + : undefined; + + if (!borrowedTokenAmountTokens || borrowedTokenAmountTokens.isLessThanOrEqualTo(0)) { + return { + code: 'EMPTY_TOKEN_AMOUNT', + }; + } + + if ( + asset.borrowCapTokens && + asset.borrowBalanceTokens.plus(borrowedTokenAmountTokens).isGreaterThan(asset.borrowCapTokens) + ) { + return { + code: 'HIGHER_THAN_BORROW_CAP', + message: t('operationForm.error.higherThanBorrowCap', { + userMaxBorrowAmount: formatTokensToReadableValue({ + value: asset.borrowCapTokens.minus(asset.borrowBalanceTokens), + token: asset.vToken.underlyingToken, + maxDecimalPlaces: asset.vToken.underlyingToken.decimals, + }), + assetBorrowCap: formatTokensToReadableValue({ + value: asset.borrowCapTokens, + token: asset.vToken.underlyingToken, + maxDecimalPlaces: asset.vToken.underlyingToken.decimals, + }), + assetBorrowBalance: formatTokensToReadableValue({ + value: asset.borrowBalanceTokens, + token: asset.vToken.underlyingToken, + maxDecimalPlaces: asset.vToken.underlyingToken.decimals, + }), + }), + }; + } + + const assetLiquidityTokens = new BigNumber(asset.liquidityCents).dividedBy( + asset.tokenPriceCents, + ); + + if (borrowedTokenAmountTokens.isGreaterThan(assetLiquidityTokens)) { + // User is trying to borrow more than available liquidity + return { + code: 'HIGHER_THAN_LIQUIDITY', + message: t('operationForm.error.higherThanAvailableLiquidity'), + }; + } + + if (borrowedTokenAmountTokens.isGreaterThan(limitTokens)) { + return { + code: 'HIGHER_THAN_AVAILABLE_AMOUNT', + message: t('operationForm.error.higherThanAvailableAmount'), + }; + } + + if ( + simulatedPool?.userHealthFactor !== undefined && + simulatedPool.userHealthFactor <= HEALTH_FACTOR_LIQUIDATION_THRESHOLD + ) { + return { + code: 'TOO_RISKY', + message: t('operationForm.error.tooRisky'), + }; + } + + if ( + simulatedPool?.userHealthFactor !== undefined && + simulatedPool.userHealthFactor < HEALTH_FACTOR_MODERATE_THRESHOLD && + !formValues.acknowledgeRisk + ) { + return { + code: 'REQUIRES_RISK_ACKNOWLEDGEMENT', + }; + } + }, [ + asset, + pool, + limitTokens, + simulatedPool, + formValues.amountTokens, + formValues.acknowledgeRisk, + t, + ]); + + return { + isFormValid: !formError, + formError, + }; +}; + +export default useFormValidation; diff --git a/apps/evm/src/pages/Market/OperationForm/BorrowForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/BorrowForm/index.tsx index 3ad0ec3ed4..ca4d0b5bc0 100644 --- a/apps/evm/src/pages/Market/OperationForm/BorrowForm/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/BorrowForm/index.tsx @@ -325,7 +325,7 @@ export const BorrowFormUi: React.FC = ({
diff --git a/apps/evm/src/pages/Market/OperationForm/OperationDetails/SwapDetails/index.tsx b/apps/evm/src/pages/Market/OperationForm/OperationDetails/SwapDetails/index.tsx index c4987a3cec..6e239a61c0 100644 --- a/apps/evm/src/pages/Market/OperationForm/OperationDetails/SwapDetails/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/OperationDetails/SwapDetails/index.tsx @@ -51,7 +51,6 @@ export const SwapDetails: React.FC = ({ swap, action, ...other {...otherProps} > displays correct swap details 1`] = `"You will repay≈ 999.999999 XVSExchange rate1 BUSD ≈ 0.003333 XVSSlippage tolerance0.5%Price impact< 0.01%"`; +exports[`RepayForm - Feature flag enabled: integratedSwap > displays correct swap details 1`] = `"You will repay≈ 999.999999 XVSSlippage tolerance0.5%Price impact< 0.01%"`; exports[`RepayForm - Feature flag enabled: integratedSwap > displays correct swap details 2`] = `"You will repay 999.999999 XVS using 299.99K BUSD"`; diff --git a/apps/evm/src/pages/Market/OperationForm/RepayForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/RepayForm/index.tsx index a42af52338..b6ced04a7a 100644 --- a/apps/evm/src/pages/Market/OperationForm/RepayForm/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/RepayForm/index.tsx @@ -384,7 +384,7 @@ export const RepayFormUi: React.FC = ({
diff --git a/apps/evm/src/pages/Market/OperationForm/SupplyForm/__tests__/__snapshots__/indexIntegratedSwap.spec.tsx.snap b/apps/evm/src/pages/Market/OperationForm/SupplyForm/__tests__/__snapshots__/indexIntegratedSwap.spec.tsx.snap index 5ea66e99f2..472d57fae3 100644 --- a/apps/evm/src/pages/Market/OperationForm/SupplyForm/__tests__/__snapshots__/indexIntegratedSwap.spec.tsx.snap +++ b/apps/evm/src/pages/Market/OperationForm/SupplyForm/__tests__/__snapshots__/indexIntegratedSwap.spec.tsx.snap @@ -1,5 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`SupplyForm - Feature flag enabled: integratedSwap > displays correct swap details 1`] = `"You will supply≈ 8.9K XVSExchange rate1 BUSD ≈ 0.029666 XVSSlippage tolerance0.5%Price impact< 0.01%"`; +exports[`SupplyForm - Feature flag enabled: integratedSwap > displays correct swap details 1`] = `"You will supply≈ 8.9K XVSSlippage tolerance0.5%Price impact< 0.01%"`; exports[`SupplyForm - Feature flag enabled: integratedSwap > displays correct swap details 2`] = `"You will supply 8.9K XVS using 299.99K BUSD"`; diff --git a/apps/evm/src/pages/Market/OperationForm/SupplyForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/SupplyForm/index.tsx index 5c3e7da52d..665f21e4fd 100644 --- a/apps/evm/src/pages/Market/OperationForm/SupplyForm/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/SupplyForm/index.tsx @@ -268,7 +268,7 @@ export const SupplyFormUi: React.FC = ({ <> - + = ({
diff --git a/apps/evm/src/pages/Market/OperationForm/WithdrawForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/WithdrawForm/index.tsx index 995b52d626..5b7cbd6f87 100644 --- a/apps/evm/src/pages/Market/OperationForm/WithdrawForm/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/WithdrawForm/index.tsx @@ -334,7 +334,7 @@ export const WithdrawFormUi: React.FC = ({
diff --git a/apps/evm/src/pages/Market/OperationForm/index.tsx b/apps/evm/src/pages/Market/OperationForm/index.tsx index f2faa38d32..e18f4e02e6 100644 --- a/apps/evm/src/pages/Market/OperationForm/index.tsx +++ b/apps/evm/src/pages/Market/OperationForm/index.tsx @@ -7,6 +7,7 @@ import { useIsFeatureEnabled } from 'hooks/useIsFeatureEnabled'; import type { Tab } from 'hooks/useTabs'; import { useTranslation } from 'libs/translations'; import type { VToken } from 'types'; +import BoostForm from './BoostForm'; import BorrowForm from './BorrowForm'; import NativeTokenBalanceWrapper from './NativeTokenBalanceWrapper'; import RepayForm from './RepayForm'; @@ -100,7 +101,17 @@ export const OperationForm: React.FC = ({ {t('operationForm.boostTabTitle')}
), - content: <>Coming soon, + content: ( + + {({ asset, pool }) => ( + + )} + + ), }); } diff --git a/apps/evm/src/pages/Vai/Repay/index.tsx b/apps/evm/src/pages/Vai/Repay/index.tsx index 2bd33cfd46..51b950cdcc 100644 --- a/apps/evm/src/pages/Vai/Repay/index.tsx +++ b/apps/evm/src/pages/Vai/Repay/index.tsx @@ -2,7 +2,7 @@ import BigNumber from 'bignumber.js'; import { useCallback, useEffect, useMemo } from 'react'; import type { SubmitHandler } from 'react-hook-form'; -import { useGetBalanceOf, useGetPool, useGetSimulatedPool, useRepayVai } from 'clients/api'; +import { useGetBalanceOf, useGetPool, useRepayVai } from 'clients/api'; import { Delimiter, LabeledInlineContent, @@ -19,6 +19,7 @@ import { useChain } from 'hooks/useChain'; import useConvertMantissaToReadableTokenString from 'hooks/useConvertMantissaToReadableTokenString'; import useDebounceValue from 'hooks/useDebounceValue'; import { useGetContractAddress } from 'hooks/useGetContractAddress'; +import { useSimulateBalanceMutations } from 'hooks/useSimulateBalanceMutations'; import useTokenApproval from 'hooks/useTokenApproval'; import { handleError } from 'libs/errors'; import { useGetToken } from 'libs/tokens'; @@ -120,15 +121,10 @@ export const Repay: React.FC = () => { }, ]; - const { data: getSimulatedPoolData } = useGetSimulatedPool( - { - pool: legacyPool, - balanceMutations, - }, - { - enabled: debouncedInputAmountTokens.isGreaterThan(0), - }, - ); + const { data: getSimulatedPoolData } = useSimulateBalanceMutations({ + pool: legacyPool, + balanceMutations, + }); const simulatedPool = getSimulatedPoolData?.pool; const isRepayingFullLoan = !!userVaiBorrowBalanceTokens?.isEqualTo(debouncedInputAmountTokens); diff --git a/apps/evm/src/types/index.ts b/apps/evm/src/types/index.ts index 13cda20cf3..b1b74d8fca 100644 --- a/apps/evm/src/types/index.ts +++ b/apps/evm/src/types/index.ts @@ -26,6 +26,7 @@ export type TransactionType = 'chain' | 'layerZero' | 'biconomy'; export type TokenAction = | 'swapAndSupply' + | 'boost' | 'supply' | 'withdraw' | 'borrow' @@ -122,7 +123,7 @@ export interface Asset { badDebtMantissa: bigint; liquidityCents: BigNumber; reserveTokens: BigNumber; - cashTokens: BigNumber; + cashTokens: BigNumber; // TODO: rename to liquidityTokens exchangeRateVTokens: BigNumber; liquidationThresholdPercentage: number; liquidationPenaltyPercentage: number;