Skip to Content
SDKVoting Guide

Voting Operations Guide

MACI supports two voting methods: whitelist mode and Pre-add-new-key anonymous mode. Whitelist mode is simple and direct, suitable for public voting; anonymous mode provides strongest privacy protection, suitable for sensitive voting scenarios.

Whitelist Mode

Suitable for users already in whitelist, simple and fast process.

Process Overview

Generate MACI Account

import { MaciClient } from '@dorafactory/maci-sdk'; const client = new MaciClient({ network: 'testnet' }); const maciKeypair = await client.genKeypairFromSign({ signer: wallet, address });

Signup (Whitelist Addresses Only)

await client.signup({ signer: wallet, address: userAddress, contractAddress: amaciContractAddress, maciKeypair: maciKeypair, gasStation: true });

Vote

const roundInfo = await client.getRoundInfo({ contractAddress }); const coordinatorPubKey = [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ]; await client.vote({ signer: wallet, address: userAddress, contractAddress: amaciContractAddress, selectedOptions: [ { idx: 0, vc: 5 }, { idx: 1, vc: 3 } ], operatorCoordPubKey: coordinatorPubKey, maciKeypair: maciKeypair, gasStation: true });

Pre-add-new-key Anonymous Mode

Suitable for users who received platform-distributed keypairs, provides highest privacy level. Operator cannot determine voter identity.

Process Overview

Receive Platform-Distributed Keypair

Platform distributes through secure channel:

{ "privateKey": "0x1234567890abcdef...", "publicKey": { "x": "0xabcdef...", "y": "0x123456..." }, "preDeactivateData": { "leaves": [ /* deactivate tree data */ ], "coordinatorPubkey": { "x": "...", "y": "..." } } }

Generate Proof and New Key Locally

import { VoterClient, genKeypair } from '@dorafactory/maci-sdk'; const voterClient = new VoterClient({ network: 'mainnet', secretKey: receivedPrivateKey }); const myNewKeypair = genKeypair(); const payload = await voterClient.buildPreAddNewKeyPayload({ stateTreeDepth: 10, coordinatorPubkey: preDeactivateData.coordinatorPubkey, deactivates: preDeactivateData.leaves, wasmFile: '/path/to/addNewKey.wasm', zkeyFile: '/path/to/addNewKey.zkey' });

Send Pre-add-new-key Transaction

await client.rawPreAddNewKey({ signer: anyWallet, contractAddress, d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newPubkey: { x: myNewKeypair.publicKey[0].toString(16), y: myNewKeypair.publicKey[1].toString(16) }, gasStation: true });

Vote

const roundInfo = await client.getRoundInfo({ contractAddress }); await client.vote({ signer: anyWallet, address: anyAddress, contractAddress, selectedOptions: [ { idx: 0, vc: 8 }, { idx: 1, vc: 6 } ], operatorCoordPubKey: [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ], maciKeypair: myNewKeypair, gasStation: true });

Gas Station Handling

Gas Station can pay transaction fees for users. If gasStation: true is enabled, need to wait for Gas Station authorization:

async function waitForGasStation( client: MaciClient, address: string, contractAddress: string ): Promise<boolean> { let hasFeegrant = false; let attempts = 0; const maxAttempts = 30; while (!hasFeegrant && attempts < maxAttempts) { hasFeegrant = await client.hasFeegrant({ address, contractAddress }); if (!hasFeegrant) { await new Promise(resolve => setTimeout(resolve, 2000)); attempts++; } } return hasFeegrant; } const hasGasStation = await waitForGasStation(client, address, contractAddress);

Voting Rules

1P1V Mode

Voice credits directly correspond to votes:

// Assume user has 100 voice credits selectedOptions: [ { idx: 0, vc: 50 }, // 50 votes { idx: 1, vc: 30 }, // 30 votes { idx: 2, vc: 20 } // 20 votes ] // Total cost = 50 + 30 + 20 = 100

QV (Quadratic Voting) Mode

Square of voice credits as consumption:

// Assume user has 100 voice credits selectedOptions: [ { idx: 0, vc: 8 }, // 8 votes, costs 64 credits (8²) { idx: 1, vc: 6 } // 6 votes, costs 36 credits (6²) ] // Total cost = 64 + 36 = 100

Revoting

Users can vote multiple times, later votes override earlier ones:

// First vote await client.vote({ selectedOptions: [{ idx: 0, vc: 5 }], // ... }); // Change mind, revote await client.vote({ selectedOptions: [{ idx: 1, vc: 5 }], // ... }); // Only second vote (option 1) is valid in the end

Complete Example Functions

Whitelist Mode

async function whitelistVoting( client: MaciClient, wallet: any, address: string, contractAddress: string, voteOptions: { idx: number; vc: number }[] ) { try { const maciKeypair = await client.genKeypairFromSign({ signer: wallet, address }); await client.signup({ signer: wallet, address, contractAddress, maciKeypair, gasStation: true }); const roundInfo = await client.getRoundInfo({ contractAddress }); await client.vote({ signer: wallet, address, contractAddress, selectedOptions: voteOptions, operatorCoordPubKey: [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ], maciKeypair, gasStation: true }); return true; } catch (error) { console.error('Voting failed:', error); return false; } }

Pre-add-new-key Anonymous Mode

async function anonymousVoting( client: MaciClient, receivedPrivateKey: string, preDeactivateData: any, anyWallet: any, anyAddress: string, contractAddress: string, voteOptions: { idx: number; vc: number }[] ) { try { const voterClient = new VoterClient({ network: 'mainnet', secretKey: receivedPrivateKey }); const myNewKeypair = genKeypair(); const payload = await voterClient.buildPreAddNewKeyPayload({ stateTreeDepth: 10, coordinatorPubkey: preDeactivateData.coordinatorPubkey, deactivates: preDeactivateData.leaves, wasmFile, zkeyFile }); await client.rawPreAddNewKey({ signer: anyWallet, contractAddress, d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newPubkey: { x: myNewKeypair.publicKey[0].toString(16), y: myNewKeypair.publicKey[1].toString(16) }, gasStation: true }); const roundInfo = await client.getRoundInfo({ contractAddress }); await client.vote({ signer: anyWallet, address: anyAddress, contractAddress, selectedOptions: voteOptions, operatorCoordPubKey: [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ], maciKeypair: myNewKeypair, gasStation: true }); return true; } catch (error) { console.error('Anonymous voting failed:', error); return false; } }

FAQ

How to calculate how many votes I can cast?

Depends on voting mode and your voice credits:

// 1P1V mode const maxVotes = voiceCredits; // 100 credits = 100 votes // QV mode const maxVotes = Math.floor(Math.sqrt(voiceCredits)); // 100 credits = 10 votes

How to choose between whitelist and anonymous mode?

Based on privacy needs:

  • Whitelist Mode: Simple and fast, but Operator knows your identity
  • Anonymous Mode: Fully anonymous, Operator doesn’t know who specifically, recommended for private voting

What if voting fails?

Check the following:

  1. Is it during voting period
  2. Whitelist mode: Is address in whitelist
  3. Anonymous mode: Do you have platform-distributed key
  4. Are voice credits sufficient
  5. Is Gas Station enabled (if using)

Can I vote for multiple options simultaneously?

Yes, add multiple options in the selectedOptions array:

selectedOptions: [ { idx: 0, vc: 5 }, { idx: 1, vc: 3 }, { idx: 2, vc: 2 } ]

How to revoke vote?

Revote and set all weights to 0, or vote for other options.

Privacy Level Comparison

ModeOperator Knows IdentityOn-chain Tracking DifficultyPrivacy LevelUse Cases
Whitelist SignupYesEasyLowSimple voting
Pre-add-new-keyNoExtremely HardHighestAnonymous voting
Last updated on