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/stargateCreate 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 creditsOne 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 creditsNext 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