Skip to content

Commit 034a99c

Browse files
authored
Merge pull request ssvlabs#1094 from ssvlabs/stage
Stage to Main
2 parents b8c77aa + bcac4f8 commit 034a99c

File tree

32 files changed

+817
-669
lines changed

32 files changed

+817
-669
lines changed

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,10 @@
2323
"test/**/__snapshots__": true,
2424
"yarn.lock": true
2525
},
26-
"cSpell.words": ["holesky", "ssvweb", "Wagmi"]
26+
"cSpell.words": [
27+
"holesky",
28+
"pageview",
29+
"ssvweb",
30+
"Wagmi"
31+
]
2732
}

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
<meta
2727
content="connect-src 'self' data: https://www.gasnow.org
28+
https://api-js.mixpanel.com
2829
https://ethereum-holesky.publicnode.com
2930
https://late-thrilling-arm.ethereum-holesky.quiknode.pro/b64c32d5e1b1664b4ed2de4faef610d2cf08ed26
3031
https://misty-purple-sailboat.quiknode.pro/7fea68f21d77d9b54fc35c3f6d68199a880f5cf0

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@safe-global/safe-apps-provider": "^0.18.0",
3333
"@safe-global/safe-apps-sdk": "^8.1.0",
3434
"@tanstack/react-query": "5.0.5",
35+
"@tanstack/react-query-devtools": "^5.51.11",
3536
"@testing-library/jest-dom": "^4.2.4",
3637
"@testing-library/react": "^9.3.2",
3738
"@testing-library/user-event": "^7.1.2",
@@ -94,6 +95,7 @@
9495
"lodash-es": "^4.17.21",
9596
"lucide-react": "^0.378.0",
9697
"mini-css-extract-plugin": "0.11.3",
98+
"mixpanel-browser": "^2.53.0",
9799
"mobx": "6.7.0",
98100
"mobx-react": "7.6.0",
99101
"mobx-undecorate": "^1.2.0",
@@ -145,6 +147,7 @@
145147
"url-loader": "4.1.1",
146148
"util": "^0.12.5",
147149
"viem": "^2.9.32",
150+
"virtua": "^0.33.3",
148151
"wagmi": "^2.9.7",
149152
"web3": "^4.7.0",
150153
"zod": "^3.23.8"
@@ -202,6 +205,7 @@
202205
"@types/jest": "^24.0.0",
203206
"@types/jsdom": "^16.2.10",
204207
"@types/jsdom-global": "^3.0.2",
208+
"@types/mixpanel-browser": "^2.49.1",
205209
"@types/mocha": "^8.2.2",
206210
"@types/node": "^15.0.2",
207211
"@types/randombytes": "^2.0.0",

src/app/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { AppTheme } from '~root/Theme';
2222
import { getFromLocalStorageByKey } from '~root/providers/localStorage.provider';
2323
import { getColors } from '~root/themes';
2424
import './globals.css';
25+
import { useTrackPageViews } from '~root/mixpanel/useTrackPageViews';
26+
import { useIdentify } from '~root/mixpanel/useIdentify';
2527

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

69+
useTrackPageViews();
70+
useIdentify();
6771
useWalletConnectivity();
6872

6973
useEffect(() => {

src/app/common/config/translations.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,6 @@ const translations = {
3939
SELECT_REMOVE_VALIDATORS: 'Select Validators to Remove',
4040
SELECT_EXIT_VALIDATORS: 'Select Validators to Exit'
4141
},
42-
BULK_TOOLTIPS: {
43-
REMOVE_VALIDATORS: (count: number) => `Bulk remove is capped at ${count} validators per batch.`,
44-
REMOVE_VALIDATORS_CHECKBOX: (count: number) => `You have reached the limit of ${count} validators`,
45-
EXIT_VALIDATORS: (count: number) => `The maximum number of validators for bulk exit is ${count}.`,
46-
EXIT_VALIDATORS_CHECKBOX: (count: number) => `You can't select more than ${count} validators per batch.`
47-
},
4842
FLOW_CONFIRMATION_DATA: {
4943
REMOVE: {
5044
title: 'Remove Validator',

src/app/common/stores/applications/SsvWeb/Validator.store.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { prepareSsvAmountToTransfer, toWei } from '~root/services/conversions.se
1212
import { transactionExecutor } from '~root/services/transaction.service';
1313
import { fetchIsRegisteredValidator, getLiquidationCollateralPerValidator } from '~root/services/validator.service';
1414
import { createPayload } from '~root/utils/dkg.utils';
15+
import { track } from '~root/mixpanel';
1516

1617
const annotations = {
1718
keyStoreFile: observable,
@@ -165,16 +166,25 @@ class ValidatorStore {
165166
return false;
166167
}
167168

169+
const publicKeys = payload.get('keyStorePublicKey') as string | string[];
170+
const validators_amount = Array.isArray(publicKeys) ? publicKeys.length : 1;
171+
const values = payload.values();
168172
return await transactionExecutor({
169173
contractMethod,
170-
payload: payload.values(),
174+
payload: values,
171175
getterTransactionState: async () => {
172176
const { validatorCount } = await getClusterData(getClusterHash(Object.values(operators), accountAddress), liquidationCollateralPeriod, minimumLiquidationCollateral);
173177
return validatorCount;
174178
},
175179
prevState: payload.get('clusterData').validatorCount,
176180
isContractWallet: isContractWallet,
177-
dispatch
181+
dispatch,
182+
onError: (error: any) => {
183+
track('Validator Registered', { validators_amount, status: 'error', error_message: error?.message ?? 'unknown error' });
184+
},
185+
onSuccess: () => {
186+
track('Validator Registered', { validators_amount, status: 'success' });
187+
}
178188
});
179189
}
180190

@@ -330,7 +340,7 @@ class ValidatorStore {
330340
* @param keyStore
331341
* @param callBack
332342
*/
333-
async setKeyStore(keyStore: any, callBack?: any) {
343+
async setKeyStore(keyStore: File, callBack?: () => void) {
334344
try {
335345
this.keyStorePrivateKey = '';
336346
this.keyStoreFile = keyStore;

src/app/components/applications/SSV/MyAccount/common/NewRemainingDays/NewRemainingDays.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import Tooltip from '~app/components/common/ToolTip/ToolTip';
99
import ProgressBar from '~app/components/applications/SSV/MyAccount/common/ProgressBar/ProgressBar';
1010
import { useStyles } from '~app/components/applications/SSV/MyAccount/common/NewRemainingDays/NewRemainingDays.styles';
1111
import LiquidationStateError, { LiquidationStateErrorType } from '~app/components/applications/SSV/MyAccount/common/LiquidationStateError/LiquidationStateError';
12+
import { ICluster } from '~app/model/cluster.model.ts';
1213

1314
type Props = {
14-
cluster: any;
15+
cluster: ICluster & { newRunWay?: number };
1516
withdrawState?: boolean;
16-
isInputFilled?: string | null;
17+
isInputFilled?: string | number | null;
1718
};
1819

1920
const NewRemainingDays = ({ cluster, withdrawState, isInputFilled = null }: Props) => {

src/app/components/applications/SSV/MyAccount/components/ClusterDashboard/ClusterDashboard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ export const ClusterDashboard = () => {
8787
) => a.runWay - b.runWay
8888
);
8989

90-
const rows = sortedClusters.map((cluster: any) => {
90+
const rows = sortedClusters.map((cluster) => {
9191
const remainingDaysValue = formatNumberToUi(cluster.runWay, true);
9292
const remainingDays = cluster.runWay && cluster.runWay !== Infinity ? `${remainingDaysValue} Days` : remainingDaysValue;
9393
return createData(
9494
longStringShorten(getClusterHash(cluster.operators, accountAddress).slice(2), 4),
9595
<Grid container style={{ gap: 8 }}>
96-
{cluster.operators.map((operator: any, index: number) => {
96+
{cluster.operators.map((operator, index: number) => {
9797
return (
9898
<Tooltip key={index} content={<OperatorDetails operator={operator} />} delayDuration={300} className="bg-gray-50 px-6 pt-4">
9999
<Grid

src/app/components/applications/SSV/MyAccount/components/EditFeeFlow/UpdateFee/UpdateFee.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ const UpdateFee = () => {
7676
}, [newFee, error]);
7777

7878
const declareNewFeeHandler = () => {
79+
const operatorFee = formatNumberToUi(getFeeForYear(fromWei(operator.fee)));
80+
setNewFee(operatorFee);
7981
setCurrentFlowStep(FeeUpdateSteps.START);
8082
};
8183

Lines changed: 47 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1+
import { xor } from 'lodash';
12
import { useEffect, useState } from 'react';
23
import { Location, useLocation, useNavigate } from 'react-router-dom';
34
import { translations } from '~app/common/config';
45
import ConfirmationStep from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/ConfirmationStep';
56
import ExitFinishPage from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/ExitFinishPage';
67
import NewBulkActions from '~app/components/applications/SSV/MyAccount/components/Validator/BulkActions/NewBulkActions';
8+
import { BULK_FLOWS } from '~app/enums/bulkFlow.enum.ts';
9+
import { useClusterValidators } from '~app/hooks/cluster/useClusterValidators';
710
import { useAppDispatch, useAppSelector } from '~app/hooks/redux.hook';
811
import { IOperator } from '~app/model/operator.model';
9-
import { BULK_FLOWS } from '~app/enums/bulkFlow.enum.ts';
10-
import { BulkValidatorData, IValidator } from '~app/model/validator.model';
12+
import { getSelectedCluster, setExcludedCluster } from '~app/redux/account.slice.ts';
1113
import { getNetworkFeeAndLiquidationCollateral } from '~app/redux/network.slice';
1214
import { getAccountAddress, getIsContractWallet } from '~app/redux/wallet.slice';
15+
import { BulkActionRouteState } from '~app/Routes';
1316
import { MAXIMUM_VALIDATOR_COUNT_FLAG } from '~lib/utils/developerHelper';
1417
import { add0x } from '~lib/utils/strings';
1518
import { exitValidators, removeValidators } from '~root/services/validatorContract.service';
16-
import { getSelectedCluster, setExcludedCluster } from '~app/redux/account.slice.ts';
17-
import { BulkActionRouteState } from '~app/Routes';
1819

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

30-
const MAX_VALIDATORS_COUNT = Number(window.localStorage.getItem(MAXIMUM_VALIDATOR_COUNT_FLAG)) || 100;
31-
32-
const BULK_ACTIONS_TOOLTIP_TITLES = {
33-
[BULK_FLOWS.BULK_REMOVE]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.REMOVE_VALIDATORS(MAX_VALIDATORS_COUNT),
34-
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.EXIT_VALIDATORS(MAX_VALIDATORS_COUNT)
35-
};
36-
37-
const BULK_ACTIONS_TOOLTIP_CHECKBOX_TITLES = {
38-
[BULK_FLOWS.BULK_REMOVE]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.REMOVE_VALIDATORS_CHECKBOX(MAX_VALIDATORS_COUNT),
39-
[BULK_FLOWS.BULK_EXIT]: translations.VALIDATOR.REMOVE_EXIT_VALIDATOR.BULK_TOOLTIPS.EXIT_VALIDATORS_CHECKBOX(MAX_VALIDATORS_COUNT)
40-
};
31+
export const MAX_VALIDATORS_COUNT = Number(window.localStorage.getItem(MAXIMUM_VALIDATOR_COUNT_FLAG)) || 100;
4132

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

4738
const BulkComponent = () => {
48-
const [selectedValidators, setSelectedValidators] = useState<Record<string, BulkValidatorData>>({});
39+
const [selectedValidators, setSelectedValidators] = useState<string[]>([]);
4940
const [currentStep, setCurrentStep] = useState(BULK_STEPS.BULK_ACTIONS);
5041
const navigate = useNavigate();
5142
const accountAddress = useAppSelector(getAccountAddress);
5243
const isContractWallet = useAppSelector(getIsContractWallet);
5344
const cluster = useAppSelector(getSelectedCluster);
45+
5446
const { liquidationCollateralPeriod, minimumLiquidationCollateral } = useAppSelector(getNetworkFeeAndLiquidationCollateral);
5547

48+
const { infiniteQuery, fetchAll, validators } = useClusterValidators(cluster);
49+
const maxSelectable = cluster.validatorCount;
50+
51+
const isAllSelected = selectedValidators.length === maxSelectable;
52+
5653
const location: Location<BulkActionRouteState> = useLocation();
5754
const { validator, currentBulkFlow } = location.state;
5855

59-
const [isLoading, setIsLoading] = useState(false);
6056
const dispatch = useAppDispatch();
6157

6258
useEffect(() => {
6359
if (validator) {
64-
setSelectedValidators({
65-
[add0x(validator.public_key)]: {
66-
validator,
67-
isSelected: true
68-
}
69-
});
60+
setSelectedValidators([add0x(validator.public_key)]);
7061
setCurrentStep(BULK_STEPS.BULK_CONFIRMATION);
7162
}
7263
}, []);
7364

74-
const selectMaxValidatorsCount = (validators: IValidator[], validatorList: Record<string, BulkValidatorData>): Record<string, BulkValidatorData> => {
75-
const isSelected = Object.values(selectedValidators).every((validator: { validator: IValidator; isSelected: boolean }) => !validator.isSelected);
76-
validators.forEach((validator: IValidator, index: number) => {
77-
validatorList[add0x(validator.public_key)] = {
78-
validator,
79-
isSelected: isSelected && index < MAX_VALIDATORS_COUNT
80-
};
81-
});
82-
return validatorList;
83-
};
65+
const onToggleAll = () => {
66+
if (!isAllSelected && validators.length < maxSelectable) {
67+
return fetchAll.mutateAsync().then((data) => {
68+
if (!data) return;
69+
return setSelectedValidators(data.slice(0, maxSelectable)?.map((validator) => add0x(validator.public_key)));
70+
});
71+
}
8472

85-
const fillSelectedValidators = (validators: IValidator[], selectAll: boolean = false) => {
86-
if (validators) {
87-
let validatorList: Record<string, BulkValidatorData> = {};
88-
if (selectAll) {
89-
validatorList = selectMaxValidatorsCount(validators, validatorList);
90-
} else {
91-
validators.forEach((validator: IValidator) => {
92-
validatorList[add0x(validator.public_key)] = {
93-
validator,
94-
isSelected: selectedValidators[add0x(validator.public_key)]?.isSelected || false
95-
};
96-
});
97-
}
98-
setSelectedValidators(validatorList);
73+
if (!isAllSelected) {
74+
setSelectedValidators(validators.slice(0, maxSelectable).map((validator) => add0x(validator.public_key)));
75+
} else {
76+
setSelectedValidators([]);
9977
}
10078
};
10179

102-
const onCheckboxClickHandler = ({ publicKey }: { publicKey: string }) => {
103-
setSelectedValidators((prevState: any) => {
104-
prevState[publicKey].isSelected = !prevState[publicKey].isSelected;
105-
return { ...prevState };
106-
});
80+
const onValidatorToggle = (publicKey: string) => {
81+
setSelectedValidators((prev) => xor(prev, [publicKey]));
10782
};
10883

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

11388
const nextStep = async () => {
114-
const selectedValidatorKeys = Object.keys(selectedValidators);
115-
const selectedValidatorValues = Object.values(selectedValidators);
11689
let res;
117-
const selectedValidatorsCount = selectedValidatorValues.filter((validator) => validator.isSelected).length;
118-
const isBulk = selectedValidatorsCount > 1;
90+
const selectedValidatorsCount = selectedValidators.length;
91+
const isBulk = selectedValidators.length > 1;
92+
const validatorPks = isBulk ? selectedValidators : selectedValidators[0];
11993
if (currentStep === BULK_STEPS.BULK_ACTIONS) {
12094
setCurrentStep(BULK_STEPS.BULK_CONFIRMATION);
12195
} else if (currentStep === BULK_STEPS.BULK_CONFIRMATION && currentBulkFlow === BULK_FLOWS.BULK_EXIT) {
122-
setIsLoading(true);
123-
const validatorIds = isBulk
124-
? selectedValidatorKeys.filter((publicKey: string) => selectedValidators[publicKey].isSelected)
125-
: add0x(selectedValidatorValues.filter((selectedValidator) => selectedValidator.isSelected)[0].validator.public_key);
12696
res = await exitValidators({
12797
isContractWallet,
128-
validatorIds,
98+
validatorIds: validatorPks,
12999
operatorIds: cluster.operators.map((operator: IOperator) => operator.id),
130100
isBulk,
131101
dispatch
132102
});
133103
if (res && !isContractWallet) {
134104
setCurrentStep(BULK_STEPS.BULK_EXIT_FINISH);
135105
}
136-
setIsLoading(false);
137106
} else if (currentStep === BULK_STEPS.BULK_EXIT_FINISH) {
138107
backToSingleClusterPage();
139108
} else {
140-
setIsLoading(true);
141109
if (selectedValidatorsCount === cluster.validatorCount && cluster.isLiquidated) {
142110
dispatch(setExcludedCluster(cluster));
143111
}
144-
const validatorPks = isBulk
145-
? selectedValidatorKeys.filter((publicKey: string) => selectedValidators[publicKey].isSelected)
146-
: add0x(validator?.public_key || selectedValidatorValues.filter((selectedValidator) => selectedValidator.isSelected)[0].validator.public_key);
147112
res = await removeValidators({
148113
cluster,
149114
accountAddress,
@@ -158,7 +123,6 @@ const BulkComponent = () => {
158123
if (res && !isContractWallet) {
159124
backToSingleClusterPage(selectedValidatorsCount);
160125
}
161-
setIsLoading(false);
162126
}
163127
};
164128

@@ -167,14 +131,20 @@ const BulkComponent = () => {
167131
if (currentStep === BULK_STEPS.BULK_ACTIONS && !validator) {
168132
return (
169133
<NewBulkActions
170-
nextStep={nextStep}
171-
tooltipTitle={BULK_ACTIONS_TOOLTIP_TITLES[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
172-
checkboxTooltipTitle={BULK_ACTIONS_TOOLTIP_CHECKBOX_TITLES[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
173-
maxValidatorsCount={MAX_VALIDATORS_COUNT}
174134
title={BULK_FLOWS_ACTION_TITLE[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
175-
fillSelectedValidators={fillSelectedValidators}
176-
selectedValidators={selectedValidators}
177-
onCheckboxClickHandler={onCheckboxClickHandler}
135+
nextStep={nextStep}
136+
listProps={{
137+
type: 'select',
138+
validators: validators,
139+
withoutSettings: true,
140+
onToggleAll,
141+
maxSelectable,
142+
selectedValidators,
143+
onValidatorToggle,
144+
isEmpty: infiniteQuery.isSuccess && validators.length === 0,
145+
infiniteScroll: infiniteQuery,
146+
isFetchingAll: fetchAll.isPending
147+
}}
178148
/>
179149
);
180150
}
@@ -184,16 +154,16 @@ const BulkComponent = () => {
184154
<ConfirmationStep
185155
stepBack={!validator ? stepBack : undefined}
186156
flowData={BULK_FLOWS_CONFIRMATION_DATA[currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE]}
187-
selectedValidators={Object.keys(selectedValidators).filter((publicKey: string) => selectedValidators[publicKey].isSelected)}
188-
isLoading={isLoading}
157+
selectedValidators={selectedValidators}
158+
isLoading={false}
189159
currentBulkFlow={currentBulkFlow ?? BULK_FLOWS.BULK_REMOVE}
190160
nextStep={nextStep}
191161
/>
192162
);
193163
}
194164

195165
// BULK_STEPS.BULK_EXIT_FINISH === currentStep
196-
return <ExitFinishPage nextStep={nextStep} selectedValidators={Object.keys(selectedValidators).filter((publicKey: string) => selectedValidators[publicKey].isSelected)} />;
166+
return <ExitFinishPage nextStep={nextStep} selectedValidators={selectedValidators} />;
197167
};
198168

199169
export default BulkComponent;

0 commit comments

Comments
 (0)