Skip to content

Commit 5332d01

Browse files
committed
feat: add address info popover, add form for main settings, more updates
1 parent e73b1ca commit 5332d01

File tree

25 files changed

+618
-153
lines changed

25 files changed

+618
-153
lines changed

assets/icons/error-triangle.svg

Lines changed: 3 additions & 0 deletions
Loading

features/create-vault/consts.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { InputProps } from '@lidofinance/lido-ui';
2-
import { FieldConfig } from 'features/create-vault/types';
2+
import { FieldConfig, FieldName } from 'features/create-vault/types';
33

44
export const CREATE_VAULT_STEPS = 3;
55

@@ -30,7 +30,7 @@ export const steps: Record<number, string> = {
3030

3131
export const getSectionNameByStep = (step: number) => steps[step];
3232

33-
export const createVaultFieldsList: FieldConfig[] = [
33+
export const createVaultFieldsList: FieldConfig<FieldName>[] = [
3434
{
3535
name: 'nodeOperator',
3636
title: 'Node Operator',
@@ -146,10 +146,20 @@ export const createVaultFieldsList: FieldConfig[] = [
146146
},
147147
];
148148

149-
export type Permission = (typeof createVaultFieldsList)[number]['name'];
149+
export const mainSettingsFields = [
150+
'nodeOperator',
151+
'assetRecoverer',
152+
'nodeOperatorFeeBP',
153+
'curatorFeeBP',
154+
'confirmExpiry',
155+
'defaultAdmin',
156+
'nodeOperatorManager',
157+
] as const;
150158

151-
export const getCreateVaultFields = (fieldsList: string[]) => {
152-
return createVaultFieldsList.filter((field) =>
153-
fieldsList.includes(field.name),
159+
export const getCreateVaultFields = <T extends FieldName>(
160+
fieldsList: readonly T[],
161+
): FieldConfig<T>[] => {
162+
return createVaultFieldsList.filter((field): field is FieldConfig<T> =>
163+
fieldsList.includes(field.name as T),
154164
);
155165
};

features/create-vault/create-vault-form/confirmation/confirmation-action/confirmation-action.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
import { FC } from 'react';
1+
import { FC, useEffect } from 'react';
22
import { useCreateVaultFormData } from 'features/create-vault/create-vault-form/create-vault-form-context';
33

44
import { Button } from '@lidofinance/lido-ui';
55
import { Container } from './styles';
6+
import { useFormContext } from 'react-hook-form';
67

78
export const ConfirmationAction: FC = () => {
89
const { step, handleSetStep } = useCreateVaultFormData();
10+
const {
11+
trigger,
12+
formState: { isValid },
13+
} = useFormContext();
14+
15+
useEffect(() => {
16+
// validate core form to unlock submit button
17+
void trigger();
18+
}, [trigger]);
919

1020
const handleSetPrevStep = () => {
1121
const prevStep = step - 1;
@@ -22,7 +32,7 @@ export const ConfirmationAction: FC = () => {
2232
>
2333
Back
2434
</Button>
25-
<Button type="submit" fullwidth>
35+
<Button disabled={!isValid} type="submit" fullwidth>
2636
Create vault
2737
</Button>
2838
</Container>

features/create-vault/create-vault-form/confirmation/confirmation-vault-info/confirmation-vault-info.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
InputDataType,
1313
ConfirmationList,
1414
FieldConfig,
15+
FieldName,
1516
} from 'features/create-vault/types';
1617
import { getCreateVaultFields } from 'features/create-vault/consts';
1718

@@ -23,7 +24,7 @@ const mainSettingsList = [
2324
'assetRecoverer',
2425
'defaultAdmin',
2526
'nodeOperatorManager',
26-
];
27+
] as const;
2728

2829
const managerPermissionsList = [
2930
'curatorFeeSetters',
@@ -38,17 +39,17 @@ const managerPermissionsList = [
3839
'validatorExitRequesters',
3940
'validatorWithdrawalTriggerers',
4041
'disconnecters',
41-
];
42+
] as const;
4243

43-
const nodeOperatorPermissionList = ['nodeOperatorFeeClaimers'];
44+
const nodeOperatorPermissionList = ['nodeOperatorFeeClaimers'] as const;
4445

4546
const mainSettings = getCreateVaultFields(mainSettingsList);
4647
const vaultManagerPermissions = getCreateVaultFields(managerPermissionsList);
4748
const nodeOperatorManagerPermissions = getCreateVaultFields(
4849
nodeOperatorPermissionList,
4950
);
5051

51-
const lists: Record<ConfirmationList, FieldConfig[]> = {
52+
const lists: Record<ConfirmationList, FieldConfig<FieldName>[]> = {
5253
mainSettings,
5354
vaultManagerPermissions,
5455
nodeOperatorManagerPermissions,

features/create-vault/create-vault-form/create-vault-form-context/create-vault-form-context.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
} from 'features/create-vault/types';
2727
import {
2828
createVaultFormValidator,
29+
createVaultSchema,
2930
CreateVaultSchema,
3031
formatCreateVaultData,
3132
} from './validation';
@@ -96,7 +97,6 @@ export const CreateFormProvider: FC<PropsWithChildren> = ({ children }) => {
9697
curatorFeeBP: 5,
9798
confirmExpiry: 36,
9899
defaultAdmin: '',
99-
confirmMainSettings: false,
100100
funders: [],
101101
withdrawers: [],
102102
minters: [],
@@ -111,7 +111,7 @@ export const CreateFormProvider: FC<PropsWithChildren> = ({ children }) => {
111111
curatorFeeClaimers: [],
112112
nodeOperatorFeeClaimers: [],
113113
},
114-
resolver: createVaultFormValidator,
114+
resolver: createVaultFormValidator(createVaultSchema),
115115
mode: 'all',
116116
});
117117

features/create-vault/create-vault-form/create-vault-form-context/validation.ts

Lines changed: 126 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { isAddress, PublicClient } from 'viem';
2-
import { normalize } from 'viem/ens';
3-
import { z, ZodError } from 'zod';
1+
import { isAddress } from 'viem';
2+
import { z, ZodError, ZodSchema } from 'zod';
43
import {
54
appendErrors,
65
FieldError,
@@ -9,16 +8,24 @@ import {
98
} from 'react-hook-form';
109
import { VaultFactoryArgs } from 'types';
1110
import {
11+
MainSettingsKeys,
1212
PermissionKeys,
13+
VaultMainSettingsType,
1314
VaultPermissionsType,
1415
} from 'features/create-vault/types';
16+
import { isValidAnyAddress } from 'utils/address-validation';
17+
import { isValidEns } from '../../../../utils/ens';
1518

1619
const INVALID_ADDRESS_MESSAGE = 'Invalid ethereum address';
1720
const INVALID_NUMBER_MIN_MESSAGE = 'Must be 0.001 or above';
1821
const INVALID_NUMBER_MAX_MESSAGE = 'Must be 99 or less';
19-
const INVALID_NUMBER_DATA_MESSAGE = { message: 'Only number is valid' };
22+
const INVALID_NUMBER_SUM_MESSAGE =
23+
"Sum of Curator's and Node Operator's fees can't be more than 100";
24+
const INVALID_NUMBER_EXPIRY_MAX_MESSAGE = 'Must be 800 or less';
25+
const INVALID_NUMBER_DATA_MESSAGE = 'Only number is valid';
26+
const INVALID_NUMBER_DATA_OBJECT_MESSAGE = { message: 'Only number is valid' };
2027

21-
const validateAddress = (value: string) => isAddress(value);
28+
const validateAddress = (value: string) => isValidAnyAddress(value);
2229

2330
const addressSchema = z
2431
.string()
@@ -29,19 +36,18 @@ export const createVaultSchema = z.object({
2936
nodeOperatorManager: addressSchema,
3037
assetRecoverer: addressSchema,
3138
nodeOperatorFeeBP: z
32-
.number(INVALID_NUMBER_DATA_MESSAGE)
39+
.number(INVALID_NUMBER_DATA_OBJECT_MESSAGE)
3340
.min(0.001, INVALID_NUMBER_MIN_MESSAGE)
3441
.max(99, INVALID_NUMBER_MAX_MESSAGE),
3542
curatorFeeBP: z.coerce
36-
.number(INVALID_NUMBER_DATA_MESSAGE)
43+
.number(INVALID_NUMBER_DATA_OBJECT_MESSAGE)
3744
.min(0.001, INVALID_NUMBER_MIN_MESSAGE)
3845
.max(99, INVALID_NUMBER_MAX_MESSAGE),
3946
confirmExpiry: z.coerce
40-
.number(INVALID_NUMBER_DATA_MESSAGE)
47+
.number(INVALID_NUMBER_DATA_OBJECT_MESSAGE)
4148
.min(1, INVALID_NUMBER_MIN_MESSAGE)
42-
.max(800, 'Must be 800 or less'),
49+
.max(800, INVALID_NUMBER_EXPIRY_MAX_MESSAGE),
4350
defaultAdmin: addressSchema,
44-
confirmMainSettings: z.boolean(),
4551
funders: z.array(addressSchema).optional(),
4652
withdrawers: z.array(addressSchema).optional(),
4753
minters: z.array(addressSchema).optional(),
@@ -114,49 +120,37 @@ export const parseZodErrorSchema = (
114120
return errors;
115121
};
116122

117-
export const createVaultFormValidator = async (values: CreateVaultSchema) => {
118-
try {
119-
const output = await createVaultSchema.parseAsync(values);
120-
return {
121-
values: output,
122-
errors: {},
123-
};
124-
} catch (err: unknown) {
125-
if (isZodError(err)) {
126-
const errors = err.errors;
123+
export const createVaultFormValidator = <T extends ZodSchema>(
124+
schema: T,
125+
): Resolver<z.infer<T>> => {
126+
return async (values: z.infer<T>) => {
127+
try {
128+
const output = schema.parse(values);
127129
return {
128-
values,
129-
errors: parseZodErrorSchema(errors, true),
130+
values: output,
131+
errors: {},
130132
};
131-
}
132-
133-
return {
134-
values: values,
135-
errors: {},
136-
};
137-
}
138-
};
133+
} catch (err: unknown) {
134+
if (isZodError(err)) {
135+
const errors = err.errors;
136+
return {
137+
values,
138+
errors: parseZodErrorSchema(errors, true),
139+
};
140+
}
139141

140-
// TODO: move to shared validators
141-
export const validateEnsDomain = async (
142-
value: string,
143-
publicClient: PublicClient,
144-
) => {
145-
try {
146-
const ensAddress = await publicClient.getEnsAddress({
147-
name: normalize(value),
148-
});
149-
150-
return !!ensAddress;
151-
} catch (e) {
152-
return false;
153-
}
142+
return {
143+
values: values,
144+
errors: {},
145+
};
146+
}
147+
};
154148
};
155149

156150
export const formatCreateVaultData = (
157151
values: CreateVaultSchema,
158152
): VaultFactoryArgs => {
159-
const { confirmMainSettings, nodeOperator, ...payload } = values;
153+
const { nodeOperator, ...payload } = values;
160154
(payload as unknown as VaultFactoryArgs).confirmExpiry = BigInt(
161155
values.confirmExpiry,
162156
);
@@ -165,9 +159,8 @@ export const formatCreateVaultData = (
165159
};
166160

167161
export const validatePermissions = (
168-
publicClient: PublicClient,
169162
getValues: UseFormGetValues<Record<string, any>>,
170-
): Resolver<VaultPermissionsType, any> => {
163+
): Resolver<VaultPermissionsType> => {
171164
return async (values: VaultPermissionsType) => {
172165
const errors = {} as Record<
173166
PermissionKeys,
@@ -185,10 +178,7 @@ export const validatePermissions = (
185178
const { value: currentValue } = field;
186179

187180
if (!isAddress(currentValue)) {
188-
const isValid = await validateEnsDomain(
189-
currentValue,
190-
publicClient,
191-
);
181+
const isValid = isValidEns(currentValue);
192182

193183
if (!isValid) {
194184
errors[key][`${index}`] = {
@@ -218,3 +208,88 @@ export const validatePermissions = (
218208
};
219209
};
220210
};
211+
212+
export const validateMainSettings: Resolver<VaultMainSettingsType> = (
213+
values: VaultMainSettingsType,
214+
) => {
215+
const errors = {} as Record<MainSettingsKeys, { message: string }>;
216+
const keysList = Object.keys(values) as MainSettingsKeys[];
217+
218+
keysList.map((key: MainSettingsKeys) => {
219+
const payload = values[key];
220+
if (typeof payload === 'string') {
221+
const isValid = validateAddress(payload);
222+
if (!isValid) {
223+
errors[key] = {
224+
message: INVALID_ADDRESS_MESSAGE,
225+
};
226+
}
227+
}
228+
229+
if (
230+
['nodeOperatorFeeBP', 'confirmExpiry', 'curatorFeeBP'].includes(key) &&
231+
typeof payload !== 'number'
232+
) {
233+
errors[key] = {
234+
message: INVALID_NUMBER_DATA_MESSAGE,
235+
};
236+
}
237+
238+
if (typeof payload === 'number') {
239+
if (key === 'nodeOperatorFeeBP') {
240+
if (payload < 0.001) {
241+
errors[key] = {
242+
message: INVALID_NUMBER_DATA_MESSAGE,
243+
};
244+
} else if (payload > 99) {
245+
errors[key] = {
246+
message: INVALID_NUMBER_MAX_MESSAGE,
247+
};
248+
} else if (values.curatorFeeBP + payload > 100) {
249+
errors[key] = {
250+
message: INVALID_NUMBER_SUM_MESSAGE,
251+
};
252+
}
253+
}
254+
255+
if (key === 'confirmExpiry') {
256+
if (payload < 1) {
257+
errors[key] = {
258+
message: INVALID_NUMBER_DATA_MESSAGE,
259+
};
260+
}
261+
262+
if (payload > 800) {
263+
errors[key] = {
264+
message: INVALID_NUMBER_EXPIRY_MAX_MESSAGE,
265+
};
266+
}
267+
}
268+
269+
if (key === 'curatorFeeBP') {
270+
if (payload < 0.001) {
271+
errors[key] = {
272+
message: INVALID_NUMBER_DATA_MESSAGE,
273+
};
274+
}
275+
276+
if (payload > 99) {
277+
errors[key] = {
278+
message: INVALID_NUMBER_MAX_MESSAGE,
279+
};
280+
}
281+
282+
if (values.nodeOperatorFeeBP + payload > 100) {
283+
errors[key] = {
284+
message: INVALID_NUMBER_SUM_MESSAGE,
285+
};
286+
}
287+
}
288+
}
289+
});
290+
291+
return {
292+
values,
293+
errors,
294+
};
295+
};

0 commit comments

Comments
 (0)