123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429 |
- import * as Yup from 'yup';
- import { schemaValidator, ActivateOpeningAtKeys } from '@joystream/types/hiring';
- import { ProposalTypes } from '@polkadot/joy-utils/types/proposals';
- import { GenericFormValues } from './forms/GenericProposalForm';
- import { InputValidationLengthConstraint } from '@joystream/types/common';
- import { FormValues as SignalFormValues } from './forms/SignalForm';
- import { FormValues as RuntimeUpgradeFormValues } from './forms/RuntimeUpgradeForm';
- import { FormValues as SetCouncilParamsFormValues } from './forms/SetCouncilParamsForm';
- import { FormValues as SpendingProposalFormValues } from './forms/SpendingProposalForm';
- import { FormValues as SetMaxValidatorCountFormValues } from './forms/SetMaxValidatorCountForm';
- import { FormValues as AddWorkingGroupLeaderOpeningFormValues } from './forms/AddWorkingGroupOpeningForm';
- import { FormValues as SetWorkingGroupMintCapacityFormValues } from './forms/SetWorkingGroupMintCapacityForm';
- import { FormValues as BeginReviewLeaderApplicationsFormValues } from './forms/BeginReviewLeaderApplicationsForm';
- import { FormValues as FillWorkingGroupLeaderOpeningFormValues } from './forms/FillWorkingGroupLeaderOpeningForm';
- import { FormValues as DecreaseWorkingGroupLeadStakeFormValues } from './forms/DecreaseWorkingGroupLeadStakeForm';
- import { FormValues as SlashWorkingGroupLeadStakeFormValues } from './forms/SlashWorkingGroupLeadStakeForm';
- import { FormValues as SetWorkingGroupLeadRewardFormValues } from './forms/SetWorkingGroupLeadRewardForm';
- import { FormValues as TerminateWorkingGroupLeaderFormValues } from './forms/TerminateWorkingGroupLeaderForm';
- // TODO: If we really need this (currency unit) we can we make "Validation" a functiction that returns an object.
- // We could then "instantialize" it in "withFormContainer" where instead of passing
- // "validationSchema" (in each form component file) we would just pass "validationSchemaKey" or just "proposalType" (ie. SetLead).
- // Then we could let the "withFormContainer" handle the actual "validationSchema" for "withFormik". In that case it could easily
- // pass stuff like totalIssuance or currencyUnit here (ie.: const validationSchema = Validation(currencyUnit, totalIssuance)[proposalType];)
- const CURRENCY_UNIT = undefined;
- // All
- const TITLE_MAX_LENGTH = 40;
- const RATIONALE_MAX_LENGTH = 3000;
- // Runtime Upgrade
- const FILE_SIZE_BYTES_MIN = 1;
- // Set Election Parameters
- const ANNOUNCING_PERIOD_MIN = 14400;
- const ANNOUNCING_PERIOD_MAX = 43200;
- const VOTING_PERIOD_MIN = 14400;
- const VOTING_PERIOD_MAX = 28800;
- const REVEALING_PERIOD_MIN = 14400;
- const REVEALING_PERIOD_MAX = 28800;
- const MIN_COUNCIL_STAKE_MIN = 1;
- const MIN_COUNCIL_STAKE_MAX = 100000;
- const NEW_TERM_DURATION_MIN = 14400;
- const NEW_TERM_DURATION_MAX = 432000;
- const CANDIDACY_LIMIT_MIN = 50;
- const CANDIDACY_LIMIT_MAX = 100;
- const COUNCIL_SIZE_MIN = 4;
- const COUNCIL_SIZE_MAX = 20;
- const MIN_VOTING_STAKE_MIN = 1;
- const MIN_VOTING_STAKE_MAX = 100000;
- // Spending
- const TOKENS_MIN = 0;
- const TOKENS_MAX = 5000000;
- // Set Validator Count
- const MAX_VALIDATOR_COUNT_MIN = 4;
- const MAX_VALIDATOR_COUNT_MAX = 300;
- // Add Working Group Leader Opening Parameters
- // TODO: Discuss the actual values
- const MIN_EXACT_BLOCK_MINUS_CURRENT = 14400 * 5; // ~5 days
- const MAX_EXACT_BLOCK_MINUS_CURRENT = 14400 * 60; // 2 months
- const MAX_REVIEW_PERIOD_LENGTH_MIN = 14400 * 5; // ~5 days
- const MAX_REVIEW_PERIOD_LENGTH_MAX = 14400 * 60; // 2 months
- const MAX_APPLICATIONS_MIN = 1;
- const MAX_APPLICATIONS_MAX = 1000;
- const APPLICATION_STAKE_VALUE_MIN = 1;
- const APPLICATION_STAKE_VALUE_MAX = 1000000;
- const ROLE_STAKE_VALUE_MIN = 1;
- const ROLE_STAKE_VALUE_MAX = 1000000;
- const TERMINATE_ROLE_UNSTAKING_MIN = 0;
- const TERMINATE_ROLE_UNSTAKING_MAX = 14 * 14400; // 14 days
- const LEAVE_ROLE_UNSTAKING_MIN = 0;
- const LEAVE_ROLE_UNSTAKING_MAX = 14 * 14400; // 14 days
- // Set Working Group Mint Capacity
- // TODO: Discuss the actual values
- const WG_MINT_CAP_MIN = 0;
- const WG_MINT_CAP_MAX = 5000000;
- // Fill Working Group Leader Opening / Set Working Group Lead Reward
- // TODO: Discuss the actual values
- const MIN_REWARD_AMOUNT = 1;
- const MAX_REWARD_AMOUNT = 100000;
- const MIN_REWARD_INTERVAL = 1;
- const MAX_REWARD_INTERVAL = 30 * 14400; // 30 days
- // 3 days margin (voting_period) to prevent FillOpeningInvalidNextPaymentBlock
- // Should we worry that much about it though?
- const MIN_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 3 * 14400;
- const MAX_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 30 * 14400; // 30 days
- // Decrease/Slash Working Group Leader Stake
- const DECREASE_LEAD_STAKE_MIN = 1;
- const SLASH_LEAD_STAKE_MIN = 1;
- // Max is validated in form component, because it depends on selected working group's leader stake
- function errorMessage (name: string, min: number | string, max: number | string, unit?: string): string {
- return `${name} should be at least ${min} and no more than ${max}${unit ? ` ${unit}.` : '.'}`;
- }
- /*
- Validation is used to validate a proposal form.
- Each proposal type should validate the fields of his form, anything is valid as long as it fits in a Yup Schema.
- In a form, validation should be injected in the Yup Schema just by accessing it in this object.
- Ex:
- // Text Form
- import Validation from 'path/to/validationSchema'
- ...
- validationSchema: Yup.object().shape({
- ...Validation.All()
- ...Validation.Text()
- }),
- */
- type ProposalTypeKeys = typeof ProposalTypes[number];
- type OutdatedProposals = 'EvictStorageProvider' | 'SetStorageRoleParameters' | 'SetLead' | 'SetContentWorkingGroupMintCapacity';
- type ValidationTypeKeys = Exclude<ProposalTypeKeys, OutdatedProposals> | 'All';
- /* eslint-disable @typescript-eslint/indent */
- // /\ This prevents eslint from trying to make "stairs" out of those multiple conditions.
- // They are more readable when one is directly under the other (switch-case style)
- type FormValuesByType<T extends ValidationTypeKeys> =
- T extends 'All' ? GenericFormValues :
- T extends 'Text' ? Omit<SignalFormValues, keyof GenericFormValues> :
- T extends 'RuntimeUpgrade' ? Omit<RuntimeUpgradeFormValues, keyof GenericFormValues> :
- T extends 'SetElectionParameters' ? Omit<SetCouncilParamsFormValues, keyof GenericFormValues> :
- T extends 'Spending' ? Omit<SpendingProposalFormValues, keyof GenericFormValues> :
- T extends 'SetValidatorCount' ? Omit<SetMaxValidatorCountFormValues, keyof GenericFormValues> :
- T extends 'AddWorkingGroupLeaderOpening' ? Omit<AddWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
- T extends 'SetWorkingGroupMintCapacity' ? Omit<SetWorkingGroupMintCapacityFormValues, keyof GenericFormValues> :
- T extends 'BeginReviewWorkingGroupLeaderApplication' ? Omit<BeginReviewLeaderApplicationsFormValues, keyof GenericFormValues> :
- T extends 'FillWorkingGroupLeaderOpening' ? Omit<FillWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
- T extends 'DecreaseWorkingGroupLeaderStake' ? Omit<DecreaseWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
- T extends 'SlashWorkingGroupLeaderStake' ? Omit<SlashWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
- T extends 'SetWorkingGroupLeaderReward' ? Omit<SetWorkingGroupLeadRewardFormValues, keyof GenericFormValues> :
- T extends 'TerminateWorkingGroupLeaderRole' ? Omit<TerminateWorkingGroupLeaderFormValues, keyof GenericFormValues> :
- never;
- type ValidationSchemaFuncParamsByType<T extends ValidationTypeKeys> =
- T extends 'Text' ? [number] :
- T extends 'RuntimeUpgrade' ? [number] :
- T extends 'AddWorkingGroupLeaderOpening' ? [number, InputValidationLengthConstraint | undefined] :
- T extends 'FillWorkingGroupLeaderOpening' ? [number] :
- T extends 'TerminateWorkingGroupLeaderRole' ? [InputValidationLengthConstraint | undefined] :
- [];
- /* eslint-enable @typescript-eslint/indent */
- type ValidationSchemaFunc<FieldValuesT extends Record<string, any>, ParamsT extends any[] = []> = (...params: ParamsT) =>
- ({ [fieldK in keyof FieldValuesT]: Yup.Schema<any> });
- type ValidationType = {
- [validationTypeK in ValidationTypeKeys]: ValidationSchemaFunc<
- FormValuesByType<validationTypeK>,
- ValidationSchemaFuncParamsByType<validationTypeK>
- >
- };
- // Helpers for common validation
- function minMaxInt (min: number, max: number, fieldName: string) {
- return Yup.number()
- .required(`${fieldName} is required!`)
- .integer(`${fieldName} must be an integer!`)
- .min(min, errorMessage(fieldName, min, max))
- .max(max, errorMessage(fieldName, min, max));
- }
- function minMaxStrFromConstraint (constraint: InputValidationLengthConstraint | undefined, fieldName: string) {
- const schema = Yup.string().required(`${fieldName} is required!`);
- return constraint
- ? (
- schema
- .min(
- constraint.min.toNumber(),
- `${fieldName} must be at least ${constraint.min.toNumber()} character(s) long`
- )
- .max(
- constraint.max.toNumber(),
- `${fieldName} cannot be more than ${constraint.max.toNumber()} character(s) long`
- )
- )
- : schema;
- }
- const Validation: ValidationType = {
- All: () => ({
- title: Yup.string()
- .required('Title is required!')
- .max(TITLE_MAX_LENGTH, `Title should be under ${TITLE_MAX_LENGTH} characters.`),
- rationale: Yup.string()
- .required('Rationale is required!')
- .max(RATIONALE_MAX_LENGTH, `Rationale should be under ${RATIONALE_MAX_LENGTH} characters.`)
- }),
- Text: (maxLength: number) => ({
- description: Yup.string()
- .required('Description is required!')
- .max(maxLength, `Description should be under ${maxLength}`)
- }),
- RuntimeUpgrade: (maxFileSize: number) => ({
- WASM: Yup.mixed()
- .test('fileArrayBuffer', 'Unexpected data format, file cannot be processed.', (value) => value instanceof ArrayBuffer)
- .test('fileSizeMin', `Minimum file size is ${FILE_SIZE_BYTES_MIN} bytes.`, (value: ArrayBuffer) => value.byteLength >= FILE_SIZE_BYTES_MIN)
- .test('fileSizeMax', `Maximum file size is ${maxFileSize} bytes.`, (value: ArrayBuffer) => value.byteLength <= maxFileSize)
- }),
- SetElectionParameters: () => ({
- announcingPeriod: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(
- ANNOUNCING_PERIOD_MIN,
- errorMessage('The announcing period', ANNOUNCING_PERIOD_MIN, ANNOUNCING_PERIOD_MAX, 'blocks')
- )
- .max(
- ANNOUNCING_PERIOD_MAX,
- errorMessage('The announcing period', ANNOUNCING_PERIOD_MIN, ANNOUNCING_PERIOD_MAX, 'blocks')
- ),
- votingPeriod: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(VOTING_PERIOD_MIN, errorMessage('The voting period', VOTING_PERIOD_MIN, VOTING_PERIOD_MAX, 'blocks'))
- .max(VOTING_PERIOD_MAX, errorMessage('The voting period', VOTING_PERIOD_MIN, VOTING_PERIOD_MAX, 'blocks')),
- minVotingStake: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(
- MIN_VOTING_STAKE_MIN,
- errorMessage('The minimum voting stake', MIN_VOTING_STAKE_MIN, MIN_VOTING_STAKE_MAX, CURRENCY_UNIT)
- )
- .max(
- MIN_VOTING_STAKE_MAX,
- errorMessage('The minimum voting stake', MIN_VOTING_STAKE_MIN, MIN_VOTING_STAKE_MAX, CURRENCY_UNIT)
- ),
- revealingPeriod: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(
- REVEALING_PERIOD_MIN,
- errorMessage('The revealing period', REVEALING_PERIOD_MIN, REVEALING_PERIOD_MAX, 'blocks')
- )
- .max(
- REVEALING_PERIOD_MAX,
- errorMessage('The revealing period', REVEALING_PERIOD_MIN, REVEALING_PERIOD_MAX, 'blocks')
- ),
- minCouncilStake: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(
- MIN_COUNCIL_STAKE_MIN,
- errorMessage('The minimum council stake', MIN_COUNCIL_STAKE_MIN, MIN_COUNCIL_STAKE_MAX, CURRENCY_UNIT)
- )
- .max(
- MIN_COUNCIL_STAKE_MAX,
- errorMessage('The minimum council stake', MIN_COUNCIL_STAKE_MIN, MIN_COUNCIL_STAKE_MAX, CURRENCY_UNIT)
- ),
- newTermDuration: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(
- NEW_TERM_DURATION_MIN,
- errorMessage('The new term duration', NEW_TERM_DURATION_MIN, NEW_TERM_DURATION_MAX, 'blocks')
- )
- .max(
- NEW_TERM_DURATION_MAX,
- errorMessage('The new term duration', NEW_TERM_DURATION_MIN, NEW_TERM_DURATION_MAX, 'blocks')
- ),
- candidacyLimit: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(CANDIDACY_LIMIT_MIN, errorMessage('The candidacy limit', CANDIDACY_LIMIT_MIN, CANDIDACY_LIMIT_MAX))
- .max(CANDIDACY_LIMIT_MAX, errorMessage('The candidacy limit', CANDIDACY_LIMIT_MIN, CANDIDACY_LIMIT_MAX)),
- councilSize: Yup.number()
- .required('All fields must be filled!')
- .integer('This field must be an integer.')
- .min(COUNCIL_SIZE_MIN, errorMessage('The council size', COUNCIL_SIZE_MIN, COUNCIL_SIZE_MAX))
- .max(COUNCIL_SIZE_MAX, errorMessage('The council size', COUNCIL_SIZE_MIN, COUNCIL_SIZE_MAX))
- }),
- Spending: () => ({
- tokens: Yup.number()
- .positive('The token amount should be positive.')
- .integer('This field must be an integer.')
- .max(TOKENS_MAX, errorMessage('The amount of tokens', TOKENS_MIN, TOKENS_MAX))
- .required('You need to specify an amount of tokens.'),
- destinationAccount: Yup.string()
- .required('Select a destination account!')
- }),
- SetValidatorCount: () => ({
- maxValidatorCount: Yup.number()
- .required('Enter the max validator count')
- .integer('This field must be an integer.')
- .min(
- MAX_VALIDATOR_COUNT_MIN,
- errorMessage('The max validator count', MAX_VALIDATOR_COUNT_MIN, MAX_VALIDATOR_COUNT_MAX)
- )
- .max(
- MAX_VALIDATOR_COUNT_MAX,
- errorMessage('The max validator count', MAX_VALIDATOR_COUNT_MIN, MAX_VALIDATOR_COUNT_MAX)
- )
- }),
- AddWorkingGroupLeaderOpening: (currentBlock: number, HRTConstraint?: InputValidationLengthConstraint) => ({
- workingGroup: Yup.string(),
- activateAt: Yup.string().required(),
- activateAtBlock: Yup.number()
- .when('activateAt', {
- is: ActivateOpeningAtKeys.ExactBlock,
- then: minMaxInt(
- MIN_EXACT_BLOCK_MINUS_CURRENT + currentBlock,
- MAX_EXACT_BLOCK_MINUS_CURRENT + currentBlock,
- 'Exact block'
- )
- }),
- maxReviewPeriodLength: minMaxInt(MAX_REVIEW_PERIOD_LENGTH_MIN, MAX_REVIEW_PERIOD_LENGTH_MAX, 'Max. review period length'),
- applicationsLimited: Yup.boolean(),
- maxApplications: Yup.number()
- .when('applicationsLimited', {
- is: true,
- then: minMaxInt(MAX_APPLICATIONS_MIN, MAX_APPLICATIONS_MAX, 'Max. number of applications')
- }),
- applicationStakeRequired: Yup.boolean(),
- applicationStakeMode: Yup.string(),
- applicationStakeValue: Yup.number()
- .when('applicationStakeRequired', {
- is: true,
- then: minMaxInt(APPLICATION_STAKE_VALUE_MIN, APPLICATION_STAKE_VALUE_MAX, 'Application stake value')
- }),
- roleStakeRequired: Yup.boolean(),
- roleStakeMode: Yup.string(),
- roleStakeValue: Yup.number()
- .when('roleStakeRequired', {
- is: true,
- then: minMaxInt(ROLE_STAKE_VALUE_MIN, ROLE_STAKE_VALUE_MAX, 'Role stake value')
- }),
- terminateRoleUnstakingPeriod: minMaxInt(
- TERMINATE_ROLE_UNSTAKING_MIN,
- TERMINATE_ROLE_UNSTAKING_MAX,
- 'Terminate role unstaking period'
- ),
- leaveRoleUnstakingPeriod: minMaxInt(
- LEAVE_ROLE_UNSTAKING_MIN,
- LEAVE_ROLE_UNSTAKING_MAX,
- 'Leave role unstaking period'
- ),
- humanReadableText: minMaxStrFromConstraint(HRTConstraint, 'human_readable_text')
- .test(
- 'schemaIsValid',
- 'Schema validation failed!',
- function (val: string) {
- let schemaObj: Record<string, unknown>;
- try {
- schemaObj = JSON.parse(val) as Record<string, unknown>;
- } catch (e) {
- return this.createError({ message: 'Schema validation failed: Invalid JSON' });
- }
- const isValid = schemaValidator(schemaObj);
- const errors = schemaValidator.errors || [];
- if (!isValid) {
- return this.createError({
- message: 'Schema validation failed: ' + errors.map((e) => `${e.message || ''}${e.dataPath && ` (${e.dataPath})`}`).join(', ')
- });
- }
- return true;
- }
- )
- }),
- SetWorkingGroupMintCapacity: () => ({
- workingGroup: Yup.string(),
- capacity: minMaxInt(WG_MINT_CAP_MIN, WG_MINT_CAP_MAX, 'Mint capacity')
- }),
- BeginReviewWorkingGroupLeaderApplication: () => ({
- workingGroup: Yup.string(),
- openingId: Yup.number().required('Select an opening!')
- }),
- FillWorkingGroupLeaderOpening: (currentBlock: number) => ({
- workingGroup: Yup.string(),
- openingId: Yup.number().required('Select an opening!'),
- successfulApplicant: Yup.number().required('Select a succesful applicant!'),
- includeReward: Yup.boolean(),
- rewardAmount: Yup.number()
- .when('includeReward', {
- is: true,
- then: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
- }),
- rewardNextBlock: Yup.number()
- .when('includeReward', {
- is: true,
- then: minMaxInt(
- MIN_NEXT_PAYMENT_BLOCK_MINUS_CURRENT + currentBlock,
- MAX_NEXT_PAYMENT_BLOCK_MINUS_CURRENT + currentBlock,
- 'Next payment block'
- )
- }),
- rewardRecurring: Yup.boolean(),
- rewardInterval: Yup.number()
- .when(['includeReward', 'rewardRecurring'], {
- is: true,
- then: minMaxInt(MIN_REWARD_INTERVAL, MAX_REWARD_INTERVAL, 'Reward interval')
- })
- }),
- DecreaseWorkingGroupLeaderStake: () => ({
- workingGroup: Yup.string(),
- amount: Yup.number()
- .required('Amount is required!')
- .min(DECREASE_LEAD_STAKE_MIN, `Amount must be greater than ${DECREASE_LEAD_STAKE_MIN}`)
- }),
- SlashWorkingGroupLeaderStake: () => ({
- workingGroup: Yup.string(),
- amount: Yup.number()
- .required('Amount is required!')
- .min(SLASH_LEAD_STAKE_MIN, `Amount must be greater than ${SLASH_LEAD_STAKE_MIN}`)
- }),
- SetWorkingGroupLeaderReward: () => ({
- workingGroup: Yup.string(),
- amount: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
- }),
- TerminateWorkingGroupLeaderRole: (rationaleConstraint?: InputValidationLengthConstraint) => ({
- workingGroup: Yup.string(),
- terminationRationale: minMaxStrFromConstraint(rationaleConstraint, 'Termination rationale'),
- slashStake: Yup.boolean()
- })
- };
- export default Validation;
|