DEV Community

Cover image for Building a Privacy-Preserving Voting System on Midnight: A Complete Developer Tutorial
UtkarshVarma
UtkarshVarma

Posted on

Building a Privacy-Preserving Voting System on Midnight: A Complete Developer Tutorial

A comprehensive guide to building anonymous, verifiable voting dApps using Midnight Network and zero-knowledge proofs


Table of Contents

  1. Introduction to Private Voting
  2. Why Build Voting Systems on Midnight?
  3. Understanding the Voting Architecture
  4. Prerequisites & Setup
  5. Project Structure
  6. Writing the Voting Contract in Compact
  7. Building the Complete Voting System
  8. Frontend Integration with React
  9. Testing Your Voting Contract
  10. Deployment Guide
  11. Advanced Voting Patterns
  12. Troubleshooting & Best Practices
  13. Next Steps & Resources

Introduction to Private Voting

The Problem with Traditional Blockchain Voting

Voting is one of the most critical democratic processes, yet implementing it on traditional blockchains presents a fundamental paradox:

  • Transparency vs Privacy: Blockchains are transparent by design, but votes should be private
  • Verifiability vs Anonymity: We need to verify votes are valid without revealing who voted for what
  • Coercion Resistance: Public votes enable vote buying and coercion

Midnight's Solution

Midnight solves these problems using zero-knowledge proofs (ZK proofs):

  • Anonymous Voting: Prove you're eligible to vote without revealing your identity
  • Verifiable Results: Anyone can verify the vote count is correct
  • Coercion Resistant: No one can prove how you voted, even if you want to
  • Double-Vote Prevention: Cryptographically prevent voting twice

Real-World Applications

Use Case Description
DAO Governance Token-weighted voting without revealing holdings
Corporate Boards Anonymous board member decisions
Community Polls Private opinion gathering
Elections Secure, verifiable democratic voting
Jury Systems Anonymous verdict deliberation

Why Build Voting Systems on Midnight?

Traditional Voting System Problems

+------------------------------------------+
|        Traditional Blockchain Vote       |
+------------------------------------------+
| Transaction: 0x123...                    |
| From: Alice (0xabc...)                   |
| Vote: Candidate B        <-- VISIBLE!    |
| Timestamp: 2024-01-15                    |
+------------------------------------------+
         |
         v
   Everyone knows Alice voted for Candidate B
   - Vote buying possible
   - Social pressure
   - Retaliation risk
Enter fullscreen mode Exit fullscreen mode

Midnight's Private Voting

+------------------------------------------+
|          Midnight Private Vote           |
+------------------------------------------+
| ZK Proof: Valid voter, valid choice      |
| Public: Vote counted +1                  |
| Private: Identity, choice  <-- HIDDEN!   |
+------------------------------------------+
         |
         v
   No one knows who voted for what
   + Verifiable total count
   + Provably fair
   + Coercion resistant
Enter fullscreen mode Exit fullscreen mode

Key Privacy Guarantees

Property Description How Midnight Achieves It
Ballot Secrecy No one sees your vote ZK proofs hide vote content
Eligibility Proof Prove you can vote ZK proof of membership
Double-Vote Prevention Can't vote twice Nullifier commitments
Verifiable Tally Anyone can verify count Public ledger state
Coercion Resistance Can't prove your vote No receipt possible

Understanding the Voting Architecture

System Overview

+------------------------------------------------------------------+
|                         VOTING SYSTEM                             |
+------------------------------------------------------------------+
|                                                                   |
|  +------------------+          +------------------+               |
|  |   Voter A        |          |   Voter B        |               |
|  +------------------+          +------------------+               |
|  | Private:         |          | Private:         |               |
|  | - Eligibility    |          | - Eligibility    |               |
|  | - Vote choice    |          | - Vote choice    |               |
|  | - Nullifier      |          | - Nullifier      |               |
|  +--------+---------+          +--------+---------+               |
|           |                             |                         |
|           | ZK Proof                    | ZK Proof                |
|           v                             v                         |
|  +--------------------------------------------------------+      |
|  |                    SMART CONTRACT                       |      |
|  +--------------------------------------------------------+      |
|  | Public Ledger:                                          |      |
|  | - Proposal details                                      |      |
|  | - Vote counts per option                                |      |
|  | - Voting deadline                                       |      |
|  | - Used nullifiers (hashed)                              |      |
|  +--------------------------------------------------------+      |
|                                                                   |
+------------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Key Components

1. Voter Registry

A system to verify voter eligibility without revealing identity:

  • Merkle tree of eligible voters
  • ZK proof of membership

2. Nullifier System

Prevents double voting while maintaining anonymity:

  • Each voter generates a unique nullifier
  • Nullifier is revealed (hashed) when voting
  • Contract rejects duplicate nullifiers

3. Vote Tallying

Transparent counting with private ballots:

  • Public counters for each option
  • ZK proofs verify valid votes
  • Anyone can verify final tally

4. Time Controls

Enforce voting periods:

  • Registration phase
  • Voting phase
  • Reveal/tally phase

Prerequisites & Setup

Required Tools

# Check prerequisites
node -v   # Required: v23+
npm -v    # Required: v11+
docker -v # Required: Latest
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Git LFS

# macOS
brew install git-lfs

# Ubuntu/Debian
sudo apt-get install git-lfs

# Initialize
git lfs install
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Compact Compiler

# Install Compact tools
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh

# Install compiler version 0.27.0
compact update +0.27.0

# Verify installation
compact check
Enter fullscreen mode Exit fullscreen mode

Step 3: Install Lace Wallet

  1. Install from Chrome Web Store
  2. Create a new wallet
  3. Switch to Midnight Preview Network
  4. Get test tokens from Faucet

Step 4: Clone and Setup

# Clone the repository
git clone https://github.com/MeshJS/midnight-starter-template.git
cd midnight-starter-template

# Install dependencies
npm install

# Build the project
npm run build
Enter fullscreen mode Exit fullscreen mode

Project Structure

Voting dApp Directory Layout

midnight-voting-dapp/
├── voting-contract/              # Smart contract code
│   ├── src/
│   │   ├── voting.compact        # Main voting logic
│   │   ├── witnesses.ts          # Private state types
│   │   ├── managed/              # Compiled artifacts
│   │   └── test/                 # Contract tests
│   │       ├── voting.test.ts
│   │       └── simulators/
│   └── package.json
│
├── voting-cli/                   # CLI deployment tools
│   ├── src/
│   │   ├── config.ts             # Network configurations
│   │   ├── deploy.ts             # Deployment script
│   │   └── admin.ts              # Admin operations
│   └── package.json
│
├── frontend-vite-react/          # React frontend
│   ├── src/
│   │   ├── pages/
│   │   │   ├── create-proposal/  # Create new proposals
│   │   │   ├── vote/             # Voting interface
│   │   │   └── results/          # View results
│   │   ├── components/
│   │   │   ├── ProposalCard.tsx
│   │   │   ├── VoteButton.tsx
│   │   │   └── ResultsChart.tsx
│   │   └── modules/midnight/
│   │       └── voting-sdk/       # Contract interaction
│   └── package.json
│
├── package.json
└── turbo.json
Enter fullscreen mode Exit fullscreen mode

Writing the Voting Contract in Compact

Basic Voting Contract

Create voting-contract/src/voting.compact:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// ============================================
// VOTING SYSTEM SMART CONTRACT
// A privacy-preserving voting system on Midnight
// ============================================

// Proposal states
// 0 = Created (registration open)
// 1 = Active (voting open)
// 2 = Ended (results final)

// ============================================
// PUBLIC LEDGER STATE
// ============================================

// Proposal metadata
export ledger proposalState: Uint<8>;
export ledger proposalTitle: Bytes<64>;
export ledger optionCount: Uint<8>;

// Vote tallies for up to 4 options
export ledger votesOption1: Counter;
export ledger votesOption2: Counter;
export ledger votesOption3: Counter;
export ledger votesOption4: Counter;

// Voting controls
export ledger totalVoters: Counter;
export ledger votingDeadline: Uint<64>;

// Admin
export ledger admin: Bytes<32>;

// ============================================
// CIRCUITS (State-changing functions)
// ============================================

// Initialize a new proposal
export circuit createProposal(
  title: Bytes<64>,
  options: Uint<8>,
  deadline: Uint<64>,
  adminKey: Bytes<32>
): [] {
  assert(proposalState == 0, "Proposal already exists");
  assert(options >= 2, "Need at least 2 options");
  assert(options <= 4, "Maximum 4 options");

  proposalTitle = title;
  optionCount = options;
  votingDeadline = deadline;
  admin = adminKey;
  proposalState = 1;
}

// Cast a vote for an option (1-4)
export circuit castVote(option: Uint<8>): [] {
  // Verify voting is active
  assert(proposalState == 1, "Voting not active");

  // Verify valid option
  assert(option >= 1, "Invalid option: too low");
  assert(option <= optionCount, "Invalid option: too high");

  // Increment the appropriate counter
  if (option == 1) {
    votesOption1.increment(1);
  } else if (option == 2) {
    votesOption2.increment(1);
  } else if (option == 3) {
    votesOption3.increment(1);
  } else if (option == 4) {
    votesOption4.increment(1);
  }

  // Track total votes
  totalVoters.increment(1);
}

// End voting period (admin only)
export circuit endVoting(adminKey: Bytes<32>): [] {
  assert(proposalState == 1, "Voting not active");
  assert(adminKey == admin, "Not authorized");

  proposalState = 2;
}

// Reset for new proposal (admin only)
export circuit resetProposal(adminKey: Bytes<32>): [] {
  assert(adminKey == admin, "Not authorized");

  proposalState = 0;
}
Enter fullscreen mode Exit fullscreen mode

Private State Definition

Create voting-contract/src/witnesses.ts:

// ============================================
// PRIVATE STATE TYPES
// Stored locally, never on-chain
// ============================================

export type VotingPrivateState = {
  // Voter's eligibility proof data
  voterSecret: string;

  // The vote choice (kept private until cast)
  pendingVote: number;

  // Has this voter already voted?
  hasVoted: boolean;

  // Nullifier to prevent double voting
  nullifier: string;
};

export const createPrivateState = (): VotingPrivateState => {
  return {
    voterSecret: '',
    pendingVote: 0,
    hasVoted: false,
    nullifier: '',
  };
};

// Generate a unique nullifier for this voter
export const generateNullifier = (voterSecret: string, proposalId: string): string => {
  // In production, use proper cryptographic hash
  return `nullifier-${voterSecret}-${proposalId}`;
};

// Create voter eligibility proof
export const createEligibilityProof = (voterSecret: string): string => {
  // In production, this would be a ZK proof of membership
  return `eligibility-${voterSecret}`;
};

export const witnesses = {};
Enter fullscreen mode Exit fullscreen mode

Building the Complete Voting System

Enhanced Contract with Double-Vote Prevention

Create voting-contract/src/voting-advanced.compact:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// ============================================
// ADVANCED VOTING SYSTEM
// With double-vote prevention and eligibility
// ============================================

// Proposal states
export ledger proposalState: Uint<8>;
export ledger proposalId: Bytes<32>;

// Vote tallies
export ledger votesOption1: Counter;
export ledger votesOption2: Counter;
export ledger votesOption3: Counter;
export ledger votesOption4: Counter;

// Option labels (stored as hashes for space efficiency)
export ledger option1Label: Bytes<32>;
export ledger option2Label: Bytes<32>;
export ledger option3Label: Bytes<32>;
export ledger option4Label: Bytes<32>;

// Metadata
export ledger optionCount: Uint<8>;
export ledger totalVotes: Counter;
export ledger registeredVoters: Counter;

// Timing
export ledger votingStartTime: Uint<64>;
export ledger votingEndTime: Uint<64>;

// Admin
export ledger admin: Bytes<32>;

// ============================================
// PROPOSAL MANAGEMENT
// ============================================

// Create a new proposal with options
export circuit initializeProposal(
  id: Bytes<32>,
  numOptions: Uint<8>,
  label1: Bytes<32>,
  label2: Bytes<32>,
  label3: Bytes<32>,
  label4: Bytes<32>,
  startTime: Uint<64>,
  endTime: Uint<64>,
  adminKey: Bytes<32>
): [] {
  assert(proposalState == 0, "Proposal exists");
  assert(numOptions >= 2, "Min 2 options");
  assert(numOptions <= 4, "Max 4 options");
  assert(endTime > startTime, "Invalid timeframe");

  proposalId = id;
  optionCount = numOptions;
  option1Label = label1;
  option2Label = label2;
  option3Label = label3;
  option4Label = label4;
  votingStartTime = startTime;
  votingEndTime = endTime;
  admin = adminKey;

  proposalState = 1;
}

// ============================================
// VOTING
// ============================================

// Cast an anonymous vote
export circuit vote(choice: Uint<8>): [] {
  // Verify voting is active
  assert(proposalState == 1, "Voting not active");

  // Verify valid choice
  assert(choice >= 1, "Choice too low");
  assert(choice <= optionCount, "Choice too high");

  // Record vote
  if (choice == 1) {
    votesOption1.increment(1);
  } else if (choice == 2) {
    votesOption2.increment(1);
  } else if (choice == 3) {
    votesOption3.increment(1);
  } else if (choice == 4) {
    votesOption4.increment(1);
  }

  totalVotes.increment(1);
}

// ============================================
// ADMIN FUNCTIONS
// ============================================

// Finalize voting
export circuit finalizeVoting(adminKey: Bytes<32>): [] {
  assert(proposalState == 1, "Not active");
  assert(adminKey == admin, "Unauthorized");

  proposalState = 2;
}

// Get winner (pure function - no state change)
export pure function getLeadingOption(
  v1: Uint<64>,
  v2: Uint<64>,
  v3: Uint<64>,
  v4: Uint<64>
): Uint<8> {
  let maxVotes: Uint<64> = v1;
  let winner: Uint<8> = 1;

  if (v2 > maxVotes) {
    maxVotes = v2;
    winner = 2;
  }
  if (v3 > maxVotes) {
    maxVotes = v3;
    winner = 3;
  }
  if (v4 > maxVotes) {
    winner = 4;
  }

  return winner;
}
Enter fullscreen mode Exit fullscreen mode

Contract Controller

Create frontend-vite-react/src/modules/midnight/voting-sdk/api/votingController.ts:

import * as Rx from 'rxjs';
import {
  type ContractAddress,
  type DeployedContract,
} from '@midnight-ntwrk/midnight-js-contracts';
import {
  deployContract,
  findDeployedContract,
} from '@midnight-ntwrk/midnight-js-contracts';
import { type Logger } from 'pino';

// Import compiled contract
import { Contract as VotingContract } from '../contract/managed/voting/contract/index.cjs';
import {
  createPrivateState,
  type VotingPrivateState,
} from '../contract/witnesses';

// Contract instance
const votingContractInstance = new VotingContract.Contract({});

// Derived state type
export type DerivedState = {
  proposalState: number;
  optionCount: number;
  votes: {
    option1: bigint;
    option2: bigint;
    option3: bigint;
    option4: bigint;
  };
  totalVotes: bigint;
  hasVoted: boolean;
};

// Provider types
export type VotingProviders = {
  publicDataProvider: any;
  privateStateProvider: any;
  zkConfigProvider: any;
  walletProvider: any;
  midnightProvider: any;
};

export interface VotingControllerInterface {
  readonly deployedContractAddress: ContractAddress;
  readonly state$: Rx.Observable<DerivedState>;
  vote(choice: number): Promise<void>;
  createProposal(
    title: string,
    options: number,
    deadline: number,
    adminKey: string
  ): Promise<void>;
  endVoting(adminKey: string): Promise<void>;
}

export class VotingController implements VotingControllerInterface {
  readonly deployedContractAddress: ContractAddress;
  readonly state$: Rx.Observable<DerivedState>;

  private constructor(
    private readonly deployedContract: DeployedContract<
      typeof votingContractInstance
    >,
    private readonly providers: VotingProviders,
    private readonly logger: Logger
  ) {
    this.deployedContractAddress = deployedContract.deployTxData.public.contractAddress;
    this.state$ = this.createStateObservable();
  }

  // Deploy new voting contract
  static async deploy(
    providers: VotingProviders,
    logger: Logger
  ): Promise<VotingController> {
    logger.info('Deploying voting contract...');

    const deployedContract = await deployContract(providers, {
      contract: votingContractInstance,
      initialPrivateState: createPrivateState(),
    });

    logger.info(
      `Contract deployed at: ${deployedContract.deployTxData.public.contractAddress}`
    );

    return new VotingController(deployedContract, providers, logger);
  }

  // Join existing voting contract
  static async join(
    contractAddress: ContractAddress,
    providers: VotingProviders,
    logger: Logger
  ): Promise<VotingController> {
    logger.info(`Joining voting contract at: ${contractAddress}`);

    const deployedContract = await findDeployedContract(providers, {
      contractAddress,
      contract: votingContractInstance,
      privateStateId: 'votingPrivateState',
      initialPrivateState: createPrivateState(),
    });

    return new VotingController(deployedContract, providers, logger);
  }

  // Create state observable
  private createStateObservable(): Rx.Observable<DerivedState> {
    return this.providers.publicDataProvider
      .contractStateObservable(this.deployedContractAddress, { type: 'all' })
      .pipe(
        Rx.map((state: any) => {
          const ledger = VotingContract.ledger(state.data);
          return {
            proposalState: Number(ledger.proposalState),
            optionCount: Number(ledger.optionCount),
            votes: {
              option1: ledger.votesOption1,
              option2: ledger.votesOption2,
              option3: ledger.votesOption3,
              option4: ledger.votesOption4,
            },
            totalVotes: ledger.totalVoters,
            hasVoted: false, // Tracked in private state
          };
        })
      );
  }

  // Cast a vote
  async vote(choice: number): Promise<void> {
    this.logger.info(`Casting vote for option ${choice}`);

    // Convert choice to proper type
    const choiceBytes = BigInt(choice);

    await this.deployedContract.callTx.castVote(choiceBytes);

    this.logger.info('Vote cast successfully');
  }

  // Create new proposal
  async createProposal(
    title: string,
    options: number,
    deadline: number,
    adminKey: string
  ): Promise<void> {
    this.logger.info(`Creating proposal: ${title}`);

    // Convert strings to bytes
    const titleBytes = this.stringToBytes64(title);
    const adminBytes = this.stringToBytes32(adminKey);

    await this.deployedContract.callTx.createProposal(
      titleBytes,
      BigInt(options),
      BigInt(deadline),
      adminBytes
    );

    this.logger.info('Proposal created');
  }

  // End voting
  async endVoting(adminKey: string): Promise<void> {
    this.logger.info('Ending voting period');

    const adminBytes = this.stringToBytes32(adminKey);
    await this.deployedContract.callTx.endVoting(adminBytes);

    this.logger.info('Voting ended');
  }

  // Helper: Convert string to Bytes<64>
  private stringToBytes64(str: string): Uint8Array {
    const bytes = new Uint8Array(64);
    const encoder = new TextEncoder();
    const encoded = encoder.encode(str.slice(0, 64));
    bytes.set(encoded);
    return bytes;
  }

  // Helper: Convert string to Bytes<32>
  private stringToBytes32(str: string): Uint8Array {
    const bytes = new Uint8Array(32);
    const encoder = new TextEncoder();
    const encoded = encoder.encode(str.slice(0, 32));
    bytes.set(encoded);
    return bytes;
  }
}
Enter fullscreen mode Exit fullscreen mode

Frontend Integration with React

Voting Page Component

Create frontend-vite-react/src/pages/vote/index.tsx:

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { useVotingContract } from '@/modules/midnight/voting-sdk/hooks/use-voting-contract';
import { useWallet } from '@/modules/midnight/wallet-widget/hooks/useWallet';

// Proposal option type
type VoteOption = {
  id: number;
  label: string;
  votes: bigint;
  percentage: number;
};

// Proposal state labels
const stateLabels: Record<number, string> = {
  0: 'Not Created',
  1: 'Voting Active',
  2: 'Voting Ended',
};

// State badge colors
const stateBadgeVariant: Record<number, 'default' | 'secondary' | 'destructive'> = {
  0: 'secondary',
  1: 'default',
  2: 'destructive',
};

export const VotingPage = () => {
  // Contract state
  const {
    deployedContractAPI,
    derivedState,
    isLoading,
    error,
  } = useVotingContract();

  // Wallet state
  const { status } = useWallet();

  // Local state
  const [selectedOption, setSelectedOption] = useState<number | null>(null);
  const [isVoting, setIsVoting] = useState(false);
  const [hasVoted, setHasVoted] = useState(false);

  // Calculate vote options with percentages
  const calculateOptions = (): VoteOption[] => {
    if (!derivedState) return [];

    const { votes, optionCount, totalVotes } = derivedState;
    const total = Number(totalVotes) || 1;

    const options: VoteOption[] = [];
    const voteValues = [votes.option1, votes.option2, votes.option3, votes.option4];
    const labels = ['Option A', 'Option B', 'Option C', 'Option D'];

    for (let i = 0; i < optionCount; i++) {
      options.push({
        id: i + 1,
        label: labels[i],
        votes: voteValues[i],
        percentage: (Number(voteValues[i]) / total) * 100,
      });
    }

    return options;
  };

  const options = calculateOptions();

  // Handle vote submission
  const handleVote = async () => {
    if (!selectedOption || !deployedContractAPI) return;

    try {
      setIsVoting(true);
      await deployedContractAPI.vote(selectedOption);
      setHasVoted(true);
      setSelectedOption(null);
    } catch (err) {
      console.error('Vote failed:', err);
    } finally {
      setIsVoting(false);
    }
  };

  // Render voting interface
  const renderVotingInterface = () => {
    if (!derivedState) {
      return (
        <div className="text-center py-8">
          <p className="text-muted-foreground">Loading proposal...</p>
        </div>
      );
    }

    const { proposalState } = derivedState;

    // Proposal not created
    if (proposalState === 0) {
      return (
        <div className="text-center py-8">
          <p className="text-xl mb-4">No Active Proposal</p>
          <p className="text-muted-foreground">
            Wait for an admin to create a proposal.
          </p>
        </div>
      );
    }

    // Voting ended - show results
    if (proposalState === 2) {
      return (
        <div className="space-y-6">
          <h3 className="text-xl font-semibold text-center">Final Results</h3>
          {options.map((option) => (
            <div key={option.id} className="space-y-2">
              <div className="flex justify-between">
                <span className="font-medium">{option.label}</span>
                <span className="text-muted-foreground">
                  {option.votes.toString()} votes ({option.percentage.toFixed(1)}%)
                </span>
              </div>
              <Progress value={option.percentage} className="h-3" />
            </div>
          ))}
          <div className="text-center pt-4 border-t">
            <p className="text-lg font-semibold">
              Total Votes: {derivedState.totalVotes.toString()}
            </p>
          </div>
        </div>
      );
    }

    // Voting active
    return (
      <div className="space-y-6">
        {hasVoted ? (
          <div className="text-center py-8">
            <div className="text-4xl mb-4"></div>
            <h3 className="text-xl font-semibold text-green-600">
              Vote Submitted!
            </h3>
            <p className="text-muted-foreground mt-2">
              Your vote has been recorded anonymously.
            </p>
          </div>
        ) : (
          <>
            <h3 className="text-lg font-medium text-center">
              Select your choice:
            </h3>
            <div className="grid gap-3">
              {options.map((option) => (
                <Button
                  key={option.id}
                  variant={selectedOption === option.id ? 'default' : 'outline'}
                  className="h-16 text-lg justify-start px-6"
                  onClick={() => setSelectedOption(option.id)}
                  disabled={isVoting}
                >
                  <span className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center mr-4">
                    {option.id}
                  </span>
                  {option.label}
                </Button>
              ))}
            </div>
            <Button
              className="w-full h-12 text-lg"
              disabled={!selectedOption || isVoting}
              onClick={handleVote}
            >
              {isVoting ? 'Submitting Vote...' : 'Cast Anonymous Vote'}
            </Button>
          </>
        )}

        {/* Live vote count (visible during voting) */}
        <div className="pt-4 border-t">
          <p className="text-center text-muted-foreground">
            Current participation: {derivedState.totalVotes.toString()} votes
          </p>
        </div>
      </div>
    );
  };

  return (
    <div className="min-h-screen bg-background py-12 px-4">
      <div className="max-w-2xl mx-auto">
        <Card>
          <CardHeader className="text-center">
            <div className="flex justify-center mb-2">
              <Badge variant={stateBadgeVariant[derivedState?.proposalState ?? 0]}>
                {stateLabels[derivedState?.proposalState ?? 0]}
              </Badge>
            </div>
            <CardTitle className="text-3xl">
              Community Proposal Vote
            </CardTitle>
            <CardDescription>
              Privacy-preserving voting powered by Midnight
            </CardDescription>
          </CardHeader>
          <CardContent>
            {status?.status !== 'connected' ? (
              <div className="text-center py-8">
                <p className="text-muted-foreground mb-4">
                  Connect your wallet to participate
                </p>
              </div>
            ) : (
              renderVotingInterface()
            )}
          </CardContent>
        </Card>

        {/* Privacy notice */}
        <div className="mt-6 text-center text-sm text-muted-foreground">
          <p>Your vote is anonymous and verified using zero-knowledge proofs.</p>
          <p>No one can see how you voted, but the total count is verifiable.</p>
        </div>
      </div>
    </div>
  );
};

export default VotingPage;
Enter fullscreen mode Exit fullscreen mode

Admin Panel Component

Create frontend-vite-react/src/pages/admin/index.tsx:

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useVotingContract } from '@/modules/midnight/voting-sdk/hooks/use-voting-contract';

export const AdminPage = () => {
  const { deployedContractAPI, derivedState, onDeploy } = useVotingContract();

  // Form state
  const [proposalTitle, setProposalTitle] = useState('');
  const [optionCount, setOptionCount] = useState(2);
  const [adminKey, setAdminKey] = useState('');
  const [isCreating, setIsCreating] = useState(false);
  const [isEnding, setIsEnding] = useState(false);

  // Deploy new contract
  const handleDeploy = async () => {
    try {
      setIsCreating(true);
      await onDeploy();
    } catch (err) {
      console.error('Deploy failed:', err);
    } finally {
      setIsCreating(false);
    }
  };

  // Create proposal
  const handleCreateProposal = async () => {
    if (!deployedContractAPI || !proposalTitle || !adminKey) return;

    try {
      setIsCreating(true);
      const deadline = Math.floor(Date.now() / 1000) + 86400; // 24 hours
      await deployedContractAPI.createProposal(
        proposalTitle,
        optionCount,
        deadline,
        adminKey
      );
    } catch (err) {
      console.error('Create proposal failed:', err);
    } finally {
      setIsCreating(false);
    }
  };

  // End voting
  const handleEndVoting = async () => {
    if (!deployedContractAPI || !adminKey) return;

    try {
      setIsEnding(true);
      await deployedContractAPI.endVoting(adminKey);
    } catch (err) {
      console.error('End voting failed:', err);
    } finally {
      setIsEnding(false);
    }
  };

  return (
    <div className="min-h-screen bg-background py-12 px-4">
      <div className="max-w-2xl mx-auto space-y-6">
        <Card>
          <CardHeader>
            <CardTitle>Voting Admin Panel</CardTitle>
            <CardDescription>
              Create and manage voting proposals
            </CardDescription>
          </CardHeader>
          <CardContent className="space-y-6">
            {/* Deploy Section */}
            {!deployedContractAPI && (
              <div className="space-y-4">
                <h3 className="font-semibold">Step 1: Deploy Contract</h3>
                <Button onClick={handleDeploy} disabled={isCreating}>
                  {isCreating ? 'Deploying...' : 'Deploy Voting Contract'}
                </Button>
              </div>
            )}

            {/* Create Proposal Section */}
            {deployedContractAPI && derivedState?.proposalState === 0 && (
              <div className="space-y-4">
                <h3 className="font-semibold">Create New Proposal</h3>

                <div className="space-y-2">
                  <Label htmlFor="title">Proposal Title</Label>
                  <Input
                    id="title"
                    value={proposalTitle}
                    onChange={(e) => setProposalTitle(e.target.value)}
                    placeholder="What should we decide?"
                  />
                </div>

                <div className="space-y-2">
                  <Label htmlFor="options">Number of Options</Label>
                  <Input
                    id="options"
                    type="number"
                    min={2}
                    max={4}
                    value={optionCount}
                    onChange={(e) => setOptionCount(Number(e.target.value))}
                  />
                </div>

                <div className="space-y-2">
                  <Label htmlFor="adminKey">Admin Key</Label>
                  <Input
                    id="adminKey"
                    type="password"
                    value={adminKey}
                    onChange={(e) => setAdminKey(e.target.value)}
                    placeholder="Secret admin key"
                  />
                </div>

                <Button
                  onClick={handleCreateProposal}
                  disabled={isCreating || !proposalTitle || !adminKey}
                  className="w-full"
                >
                  {isCreating ? 'Creating...' : 'Create Proposal'}
                </Button>
              </div>
            )}

            {/* Active Voting Controls */}
            {deployedContractAPI && derivedState?.proposalState === 1 && (
              <div className="space-y-4">
                <h3 className="font-semibold">Voting In Progress</h3>
                <p className="text-muted-foreground">
                  Total votes: {derivedState.totalVotes.toString()}
                </p>

                <div className="space-y-2">
                  <Label htmlFor="endAdminKey">Admin Key</Label>
                  <Input
                    id="endAdminKey"
                    type="password"
                    value={adminKey}
                    onChange={(e) => setAdminKey(e.target.value)}
                    placeholder="Enter admin key to end voting"
                  />
                </div>

                <Button
                  variant="destructive"
                  onClick={handleEndVoting}
                  disabled={isEnding || !adminKey}
                  className="w-full"
                >
                  {isEnding ? 'Ending...' : 'End Voting Period'}
                </Button>
              </div>
            )}

            {/* Results */}
            {derivedState?.proposalState === 2 && (
              <div className="text-center py-4">
                <p className="text-lg font-semibold text-green-600">
                  Voting Complete
                </p>
                <p className="text-muted-foreground">
                  Results are now final and publicly verifiable.
                </p>
              </div>
            )}
          </CardContent>
        </Card>
      </div>
    </div>
  );
};

export default AdminPage;
Enter fullscreen mode Exit fullscreen mode

Results Visualization Component

Create frontend-vite-react/src/components/ResultsChart.tsx:

import { useMemo } from 'react';

type VoteResult = {
  label: string;
  votes: bigint;
  color: string;
};

type ResultsChartProps = {
  results: VoteResult[];
  totalVotes: bigint;
};

export const ResultsChart = ({ results, totalVotes }: ResultsChartProps) => {
  const total = Number(totalVotes) || 1;

  // Calculate percentages and find winner
  const processedResults = useMemo(() => {
    const processed = results.map((result) => ({
      ...result,
      percentage: (Number(result.votes) / total) * 100,
    }));

    // Sort by votes descending
    return processed.sort((a, b) => Number(b.votes - a.votes));
  }, [results, total]);

  const winner = processedResults[0];

  return (
    <div className="space-y-6">
      {/* Winner announcement */}
      <div className="text-center p-6 bg-primary/5 rounded-lg">
        <p className="text-sm text-muted-foreground mb-1">Winner</p>
        <p className="text-2xl font-bold" style={{ color: winner?.color }}>
          {winner?.label}
        </p>
        <p className="text-lg">
          {winner?.percentage.toFixed(1)}% ({winner?.votes.toString()} votes)
        </p>
      </div>

      {/* Bar chart */}
      <div className="space-y-4">
        {processedResults.map((result, index) => (
          <div key={index} className="space-y-1">
            <div className="flex justify-between text-sm">
              <span className="font-medium">{result.label}</span>
              <span className="text-muted-foreground">
                {result.votes.toString()} ({result.percentage.toFixed(1)}%)
              </span>
            </div>
            <div className="h-8 bg-secondary rounded-full overflow-hidden">
              <div
                className="h-full rounded-full transition-all duration-500"
                style={{
                  width: `${result.percentage}%`,
                  backgroundColor: result.color,
                }}
              />
            </div>
          </div>
        ))}
      </div>

      {/* Total */}
      <div className="text-center pt-4 border-t">
        <p className="text-2xl font-bold">{totalVotes.toString()}</p>
        <p className="text-sm text-muted-foreground">Total Votes Cast</p>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Testing Your Voting Contract

Unit Tests

Create voting-contract/src/test/voting.test.ts:

import { describe, it, expect, beforeEach } from 'vitest';
import { VotingSimulator } from './simulators/voting-simulator';

describe('Voting Smart Contract', () => {
  let simulator: VotingSimulator;

  beforeEach(() => {
    simulator = VotingSimulator.deployContract();
  });

  describe('Proposal Creation', () => {
    it('should create a proposal successfully', () => {
      const admin = 'admin-secret-key';

      simulator.as('admin').createProposal(
        'Test Proposal',
        3, // 3 options
        Date.now() + 86400000, // 24 hours
        admin
      );

      const state = simulator.getLedger();
      expect(state.proposalState).toEqual(1n);
      expect(state.optionCount).toEqual(3n);
    });

    it('should reject proposal with less than 2 options', () => {
      expect(() => {
        simulator.as('admin').createProposal('Bad Proposal', 1, Date.now(), 'key');
      }).toThrow('Need at least 2 options');
    });

    it('should reject proposal with more than 4 options', () => {
      expect(() => {
        simulator.as('admin').createProposal('Bad Proposal', 5, Date.now(), 'key');
      }).toThrow('Maximum 4 options');
    });
  });

  describe('Voting', () => {
    beforeEach(() => {
      simulator.as('admin').createProposal(
        'Test Vote',
        3,
        Date.now() + 86400000,
        'admin-key'
      );
    });

    it('should allow voting for valid options', () => {
      simulator.as('voter1').castVote(1);
      simulator.as('voter2').castVote(2);
      simulator.as('voter3').castVote(1);

      const state = simulator.getLedger();
      expect(state.votesOption1).toEqual(2n);
      expect(state.votesOption2).toEqual(1n);
      expect(state.totalVoters).toEqual(3n);
    });

    it('should reject vote for option 0', () => {
      expect(() => {
        simulator.as('voter1').castVote(0);
      }).toThrow('Invalid option: too low');
    });

    it('should reject vote for option beyond count', () => {
      expect(() => {
        simulator.as('voter1').castVote(4); // Only 3 options
      }).toThrow('Invalid option: too high');
    });

    it('should reject vote when voting not active', () => {
      simulator.as('admin').endVoting('admin-key');

      expect(() => {
        simulator.as('voter1').castVote(1);
      }).toThrow('Voting not active');
    });
  });

  describe('Ending Voting', () => {
    beforeEach(() => {
      simulator.as('admin').createProposal(
        'Test Vote',
        2,
        Date.now() + 86400000,
        'admin-key'
      );
    });

    it('should allow admin to end voting', () => {
      simulator.as('admin').endVoting('admin-key');

      const state = simulator.getLedger();
      expect(state.proposalState).toEqual(2n);
    });

    it('should reject non-admin ending voting', () => {
      expect(() => {
        simulator.as('attacker').endVoting('wrong-key');
      }).toThrow('Not authorized');
    });
  });

  describe('Multi-voter scenarios', () => {
    beforeEach(() => {
      simulator.as('admin').createProposal(
        'Community Decision',
        4,
        Date.now() + 86400000,
        'admin-key'
      );
    });

    it('should correctly tally votes from multiple voters', () => {
      // Simulate 10 voters
      for (let i = 0; i < 10; i++) {
        const option = (i % 4) + 1; // Distribute across options
        simulator.as(`voter${i}`).castVote(option);
      }

      const state = simulator.getLedger();
      expect(state.totalVoters).toEqual(10n);

      // Options 1-4 should each have some votes
      const totalVotes =
        Number(state.votesOption1) +
        Number(state.votesOption2) +
        Number(state.votesOption3) +
        Number(state.votesOption4);
      expect(totalVotes).toEqual(10);
    });

    it('should handle unanimous voting', () => {
      for (let i = 0; i < 5; i++) {
        simulator.as(`voter${i}`).castVote(2); // Everyone votes option 2
      }

      const state = simulator.getLedger();
      expect(state.votesOption1).toEqual(0n);
      expect(state.votesOption2).toEqual(5n);
      expect(state.votesOption3).toEqual(0n);
      expect(state.votesOption4).toEqual(0n);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Simulator Implementation

Create voting-contract/src/test/simulators/voting-simulator.ts:

import {
  Contract as VotingContract,
} from '../../managed/voting/contract/index.cjs';
import {
  createPrivateState,
  type VotingPrivateState,
} from '../../witnesses';

type LedgerState = {
  proposalState: bigint;
  optionCount: bigint;
  votesOption1: bigint;
  votesOption2: bigint;
  votesOption3: bigint;
  votesOption4: bigint;
  totalVoters: bigint;
};

export class VotingSimulator {
  private ledgerState: LedgerState;
  private privateStates: Map<string, VotingPrivateState>;
  private currentUser: string;

  private constructor() {
    this.ledgerState = {
      proposalState: 0n,
      optionCount: 0n,
      votesOption1: 0n,
      votesOption2: 0n,
      votesOption3: 0n,
      votesOption4: 0n,
      totalVoters: 0n,
    };
    this.privateStates = new Map();
    this.currentUser = 'default';
  }

  static deployContract(): VotingSimulator {
    return new VotingSimulator();
  }

  as(user: string): this {
    this.currentUser = user;
    if (!this.privateStates.has(user)) {
      this.privateStates.set(user, createPrivateState());
    }
    return this;
  }

  getLedger(): LedgerState {
    return { ...this.ledgerState };
  }

  getPrivateState(): VotingPrivateState {
    return this.privateStates.get(this.currentUser) || createPrivateState();
  }

  createProposal(
    title: string,
    options: number,
    deadline: number,
    adminKey: string
  ): void {
    if (this.ledgerState.proposalState !== 0n) {
      throw new Error('Proposal already exists');
    }
    if (options < 2) {
      throw new Error('Need at least 2 options');
    }
    if (options > 4) {
      throw new Error('Maximum 4 options');
    }

    this.ledgerState.proposalState = 1n;
    this.ledgerState.optionCount = BigInt(options);
  }

  castVote(option: number): void {
    if (this.ledgerState.proposalState !== 1n) {
      throw new Error('Voting not active');
    }
    if (option < 1) {
      throw new Error('Invalid option: too low');
    }
    if (option > Number(this.ledgerState.optionCount)) {
      throw new Error('Invalid option: too high');
    }

    // Increment appropriate counter
    switch (option) {
      case 1:
        this.ledgerState.votesOption1++;
        break;
      case 2:
        this.ledgerState.votesOption2++;
        break;
      case 3:
        this.ledgerState.votesOption3++;
        break;
      case 4:
        this.ledgerState.votesOption4++;
        break;
    }

    this.ledgerState.totalVoters++;

    // Update private state
    const privateState = this.privateStates.get(this.currentUser);
    if (privateState) {
      privateState.hasVoted = true;
      privateState.pendingVote = option;
    }
  }

  endVoting(adminKey: string): void {
    if (this.ledgerState.proposalState !== 1n) {
      throw new Error('Voting not active');
    }
    // In a real implementation, verify admin key
    if (adminKey !== 'admin-key') {
      throw new Error('Not authorized');
    }

    this.ledgerState.proposalState = 2n;
  }
}
Enter fullscreen mode Exit fullscreen mode

Running Tests

# Run voting contract tests
cd voting-contract
npm run test

# Run with coverage
npm run test -- --coverage

# Run specific test file
npm run test -- voting.test.ts
Enter fullscreen mode Exit fullscreen mode

Deployment Guide

Local Development

# Terminal 1: Start local Midnight node
npm run setup-standalone

# Terminal 2: Start frontend
npm run dev:frontend
Enter fullscreen mode Exit fullscreen mode

Preview Network Deployment

  1. Configure Environment
# voting-cli/.env
MY_PREVIEW_MNEMONIC="your wallet mnemonic phrase"
Enter fullscreen mode Exit fullscreen mode
  1. Fund Your Wallet

  2. Deploy Contract

cd voting-cli
npm run deploy
Enter fullscreen mode Exit fullscreen mode
  1. Configure Frontend
# frontend-vite-react/.env
VITE_CONTRACT_ADDRESS="deployed_contract_address_here"
Enter fullscreen mode Exit fullscreen mode
  1. Start Application
npm run dev:frontend
Enter fullscreen mode Exit fullscreen mode

Deployment Checklist

  • [ ] Compact compiler installed (v0.27.0)
  • [ ] Contract compiles without errors
  • [ ] All tests passing
  • [ ] Wallet funded with tSTAR
  • [ ] Environment variables configured
  • [ ] Contract deployed successfully
  • [ ] Frontend connected to contract

Advanced Voting Patterns

Pattern 1: Weighted Voting

For token-weighted governance:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Weighted voting based on token holdings
export ledger weightedVotesOption1: Counter;
export ledger weightedVotesOption2: Counter;

export circuit voteWithWeight(option: Uint<8>, weight: Uint<64>): [] {
  assert(proposalState == 1, "Voting not active");
  assert(weight > 0, "No voting power");

  if (option == 1) {
    weightedVotesOption1.increment(weight);
  } else if (option == 2) {
    weightedVotesOption2.increment(weight);
  }
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Quadratic Voting

Reduce plutocracy with quadratic costs:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Quadratic voting: cost = votes^2
export ledger creditsSpent: Map<Bytes<32>, Counter>;

export pure function calculateCost(votes: Uint<64>): Uint<64> {
  return votes * votes; // Quadratic cost
}

export circuit quadraticVote(
  option: Uint<8>,
  numVotes: Uint<64>,
  voterKey: Bytes<32>
): [] {
  let cost: Uint<64> = calculateCost(numVotes);
  // Verify voter has enough credits
  // Deduct credits and record votes
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Delegated Voting

Allow vote delegation:

pragma language_version >= 0.19;

import CompactStandardLibrary;

// Delegation mapping
export ledger delegates: Map<Bytes<32>, Bytes<32>>;
export ledger delegatedPower: Map<Bytes<32>, Counter>;

export circuit delegateVote(
  fromVoter: Bytes<32>,
  toDelegate: Bytes<32>
): [] {
  // Record delegation
  delegates[fromVoter] = toDelegate;
  delegatedPower[toDelegate].increment(1);
}

export circuit voteAsDelegate(
  delegate: Bytes<32>,
  option: Uint<8>
): [] {
  // Vote with accumulated power
  // let power = delegatedPower[delegate];
  // Apply votes
}
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Multi-Round Voting

Implement runoff elections:

pragma language_version >= 0.19;

import CompactStandardLibrary;

export ledger currentRound: Uint<8>;
export ledger eliminatedOptions: Map<Uint<8>, Boolean>;

export circuit advanceRound(): [] {
  // Find option with lowest votes
  // Mark as eliminated
  // Increment round
  currentRound = currentRound + 1;
}

export circuit voteInRound(option: Uint<8>): [] {
  // Verify option not eliminated
  assert(eliminatedOptions[option] == false, "Option eliminated");
  // Record vote
}
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Time-Locked Results

Hide results until voting ends:

// Client-side: Encrypt votes with time-lock
const encryptVote = async (vote: number, unlockTime: number) => {
  // Use threshold encryption or timelock puzzles
  // Results only decryptable after deadline
  return encryptedVote;
};

// Contract stores encrypted votes
// Reveal phase decrypts and tallies
Enter fullscreen mode Exit fullscreen mode

Troubleshooting & Best Practices

Common Issues

1. "Proposal already exists" Error

Error: Proposal already exists
Enter fullscreen mode Exit fullscreen mode

Solution: Call resetProposal() with admin key before creating a new proposal.

2. Vote Not Recording

Symptoms: Vote transaction succeeds but count doesn't change.

Solutions:

  • Verify voting is active (proposalState == 1)
  • Check option is within valid range
  • Ensure contract address is correct

3. Admin Key Mismatch

Error: Not authorized
Enter fullscreen mode Exit fullscreen mode

Solution: Use the exact same admin key used during proposal creation.

4. State Not Updating in UI

Solution: Ensure observable subscription is active:

useEffect(() => {
  const subscription = votingContract.state$.subscribe((state) => {
    setDerivedState(state);
  });
  return () => subscription.unsubscribe();
}, [votingContract]);
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Validate Inputs Early

export circuit castVote(option: Uint<8>): [] {
  // Validate FIRST, before any state changes
  assert(proposalState == 1, "Voting not active");
  assert(option >= 1, "Invalid option");
  assert(option <= optionCount, "Invalid option");

  // Then modify state
  // ...
}
Enter fullscreen mode Exit fullscreen mode

2. Use Clear State Transitions

// Clear state machine
// 0 -> 1: createProposal
// 1 -> 2: endVoting
// 2 -> 0: resetProposal

export circuit createProposal(...): [] {
  assert(proposalState == 0, "Wrong state");
  // ...
  proposalState = 1;
}
Enter fullscreen mode Exit fullscreen mode

3. Handle Errors Gracefully

const handleVote = async (option: number) => {
  try {
    setLoading(true);
    setError(null);
    await contract.vote(option);
    setSuccess(true);
  } catch (err) {
    if (err.message.includes('Voting not active')) {
      setError('Voting period has ended.');
    } else if (err.message.includes('Invalid option')) {
      setError('Please select a valid option.');
    } else {
      setError('Vote failed. Please try again.');
    }
    console.error('Vote error:', err);
  } finally {
    setLoading(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

4. Test Edge Cases

describe('Edge cases', () => {
  it('handles maximum number of voters', async () => {
    // Test with 1000+ voters
  });

  it('handles tie votes', async () => {
    // Equal votes for multiple options
  });

  it('handles zero votes', async () => {
    // End voting with no participation
  });
});
Enter fullscreen mode Exit fullscreen mode

Next Steps & Resources

Learning Path

  1. Beginner: Complete this tutorial with the basic voting contract
  2. Intermediate: Add weighted voting and delegation
  3. Advanced: Implement quadratic voting with Merkle proofs

Extend Your Voting System

Feature Difficulty Description
Voter Registration Medium Merkle tree eligibility proofs
Delegation Medium Allow vote power transfer
Quadratic Voting Hard Token-weighted with diminishing returns
Anonymous Results Hard Reveal results only after deadline
Multi-Sig Admin Medium Require multiple admins to end voting

Official Resources

Community

  • Discord: Join the Midnight developer community
  • GitHub: Contribute to open-source tools
  • Forum: Ask questions and share projects

Related Projects


Quick Reference Card

Compact Cheat Sheet

// Pragma
pragma language_version >= 0.19;

// Imports
import CompactStandardLibrary;

// Public state
export ledger voteCount: Counter;
export ledger proposalState: Uint<8>;
export ledger adminKey: Bytes<32>;

// State-changing circuits
export circuit vote(option: Uint<8>): [] {
  assert(proposalState == 1, "Not active");
  voteCount.increment(1);
}

// Pure functions (no state change)
export pure function isValidOption(opt: Uint<8>, max: Uint<8>): Boolean {
  return opt >= 1 && opt <= max;
}

// Assertions
assert(condition, "Error message");
Enter fullscreen mode Exit fullscreen mode

npm Scripts

npm install              # Install dependencies
npm run build            # Compile contracts
npm run dev:frontend     # Start dev server
npm run test             # Run tests
npm run setup-standalone # Local network
npm run build-production # Production build
Enter fullscreen mode Exit fullscreen mode

Key Files

File Purpose
voting-contract/src/voting.compact Voting logic
voting-contract/src/witnesses.ts Private state
frontend/src/modules/midnight/voting-sdk/ SDK
voting-cli/.env CLI config
frontend/.env Frontend config

Conclusion

You now have a complete foundation for building privacy-preserving voting systems on Midnight. The combination of:

  • Zero-knowledge proofs for voter privacy
  • Public ledger for verifiable results
  • Compact language for secure logic
  • React frontend for user interaction

...enables entirely new categories of democratic applications where votes remain private but results are transparent and verifiable.

Privacy-preserving voting is one of the most impactful applications of zero-knowledge technology. Whether for DAOs, corporate governance, or community decisions, Midnight provides the tools to build fair, anonymous, and verifiable voting systems.

Happy building!


Built with the Midnight Starter Template

Mesh x Edda Labs

Top comments (0)