ποΈ Key upload (#120)
* UI cleanup on joint key select page * Renamed next -> moveToNextStep per PR * rename joint key retrieved to joint key upload * Remove unnecessary text from joint key upload page * Fix merge conflicts * Read uploaded file * Update nswag client with more complete context object * Parse the key.json file and set it into the SubmitElectionRequest * Validation * Fix linting errors and add internationalization * css cleanup * fix more linting errors
This commit is contained in:
Π ΠΎΠ΄ΠΈΡΠ΅Π»Ρ
cd13d80da3
ΠΠΎΠΌΠΌΠΈΡ
e92d04d2ff
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"commitment_hash": "4F642D946DB82268AF873F6359D85019CF9A3658B6655942AE7CF84F7382505B",
|
||||
"crypto_base_hash": "B3B8CEA020B4C1CA796060D5B54ED24F20BEAB937F32C480D5648D3D38DC0882",
|
||||
"crypto_extended_base_hash": "30CD485CBD768C6C2BB92867876D401509E94701B8D24F6CD89A28A1D0579862",
|
||||
"elgamal_public_key": "B1CCFB49E3D0EA64FB0FE059291BE8C29AF358DFF7FCF8D546E1D846E120A7ECB9FB0BF4B1842F7FED78E57C3583A10154706194417D732CD88753BEB567267F004B6B14BF77AD60C552315072B6023A983241F902DD2B74B815F7F29ACC67D784858BDBB27D3865A50BAC6619E58B57B7758CFF6830A22E169F63A605164C7F1BA2CA790ACD5854CF8C980D01060EAADC484FC0F3554098581BB241FA60ACA932B8103BC8BFEC7D7F5779DEDB6837D084672ABB4BFA17D49C41DB8D6A5435827C1C1C86CA3EBA585181EEC64C76613D19CF4C97A617D0B19EF33A190777FCCE40C8667D9927005B9291B64E7FD0A589384B588717A6A5E632D3226BB8FC482369744881773BEC3B8EAAA37071B0797F5FA5E048A029693585AF68C006B19DE2B94E19334607ECBF69AE5D4C3FEDFC7B72D509D1139E58DDC907D1F43F9EEE90BAAB9D0C1D48E0E5D01EB259CAD7F7F24CE68AAD57E5C974032931C7EAFEBF1238F2FA8D5E5AEC297164E78E0E809E2F61B1228B7EF319837CE085E6DA47DF949027DC54C7A648BC0F2A6032184A9411966C48F528B4168B5633566CD481489CB1FAB52183658EB487423FB18A120E8B30832338A4CB72092734A4D01DB47AE0CC3C48FCA02CF432F5C334FC7C900C57BD4D506B8B84A7030863DC46463EC198734EC7D4CB8C7600CA2137EE36512BE1DAA076616C911A4BBE91B9E43F6745E7"
|
||||
}
|
|
@ -7,7 +7,7 @@ import routeIds from '../../routes/RouteIds';
|
|||
import { createEnumStepper } from '../../utils/EnumStepper';
|
||||
import WizardStep from '../WizardStep';
|
||||
import {
|
||||
JointKeyRetrievedStep,
|
||||
JointKeyUploadStep,
|
||||
JointKeySelectStep,
|
||||
ManifestMenuStep,
|
||||
ManifestPreviewStep,
|
||||
|
@ -62,7 +62,7 @@ export const ElectionSetupWizard: React.FC = () => {
|
|||
<JointKeySelectStep onNext={handleNext} onChanged={handleChanged} />
|
||||
</WizardStep>
|
||||
<WizardStep active={step === ElectionSetupStep.JointKeyRetrieved}>
|
||||
<JointKeyRetrievedStep onNext={handleNext} />
|
||||
<JointKeyUploadStep onNext={handleNext} onChanged={handleChanged} />
|
||||
</WizardStep>
|
||||
<WizardStep active={step === ElectionSetupStep.ManifestMenu}>
|
||||
<ManifestMenuStep
|
||||
|
|
|
@ -5,6 +5,7 @@ import InfoIcon from '@mui/icons-material/Info';
|
|||
|
||||
import { useIntl } from 'react-intl';
|
||||
import { SubmitElectionRequest } from '@electionguard/api-client';
|
||||
import { Foundation as FoundationIcon } from '@mui/icons-material';
|
||||
import FormattedButton from '../../FormattedButton';
|
||||
import IconHeader from '../../IconHeader';
|
||||
import { Message, MessageId } from '../../../lang';
|
||||
|
@ -48,7 +49,10 @@ const BasicInfoStep: React.FC<SetupInstructionsStepProps> = ({
|
|||
|
||||
return (
|
||||
<Container maxWidth="md" className={classes.root}>
|
||||
<IconHeader title={new Message(MessageId.ElectionSetup_BasicInfo_Title)} />
|
||||
<IconHeader
|
||||
title={new Message(MessageId.ElectionSetup_BasicInfo_Title)}
|
||||
Icon={FoundationIcon}
|
||||
/>
|
||||
<Container maxWidth="xs">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import { Box, Button, Container } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { VpnKey as KeyIcon } from '@mui/icons-material';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import IconHeader from '../../IconHeader';
|
||||
import InternationalText from '../../InternationalText';
|
||||
import { Message, MessageId } from '../../../lang';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
},
|
||||
content: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
spaced: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export interface JointKeyRetrievedStepProps {
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joint Key Select Step for Election Setup
|
||||
*/
|
||||
const JointKeyRetrievedStep: React.FC<JointKeyRetrievedStepProps> = ({ onNext }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const onButtonClick = () => {
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyItems="center"
|
||||
alignItems="center"
|
||||
className={classes.root}
|
||||
>
|
||||
<Container maxWidth="md" className={classes.content}>
|
||||
<IconHeader
|
||||
title={
|
||||
new Message(
|
||||
MessageId.ElectionSetupJointKeyRetrievedTitle,
|
||||
'Joint Key Retrieved'
|
||||
)
|
||||
}
|
||||
Icon={KeyIcon}
|
||||
/>
|
||||
<Box display="flex" flexDirection="column" alignItems="center">
|
||||
<InternationalText
|
||||
className={classes.spaced}
|
||||
variant="h5"
|
||||
component="h2"
|
||||
id={MessageId.ElectionSetupJointKeyRetrievedCTA}
|
||||
/>
|
||||
<InternationalText
|
||||
className={classes.spaced}
|
||||
id={MessageId.ElectionSetupJointKeyRetrievedDescription}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={classes.spaced}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={onButtonClick}
|
||||
>
|
||||
<FormattedMessage id={MessageId.ElectionSetupJointKeyRetreivedNext} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default JointKeyRetrievedStep;
|
|
@ -8,7 +8,6 @@ import {
|
|||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { SaveAlt as SaveIcon } from '@mui/icons-material';
|
||||
|
@ -17,10 +16,16 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// todo: Remove useGetJointKeys - import { useGetJointKeys } from '@electionguard/api-client';
|
||||
|
||||
import { Message, MessageId, loremIpsum } from '../../../lang';
|
||||
import { Message, MessageId } from '../../../lang';
|
||||
import IconHeader from '../../IconHeader';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
spaced: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
|
@ -79,36 +84,24 @@ const JointKeySelectStep: React.FC<JointKeySelectStepProps> = ({ onNext, onChang
|
|||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="md">
|
||||
<Container maxWidth="md" className={classes.root}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<IconHeader
|
||||
title={
|
||||
new Message(
|
||||
MessageId.ElectionSetupJointKeySelectTitle,
|
||||
'Pull Guardian Keys'
|
||||
)
|
||||
}
|
||||
title={new Message(MessageId.ElectionSetup_JointKeySelect_Title)}
|
||||
Icon={SaveIcon}
|
||||
/>
|
||||
<Typography className={classes.spaced}>
|
||||
<FormattedMessage
|
||||
id={MessageId.ElectionSetupJointKeySelectDescription}
|
||||
defaultMessage={loremIpsum}
|
||||
/>
|
||||
</Typography>
|
||||
<Box display="flex" flexDirection="column" alignItems="start">
|
||||
<FormControl className={classes.control}>
|
||||
<InputLabel id="joint-key-select-label" className={classes.inputLabel}>
|
||||
<FormattedMessage
|
||||
id={MessageId.ElectionSetupJointKeySelectPrompt}
|
||||
id={MessageId.ElectionSetup_JointKeySelect_Prompt}
|
||||
defaultMessage="Select Key for Election"
|
||||
/>
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="joint-key-select-label"
|
||||
id="joint-key-select"
|
||||
label="Hi there"
|
||||
value={keyCeremony ? keyCeremony.key_name : ''}
|
||||
onChange={onKeySelect}
|
||||
>
|
||||
|
@ -127,10 +120,7 @@ const JointKeySelectStep: React.FC<JointKeySelectStepProps> = ({ onNext, onChang
|
|||
color="secondary"
|
||||
onClick={onNextClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={MessageId.ElectionSetupJointKeySelectNext}
|
||||
defaultMessage="Pull keys for selected election"
|
||||
/>
|
||||
<FormattedMessage id={MessageId.ElectionSetup_JointKeySelect_Next} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
import { Button, CircularProgress, Container } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { VpnKey as KeyIcon } from '@mui/icons-material';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { CiphertextElectionContext, SubmitElectionRequest } from '@electionguard/api-client';
|
||||
import IconHeader from '../../IconHeader';
|
||||
import { Message, MessageId } from '../../../lang';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
},
|
||||
button: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
error: {
|
||||
color: 'red',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export interface JointKeyUploadStepProps {
|
||||
onNext: () => void;
|
||||
onChanged: (newSubmitElectionRequest: SubmitElectionRequest) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joint Key Select Step for Election Setup
|
||||
*/
|
||||
const JointKeyUploadStep: React.FC<JointKeyUploadStepProps> = ({ onNext, onChanged }) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
|
||||
const setIntlError = (id: string) => {
|
||||
const message = intl.formatMessage({
|
||||
id,
|
||||
});
|
||||
setError(message);
|
||||
};
|
||||
|
||||
const onFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e?.target?.files?.length) {
|
||||
setUploading(true);
|
||||
const file = e.target.files[0];
|
||||
const text = await file.text();
|
||||
try {
|
||||
const keys = JSON.parse(text);
|
||||
const context = {
|
||||
commitment_hash: keys.commitment_hash,
|
||||
crypto_base_hash: keys.crypto_base_hash,
|
||||
crypto_extended_base_hash: keys.crypto_extended_base_hash,
|
||||
elgamal_public_key: keys.elgamal_public_key,
|
||||
} as CiphertextElectionContext;
|
||||
const fileValid =
|
||||
context.commitment_hash &&
|
||||
context.crypto_base_hash &&
|
||||
context.crypto_extended_base_hash &&
|
||||
context.elgamal_public_key;
|
||||
if (!fileValid) {
|
||||
throw new Error();
|
||||
}
|
||||
onChanged({ context } as SubmitElectionRequest);
|
||||
onNext();
|
||||
} catch (ex) {
|
||||
setIntlError(MessageId.ElectionSetup_JointKeyUpload_InvalidFile);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
} else {
|
||||
setIntlError(MessageId.ElectionSetup_JointKeyUpload_NoFile);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="md" className={classes.content}>
|
||||
<IconHeader
|
||||
title={new Message(MessageId.ElectionSetup_JointKeyUpload_Title)}
|
||||
Icon={KeyIcon}
|
||||
/>
|
||||
|
||||
<div className={classes.error}>{error}</div>
|
||||
|
||||
<Button
|
||||
disabled={uploading}
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
component="label"
|
||||
className={classes.button}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={MessageId.ElectionSetupUploadManifestUpload}
|
||||
defaultMessage="Select Files to Upload"
|
||||
/>
|
||||
<input
|
||||
id="manifest-upload"
|
||||
accept="application/JSON"
|
||||
type="file"
|
||||
hidden
|
||||
onChange={(e) => onFileUpload(e)}
|
||||
/>
|
||||
</Button>
|
||||
{uploading && <CircularProgress size={24} variant="indeterminate" />}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default JointKeyUploadStep;
|
|
@ -4,8 +4,8 @@ export type { SetupInstructionsStepProps } from './BasicInfoStep';
|
|||
export { default as JointKeySelectStep } from './JointKeySelectStep';
|
||||
export type { JointKeySelectStepProps } from './JointKeySelectStep';
|
||||
|
||||
export { default as JointKeyRetrievedStep } from './JointKeyRetrievedStep';
|
||||
export type { JointKeyRetrievedStepProps } from './JointKeyRetrievedStep';
|
||||
export { default as JointKeyUploadStep } from './JointKeyUploadStep';
|
||||
export type { JointKeyUploadStepProps as JointKeyRetrievedStepProps } from './JointKeyUploadStep';
|
||||
|
||||
export { default as ManifestMenuStep } from './ManifestMenuStep';
|
||||
export type { ManifestMenuStepProps } from './ManifestMenuStep';
|
||||
|
|
|
@ -31,15 +31,13 @@ enum MessageId {
|
|||
ElectionSetupManifestMenuAbout = 'election_setup.manifest_menu.about',
|
||||
ElectionSetupManifestMenuPrompt = 'election_setup.manifest_menu.prompt',
|
||||
|
||||
ElectionSetupJointKeyRetrievedTitle = 'election_setup.joint_key_retrieved.title',
|
||||
ElectionSetupJointKeyRetrievedCTA = 'election_setup.joint_key_retrieved.cta',
|
||||
ElectionSetupJointKeyRetrievedDescription = 'election_setup.joint_key_retrieved.description',
|
||||
ElectionSetupJointKeyRetreivedNext = 'election_setup.joint_key_retrieved.next',
|
||||
ElectionSetup_JointKeyUpload_Title = 'election_setup.joint_key_upload.title',
|
||||
ElectionSetup_JointKeyUpload_NoFile = 'election_setup.joint_key_upload.no_file',
|
||||
ElectionSetup_JointKeyUpload_InvalidFile = 'election_setup.joint_key_upload.invalid_file',
|
||||
|
||||
ElectionSetupJointKeySelectTitle = 'election_setup.joint_key_select.title',
|
||||
ElectionSetupJointKeySelectDescription = 'election_setup.joint_key_select.description',
|
||||
ElectionSetupJointKeySelectPrompt = 'election_setup.joint_key_select.prompt',
|
||||
ElectionSetupJointKeySelectNext = 'election_setup.joint_key_select.next',
|
||||
ElectionSetup_JointKeySelect_Title = 'election_setup.joint_key_select.title',
|
||||
ElectionSetup_JointKeySelect_Prompt = 'election_setup.joint_key_select.prompt',
|
||||
ElectionSetup_JointKeySelect_Next = 'election_setup.joint_key_select.next',
|
||||
|
||||
ElectionSetupManifestPreviewTitle = 'election_setup.manifest_preview.title',
|
||||
ElectionSetupManifestPreviewPropertyName = 'election_setup.manifest_preview.property.name',
|
||||
|
|
|
@ -24,15 +24,13 @@
|
|||
"election_setup.manifest_menu.title": "Add Election Manifest",
|
||||
"election_setup.manifest_menu.prompt": "Select what you would like to do:",
|
||||
|
||||
"election_setup.joint_key_retrieved.title": "Joint Key Retrieved",
|
||||
"election_setup.joint_key_retrieved.cta": "Create a new election with retrieved key",
|
||||
"election_setup.joint_key_retrieved.description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis feugiat magna nec nibh congue, non pretium mauris feugiat. Nam commodo ultrices semper. Praesent hendrerit ut nibh nec mollis. Ut fermentum maximus nibh nec vulputate. Fusce ultricies, arcu quis faucibus egestas, ligula tellus placerat orci, sed scelerisque nisl mi eu nisi. Quisque pulvinar justo justo, non tristique enim pretium non. Cras eu lacus gravida, eleifend magna at, ultricies tortor.",
|
||||
"election_setup.joint_key_retrieved.next": "Continue",
|
||||
"election_setup.joint_key_upload.title": "Upload Joint Key",
|
||||
"election_setup.joint_key_upload.no_file": "No file found. Please try again.",
|
||||
"election_setup.joint_key_upload.invalid_file": "That file didn't look quite right. Please ensure it's the same key file that was produced by an election ceremony.",
|
||||
|
||||
"election_setup.joint_key_select.title": "Pull Guardian Keys",
|
||||
"election_setup.joint_key_select.description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis feugiat magna nec nibh congue, non pretium mauris feugiat. Nam commodo ultrices semper. Praesent hendrerit ut nibh nec mollis. Ut fermentum maximus nibh nec vulputate. Fusce ultricies, arcu quis faucibus egestas, ligula tellus placerat orci, sed scelerisque nisl mi eu nisi. Quisque pulvinar justo justo, non tristique enim pretium non. Cras eu lacus gravida, eleifend magna at, ultricies tortor.",
|
||||
"election_setup.joint_key_select.prompt": "Select Key for Election",
|
||||
"election_setup.joint_key_select.next": "Pull keys for selected election",
|
||||
"election_setup.joint_key_select.title": "Select Guardian Key",
|
||||
"election_setup.joint_key_select.prompt": "Guardian Key",
|
||||
"election_setup.joint_key_select.next": "Next Step",
|
||||
|
||||
"election_setup.manifest_preview.title": "Election Manifest Uploaded",
|
||||
"election_setup.manifest_preview.property.name": "Name",
|
||||
|
|
|
@ -2962,10 +2962,21 @@ export interface Body_login_for_access_token_api_v1_auth_login_post {
|
|||
export interface CastBallotsRequest {
|
||||
election_id?: string;
|
||||
manifest?: any;
|
||||
context?: any;
|
||||
context?: CiphertextElectionContext;
|
||||
ballots: any[];
|
||||
}
|
||||
|
||||
/** The meta-data required for an election including keys, manifest, number of guardians, and quorum */
|
||||
export interface CiphertextElectionContext {
|
||||
number_of_guardians: number;
|
||||
quorum: number;
|
||||
elgamal_public_key: string;
|
||||
commitment_hash: string;
|
||||
manifest_hash: string;
|
||||
crypto_base_hash: string;
|
||||
crypto_extended_base_hash: string;
|
||||
}
|
||||
|
||||
/** A Tally for a specific election. */
|
||||
export interface CiphertextTally {
|
||||
election_id: string;
|
||||
|
@ -2999,7 +3010,7 @@ export interface CreateElectionRequest {
|
|||
export interface DecryptBallotsWithSharesRequest {
|
||||
encrypted_ballots: any[];
|
||||
shares: { [key: string]: any[]; };
|
||||
context?: any;
|
||||
context: CiphertextElectionContext;
|
||||
}
|
||||
|
||||
/** A request to decrypt a specific tally. Can optionally include the tally to decrypt. */
|
||||
|
@ -3025,7 +3036,7 @@ export interface Election {
|
|||
election_id: string;
|
||||
key_name: string;
|
||||
state: ElectionState;
|
||||
context?: any;
|
||||
context: CiphertextElectionContext;
|
||||
manifest?: any;
|
||||
}
|
||||
|
||||
|
@ -3192,7 +3203,7 @@ export interface MakeElectionContextRequest {
|
|||
export interface MakeElectionContextResponse {
|
||||
status?: App__api__v1__models__base__ResponseStatus;
|
||||
message?: string;
|
||||
context?: any;
|
||||
context: CiphertextElectionContext;
|
||||
}
|
||||
|
||||
/** A basic model object */
|
||||
|
@ -3273,7 +3284,7 @@ export interface Settings {
|
|||
export interface SpoilBallotsRequest {
|
||||
election_id?: string;
|
||||
manifest?: any;
|
||||
context?: any;
|
||||
context?: CiphertextElectionContext;
|
||||
ballots: any[];
|
||||
}
|
||||
|
||||
|
@ -3287,7 +3298,7 @@ export enum StorageMode {
|
|||
export interface SubmitBallotsRequest {
|
||||
election_id?: string;
|
||||
manifest?: any;
|
||||
context?: any;
|
||||
context?: CiphertextElectionContext;
|
||||
ballots: any[];
|
||||
}
|
||||
|
||||
|
@ -3295,7 +3306,7 @@ export interface SubmitBallotsRequest {
|
|||
export interface SubmitElectionRequest {
|
||||
election_id: string;
|
||||
key_name: string;
|
||||
context?: any;
|
||||
context: CiphertextElectionContext;
|
||||
manifest?: any;
|
||||
}
|
||||
|
||||
|
@ -3326,7 +3337,7 @@ export interface ValidateBallotRequest {
|
|||
schema_override?: any;
|
||||
ballot?: any;
|
||||
manifest?: any;
|
||||
context?: any;
|
||||
context: CiphertextElectionContext;
|
||||
}
|
||||
|
||||
/** A request to validate an Election Description. */
|
||||
|
|
ΠΠ°Π³ΡΡΠ·ΠΊΠ°β¦
Π‘ΡΡΠ»ΠΊΠ° Π² Π½ΠΎΠ²ΠΎΠΉ Π·Π°Π΄Π°ΡΠ΅