Skip to content

Commit

Permalink
Merge pull request #224 from lidofinance/develop
Browse files Browse the repository at this point in the history
Merge develop to main
  • Loading branch information
jake4take authored Feb 1, 2024
2 parents 6d99877 + 95c1c28 commit fbc99c9
Show file tree
Hide file tree
Showing 23 changed files with 249 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ const useStakeFormNetworkData = (): StakeFormNetworkData => {

return {
stethBalance,
etherBalance,
isMultisig: isMultisigLoading ? undefined : isMultisig,
stakeableEther,
stakingLimitInfo,
gasCost,
Expand Down
7 changes: 6 additions & 1 deletion features/stake/stake-form/stake-form-context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type StakeFormInput = {
};

export type StakeFormNetworkData = {
etherBalance?: BigNumber;
isMultisig?: boolean;
stethBalance?: BigNumber;
stakeableEther?: BigNumber;
stakingLimitInfo?: StakeLimitFullInfo;
Expand All @@ -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;
};
76 changes: 63 additions & 13 deletions features/stake/stake-form/stake-form-context/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -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,
);
Expand All @@ -44,37 +50,81 @@ 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 {
values,
errors: {},
};
} catch (error) {
return handleResolverValidationError(error, 'StakeForm', 'amount');
return handleResolverValidationError(error, 'StakeForm', 'referral');
}
};

export const useStakeFormValidationContext = (
networkData: StakeFormNetworkData,
): Promise<StakeFormValidationContext> => {
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;
};
7 changes: 6 additions & 1 deletion features/stake/stake-form/stake-form-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export const StakeFormInfo = () => {
return (
<DataTable data-testid="stakeFormInfo">
<DataTableRow title="You will receive" data-testid="youWillReceive">
<FormatToken amount={amount ?? Zero} symbol="stETH" />
<FormatToken
amount={amount ?? Zero}
symbol="stETH"
showAmountTip
trimEllipsis
/>
</DataTableRow>
<DataTableRow title="Exchange rate" data-testid="exchangeRate">
1 ETH = 1 stETH
Expand Down
5 changes: 4 additions & 1 deletion features/withdrawals/request/form/options/options-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ const LidoButton: React.FC<OptionButtonProps> = ({ isActive, onClick }) => {
);
};

const toFloor = (num: number): string =>
(Math.floor(num * 10000) / 10000).toString();

const DexButton: React.FC<OptionButtonProps> = ({ 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 (
<OptionsPickerButton
data-testid="dexOptions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ export const RequestFormProvider: FC<PropsWithChildren> = ({ children }) => {
);
const formObject = useForm<
RequestFormInputType,
Promise<RequestFormValidationContextType>
RequestFormValidationContextType
>({
defaultValues: {
amount: null,
token: TOKENS.STETH,
mode: 'lido',
requests: null,
},
context: validationContext.awaiter,
context: validationContext,
criteriaMode: 'firstError',
mode: 'onChange',
resolver: RequestFormValidationResolver,
Expand Down
5 changes: 5 additions & 0 deletions features/withdrawals/request/request-form-context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export type RequestFormInputType = {
} & ValidationResults;

export type RequestFormValidationContextType = {
active: boolean;
asyncContext: Promise<RequestFormValidationAsyncContextType>;
setIntermediateValidationResults: Dispatch<SetStateAction<ValidationResults>>;
};

export type RequestFormValidationAsyncContextType = {
minUnstakeSteth: BigNumber;
minUnstakeWSteth: BigNumber;
balanceSteth: BigNumber;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -44,7 +50,6 @@ export const useValidationContext = (
minUnstakeWSteth,
maxRequestCount,
stethTotalSupply,
setIntermediateValidationResults,
}
: undefined;
return validationContextObject;
Expand All @@ -56,9 +61,11 @@ export const useValidationContext = (
maxRequestCount,
minUnstakeSteth,
minUnstakeWSteth,
setIntermediateValidationResults,
stethTotalSupply,
]);

return useAwaiter(context);
const asyncContext =
useAwaiter<RequestFormValidationAsyncContextType>(context).awaiter;

return { active, asyncContext, setIntermediateValidationResults };
};
32 changes: 20 additions & 12 deletions features/withdrawals/request/request-form-context/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand All @@ -140,43 +143,48 @@ const transformContext = (
// returns values or errors
export const RequestFormValidationResolver: Resolver<
RequestFormInputType,
Promise<RequestFormValidationContextType>
> = 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,
maxAmountPerRequest,
minAmountPerRequest,
maxRequestCount,
stethTotalSupply,
} = transformContext(context, values);
} = transformContext(awaitedContext, values);

if (isSteth) {
tvlJokeValidate('amount', amount, stethTotalSupply, balance);
}

// early validation exit for dex option
if (mode === 'dex') {
return { values, errors: {} };
return { values, errors: { requests: 'wallet not connected' } };
}

validateBignumberMin(
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions features/wsteth/unwrap/unwrap-form/unwrap-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const UnwrapStats = () => {
data-testid="youWillReceive"
amount={willReceiveStETH}
symbol="stETH"
showAmountTip
trimEllipsis
/>
</DataTableRow>
</DataTable>
Expand Down
2 changes: 2 additions & 0 deletions features/wsteth/wrap/wrap-form/wrap-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export const WrapFormStats = () => {
amount={willReceiveWsteth}
data-testid="youWillReceive"
symbol="wstETH"
showAmountTip
trimEllipsis
/>
</DataTableRow>
</DataTable>
Expand Down
4 changes: 4 additions & 0 deletions pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ export default class MyDocument extends Document {
return (
<Html lang="en">
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
{dynamics.ipfsMode && (
<meta
httpEquiv="Content-Security-Policy"
Expand Down
Loading

0 comments on commit fbc99c9

Please sign in to comment.