Skip to Content
IntroductionQuick Start

Quick Start

Let’s create an anonymous proposal voting system for your community. This guide demonstrates how to implement this scenario using the MACI SDK.

Environment Setup

Requirements:

  • Node.js 16+
  • npm or pnpm
  • A testnet wallet private key

Install SDK

mkdir my-voting-app && cd my-voting-app npm init -y npm install @dorafactory/maci-sdk @cosmjs/proto-signing @cosmjs/stargate

Create Voting Round

Create create-vote.js:

import { MaciClient, MaciCircuitType } from '@dorafactory/maci-sdk'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; const client = new MaciClient({ network: 'testnet' }); async function createVote() { const wallet = await DirectSecp256k1Wallet.fromKey( Buffer.from(process.env.PRIVATE_KEY, 'hex'), 'dora' ); const round = await client.createAMaciRound({ signer: wallet, operator: 'dora1...', // Operator address startVoting: new Date(), endVoting: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), title: 'Community Proposal Vote', voteOptionMap: [ 'Proposal A: Increase Development Funding', 'Proposal B: Improve Infrastructure', 'Proposal C: Marketing Campaign' ], circuitType: MaciCircuitType.QV, maxVoter: 100, voiceCreditAmount: '100', whitelist: { addresses: ['dora1...', 'dora1...'] // Whitelist addresses } }); console.log('Vote created:', round.contractAddress); return round.contractAddress; } createVote().catch(console.error);

Participate in Voting

Create vote.js:

import { MaciClient } from '@dorafactory/maci-sdk'; import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; const client = new MaciClient({ network: 'testnet' }); const contractAddress = 'dora1...'; // Contract address from previous step async function castVote() { const wallet = await DirectSecp256k1Wallet.fromKey( Buffer.from(process.env.PRIVATE_KEY, 'hex'), 'dora' ); const [account] = await wallet.getAccounts(); // Generate MACI keypair from wallet const maciKeypair = await client.genKeypairFromSign({ signer: wallet, address: account.address }); // Sign up (whitelist only) await client.signup({ signer: wallet, address: account.address, contractAddress, maciKeypair, gasStation: true }); // Get operator public key const roundInfo = await client.getRoundInfo({ contractAddress }); // Cast vote await client.vote({ signer: wallet, address: account.address, contractAddress, selectedOptions: [ { idx: 0, vc: 8 }, // Proposal A: 8 votes (costs 64 credits in QV) { idx: 2, vc: 6 } // Proposal C: 6 votes (costs 36 credits in QV) ], operatorCoordPubKey: [ BigInt(roundInfo.coordinatorPubkeyX), BigInt(roundInfo.coordinatorPubkeyY) ], maciKeypair, gasStation: true }); console.log('Vote submitted successfully'); } castVote().catch(console.error);

Anonymous Voting (Optional)

For complete anonymity, use the pre-add-new-key method:

import { VoterClient, genKeypair } from '@dorafactory/maci-sdk'; async function anonymousVote() { // Use platform-distributed key const voterClient = new VoterClient({ network: 'testnet', secretKey: receivedPrivateKey }); // Generate your own new keypair const myKeypair = genKeypair(); // Generate ZK proof const payload = await voterClient.buildPreAddNewKeyPayload({ stateTreeDepth: 10, coordinatorPubkey: preDeactivateData.coordinatorPubkey, deactivates: preDeactivateData.leaves, wasmFile: './addNewKey.wasm', zkeyFile: './addNewKey.zkey' }); // Send transaction from any address await client.rawPreAddNewKey({ signer: anyWallet, contractAddress, d: payload.d, proof: payload.proof, nullifier: payload.nullifier, newPubkey: { x: myKeypair.publicKey[0].toString(16), y: myKeypair.publicKey[1].toString(16) }, gasStation: true }); // Vote with new keypair await client.vote({ signer: anyWallet, address: anyAddress, contractAddress, selectedOptions: [{ idx: 0, vc: 10 }], operatorCoordPubKey: [...], maciKeypair: myKeypair, gasStation: true }); }

Query Results

const round = await client.getRoundInfo({ contractAddress }); console.log('Voting status:', round.status); console.log('Participants:', round.numSignups); console.log('Vote messages:', round.numMessages); // Results available after operator processing if (round.results) { console.log('Vote results:', round.results); }

Common Issues

”Not in whitelist” Error

Your address is not whitelisted. Check:

  • Did you configure the whitelist correctly when creating the vote?
  • Is the address you’re using in the whitelist?
  • If using Oracle mode, do you meet token holding requirements?

”Insufficient voice credits” Error

Vote consumption exceeds available credits. In QV mode, the square of votes is the cost:

  • 10 votes costs 100 credits
  • 8 votes costs 64 credits

Check your voice credit balance and voting allowance.

Gas Station Unavailable

If hasFeegrant returns false:

  • Wait a few minutes and retry, Gas Station may still be configuring
  • Or ensure wallet has sufficient balance to pay gas fees, set gasStation: false

Operator Public Key Error

Query correct operator information from Registry contract:

const operators = await client.indexer.getOperators(); const operator = operators.find(op => op.address === 'dora1...'); console.log('Operator pubkey:', operator.pubkey);

Quadratic Voting (QV) vs One Person One Vote (1P1V)

Quadratic Voting: Square of votes is the cost, encourages distributed voting

// 100 credits available [ { idx: 0, vc: 8 }, // 8² = 64 credits { idx: 1, vc: 6 } // 6² = 36 credits ] // Total cost: 100 credits

One Person One Vote: Votes directly equal cost

// 100 credits available [ { idx: 0, vc: 60 }, // 60 credits { idx: 1, vc: 40 } // 40 credits ] // Total cost: 100 credits

Next Steps

Understand Principles: Read Protocol Overview to learn how MACI works

Complete Examples: Check Full Code Examples to learn more scenarios

API Reference: Refer to SDK Documentation for all available features

Contract Interaction: Learn Contract Architecture to understand the underlying implementation

Last updated on