Skip to content

Commit

Permalink
Merge pull request ssvlabs#1094 from ssvlabs/stage
Browse files Browse the repository at this point in the history
Stage to Main
  • Loading branch information
IlyaVi authored Jul 31, 2024
2 parents b8c77aa + bcac4f8 commit 034a99c
Show file tree
Hide file tree
Showing 32 changed files with 817 additions and 669 deletions.
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@
"test/**/__snapshots__": true,
"yarn.lock": true
},
"cSpell.words": ["holesky", "ssvweb", "Wagmi"]
"cSpell.words": [
"holesky",
"pageview",
"ssvweb",
"Wagmi"
]
}
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<meta
content="connect-src 'self' data: https://www.gasnow.org
https://api-js.mixpanel.com
https://ethereum-holesky.publicnode.com
https://late-thrilling-arm.ethereum-holesky.quiknode.pro/b64c32d5e1b1664b4ed2de4faef610d2cf08ed26
https://misty-purple-sailboat.quiknode.pro/7fea68f21d77d9b54fc35c3f6d68199a880f5cf0
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@safe-global/safe-apps-provider": "^0.18.0",
"@safe-global/safe-apps-sdk": "^8.1.0",
"@tanstack/react-query": "5.0.5",
"@tanstack/react-query-devtools": "^5.51.11",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
Expand Down Expand Up @@ -94,6 +95,7 @@
"lodash-es": "^4.17.21",
"lucide-react": "^0.378.0",
"mini-css-extract-plugin": "0.11.3",
"mixpanel-browser": "^2.53.0",
"mobx": "6.7.0",
"mobx-react": "7.6.0",
"mobx-undecorate": "^1.2.0",
Expand Down Expand Up @@ -145,6 +147,7 @@
"url-loader": "4.1.1",
"util": "^0.12.5",
"viem": "^2.9.32",
"virtua": "^0.33.3",
"wagmi": "^2.9.7",
"web3": "^4.7.0",
"zod": "^3.23.8"
Expand Down Expand Up @@ -202,6 +205,7 @@
"@types/jest": "^24.0.0",
"@types/jsdom": "^16.2.10",
"@types/jsdom-global": "^3.0.2",
"@types/mixpanel-browser": "^2.49.1",
"@types/mocha": "^8.2.2",
"@types/node": "^15.0.2",
"@types/randombytes": "^2.0.0",
Expand Down
4 changes: 4 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { AppTheme } from '~root/Theme';
import { getFromLocalStorageByKey } from '~root/providers/localStorage.provider';
import { getColors } from '~root/themes';
import './globals.css';
import { useTrackPageViews } from '~root/mixpanel/useTrackPageViews';
import { useIdentify } from '~root/mixpanel/useIdentify';

const LoaderWrapper = styled.div<{ theme: any }>`
display: flex;
Expand Down Expand Up @@ -64,6 +66,8 @@ const App = () => {
const accountAddress = useAppSelector(getAccountAddress);
const { navigateToRoot } = useNavigateToRoot();

useTrackPageViews();
useIdentify();
useWalletConnectivity();

useEffect(() => {
Expand Down
6 changes: 0 additions & 6 deletions src/app/common/config/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ const translations = {
SELECT_REMOVE_VALIDATORS: 'Select Validators to Remove',
SELECT_EXIT_VALIDATORS: 'Select Validators to Exit'
},
BULK_TOOLTIPS: {
REMOVE_VALIDATORS: (count: number) => `Bulk remove is capped at ${count} validators per batch.`,
REMOVE_VALIDATORS_CHECKBOX: (count: number) => `You have reached the limit of ${count} validators`,
EXIT_VALIDATORS: (count: number) => `The maximum number of validators for bulk exit is ${count}.`,
EXIT_VALIDATORS_CHECKBOX: (count: number) => `You can't select more than ${count} validators per batch.`
},
FLOW_CONFIRMATION_DATA: {
REMOVE: {
title: 'Remove Validator',
Expand Down
16 changes: 13 additions & 3 deletions src/app/common/stores/applications/SsvWeb/Validator.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { prepareSsvAmountToTransfer, toWei } from '~root/services/conversions.se
import { transactionExecutor } from '~root/services/transaction.service';
import { fetchIsRegisteredValidator, getLiquidationCollateralPerValidator } from '~root/services/validator.service';
import { createPayload } from '~root/utils/dkg.utils';
import { track } from '~root/mixpanel';

const annotations = {
keyStoreFile: observable,
Expand Down Expand Up @@ -165,16 +166,25 @@ class ValidatorStore {
return false;
}

const publicKeys = payload.get('keyStorePublicKey') as string | string[];
const validators_amount = Array.isArray(publicKeys) ? publicKeys.length : 1;
const values = payload.values();
return await transactionExecutor({
contractMethod,
payload: payload.values(),
payload: values,
getterTransactionState: async () => {
const { validatorCount } = await getClusterData(getClusterHash(Object.values(operators), accountAddress), liquidationCollateralPeriod, minimumLiquidationCollateral);
return validatorCount;
},
prevState: payload.get('clusterData').validatorCount,
isContractWallet: isContractWallet,
dispatch
dispatch,
onError: (error: any) => {
track('Validator Registered', { validators_amount, status: 'error', error_message: error?.message ?? 'unknown error' });
},
onSuccess: () => {
track('Validator Registered', { validators_amount, status: 'success' });
}
});
}

Expand Down Expand Up @@ -330,7 +340,7 @@ class ValidatorStore {
* @param keyStore
* @param callBack
*/
async setKeyStore(keyStore: any, callBack?: any) {
async setKeyStore(keyStore: File, callBack?: () => void) {
try {
this.keyStorePrivateKey = '';
this.keyStoreFile = keyStore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import Tooltip from '~app/components/common/ToolTip/ToolTip';
import ProgressBar from '~app/components/applications/SSV/MyAccount/common/ProgressBar/ProgressBar';
import { useStyles } from '~app/components/applications/SSV/MyAccount/common/NewRemainingDays/NewRemainingDays.styles';
import LiquidationStateError, { LiquidationStateErrorType } from '~app/components/applications/SSV/MyAccount/common/LiquidationStateError/LiquidationStateError';
import { ICluster } from '~app/model/cluster.model.ts';

type Props = {
cluster: any;
cluster: ICluster & { newRunWay?: number };
withdrawState?: boolean;
isInputFilled?: string | null;
isInputFilled?: string | number | null;
};

const NewRemainingDays = ({ cluster, withdrawState, isInputFilled = null }: Props) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ export const ClusterDashboard = () => {
) => a.runWay - b.runWay
);

const rows = sortedClusters.map((cluster: any) => {
const rows = sortedClusters.map((cluster) => {
const remainingDaysValue = formatNumberToUi(cluster.runWay, true);
const remainingDays = cluster.runWay && cluster.runWay !== Infinity ? `${remainingDaysValue} Days` : remainingDaysValue;
return createData(
longStringShorten(getClusterHash(cluster.operators, accountAddress).slice(2), 4),
<Grid container style={{ gap: 8 }}>
{cluster.operators.map((operator: any, index: number) => {
{cluster.operators.map((operator, index: number) => {
return (
<Tooltip key={index} content={<OperatorDetails operator={operator} />} delayDuration={300} className="bg-gray-50 px-6 pt-4">
<Grid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const UpdateFee = () => {
}, [newFee, error]);

const declareNewFeeHandler = () => {
const operatorFee = formatNumberToUi(getFeeForYear(fromWei(operator.fee)));
setNewFee(operatorFee);
setCurrentFlowStep(FeeUpdateSteps.START);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { xor } from 'lodash';
import { useEffect, useState } from 'react';
import { Location, useLocation, useNavigate } from 'react-router-dom';
import { translations } from '~app/common/config';
import ConfirmationStep from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/ConfirmationStep';
import ExitFinishPage from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/ExitFinishPage';
import NewBulkActions from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/NewBulkActions';
import { BULK_FLOWS } from '~app/enums/bulkFlow.enum.ts';
import { useClusterValidators } from '~app/hooks/cluster/useClusterValidators';
import { useAppDispatch, useAppSelector } from '~app/hooks/redux.hook';
import { IOperator } from '~app/model/operator.model';
import { BULK_FLOWS } from '~app/enums/bulkFlow.enum.ts';
import { BulkValidatorData, IValidator } from '~app/model/validator.model';
import { getSelectedCluster, setExcludedCluster } from '~app/redux/account.slice.ts';
import { getNetworkFeeAndLiquidationCollateral } from '~app/redux/network.slice';
import { getAccountAddress, getIsContractWallet } from '~app/redux/wallet.slice';
import { BulkActionRouteState } from '~app/Routes';
import { MAXIMUM_VALIDATOR_COUNT_FLAG } from '~lib/utils/developerHelper';
import { add0x } from '~lib/utils/strings';
import { exitValidators, removeValidators } from '~root/services/validatorContract.service';
import { getSelectedCluster, setExcludedCluster } from '~app/redux/account.slice.ts';
import { BulkActionRouteState } from '~app/Routes';

enum BULK_STEPS {
BULK_ACTIONS = 'BULK_ACTIONS',
Expand All @@ -27,123 +28,87 @@ const BULK_FLOWS_ACTION_TITLE = {
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TITLES.SELECT_EXIT_VALIDATORS
};

const MAX_VALIDATORS_COUNT = Number(window.localStorage.getItem(MAXIMUM_VALIDATOR_COUNT_FLAG)) || 100;

const BULK_ACTIONS_TOOLTIP_TITLES = {
[BULK_FLOWS.BULK_REMOVE]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.REMOVE_VALIDATORS(MAX_VALIDATORS_COUNT),
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.EXIT_VALIDATORS(MAX_VALIDATORS_COUNT)
};

const BULK_ACTIONS_TOOLTIP_CHECKBOX_TITLES = {
[BULK_FLOWS.BULK_REMOVE]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.REMOVE_VALIDATORS_CHECKBOX(MAX_VALIDATORS_COUNT),
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.EXIT_VALIDATORS_CHECKBOX(MAX_VALIDATORS_COUNT)
};
export const MAX_VALIDATORS_COUNT = Number(window.localStorage.getItem(MAXIMUM_VALIDATOR_COUNT_FLAG)) || 100;

const BULK_FLOWS_CONFIRMATION_DATA = {
[BULK_FLOWS.BULK_REMOVE]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.FLOW_CONFIRMATION_DATA.REMOVE,
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.FLOW_CONFIRMATION_DATA.EXIT
};

const BulkComponent = () => {
const [selectedValidators, setSelectedValidators] = useState<Record<string, BulkValidatorData>>({});
const [selectedValidators, setSelectedValidators] = useState<string[]>([]);
const [currentStep, setCurrentStep] = useState(BULK_STEPS.BULK_ACTIONS);
const navigate = useNavigate();
const accountAddress = useAppSelector(getAccountAddress);
const isContractWallet = useAppSelector(getIsContractWallet);
const cluster = useAppSelector(getSelectedCluster);

const { liquidationCollateralPeriod, minimumLiquidationCollateral } = useAppSelector(getNetworkFeeAndLiquidationCollateral);

const { infiniteQuery, fetchAll, validators } = useClusterValidators(cluster);
const maxSelectable = cluster.validatorCount;

const isAllSelected = selectedValidators.length === maxSelectable;

const location: Location<BulkActionRouteState> = useLocation();
const { validator, currentBulkFlow } = location.state;

const [isLoading, setIsLoading] = useState(false);
const dispatch = useAppDispatch();

useEffect(() => {
if (validator) {
setSelectedValidators({
[add0x(validator.public_key)]: {
validator,
isSelected: true
}
});
setSelectedValidators([add0x(validator.public_key)]);
setCurrentStep(BULK_STEPS.BULK_CONFIRMATION);
}
}, []);

const selectMaxValidatorsCount = (validators: IValidator[], validatorList: Record<string, BulkValidatorData>): Record<string, BulkValidatorData> => {
const isSelected = Object.values(selectedValidators).every((validator: { validator: IValidator; isSelected: boolean }) => !validator.isSelected);
validators.forEach((validator: IValidator, index: number) => {
validatorList[add0x(validator.public_key)] = {
validator,
isSelected: isSelected && index < MAX_VALIDATORS_COUNT
};
});
return validatorList;
};
const onToggleAll = () => {
if (!isAllSelected && validators.length < maxSelectable) {
return fetchAll.mutateAsync().then((data) => {
if (!data) return;
return setSelectedValidators(data.slice(0, maxSelectable)?.map((validator) => add0x(validator.public_key)));
});
}

const fillSelectedValidators = (validators: IValidator[], selectAll: boolean = false) => {
if (validators) {
let validatorList: Record<string, BulkValidatorData> = {};
if (selectAll) {
validatorList = selectMaxValidatorsCount(validators, validatorList);
} else {
validators.forEach((validator: IValidator) => {
validatorList[add0x(validator.public_key)] = {
validator,
isSelected: selectedValidators[add0x(validator.public_key)]?.isSelected || false
};
});
}
setSelectedValidators(validatorList);
if (!isAllSelected) {
setSelectedValidators(validators.slice(0, maxSelectable).map((validator) => add0x(validator.public_key)));
} else {
setSelectedValidators([]);
}
};

const onCheckboxClickHandler = ({ publicKey }: { publicKey: string }) => {
setSelectedValidators((prevState: any) => {
prevState[publicKey].isSelected = !prevState[publicKey].isSelected;
return { ...prevState };
});
const onValidatorToggle = (publicKey: string) => {
setSelectedValidators((prev) => xor(prev, [publicKey]));
};

const backToSingleClusterPage = (validatorsCount?: number) => {
navigate(validatorsCount === cluster.validatorCount && cluster.isLiquidated ? -2 : -1);
};

const nextStep = async () => {
const selectedValidatorKeys = Object.keys(selectedValidators);
const selectedValidatorValues = Object.values(selectedValidators);
let res;
const selectedValidatorsCount = selectedValidatorValues.filter((validator) => validator.isSelected).length;
const isBulk = selectedValidatorsCount > 1;
const selectedValidatorsCount = selectedValidators.length;
const isBulk = selectedValidators.length > 1;
const validatorPks = isBulk ? selectedValidators : selectedValidators[0];
if (currentStep === BULK_STEPS.BULK_ACTIONS) {
setCurrentStep(BULK_STEPS.BULK_CONFIRMATION);
} else if (currentStep === BULK_STEPS.BULK_CONFIRMATION && currentBulkFlow === BULK_FLOWS.BULK_EXIT) {
setIsLoading(true);
const validatorIds = isBulk
? selectedValidatorKeys.filter((publicKey: string) => selectedValidators[publicKey].isSelected)
: add0x(selectedValidatorValues.filter((selectedValidator) => selectedValidator.isSelected)[0].validator.public_key);
res = await exitValidators({
isContractWallet,
validatorIds,
validatorIds: validatorPks,
operatorIds: cluster.operators.map((operator: IOperator) => operator.id),
isBulk,
dispatch
});
if (res && !isContractWallet) {
setCurrentStep(BULK_STEPS.BULK_EXIT_FINISH);
}
setIsLoading(false);
} else if (currentStep === BULK_STEPS.BULK_EXIT_FINISH) {
backToSingleClusterPage();
} else {
setIsLoading(true);
if (selectedValidatorsCount === cluster.validatorCount && cluster.isLiquidated) {
dispatch(setExcludedCluster(cluster));
}
const validatorPks = isBulk
? selectedValidatorKeys.filter((publicKey: string) => selectedValidators[publicKey].isSelected)
: add0x(validator?.public_key || selectedValidatorValues.filter((selectedValidator) => selectedValidator.isSelected)[0].validator.public_key);
res = await removeValidators({
cluster,
accountAddress,
Expand All @@ -158,7 +123,6 @@ const BulkComponent = () => {
if (res && !isContractWallet) {
backToSingleClusterPage(selectedValidatorsCount);
}
setIsLoading(false);
}
};

Expand All @@ -167,14 +131,20 @@ const BulkComponent = () => {
if (currentStep === BULK_STEPS.BULK_ACTIONS && !validator) {
return (
<NewBulkActions
nextStep={nextStep}
tooltipTitle={BULK_ACTIONS_TOOLTIP_TITLES[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
checkboxTooltipTitle={BULK_ACTIONS_TOOLTIP_CHECKBOX_TITLES[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
maxValidatorsCount={MAX_VALIDATORS_COUNT}
title={BULK_FLOWS_ACTION_TITLE[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
fillSelectedValidators={fillSelectedValidators}
selectedValidators={selectedValidators}
onCheckboxClickHandler={onCheckboxClickHandler}
nextStep={nextStep}
listProps={{
type: 'select',
validators: validators,
withoutSettings: true,
onToggleAll,
maxSelectable,
selectedValidators,
onValidatorToggle,
isEmpty: infiniteQuery.isSuccess && validators.length === 0,
infiniteScroll: infiniteQuery,
isFetchingAll: fetchAll.isPending
}}
/>
);
}
Expand All @@ -184,16 +154,16 @@ const BulkComponent = () => {
<ConfirmationStep
stepBack={!validator ? stepBack : undefined}
flowData={BULK_FLOWS_CONFIRMATION_DATA[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
selectedValidators={Object.keys(selectedValidators).filter((publicKey: string) => selectedValidators[publicKey].isSelected)}
isLoading={isLoading}
selectedValidators={selectedValidators}
isLoading={false}
currentBulkFlow={currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE}
nextStep={nextStep}
/>
);
}

// BULK_STEPS.BULK_EXIT_FINISH === currentStep
return <ExitFinishPage nextStep={nextStep} selectedValidators={Object.keys(selectedValidators).filter((publicKey: string) => selectedValidators[publicKey].isSelected)} />;
return <ExitFinishPage nextStep={nextStep} selectedValidators={selectedValidators} />;
};

export default BulkComponent;
Loading

0 comments on commit 034a99c

Please sign in to comment.