UNPKG

@vocdoni/sdk

Version:

⚒️An SDK for building applications on top of Vocdoni API

1,181 lines (1,071 loc) 36.6 kB
import { Signer } from '@ethersproject/abstract-signer'; import { keccak256 } from '@ethersproject/keccak256'; import { Wallet } from '@ethersproject/wallet'; import { Buffer } from 'buffer'; import invariant from 'tiny-invariant'; import { AccountCore } from './core/account'; import { ElectionCore } from './core/election'; import { VoteCore } from './core/vote'; import { Account, AllElectionStatus, AnonymousVote, CensusType, CspVote, ElectionStatus, ElectionStatusReady, HasAlreadyVotedOptions, IsAbleToVoteOptions, IsInCensusOptions, PlainCensus, PublishedElection, RemoteSigner, SendTokensOptions, StrategyCensus, TokenCensus, UnpublishedElection, Vote, VotesLeftCountOptions, WeightedCensus, } from './types'; import { API_URL, CENSUS_ASYNC, CENSUS_ASYNC_WAIT_TIME, CENSUS_CHUNK_SIZE, EXPLORER_URL, FAUCET_URL, TX_WAIT_OPTIONS, } from './util/constants'; import { AccountData, AccountService, AnonymousService, ArchivedAccountData, CensusProof, CensusService, ChainCircuits, ChainService, CspCensusProof, CspProofType, CspService, ElectionCreationSteps, ElectionCreationStepValue, ElectionListWithPagination, ElectionService, FaucetOptions, FaucetService, FetchElectionsParametersWithPagination, FileService, VoteService, VoteSteps, VoteStepValue, ZkProof, } from './services'; import { isAddress } from '@ethersproject/address'; export enum EnvOptions { DEV = 'dev', STG = 'stg', PROD = 'prod', } /** * Specify custom retry times and attempts when waiting for a transaction. * * @typedef TxWaitOptions * @property {number | null} retry_time * @property {number | null} attempts */ type TxWaitOptions = { retry_time?: number; attempts?: number; }; /** * Specify custom census service options. * * @typedef CensusOptions * @property {boolean} async If the census upload has to be done asynchronously * @property {number | null} wait_time The waiting time between each check * @property {number | null} chunk The size of the chunks to be uploaded each request */ type CensusOptions = { async: boolean; wait_time?: number; chunk?: number; }; /** * Optional VocdoniSDKClient arguments * * @typedef ClientOptions * @property {EnvOptions} env enum with possible values `DEV`, `STG`, `PROD` * @property {string | null } api_url API url location * @property {Wallet | Signer | RemoteSigner | null} wallet `Wallet` or `Signer` object from `ethersproject` library * @property {string | null} electionId Required by other methods like `submitVote` or `createElection`. * @property {FaucetOptions | null} faucet Specify custom Faucet options */ export type ClientOptions = { env: EnvOptions; api_url?: string; wallet?: Wallet | Signer | RemoteSigner; electionId?: string; faucet?: Partial<FaucetOptions>; tx_wait?: TxWaitOptions; census?: CensusOptions; }; /** * Main Vocdoni client object. It's a wrapper for all the methods in api, core * and types, allowing you to easily use the vocdoni API from a single entry * point. */ export class VocdoniSDKClient { private accountData: AccountData | ArchivedAccountData | null = null; private election: UnpublishedElection | PublishedElection | null = null; public censusService: CensusService; public chainService: ChainService; public anonymousService: AnonymousService; public cspService: CspService; public electionService: ElectionService; public voteService: VoteService; public fileService: FileService; public faucetService: FaucetService; public accountService: AccountService; public url: string; public wallet: Wallet | Signer | RemoteSigner | null; public electionId: string | null; public explorerUrl: string; /** * Instantiate new VocdoniSDK client. * * To instantiate the client just pass the `ClientOptions` you want or empty object to let defaults. * * `const client = new VocdoniSDKClient({EnvOptions.PROD})` * * @param opts - optional arguments */ constructor(opts: ClientOptions) { this.url = opts.api_url ?? API_URL[opts.env]; this.wallet = opts.wallet; this.electionId = opts.electionId; this.explorerUrl = EXPLORER_URL[opts.env]; this.censusService = new CensusService({ url: this.url, chunk_size: opts.census?.chunk ?? CENSUS_CHUNK_SIZE, async: { async: opts.census?.async ?? CENSUS_ASYNC, wait: opts.census?.wait_time ?? CENSUS_ASYNC_WAIT_TIME }, }); this.fileService = new FileService({ url: this.url }); this.chainService = new ChainService({ url: this.url, txWait: { retryTime: opts.tx_wait?.retry_time ?? TX_WAIT_OPTIONS.retry_time, attempts: opts.tx_wait?.attempts ?? TX_WAIT_OPTIONS.attempts, }, }); this.faucetService = new FaucetService({ url: opts.faucet?.url ?? FAUCET_URL[opts.env] ?? undefined, token_limit: opts.faucet?.token_limit, }); this.anonymousService = new AnonymousService({ url: this.url }); this.electionService = new ElectionService({ url: this.url, censusService: this.censusService, chainService: this.chainService, }); this.voteService = new VoteService({ url: this.url, chainService: this.chainService, }); this.cspService = new CspService({}); this.accountService = new AccountService({ url: this.url, chainService: this.chainService, }); } /** * Sets an election id. Required by other methods like submitVote or createElection. * @category Election * * @param electionId - Election id string */ setElectionId(electionId: string) { this.electionId = electionId; } /** * Fetches account information. * @category Account * * @param address - The account address to fetch the information */ async fetchAccountInfo(address?: string): Promise<AccountData | ArchivedAccountData> { if (!this.wallet && !address) { throw Error('No account set'); } else if (address) { this.accountData = await this.accountService.fetchAccountInfo(address); } else { this.accountData = await this.wallet .getAddress() .then((address) => this.accountService.fetchAccountInfo(address)); } return this.accountData; } /** * Fetches account. * @category Account * * @param address - The account address to fetch the information */ async fetchAccount(address?: string): Promise<AccountData> { if (!this.wallet && !address) { throw Error('No account set'); } else if (address) { this.accountData = await this.accountService.fetchAccountInfo(address); } else { this.accountData = await this.wallet .getAddress() .then((address) => this.accountService.fetchAccountInfo(address)); } const isAccount = (account: AccountData | ArchivedAccountData): account is AccountData => { return (account as AccountData).nonce !== undefined; }; if (!isAccount(this.accountData)) { throw Error('Account is archived'); } return this.accountData; } /** * Fetches info about an election. * @category Election * * @param electionId - The id of the election * @param password - The password to decrypt the metadata */ async fetchElection(electionId?: string, password?: string): Promise<PublishedElection> { invariant(this.electionId || electionId, 'No election set'); this.election = await this.electionService.fetchElection(electionId ?? this.electionId, password); return this.election; } /** * Fetches info about all elections * @category Election * * @param params - The parameters to filter the elections */ async fetchElections(params?: Partial<FetchElectionsParametersWithPagination>): Promise<ElectionListWithPagination> { return this.electionService.fetchElections(params); } /** * Fetches proof that an address is part of the specified census. * * @param censusId - * @param wallet - */ private fetchProofForWallet(censusId: string, wallet: Wallet | Signer | RemoteSigner): Promise<CensusProof> { return wallet.getAddress().then((address) => this.censusService.fetchProof(censusId, address)); } private setAccountSIK( electionId: string, signature: string, password: string, censusProof: CensusProof, wallet: Wallet | Signer | RemoteSigner ): Promise<void> { return wallet .getAddress() .then((address) => AnonymousService.calcSik(address, signature, password)) .then((calculatedSIK) => { const registerSIKTx = AccountCore.generateRegisterSIKTransaction( electionId, calculatedSIK, censusProof.proof, censusProof.value ); return this.accountService.signTransaction(registerSIKTx.tx, registerSIKTx.message, wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((hash) => this.chainService.waitForTransaction(hash)); } /** * Calculates ZK proof from given wallet. * * @param election - * @param wallet - * @param signature - * @param votePackage - * @param password - */ private async calcZKProofForWallet( election: PublishedElection, wallet: Wallet | Signer | RemoteSigner, signature: string, votePackage: Buffer, password: string = '0' ): Promise<ZkProof> { const [address, censusProof] = await Promise.all([ wallet.getAddress(), this.fetchProofForWallet(election.census.censusId, wallet), ]); return this.anonymousService .fetchAccountSIK(address) .catch(() => this.setAccountSIK(election.id, signature, password, censusProof, wallet)) .then(() => this.anonymousService.fetchZKProof(address)) .then((zkProof) => AnonymousService.prepareCircuitInputs( election.id, address, password, signature, censusProof.value, censusProof.value, zkProof.censusRoot, zkProof.censusSiblings, censusProof.root, censusProof.siblings, votePackage ) ) .then((circuits) => this.anonymousService.generateZkProof(circuits)); } /** * Creates an account with information. * @category Account * * @param options - Additional options, * like extra information of the account, or the faucet package string. */ async createAccountInfo(options: { account: Account; faucetPackage?: string; signedSikPayload?: string; password?: string; }): Promise<AccountData> { invariant(this.wallet, 'No wallet or signer set'); invariant(options.account, 'No account'); const faucetPayload = options.faucetPackage ?? (await this.wallet.getAddress().then((address) => this.faucetService.fetchPayload(address))); const faucetPackage = this.faucetService.parseFaucetPackage(faucetPayload); const address = await this.wallet.getAddress(); const calculatedSik = options?.signedSikPayload ? await AnonymousService.calcSik(address, options.signedSikPayload, options.password) : null; const accountData = Promise.all([ this.fetchChainId(), this.fileService.calculateCID(JSON.stringify(options.account.generateMetadata())), ]).then((data) => AccountCore.generateCreateAccountTransaction( address, JSON.stringify(options.account.generateMetadata()), data[1], faucetPackage.payload, faucetPackage.signature, calculatedSik ) ); return this.setAccountInfo(accountData); } /** * Updates an account with information * @category Account * * @param account - Account data. */ updateAccountInfo(account: Account): Promise<AccountData> { invariant(this.wallet, 'No wallet or signer set'); invariant(account, 'No account'); const accountData = Promise.all([ this.fetchAccount(), this.fetchChainId(), this.fileService.calculateCID(JSON.stringify(account.generateMetadata())), ]).then((data) => AccountCore.generateUpdateAccountTransaction( data[0].address, data[0].nonce, JSON.stringify(account.generateMetadata()), data[2] ) ); return this.setAccountInfo(accountData); } /** * Updates an account with information * @category Account * * @param promAccountData - Account data promise in Tx form. */ private setAccountInfo( promAccountData: Promise<{ tx: Uint8Array; metadata: string; message: string }> ): Promise<AccountData> { const accountTx = promAccountData.then((setAccountInfoTx) => this.accountService.signTransaction(setAccountInfoTx.tx, setAccountInfoTx.message, this.wallet) ); return Promise.all([promAccountData, accountTx]) .then((accountInfo) => this.accountService.setInfo(accountInfo[1], accountInfo[0].metadata)) .then((txHash) => this.chainService.waitForTransaction(txHash)) .then(() => this.fetchAccount()); } /** * Registers an account against vochain, so it can create new elections. * @category Account * * @param options - Additional options, like extra information of the account, * or the faucet package string */ createAccount(options?: { account?: Account; faucetPackage?: string; sik?: boolean; password?: string; }): Promise<AccountData> { invariant(this.wallet, 'No wallet or signer set'); const settings = { account: null, faucetPackage: null, sik: true, password: '0', ...options, }; return this.fetchAccount().catch(() => { if (settings?.sik) { return this.anonymousService.signSIKPayload(this.wallet).then((signedPayload) => this.createAccountInfo({ account: settings?.account ?? new Account(), faucetPackage: settings?.faucetPackage, signedSikPayload: signedPayload, password: settings.password, }) ); } return this.createAccountInfo({ account: settings?.account ?? new Account(), faucetPackage: settings?.faucetPackage, }); }); } /** * Send tokens from one account to another. * * @param options - Options for send tokens */ sendTokens(options: SendTokensOptions): Promise<void> { const settings = { wallet: options.wallet ?? this.wallet, ...options, }; invariant(settings.wallet, 'No wallet or signer set or given'); invariant(settings.to && isAddress(settings.to), 'No destination address given'); invariant(settings.amount && settings.amount > 0, 'No amount given'); return Promise.all([this.fetchAccount(), settings.wallet.getAddress()]) .then(([accountData, fromAddress]) => { const transferTx = AccountCore.generateTransferTransaction( accountData.nonce, fromAddress, settings.to, settings.amount ); return this.accountService.signTransaction(transferTx.tx, transferTx.message, settings.wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((txHash) => this.chainService.waitForTransaction(txHash)); } /** * Calls the faucet to get new tokens. Under development environments, if no faucet package is provided, one is created and tokens are allocated. * * @param faucetPackage - The faucet package * @returns Account data information updated with new balance */ collectFaucetTokens(faucetPackage?: string): Promise<AccountData> { invariant(this.wallet, 'No wallet or signer set'); const faucet = faucetPackage ? Promise.resolve(faucetPackage) : this.wallet.getAddress().then((address) => this.faucetService.fetchPayload(address)); return Promise.all([this.fetchAccount(), faucet]) .then(([account, faucet]) => { const faucetPackage = this.faucetService.parseFaucetPackage(faucet); const collectFaucetTx = AccountCore.generateCollectFaucetTransaction( account.nonce, faucetPackage.payload, faucetPackage.signature ); return this.accountService.signTransaction(collectFaucetTx.tx, collectFaucetTx.message, this.wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((hash) => this.chainService.waitForTransaction(hash)) .then(() => this.fetchAccount()); } /** * Creates a new voting election. * @category Election * * @param election - The election object to be created. * @returns Resulting election id. */ async createElection(election: UnpublishedElection): Promise<string> { for await (const step of this.createElectionSteps(election)) { switch (step.key) { case ElectionCreationSteps.DONE: return step.electionId; } } throw new Error('There was an error creating the election'); } /** * Creates a new voting election by steps with async returns. * @category Election * * @param election - The election object to be created. * @returns The async step returns. */ async *createElectionSteps(election: UnpublishedElection): AsyncGenerator<ElectionCreationStepValue> { invariant( election.maxCensusSize || election.census.type !== CensusType.CSP, 'CSP Census needs a max census size set in the election' ); const chainData = await this.chainService.fetchChainData(); yield { key: ElectionCreationSteps.GET_CHAIN_DATA, }; if (election.electionType.anonymous && election.census.type !== CensusType.CSP) { election.census.type = CensusType.ANONYMOUS; } if (!election.census.isPublished) { await this.censusService.createCensus(election.census as PlainCensus | WeightedCensus); } else if (!election.maxCensusSize && !election.census.size) { await this.censusService.get(election.census.censusId).then((censusInfo) => { election.census.size = censusInfo.size; election.census.weight = censusInfo.weight; }); } else if (election.maxCensusSize && election.maxCensusSize > chainData.maxCensusSize) { throw new Error('Max census size for the election is greater than allowed size: ' + chainData.maxCensusSize); } if (election.census instanceof TokenCensus) { election.meta = { ...election.meta, ...{ token: election.census.token } }; } if (election.census instanceof StrategyCensus) { election.meta = { ...election.meta, ...{ strategy: election.census.strategy } }; } yield { key: ElectionCreationSteps.CENSUS_CREATED, }; const account = await this.fetchAccount(); yield { key: ElectionCreationSteps.GET_ACCOUNT_DATA, }; const cid = await this.fileService.calculateCID(election.summarizeMetadata()); yield { key: ElectionCreationSteps.GET_DATA_PIN, }; const electionTxData = ElectionCore.generateNewElectionTransaction(election, cid, account.address, account.nonce); yield { key: ElectionCreationSteps.GENERATE_TX, }; const signedElectionTx = await this.electionService.signTransaction( electionTxData.tx, electionTxData.message, this.wallet ); yield { key: ElectionCreationSteps.SIGN_TX, }; const electionTx = await this.electionService.create(signedElectionTx, electionTxData.metadata); yield { key: ElectionCreationSteps.CREATING, txHash: electionTx.txHash, }; const electionId = await this.chainService.waitForTransaction(electionTx.txHash).then(() => electionTx.electionID); yield { key: ElectionCreationSteps.DONE, electionId, }; } /** * Ends an election. * @category Election * * @param electionId - The id of the election */ endElection(electionId?: string): Promise<void> { return this.changeElectionStatus(electionId, ElectionStatus.ENDED); } /** * Pauses an election. * @category Election * * @param electionId - The id of the election */ pauseElection(electionId?: string): Promise<void> { return this.changeElectionStatus(electionId, ElectionStatus.PAUSED); } /** * Cancels an election. * @category Election * * @param electionId - The id of the election */ cancelElection(electionId?: string): Promise<void> { return this.changeElectionStatus(electionId, ElectionStatus.CANCELED); } /** * Continues an election. * @category Election * * @param electionId - The id of the election */ continueElection(electionId?: string): Promise<void> { return this.changeElectionStatus(electionId, ElectionStatusReady.READY); } /** * Changes the status of an election. * @category Election * * @param electionId - The id of the election * @param newStatus - The new status */ private changeElectionStatus(electionId: string, newStatus: AllElectionStatus): Promise<void> { if (!this.electionId && !electionId) { throw Error('No election set'); } return this.fetchAccount() .then((accountData) => { const setElectionStatusTx = ElectionCore.generateSetElectionStatusTransaction( electionId ?? this.electionId, accountData.nonce, newStatus ); return this.electionService.signTransaction(setElectionStatusTx.tx, setElectionStatusTx.message, this.wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((hash) => this.chainService.waitForTransaction(hash)); } /** * Changes the census of an election. * @category Election * * @param electionId - The id of the election * @param censusId - The new census id (root) * @param censusURI - The new census URI * @param maxCensusSize - The new max census size */ public changeElectionCensus( electionId: string, censusId: string, censusURI: string, maxCensusSize?: number ): Promise<void> { if (!this.electionId && !electionId) { throw Error('No election set'); } return this.fetchAccount() .then((accountData) => { const setElectionCensusTx = ElectionCore.generateSetElectionCensusTransaction( electionId ?? this.electionId, accountData.nonce, censusId, censusURI, maxCensusSize ); return this.electionService.signTransaction(setElectionCensusTx.tx, setElectionCensusTx.message, this.wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((hash) => this.chainService.waitForTransaction(hash)); } /** * Changes the max census size of an election. * @category Election * * @param electionId - The id of the election * @param maxCensusSize - The new max census size */ public changeElectionMaxCensusSize(electionId: string, maxCensusSize: number): Promise<void> { if (!this.electionId && !electionId) { throw Error('No election set'); } return this.fetchElection(electionId ?? this.electionId).then((election) => this.changeElectionCensus( electionId ?? this.electionId, election.census.censusId, election.census.censusURI, maxCensusSize ) ); } /** * Changes the duration of an election. * @category Election * * @param electionId - The id of the election * @param duration - The new duration of the election */ public changeElectionDuration(electionId: string, duration: number): Promise<void> { return this.fetchAccount() .then((accountData) => { const setElectionCensusTx = ElectionCore.generateSetElectionDurationTransaction( electionId, accountData.nonce, duration ); return this.electionService.signTransaction(setElectionCensusTx.tx, setElectionCensusTx.message, this.wallet); }) .then((signedTx) => this.chainService.submitTx(signedTx)) .then((hash) => this.chainService.waitForTransaction(hash)); } /** * Changes the end date of an election. * @category Election * * @param electionId - The id of the election * @param endDate - The new end date */ public changeElectionEndDate(electionId: string, endDate: string | number | Date): Promise<void> { const date = new Date(endDate); invariant(date instanceof Date, 'Date not valid'); if (!this.electionId && !electionId) { throw Error('No election set'); } return this.fetchElection(electionId ?? this.electionId).then((election) => this.changeElectionDuration(electionId ?? this.electionId, (date.getTime() - election.startDate.getTime()) / 1000) ); } /** * Checks if the user is in census. * @category Voting * * @param options - Options for is in census */ async isInCensus(options?: IsInCensusOptions): Promise<boolean> { const settings = { wallet: options?.wallet ?? this.wallet, electionId: options?.electionId ?? this.electionId, ...options, }; invariant(settings.wallet, 'No wallet or signer set or given'); invariant(settings.electionId, 'No election identifier set or given'); return this.fetchElection(settings.electionId) .then((election) => this.fetchProofForWallet(election.census.censusId, settings.wallet)) .then(() => true) .catch(() => false); } /** * Checks if the user has already voted * @category Voting * * @param options - Options for has already voted * @returns The id of the vote */ async hasAlreadyVoted(options?: HasAlreadyVotedOptions): Promise<string> { const settings = { wallet: options?.wallet ?? this.wallet, electionId: options?.electionId ?? this.electionId, ...options, }; invariant(settings.wallet, 'No wallet or signer set or given'); invariant(settings.electionId, 'No election identifier set or given'); const election = await this.fetchElection(settings.electionId); if (election.electionType.anonymous && !settings?.voteId) { throw Error('This function cannot be used without a vote identifier for an anonymous election'); } return settings.wallet .getAddress() .then((address) => this.voteService.info( election.electionType.anonymous ? settings.voteId : keccak256(address.toLowerCase() + election.id) ) ) .then((voteInfo) => voteInfo.voteID) .catch(() => null); } /** * Checks if the user is able to vote * @category Voting * * @param options - Options for is able to vote */ isAbleToVote(options?: IsAbleToVoteOptions): Promise<boolean> { return this.votesLeftCount(options).then((votesLeftCount) => votesLeftCount > 0); } /** * Checks how many times a user can submit their vote * @category Voting * * @param options - Options for votes left count */ async votesLeftCount(options?: VotesLeftCountOptions): Promise<number> { const settings = { wallet: options?.wallet ?? this.wallet, electionId: options?.electionId ?? this.electionId, ...options, }; invariant(settings.wallet, 'No wallet or signer set or given'); invariant(settings.electionId, 'No election identifier set or given'); const election = await this.fetchElection(settings.electionId); if (election.electionType.anonymous && !settings?.voteId) { throw Error('This function cannot be used without a vote identifier for an anonymous election'); } const isInCensus = await this.isInCensus({ electionId: election.id }); if (!isInCensus) { return Promise.resolve(0); } return this.wallet .getAddress() .then((address) => this.voteService.info( election.electionType.anonymous ? settings.voteId : keccak256(address.toLowerCase() + election.id) ) ) .then((voteInfo) => election.voteType.maxVoteOverwrites - voteInfo.overwriteCount) .catch(() => election.voteType.maxVoteOverwrites + 1); } /** * Submits a vote. * @category Voting * * @param vote - The vote (or votes) to be sent. * @returns Vote confirmation id. */ async submitVote(vote: Vote | CspVote | AnonymousVote): Promise<string> { for await (const step of this.submitVoteSteps(vote)) { switch (step.key) { case VoteSteps.DONE: return step.voteId; } } throw new Error('There was an error submitting the vote'); } /** * Submits a vote by steps. * @category Voting * * @param vote - The vote (or votes) to be sent. * @returns Vote confirmation id. */ async *submitVoteSteps(vote: Vote | CspVote | AnonymousVote): AsyncGenerator<VoteStepValue> { if (this.election instanceof UnpublishedElection) { throw Error('Election is not published'); } if (!this.wallet) { throw Error('No wallet set'); } const election = await this.fetchElection(); yield { key: VoteSteps.GET_ELECTION, electionId: election.id, }; let processKeys = null; if (election?.electionType.secretUntilTheEnd) { processKeys = await this.electionService.keys(election.id).then((encryptionKeys) => ({ encryptionPubKeys: encryptionKeys.publicKeys, })); } const { votePackage } = VoteCore.packageVoteContent(vote.votes, processKeys); let censusProof: CspCensusProof | CensusProof | ZkProof; if (election.census.type == CensusType.WEIGHTED) { censusProof = await this.fetchProofForWallet(election.census.censusId, this.wallet); yield { key: VoteSteps.GET_PROOF, }; } else if (election.census.type == CensusType.ANONYMOUS) { let signature: string; if (vote instanceof AnonymousVote) { signature = vote.signature ?? (await this.anonymousService.signSIKPayload(this.wallet)); } else { signature = await this.anonymousService.signSIKPayload(this.wallet); } yield { key: VoteSteps.GET_SIGNATURE, signature, }; if (vote instanceof AnonymousVote) { censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, votePackage, vote.password); } else { censusProof = await this.calcZKProofForWallet(election, this.wallet, signature, votePackage); } yield { key: VoteSteps.CALC_ZK_PROOF, }; } else if (election.census.type == CensusType.CSP && vote instanceof CspVote) { censusProof = { address: await this.wallet.getAddress(), signature: vote.signature, proof_type: vote.proof_type, }; yield { key: VoteSteps.GET_PROOF, }; } else { throw new Error('No valid vote for this election'); } let voteTx: { tx: Uint8Array; message: string }; voteTx = VoteCore.generateVoteTransaction(election, censusProof, vote, processKeys, votePackage); yield { key: VoteSteps.GENERATE_TX, }; let payload: string; if (!this.election.electionType.anonymous) { payload = await this.voteService.signTransaction(voteTx.tx, voteTx.message, this.wallet); yield { key: VoteSteps.SIGN_TX, }; } else { payload = this.voteService.encodeTransaction(voteTx.tx); } // Vote const voteId = await this.voteService .vote(payload) .then((apiResponse) => this.chainService.waitForTransaction(apiResponse.txHash).then(() => apiResponse.voteID)); yield { key: VoteSteps.DONE, voteId, }; } /** * Assigns a random Wallet to the client and returns its private key. * * @returns The private key. */ public generateRandomWallet(): string { const wallet = Wallet.createRandom(); this.wallet = wallet; return wallet.privateKey; } /** * Returns a Wallet based on the inputs. * * @param data - The data inputs which should generate the Wallet * @returns The deterministic wallet. */ public static generateWalletFromData(data: string | string[]): Wallet { const inputs = Array.isArray(data) ? data : [data]; const hash = inputs.reduce((acc, curr) => acc + curr, ''); return new Wallet(keccak256(Buffer.from(hash))); } /** * This functions will be deprecated */ /** * Fetches proof that an address is part of the specified census. * * @param censusId - Census we want to check the address against * @param key - The address to be found */ async fetchProof(censusId: string, key: string): Promise<CensusProof> { return this.censusService.fetchProof(censusId, key); } /** * Publishes the given census. * * @param census - The census to be published. */ createCensus(census: PlainCensus | WeightedCensus): Promise<void> { return this.censusService.createCensus(census); } /** * Fetches the information of a given census. * * @param censusId - */ fetchCensusInfo(censusId: string): Promise<{ size: number; weight: bigint; type: CensusType }> { return this.censusService.get(censusId); } /** * Fetches circuits for anonymous voting * * @param circuits - Additional options for custom circuits */ fetchCircuits(circuits?: Omit<ChainCircuits, 'zKeyData' | 'vKeyData' | 'wasmData'>): Promise<ChainCircuits> { return this.anonymousService.fetchCircuits(circuits); } /** * Sets circuits for anonymous voting * * @param circuits - Custom circuits */ setCircuits(circuits: ChainCircuits): ChainCircuits { return this.anonymousService.setCircuits(circuits); } async cspUrl(): Promise<string> { invariant(this.electionId, 'No election id set'); return this.fetchElection(this.electionId).then((election) => this.cspService.setUrlFromElection(election)); } async cspInfo() { return this.cspService.setInfo(); } async cspStep(stepNumber: number, data: any[], authToken?: string) { invariant(this.electionId, 'No election id set'); await this.cspUrl(); await this.cspInfo(); return this.cspService.cspStep(this.electionId, stepNumber, data, authToken); } async cspSign(address: string, token: string) { invariant(this.electionId, 'No election id set'); return this.cspService.cspSign(this.electionId, address, token); } cspVote(vote: Vote, signature: string, proof_type?: CspProofType) { return this.cspService.cspVote(vote, signature, proof_type); } /** * Fetches blockchain costs information if needed. * */ fetchChainCosts() { return this.chainService.fetchChainCosts(); } /** * Fetches blockchain information if needed and returns the chain id. * */ fetchChainId(): Promise<string> { return this.chainService.fetchChainData().then((chainData) => chainData.chainId); } /** * Estimates the election cost * @category Election * * @returns The cost in tokens. */ public estimateElectionCost(election: UnpublishedElection): Promise<number> { return this.electionService.estimateElectionCost(election); } /** * Calculate the election cost * @category Election * * @returns The cost in tokens. */ public calculateElectionCost(election: UnpublishedElection): Promise<number> { return this.electionService.calculateElectionCost(election); } /** * Fetches the CID expected for the specified data content. * * @param data - The data of which we want the CID of * @returns Resulting CID */ calculateCID(data: string): Promise<string> { return this.fileService.calculateCID(data); } /** * Fetches a faucet payload. Only for development. * */ fetchFaucetPayload(): Promise<string> { invariant(this.wallet, 'No wallet or signer set'); return this.wallet.getAddress().then((address) => this.faucetService.fetchPayload(address)); } /** * Parses a faucet package. * * @param faucetPackage - The encoded faucet package */ parseFaucetPackage(faucetPackage: string) { return this.faucetService.parseFaucetPackage(faucetPackage); } /** * A convenience method to wait for a transaction to be executed. It will * loop trying to get the transaction information, and will retry every time * it fails. * * @param tx - Transaction to wait for * @param wait - The delay in milliseconds between tries * @param attempts - The attempts to try before failing */ waitForTransaction(tx: string, wait?: number, attempts?: number): Promise<void> { return this.chainService.waitForTransaction(tx, wait, attempts); } }