diff --git a/features/stake/stake-form/stake-form-context/stake-form-context.tsx b/features/stake/stake-form/stake-form-context/stake-form-context.tsx index a08e55218..3fe205198 100644 --- a/features/stake/stake-form/stake-form-context/stake-form-context.tsx +++ b/features/stake/stake-form/stake-form-context/stake-form-context.tsx @@ -108,6 +108,8 @@ const useStakeFormNetworkData = (): StakeFormNetworkData => { return { stethBalance, + etherBalance, + isMultisig: isMultisigLoading ? undefined : isMultisig, stakeableEther, stakingLimitInfo, gasCost, diff --git a/features/stake/stake-form/stake-form-context/types.ts b/features/stake/stake-form/stake-form-context/types.ts index 406fc046d..f0b8c410a 100644 --- a/features/stake/stake-form/stake-form-context/types.ts +++ b/features/stake/stake-form/stake-form-context/types.ts @@ -10,6 +10,8 @@ export type StakeFormInput = { }; export type StakeFormNetworkData = { + etherBalance?: BigNumber; + isMultisig?: boolean; stethBalance?: BigNumber; stakeableEther?: BigNumber; stakingLimitInfo?: StakeLimitFullInfo; @@ -21,6 +23,9 @@ export type StakeFormNetworkData = { export type StakeFormValidationContext = { active: boolean; - maxAmount: BigNumber; stakingLimitLevel: LIMIT_LEVEL; + currentStakeLimit: BigNumber; + gasCost: BigNumber; + etherBalance: BigNumber; + isMultisig: boolean; }; diff --git a/features/stake/stake-form/stake-form-context/validation.ts b/features/stake/stake-form/stake-form-context/validation.ts index f7947a8be..306d78bea 100644 --- a/features/stake/stake-form/stake-form-context/validation.ts +++ b/features/stake/stake-form/stake-form-context/validation.ts @@ -2,14 +2,14 @@ import { useMemo } from 'react'; import invariant from 'tiny-invariant'; import { formatEther } from '@ethersproject/units'; import { useWeb3 } from 'reef-knot/web3-react'; +import { Zero } from '@ethersproject/constants'; import { validateEtherAmount } from 'shared/hook-form/validation/validate-ether-amount'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; import { handleResolverValidationError } from 'shared/hook-form/validation/validation-error'; import { validateBignumberMax } from 'shared/hook-form/validation/validate-bignumber-max'; +import { validateStakeLimit } from 'shared/hook-form/validation/validate-stake-limit'; import { awaitWithTimeout } from 'utils/await-with-timeout'; -import { getTokenDisplayName } from 'utils/getTokenDisplayName'; - import { useAwaiter } from 'shared/hooks/use-awaiter'; import type { Resolver } from 'react-hook-form'; @@ -18,7 +18,6 @@ import type { StakeFormNetworkData, StakeFormValidationContext, } from './types'; -import { validateStakeLimit } from 'shared/hook-form/validation/validate-stake-limit'; export const stakeFormValidationResolver: Resolver< StakeFormInput, @@ -33,7 +32,14 @@ export const stakeFormValidationResolver: Resolver< validateEtherAmount('amount', amount, 'ETH'); - const { maxAmount, active, stakingLimitLevel } = await awaitWithTimeout( + const { + active, + stakingLimitLevel, + currentStakeLimit, + etherBalance, + gasCost, + isMultisig, + } = await awaitWithTimeout( validationContextPromise, VALIDATION_CONTEXT_TIMEOUT, ); @@ -44,11 +50,47 @@ export const stakeFormValidationResolver: Resolver< validateBignumberMax( 'amount', amount, - maxAmount, - `${getTokenDisplayName( - 'ETH', - )} amount must not be greater than ${formatEther(maxAmount)}`, + etherBalance, + `Entered ETH amount exceeds your available balance of ${formatEther( + etherBalance, + )}`, ); + + validateBignumberMax( + 'amount', + amount, + currentStakeLimit, + `Entered ETH amount exceeds current staking limit of ${formatEther( + currentStakeLimit, + )}`, + ); + + if (!isMultisig) { + const gasPaddedBalance = etherBalance.sub(gasCost); + + validateBignumberMax( + 'amount', + Zero, + gasPaddedBalance, + `Ensure you have sufficient ETH to cover the gas cost of ${formatEther( + gasCost, + )}`, + ); + + validateBignumberMax( + 'amount', + amount, + gasPaddedBalance, + `Enter ETH amount less than ${formatEther( + gasPaddedBalance, + )} to ensure you leave enough ETH for gas fees`, + ); + } + } else { + return { + values, + errors: { referral: 'wallet not connected' }, + }; } return { @@ -56,7 +98,7 @@ export const stakeFormValidationResolver: Resolver< errors: {}, }; } catch (error) { - return handleResolverValidationError(error, 'StakeForm', 'amount'); + return handleResolverValidationError(error, 'StakeForm', 'referral'); } }; @@ -64,17 +106,25 @@ export const useStakeFormValidationContext = ( networkData: StakeFormNetworkData, ): Promise => { const { active } = useWeb3(); - const { maxAmount, stakingLimitInfo } = networkData; + const { stakingLimitInfo, etherBalance, isMultisig, gasCost } = networkData; const validationContextAwaited = useMemo(() => { - if (active && maxAmount && stakingLimitInfo) { + if ( + stakingLimitInfo && + // we ether not connected or must have all account related data + (!active || (etherBalance && gasCost && isMultisig !== undefined)) + ) { return { active, - maxAmount, stakingLimitLevel: stakingLimitInfo.stakeLimitLevel, + currentStakeLimit: stakingLimitInfo.currentStakeLimit, + // condition above guaranties stubs will only be passed when active = false + etherBalance: etherBalance ?? Zero, + gasCost: gasCost ?? Zero, + isMultisig: isMultisig ?? false, }; } return undefined; - }, [active, maxAmount, stakingLimitInfo]); + }, [active, etherBalance, gasCost, isMultisig, stakingLimitInfo]); return useAwaiter(validationContextAwaited).awaiter; }; diff --git a/features/stake/stake-form/stake-form-info.tsx b/features/stake/stake-form/stake-form-info.tsx index 803592c0c..33b41ebdb 100644 --- a/features/stake/stake-form/stake-form-info.tsx +++ b/features/stake/stake-form/stake-form-info.tsx @@ -25,7 +25,12 @@ export const StakeFormInfo = () => { return ( - + 1 ETH = 1 stETH diff --git a/features/withdrawals/request/form/options/options-picker.tsx b/features/withdrawals/request/form/options/options-picker.tsx index f45dd0546..7719f6f7a 100644 --- a/features/withdrawals/request/form/options/options-picker.tsx +++ b/features/withdrawals/request/form/options/options-picker.tsx @@ -73,11 +73,14 @@ const LidoButton: React.FC = ({ isActive, onClick }) => { ); }; +const toFloor = (num: number): string => + (Math.floor(num * 10000) / 10000).toString(); + const DexButton: React.FC = ({ isActive, onClick }) => { const { loading, bestRate } = useWithdrawalRates({ fallbackValue: DEFAULT_VALUE_FOR_RATE, }); - const bestRateValue = bestRate ? `1 : ${bestRate.toFixed(4)}` : '-'; + const bestRateValue = bestRate ? `1 : ${toFloor(bestRate)}` : '-'; return ( = ({ children }) => { ); const formObject = useForm< RequestFormInputType, - Promise + RequestFormValidationContextType >({ defaultValues: { amount: null, @@ -76,7 +76,7 @@ export const RequestFormProvider: FC = ({ children }) => { mode: 'lido', requests: null, }, - context: validationContext.awaiter, + context: validationContext, criteriaMode: 'firstError', mode: 'onChange', resolver: RequestFormValidationResolver, diff --git a/features/withdrawals/request/request-form-context/types.ts b/features/withdrawals/request/request-form-context/types.ts index 13f1aad72..4ab9f0e10 100644 --- a/features/withdrawals/request/request-form-context/types.ts +++ b/features/withdrawals/request/request-form-context/types.ts @@ -14,7 +14,12 @@ export type RequestFormInputType = { } & ValidationResults; export type RequestFormValidationContextType = { + active: boolean; + asyncContext: Promise; setIntermediateValidationResults: Dispatch>; +}; + +export type RequestFormValidationAsyncContextType = { minUnstakeSteth: BigNumber; minUnstakeWSteth: BigNumber; balanceSteth: BigNumber; diff --git a/features/withdrawals/request/request-form-context/use-validation-context.ts b/features/withdrawals/request/request-form-context/use-validation-context.ts index 2e314bbe4..7162e629e 100644 --- a/features/withdrawals/request/request-form-context/use-validation-context.ts +++ b/features/withdrawals/request/request-form-context/use-validation-context.ts @@ -5,13 +5,19 @@ import { import { useMemo } from 'react'; import { useIsLedgerLive } from 'shared/hooks/useIsLedgerLive'; import { useAwaiter } from 'shared/hooks/use-awaiter'; -import { RequestFormDataType, RequestFormValidationContextType } from './types'; +import type { + RequestFormDataType, + RequestFormValidationAsyncContextType, + RequestFormValidationContextType, +} from './types'; +import { useWeb3 } from 'reef-knot/web3-react'; // Prepares validation context object from request form data export const useValidationContext = ( requestData: RequestFormDataType, setIntermediateValidationResults: RequestFormValidationContextType['setIntermediateValidationResults'], -) => { +): RequestFormValidationContextType => { + const { active } = useWeb3(); const isLedgerLive = useIsLedgerLive(); const maxRequestCount = isLedgerLive ? MAX_REQUESTS_COUNT_LEDGER_LIMIT @@ -44,7 +50,6 @@ export const useValidationContext = ( minUnstakeWSteth, maxRequestCount, stethTotalSupply, - setIntermediateValidationResults, } : undefined; return validationContextObject; @@ -56,9 +61,11 @@ export const useValidationContext = ( maxRequestCount, minUnstakeSteth, minUnstakeWSteth, - setIntermediateValidationResults, stethTotalSupply, ]); - return useAwaiter(context); + const asyncContext = + useAwaiter(context).awaiter; + + return { active, asyncContext, setIntermediateValidationResults }; }; diff --git a/features/withdrawals/request/request-form-context/validators.ts b/features/withdrawals/request/request-form-context/validators.ts index 97a133b9f..0e6dd3e6e 100644 --- a/features/withdrawals/request/request-form-context/validators.ts +++ b/features/withdrawals/request/request-form-context/validators.ts @@ -6,9 +6,10 @@ import { Resolver } from 'react-hook-form'; import { TokensWithdrawable } from 'features/withdrawals/types/tokens-withdrawable'; import { - RequestFormValidationContextType, + RequestFormValidationAsyncContextType, RequestFormInputType, ValidationResults, + RequestFormValidationContextType, } from '.'; import { VALIDATION_CONTEXT_TIMEOUT } from 'features/withdrawals/withdrawals-constants'; @@ -51,7 +52,9 @@ export class ValidationSplitRequest extends ValidationError { } const messageMinUnstake = (min: BigNumber, token: TokensWithdrawable) => - `Minimum unstake amount is ${formatEther(min)} ${getTokenDisplayName(token)}`; + `Minimum withdraw amount is ${formatEther(min)} ${getTokenDisplayName( + token, + )}`; const messageMaxAmount = (max: BigNumber, token: TokensWithdrawable) => `${getTokenDisplayName(token)} amount must not be greater than ${formatEther( @@ -117,7 +120,7 @@ const tvlJokeValidate = ( // helper to get filter out context values const transformContext = ( - context: RequestFormValidationContextType, + context: RequestFormValidationAsyncContextType, values: RequestFormInputType, ) => { const isSteth = values.token === TOKENS.STETH; @@ -140,27 +143,32 @@ const transformContext = ( // returns values or errors export const RequestFormValidationResolver: Resolver< RequestFormInputType, - Promise -> = async (values, contextPromise) => { + RequestFormValidationContextType +> = async (values, context) => { const { amount, mode, token } = values; const validationResults: ValidationResults = { requests: null, }; let setResults; try { + invariant(context, 'must have context promise'); + setResults = context.setIntermediateValidationResults; + // this check does not require context and can be placed first // also limits context missing edge cases on page start validateEtherAmount('amount', amount, token); + // early return + if (!context.active) return { values, errors: {} }; + // wait for context promise with timeout and extract relevant data // validation function only waits limited time for data and fails validation otherwise // most of the time data will already be available - invariant(contextPromise, 'must have context promise'); - const context = await awaitWithTimeout( - contextPromise, + const awaitedContext = await awaitWithTimeout( + context.asyncContext, VALIDATION_CONTEXT_TIMEOUT, ); - setResults = context.setIntermediateValidationResults; + const { isSteth, balance, @@ -168,7 +176,7 @@ export const RequestFormValidationResolver: Resolver< minAmountPerRequest, maxRequestCount, stethTotalSupply, - } = transformContext(context, values); + } = transformContext(awaitedContext, values); if (isSteth) { tvlJokeValidate('amount', amount, stethTotalSupply, balance); @@ -176,7 +184,7 @@ export const RequestFormValidationResolver: Resolver< // early validation exit for dex option if (mode === 'dex') { - return { values, errors: {} }; + return { values, errors: { requests: 'wallet not connected' } }; } validateBignumberMin( @@ -210,7 +218,7 @@ export const RequestFormValidationResolver: Resolver< return handleResolverValidationError( error, 'WithdrawalRequestForm', - 'amount', + 'requests', ); } finally { // no matter validation result save results for the UI to show diff --git a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx index 66bbf95d8..2875ca65d 100644 --- a/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx +++ b/features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx @@ -27,6 +27,8 @@ export const UnwrapStats = () => { data-testid="youWillReceive" amount={willReceiveStETH} symbol="stETH" + showAmountTip + trimEllipsis /> diff --git a/features/wsteth/wrap/wrap-form/wrap-stats.tsx b/features/wsteth/wrap/wrap-form/wrap-stats.tsx index be578f8cd..df11f848b 100644 --- a/features/wsteth/wrap/wrap-form/wrap-stats.tsx +++ b/features/wsteth/wrap/wrap-form/wrap-stats.tsx @@ -67,6 +67,8 @@ export const WrapFormStats = () => { amount={willReceiveWsteth} data-testid="youWillReceive" symbol="wstETH" + showAmountTip + trimEllipsis /> diff --git a/pages/_document.tsx b/pages/_document.tsx index 67d48a51c..5999ad6ec 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -75,6 +75,10 @@ export default class MyDocument extends Document { return ( + {dynamics.ipfsMode && ( { + res.status(403); + return; + // TODO: enable test in test/consts.ts const token = await validateAndGetQueryToken(req, res); const cacheKey = `${CACHE_ONE_INCH_RATE_KEY}-${token}`; const cachedOneInchRate = cache.get(cacheKey); diff --git a/shared/components/input-amount/input-amount.tsx b/shared/components/input-amount/input-amount.tsx index d9a5b5c0e..1d55cccc9 100644 --- a/shared/components/input-amount/input-amount.tsx +++ b/shared/components/input-amount/input-amount.tsx @@ -5,7 +5,9 @@ import { MouseEvent, useCallback, useEffect, - useState, + useImperativeHandle, + useMemo, + useRef, } from 'react'; import { BigNumber } from 'ethers'; @@ -49,62 +51,99 @@ export const InputAmount = forwardRef( }, ref, ) => { - const [stringValue, setStringValue] = useState(() => - value ? formatEther(value) : '', - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + const defaultValue = useMemo(() => (value ? formatEther(value) : ''), []); + + const lastInputValue = useRef(defaultValue); + const inputRef = useRef(null); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + useImperativeHandle(ref, () => inputRef.current!, []); const handleChange = useCallback( (e: ChangeEvent) => { - e.currentTarget.value = e.currentTarget.value.trim(); + // will accumulate changes without committing to dom + let currentValue = e.currentTarget.value; + const immutableValue = e.currentTarget.value; + const caretPosition = e.currentTarget.selectionStart ?? 0; + + currentValue = currentValue.trim(); // Support for devices where inputMode="decimal" showing keyboard with comma as decimal delimiter - if (e.currentTarget.value.includes(',')) { - e.currentTarget.value = e.currentTarget.value.replaceAll(',', '.'); + if (currentValue.includes(',')) { + currentValue = currentValue.replaceAll(',', '.'); } // delete negative sign - if (e.currentTarget.value.includes('-')) { - e.currentTarget.value = e.currentTarget.value.replaceAll('-', ''); + if (currentValue.includes('-')) { + currentValue = currentValue.replaceAll('-', ''); } // Prepend zero when user types just a dot symbol for "0." - if (e.currentTarget.value === '.') { - e.currentTarget.value = '0.'; - } - - // guards against non numbers (no matter overflow or whitespace) - // empty whitespace is cast to 0, so not NaN - if (isNaN(Number(e.currentTarget.value))) { - return; + if (currentValue === '.') { + currentValue = '0.'; } - if (e.currentTarget.value === '') { + if (currentValue === '') { onChange?.(null); - } + } else { + const value = parseEtherSafe(currentValue); + // invalid value, so we rollback to last valid value + if (!value) { + const rollbackCaretPosition = + caretPosition - + Math.min( + e.currentTarget.value.length - lastInputValue.current.length, + ); + // rollback value (caret moves to end) + e.currentTarget.value = lastInputValue.current; + // rollback caret + e.currentTarget.setSelectionRange( + rollbackCaretPosition, + rollbackCaretPosition, + ); + return; + } - const value = parseEtherSafe(e.currentTarget.value); - if (value) { const cappedValue = value.gt(MaxUint256) ? MaxUint256 : value; + if (value.gt(MaxUint256)) { + currentValue = formatEther(MaxUint256); + } onChange?.(cappedValue); } - // we set string value anyway to allow intermediate input - setStringValue(e.currentTarget.value); + // commit change to dom + e.currentTarget.value = currentValue; + // if there is a diff due to soft change, adjust caret to remain in same place + if (currentValue != immutableValue) { + const rollbackCaretPosition = + caretPosition - + Math.min(immutableValue.length - currentValue.length); + e.currentTarget.setSelectionRange( + rollbackCaretPosition, + rollbackCaretPosition, + ); + } + lastInputValue.current = currentValue; }, [onChange], ); useEffect(() => { - if (!value) setStringValue(''); - else { - const parsedValue = parseEtherSafe(stringValue); + const input = inputRef.current; + if (!input) return; + if (!value) { + input.value = ''; + } else { + const parsedValue = parseEtherSafe(input.value); // only change string state if casted values differ // this allows user to enter 0.100 without immediate change to 0.1 if (!parsedValue || !parsedValue.eq(value)) { - setStringValue(formatEther(value)); + input.value = formatEther(value); + // prevents rollback to incorrect value in onChange + lastInputValue.current = input.value; } } - }, [stringValue, value]); + }, [value]); const handleClickMax = onChange && maxValue?.gt(0) @@ -130,9 +169,9 @@ export const InputAmount = forwardRef( ) } inputMode="decimal" - value={stringValue} + defaultValue={defaultValue} onChange={handleChange} - ref={ref} + ref={inputRef} /> ); }, diff --git a/shared/components/layout/footer/styles.tsx b/shared/components/layout/footer/styles.tsx index 45c98c3af..f94800718 100644 --- a/shared/components/layout/footer/styles.tsx +++ b/shared/components/layout/footer/styles.tsx @@ -4,7 +4,7 @@ import { Container, Link } from '@lidofinance/lido-ui'; import { LogoLido } from 'shared/components/logos/logos'; import { NAV_MOBILE_MEDIA } from 'styles/constants'; -export const FooterStyle = styled((props) => )` +export const FooterStyle = styled(Container)` position: relative; box-sizing: border-box; color: var(--lido-color-text); @@ -24,9 +24,12 @@ export const FooterStyle = styled((props) => )` `; export const FooterLink = styled(Link)` - font-size: ${({ theme }) => theme.fontSizesMap.xxs}px; + display: flex; + align-items: center; line-height: 20px; + vertical-align: middle; color: var(--lido-color-textSecondary); + font-size: ${({ theme }) => theme.fontSizesMap.xxs}px; font-weight: 400; &:visited { @@ -39,17 +42,13 @@ export const FooterLink = styled(Link)` `; export const LinkDivider = styled.div` - background: var(--lido-color-textSecondary); + background: var(--lido-color-border); width: 1px; - margin: 2px 6px; + margin: 2px 16px; `; export const LogoLidoStyle = styled(LogoLido)` - margin-right: 44px; - - ${NAV_MOBILE_MEDIA} { - display: none; - } + margin-right: 32px; `; export const FooterDivider = styled.div` diff --git a/shared/formatters/format-token.tsx b/shared/formatters/format-token.tsx index 5dd8ad1f7..e21b80100 100644 --- a/shared/formatters/format-token.tsx +++ b/shared/formatters/format-token.tsx @@ -10,6 +10,7 @@ export type FormatTokenProps = { maxDecimalDigits?: number; maxTotalLength?: number; showAmountTip?: boolean; + trimEllipsis?: boolean; }; export type FormatTokenComponent = Component<'span', FormatTokenProps>; @@ -20,12 +21,14 @@ export const FormatToken: FormatTokenComponent = ({ maxDecimalDigits = 4, maxTotalLength = 15, showAmountTip = false, + trimEllipsis, ...rest }) => { const { actual, isTrimmed, trimmed } = useFormattedBalance( amount, maxDecimalDigits, maxTotalLength, + trimEllipsis, ); const showTooltip = showAmountTip && isTrimmed; diff --git a/shared/hook-form/controls/submit-button-hook-form.tsx b/shared/hook-form/controls/submit-button-hook-form.tsx index f176a6c84..60f13deae 100644 --- a/shared/hook-form/controls/submit-button-hook-form.tsx +++ b/shared/hook-form/controls/submit-button-hook-form.tsx @@ -8,7 +8,7 @@ import { isValidationErrorTypeValidate } from '../validation/validation-error'; type SubmitButtonHookFormProps = Partial< React.ComponentProps > & { - errorField: string; + errorField?: string; isLocked?: boolean; }; @@ -23,7 +23,8 @@ export const SubmitButtonHookForm: React.FC = ({ const { isValidating, isSubmitting } = useFormState(); const { errors } = useFormState>(); const disabled = - (!!errors[errorField] && + (errorField && + !!errors[errorField] && isValidationErrorTypeValidate(errors[errorField]?.type)) || disabledProp; diff --git a/shared/hook-form/controls/token-amount-input-hook-form.tsx b/shared/hook-form/controls/token-amount-input-hook-form.tsx index 5251d9858..c278c442b 100644 --- a/shared/hook-form/controls/token-amount-input-hook-form.tsx +++ b/shared/hook-form/controls/token-amount-input-hook-form.tsx @@ -30,7 +30,8 @@ export const TokenAmountInputHookForm = ({ fieldState: { error }, } = useController({ name: fieldName }); const hasErrorHighlight = isValidationErrorTypeValidate(error?.type); - const errorMessage = hasErrorHighlight && error?.message; + // allows to show error state without message + const errorMessage = hasErrorHighlight && (error?.message || true); return ( { - let maxAmount: BigNumber | undefined = undefined; - if (!balance || isLoading) return maxAmount; - maxAmount = balance; + if (!balance || isLoading) return undefined; + + let maxAmount: BigNumber | undefined = balance; if (limit && balance.gt(limit)) { maxAmount = limit; diff --git a/test/consts.ts b/test/consts.ts index 70b5c1138..8db7fef22 100644 --- a/test/consts.ts +++ b/test/consts.ts @@ -160,17 +160,18 @@ const LIDO_STATS_SCHEMA = { }; export const GET_REQUESTS: GetRequest[] = [ - { - uri: '/api/oneinch-rate', - schema: { - type: 'object', - properties: { - rate: { type: 'number', min: 0 }, - }, - required: ['rate'], - additionalProperties: false, - }, - }, + // TODO: enabled when bringing back 1inch endpoint + // { + // uri: '/api/oneinch-rate', + // schema: { + // type: 'object', + // properties: { + // rate: { type: 'number', min: 0 }, + // }, + // required: ['rate'], + // additionalProperties: false, + // }, + // }, { uri: `/api/short-lido-stats?chainId=${CONFIG.STAND_CONFIG.chainId}`, schema: { diff --git a/utils/formatBalance.ts b/utils/formatBalance.ts index eac9ff6a0..8aad1f7e6 100644 --- a/utils/formatBalance.ts +++ b/utils/formatBalance.ts @@ -42,6 +42,7 @@ export const formatBalanceWithTrimmed = ( balance = Zero, maxDecimalDigits = 4, maxTotalLength = 30, + trimEllipsis?: boolean, ) => { const balanceString = formatEther(balance); let trimmed = balanceString; @@ -64,6 +65,10 @@ export const formatBalanceWithTrimmed = ( } } + if (isTrimmed && trimEllipsis && !trimmed.includes('...')) { + trimmed += '...'; + } + return { actual: balanceString, trimmed, @@ -75,9 +80,16 @@ export const useFormattedBalance = ( balance = Zero, maxDecimalDigits = 4, maxTotalLength = 30, + trimEllipsis?: boolean, ) => { return useMemo( - () => formatBalanceWithTrimmed(balance, maxDecimalDigits, maxTotalLength), - [balance, maxDecimalDigits, maxTotalLength], + () => + formatBalanceWithTrimmed( + balance, + maxDecimalDigits, + maxTotalLength, + trimEllipsis, + ), + [balance, maxDecimalDigits, maxTotalLength, trimEllipsis], ); };