---
# System prepended metadata

title: Building a Voting dApp on Starknet

---

# Building a Voting dApp on Starknet

A complete step-by-step guide to building a decentralized voting application on Starknet using Cairo and Next.js.

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Project Setup](#project-setup)
3. [Building the Smart Contract](#building-the-smart-contract)
4. [Writing Contract Tests](#writing-contract-tests)
5. [Deploying the Contract](#deploying-the-contract)
6. [Building the Frontend](#building-the-frontend)
7. [Testing the Application](#testing-the-application)

---

## Prerequisites

Before starting, ensure you have:

- Node.js (v22 or higher)
- Yarn package manager
- Git
- A Starknet wallet (Argent X or Braavos)
- Testnet ETH/STRK for Sepolia

### Required Knowledge

- Basic understanding of blockchain concepts
- JavaScript/TypeScript fundamentals
- React basics
- Cairo programming basics

---

## Project Setup

### Step 1: Create a Scaffold-Stark Project

```bash
npx create-stark@latest
```

When prompted:

- **Project Name:** `voting-dapp` (or your preferred name)
- **Development Tools:** Choose native tools or Dev Containers

### Step 2: Navigate to Project Directory

```bash
cd voting-dapp
```

### Step 3: Install Dependencies

```bash
yarn install
```

### Step 4: Verify Installation

```bash
# Terminal 1: Start local blockchain
yarn chain

# Terminal 2: In a new terminal
yarn deploy

# Terminal 3: In another new terminal
yarn start
```

Visit `http://localhost:3000` to verify the scaffold is working.

---

## Building the Smart Contract

### Step 1: Create the Voting Contract File

Navigate to `packages/snfoundry/contracts/src/` and create a new file called `voting_contract.cairo`:

```bash
touch packages/snfoundry/contracts/src/voting_contract.cairo
```

### Step 2: Write the Voting Contract

Paste the following code into `voting_contract.cairo`:

```cairo
use starknet::ContractAddress;

#[starknet::interface]
pub trait IVoting<TContractState> {
    fn register_voter(ref self: TContractState);
    fn create_proposal(ref self: TContractState, description: ByteArray);
    fn vote(ref self: TContractState, proposal_id: u256, vote: bool);
    fn get_proposal_details(self: @TContractState, proposal_id: u256) -> ProposalDetails;
    fn get_proposal_count(self: @TContractState) -> u256;
    fn is_voter_registered(self: @TContractState, voter: ContractAddress) -> bool;
}

#[derive(Drop, Serde, starknet::Store)]
pub struct Proposal {
    pub id: u256,
    pub description: ByteArray,
    pub yes_votes: u256,
    pub no_votes: u256,
    pub ended: bool,
}

#[derive(Drop, Serde)]
pub struct ProposalDetails {
    pub id: u256,
    pub description: ByteArray,
    pub yes_votes: u256,
    pub no_votes: u256,
    pub ended: bool,
}

#[starknet::contract]
pub mod VotingContract {
    use openzeppelin_access::ownable::OwnableComponent;
    use starknet::storage::{
        Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess,
        StoragePointerWriteAccess,
    };
    use starknet::{ContractAddress, get_caller_address};
    use super::{IVoting, Proposal, ProposalDetails};

    component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

    #[abi(embed_v0)]
    impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
    impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        #[flat]
        OwnableEvent: OwnableComponent::Event,
        VoterRegistered: VoterRegistered,
        ProposalCreated: ProposalCreated,
        VoteCast: VoteCast,
    }

    #[derive(Drop, starknet::Event)]
    struct VoterRegistered {
        #[key]
        voter: ContractAddress,
    }

    #[derive(Drop, starknet::Event)]
    struct ProposalCreated {
        #[key]
        proposal_id: u256,
        description: ByteArray,
    }

    #[derive(Drop, starknet::Event)]
    struct VoteCast {
        #[key]
        proposal_id: u256,
        #[key]
        voter: ContractAddress,
        vote: bool,
    }

    #[storage]
    struct Storage {
        registered_voters: Map<ContractAddress, bool>,
        proposals: Map<u256, Proposal>,
        proposal_count: u256,
        has_voted: Map<(u256, ContractAddress), bool>,
        #[substorage(v0)]
        ownable: OwnableComponent::Storage,
    }

    #[constructor]
    fn constructor(ref self: ContractState, owner: ContractAddress) {
        self.ownable.initializer(owner);
        self.proposal_count.write(0);
    }

    #[abi(embed_v0)]
    impl VotingImpl of IVoting<ContractState> {
        fn register_voter(ref self: ContractState) {
            let caller = get_caller_address();
            assert!(!self.registered_voters.read(caller), "Already registered");

            self.registered_voters.write(caller, true);
            self.emit(VoterRegistered { voter: caller });
        }

        fn create_proposal(ref self: ContractState, description: ByteArray) {
            self.ownable.assert_only_owner();

            let proposal_id = self.proposal_count.read();
            let new_proposal = Proposal {
                id: proposal_id,
                description: description.clone(),
                yes_votes: 0,
                no_votes: 0,
                ended: false,
            };

            self.proposals.write(proposal_id, new_proposal);
            self.proposal_count.write(proposal_id + 1);

            self.emit(ProposalCreated { proposal_id, description });
        }

        fn vote(ref self: ContractState, proposal_id: u256, vote: bool) {
            let caller = get_caller_address();

            assert!(self.registered_voters.read(caller), "Not registered");
            assert!(proposal_id < self.proposal_count.read(), "Invalid proposal");
            assert!(!self.has_voted.read((proposal_id, caller)), "Already voted");

            let mut proposal = self.proposals.read(proposal_id);
            assert!(!proposal.ended, "Proposal ended");

            if vote {
                proposal.yes_votes += 1;
            } else {
                proposal.no_votes += 1;
            }

            self.proposals.write(proposal_id, proposal);
            self.has_voted.write((proposal_id, caller), true);

            self.emit(VoteCast { proposal_id, voter: caller, vote });
        }

        fn get_proposal_details(self: @ContractState, proposal_id: u256) -> ProposalDetails {
            assert!(proposal_id < self.proposal_count.read(), "Invalid proposal");
            let proposal = self.proposals.read(proposal_id);

            ProposalDetails {
                id: proposal.id,
                description: proposal.description,
                yes_votes: proposal.yes_votes,
                no_votes: proposal.no_votes,
                ended: proposal.ended,
            }
        }

        fn get_proposal_count(self: @ContractState) -> u256 {
            self.proposal_count.read()
        }

        fn is_voter_registered(self: @ContractState, voter: ContractAddress) -> bool {
            self.registered_voters.read(voter)
        }
    }
}
```

#### 📖 Code Explanation

This voting contract demonstrates several key Cairo concepts:

**1. Interface Definition (`IVoting`)**

- Defines 6 public functions that external applications can call
- `register_voter()`: Allows anyone to register as a voter
- `create_proposal()`: Owner-only function to create new proposals
- `vote()`: Registered voters can cast yes/no votes
- `get_proposal_details()`: Read proposal information
- `get_proposal_count()`: Get total number of proposals
- `is_voter_registered()`: Check if an address is registered

**2. Data Structures**

- `Proposal` struct: Stored on-chain with all proposal data
- `ProposalDetails` struct: Used to return data to external callers
- Both contain: id, description, vote counts, and ended status

**3. Storage**

- `registered_voters`: Maps addresses to registration status
- `proposals`: Maps proposal IDs to proposal data
- `proposal_count`: Tracks total proposals created
- `has_voted`: Prevents double voting using composite key (proposal_id, voter)
- `ownable`: OpenZeppelin component for access control

**4. Access Control**

- Uses OpenZeppelin's `OwnableComponent` for secure owner management
- Only owner can create proposals (`assert_only_owner()`)
- Anyone can register as voter
- Only registered voters can vote

**5. Security Features**

- `assert!` statements validate all inputs before execution
- Double voting prevented by tracking (proposal_id, voter) pairs
- Registration required before voting
- Events emitted for transparency

**6. Events**

- `VoterRegistered`: Logged when someone registers
- `ProposalCreated`: Logged when proposal is created
- `VoteCast`: Logged when someone votes
- Events enable tracking all actions on the blockchain

### Step 3: Register the Contract in lib.cairo

Open `packages/snfoundry/contracts/src/lib.cairo` and add:

```cairo
pub mod your_contract;
pub mod voting_contract;
```

### Step 4: Compile the Contract

```bash
cd packages/snfoundry/contracts
scarb build
```

You should see: `✓ Compiled successfully`

---

## Writing Contract Tests

### Step 1: Create Test File

Create `packages/snfoundry/contracts/tests/test_voting.cairo`:

```bash
touch packages/snfoundry/contracts/tests/test_voting.cairo
```

### Step 2: Write Comprehensive Tests

Paste the following test code:

```cairo
use snforge_std::{declare, ContractClassTrait, DeclareResultTrait, start_cheat_caller_address};
use starknet::ContractAddress;
use core::starknet::contract_address::contract_address_const;
use contracts::voting_contract::{IVotingDispatcher, IVotingDispatcherTrait};

fn deploy_voting_contract() -> (IVotingDispatcher, ContractAddress) {
    let owner: ContractAddress = contract_address_const::<'owner'>();

    let contract = declare("VotingContract").unwrap().contract_class();
    let mut constructor_calldata = array![owner.into()];

    let (contract_address, _) = contract.deploy(@constructor_calldata).unwrap();
    let dispatcher = IVotingDispatcher { contract_address };

    (dispatcher, owner)
}

#[test]
fn test_voter_registration() {
    let (voting_contract, _owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.register_voter();

    let is_registered = voting_contract.is_voter_registered(voter);
    assert!(is_registered, "Voter should be registered");
}

#[test]
#[should_panic]
fn test_double_registration_fails() {
    let (voting_contract, _owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);

    voting_contract.register_voter();
    voting_contract.register_voter();
}

#[test]
fn test_create_proposal() {
    let (voting_contract, owner) = deploy_voting_contract();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Should we upgrade the protocol?");

    let count = voting_contract.get_proposal_count();
    assert!(count == 1, "Proposal count should be 1");

    let proposal = voting_contract.get_proposal_details(0);
    assert!(proposal.id == 0, "Proposal ID should be 0");
    assert!(proposal.yes_votes == 0, "Yes votes should be 0");
    assert!(proposal.no_votes == 0, "No votes should be 0");
    assert!(!proposal.ended, "Proposal should not be ended");
}

#[test]
#[should_panic]
fn test_non_owner_cannot_create_proposal() {
    let (voting_contract, _owner) = deploy_voting_contract();
    let non_owner: ContractAddress = contract_address_const::<'non_owner'>();

    start_cheat_caller_address(voting_contract.contract_address, non_owner);
    voting_contract.create_proposal("This should fail");
}

#[test]
fn test_voting_yes() {
    let (voting_contract, owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Should we do this?");

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.vote(0, true);

    let proposal = voting_contract.get_proposal_details(0);
    assert!(proposal.yes_votes == 1, "Yes votes should be 1");
    assert!(proposal.no_votes == 0, "No votes should be 0");
}

#[test]
fn test_voting_no() {
    let (voting_contract, owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Should we do this?");

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.vote(0, false);

    let proposal = voting_contract.get_proposal_details(0);
    assert!(proposal.yes_votes == 0, "Yes votes should be 0");
    assert!(proposal.no_votes == 1, "No votes should be 1");
}

#[test]
fn test_multiple_voters() {
    let (voting_contract, owner) = deploy_voting_contract();
    let voter1: ContractAddress = contract_address_const::<'voter1'>();
    let voter2: ContractAddress = contract_address_const::<'voter2'>();
    let voter3: ContractAddress = contract_address_const::<'voter3'>();

    start_cheat_caller_address(voting_contract.contract_address, voter1);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, voter2);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, voter3);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Multiple voters test");

    start_cheat_caller_address(voting_contract.contract_address, voter1);
    voting_contract.vote(0, true);

    start_cheat_caller_address(voting_contract.contract_address, voter2);
    voting_contract.vote(0, true);

    start_cheat_caller_address(voting_contract.contract_address, voter3);
    voting_contract.vote(0, false);

    let proposal = voting_contract.get_proposal_details(0);
    assert!(proposal.yes_votes == 2, "Yes votes should be 2");
    assert!(proposal.no_votes == 1, "No votes should be 1");
}

#[test]
#[should_panic]
fn test_unregistered_voter_cannot_vote() {
    let (voting_contract, owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Test proposal");

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.vote(0, true);
}

#[test]
#[should_panic]
fn test_cannot_vote_twice() {
    let (voting_contract, owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.register_voter();

    start_cheat_caller_address(voting_contract.contract_address, owner);
    voting_contract.create_proposal("Test proposal");

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.vote(0, true);
    voting_contract.vote(0, true);
}

#[test]
#[should_panic]
fn test_vote_on_invalid_proposal() {
    let (voting_contract, _owner) = deploy_voting_contract();
    let voter: ContractAddress = contract_address_const::<'voter1'>();

    start_cheat_caller_address(voting_contract.contract_address, voter);
    voting_contract.register_voter();

    voting_contract.vote(999, true);
}
```

#### 📖 Code Explanation

This test suite validates all contract functionality:

**1. Test Helper Function**

- `deploy_voting_contract()`: Deploys a fresh contract instance for each test
- Creates owner address and deploys contract with owner as constructor arg
- Returns dispatcher (contract interface) and owner address

**2. Voter Registration Tests**

- `test_voter_registration()`: Verifies users can register successfully
- `test_double_registration_fails()`: Ensures same user can't register twice
- Uses `start_cheat_caller_address()` to simulate different callers

**3. Proposal Creation Tests**

- `test_create_proposal()`: Validates owner can create proposals
- Checks proposal count increments correctly
- Verifies proposal data stored properly (id, votes start at 0)
- `test_non_owner_cannot_create_proposal()`: Security test - non-owners blocked

**4. Voting Tests**

- `test_voting_yes()`: Tests voting "yes" increments yes_votes
- `test_voting_no()`: Tests voting "no" increments no_votes
- `test_multiple_voters()`: Validates multiple users can vote on same proposal
- Each test verifies vote counts are accurate

**5. Security Tests**

- `test_unregistered_voter_cannot_vote()`: Prevents unregistered voting
- `test_cannot_vote_twice()`: Prevents double voting
- `test_vote_on_invalid_proposal()`: Validates proposal ID exists
- All use `#[should_panic]` to expect failure

**6. Testing Pattern**

- Each test is isolated (fresh contract deployment)
- Uses `start_cheat_caller_address()` to simulate different users
- Tests both success paths and failure paths
- Comprehensive coverage of all contract functions

### Step 3: Run Tests

```bash
cd packages/snfoundry/contracts
snforge test
```

You should see all 12 tests passing ✅

---

## Deploying the Contract

### Step 1: Update Deployment Script

Open `packages/snfoundry/scripts-ts/deploy.ts` and update the `deployScript` function:

```typescript
const deployScript = async (): Promise<void> => {
    await deployContract({
        contract: "YourContract",
        constructorArgs: {
            owner: deployer.address,
        },
    });

    await deployContract({
        contract: "VotingContract",
        constructorArgs: {
            owner: deployer.address,
        },
    });
};
```

#### 📖 Code Explanation

**Deployment Script Structure:**

1. **First Contract**: Deploys `YourContract` (the default scaffold contract)
    - Uses `deployer.address` as the owner
    - This is the example contract that comes with Scaffold-Stark

2. **Second Contract**: Deploys `VotingContract` (your new contract)
    - Also uses `deployer.address` as owner
    - Owner will be able to create proposals

3. **Constructor Arguments**:
    - Both contracts require an `owner` parameter
    - The deployer's wallet address is set as owner
    - Owner has special privileges (creating proposals in VotingContract)

4. **Deployment Order**:
    - Contracts deploy sequentially
    - Each deployment returns contract address
    - Addresses auto-saved to `deployedContracts.ts` for frontend use

### Step 2: Configure Network

Open `packages/nextjs/scaffold.config.ts` and set target network:

```typescript
const scaffoldConfig = {
    targetNetworks: [chains.sepolia], // Change from devnet to sepolia
    onlyLocalBurnerWallet: false,
    pollingInterval: 30_000,
    autoConnectTTL: 60000,
    walletAutoConnect: true,
} as const satisfies ScaffoldConfig;
```

#### 📖 Code Explanation

**Network Configuration:**

1. **Target Networks**:
    - `targetNetworks: [chains.sepolia]`: Sets which blockchain to use
    - Changed from `chains.devnet` (local) to `chains.sepolia` (testnet)
    - Your contract is deployed on Sepolia, so frontend must match

2. **Configuration Options**:
    - `onlyLocalBurnerWallet: false`: Allows real wallets (Argent X, Braavos)
    - `pollingInterval: 30_000`: Fetch new data every 30 seconds
    - `autoConnectTTL: 60000`: Keep wallet connected for 60 seconds
    - `walletAutoConnect: true`: Reconnect wallet on page reload

3. **Why Sepolia**:
    - Public testnet accessible to everyone
    - Free testnet tokens available from faucets
    - Persistent (unlike local devnet)
    - Similar to mainnet but for testing

4. **Impact on Frontend**:
    - All contract reads/writes go to Sepolia
    - Wallet must be on Sepolia network
    - Uses Sepolia RPC endpoints
    - Block explorer links point to Sepolia

5. **Type Safety**:
    - `as const satisfies ScaffoldConfig`: Ensures type correctness
    - TypeScript validates all configuration
    - Prevents typos and invalid values

### Step 3: Setup Environment Variables

Create `.env` in `packages/snfoundry/`:

```bash
# Sepolia
ACCOUNT_ADDRESS_SEPOLIA=0x...  # Your wallet address
PRIVATE_KEY_SEPOLIA=0x...      # Your private key
```

**⚠️ Security Warning:** Never commit your `.env` file to Git!

### Step 4: Deploy to Sepolia

```bash
yarn deploy --network sepolia
```

Save the contract address from the output!

---

## Building the Frontend

### Step 1: Export Scaffold-Stark Hooks

Open `packages/nextjs/hooks/scaffold-stark/index.ts` and add:

```typescript
export * from "./useOutsideClick";
export * from "./useDeployedContractInfo";
export * from "./useNetworkColor";
export * from "./useAnimationConfig";
export * from "./useTransactor";
export * from "./useAutoConnect";
export * from "./useSwitchNetwork";
export * from "./useScaffoldReadContract";
export * from "./useScaffoldWriteContract";
export * from "./useScaffoldContract";
export * from "./useScaffoldEventHistory";
export * from "./useScaffoldWatchContractEvent";
export * from "./useScaffoldMultiWriteContract";
export * from "./useTargetNetwork";
```

#### 📖 Code Explanation

**Exporting Scaffold-Stark Hooks:**

1. **Purpose**:
    - Makes all Scaffold-Stark custom hooks available for import
    - Centralizes exports in one file for cleaner imports
    - Required for TypeScript to find the hooks

2. **Key Hooks for Voting dApp**:
    - `useScaffoldReadContract`: Read data from smart contracts
    - `useScaffoldWriteContract`: Write data to smart contracts
    - `useTargetNetwork`: Get current network configuration
    - `useTransactor`: Handle transaction with UI feedback

3. **Why This is Needed**:
    - By default, not all hooks are exported from index.ts
    - Your components import from `~~/hooks/scaffold-stark`
    - This file determines what's available
    - Without these exports, you'll get "module not found" errors

4. **Best Practice**:
    - Export all hooks even if not currently used
    - Enables easy access for future features
    - Follows standard module export pattern

### Step 2: Create Voting Page

Create `packages/nextjs/app/voting/page.tsx`:

```typescript
"use client";

import { useState, useEffect } from "react";
import { useAccount } from "@starknet-react/core";
import { VoterRegistration } from "./_components/VoterRegistration";
import { CreateProposal } from "./_components/CreateProposal";
import { ProposalList } from "./_components/ProposalList";
import { useScaffoldReadContract } from "~~/hooks/scaffold-stark";
import { Address } from "~~/components/scaffold-stark";

export default function VotingPage() {
    const { address: connectedAddress } = useAccount();

    const { data: isRegisteredData, refetch: refetchIsRegistered } =
        useScaffoldReadContract({
            contractName: "VotingContract",
            functionName: "is_voter_registered",
            args: [connectedAddress as `0x${string}`],
            enabled: !!connectedAddress,
        });
    const isRegistered = isRegisteredData as unknown as boolean | undefined;

    const { data: contractOwnerData } = useScaffoldReadContract({
        contractName: "VotingContract",
        functionName: "owner",
    });
    const contractOwner = contractOwnerData as unknown as
        | `0x${string}`
        | undefined;

    const { data: proposalCountData, refetch: refetchProposalCount } =
        useScaffoldReadContract({
            contractName: "VotingContract",
            functionName: "get_proposal_count",
        });
    const proposalCount = proposalCountData as unknown as bigint | undefined;

    const isOwner =
        connectedAddress &&
        contractOwner &&
        connectedAddress.toLowerCase() === contractOwner.toLowerCase();

    return (
        <div className="flex items-center flex-col flex-grow pt-10">
            <div className="px-5 w-full max-w-7xl">
                <div className="text-center mb-8">
                    <h1 className="text-4xl font-bold mb-2">🗳️ Voting dApp</h1>
                    <p className="text-lg opacity-80">
                        Decentralized voting powered by Starknet
                    </p>
                    {contractOwner && (
                        <div className="mt-4 text-sm">
                            <span className="opacity-70">Contract Owner: </span>
                            <Address address={contractOwner} />
                        </div>
                    )}
                </div>

                {!connectedAddress ? (
                    <div className="bg-base-200 rounded-xl p-8 text-center">
                        <h2 className="text-2xl mb-4">👋 Welcome!</h2>
                        <p className="text-lg mb-4">
                            Please connect your wallet to participate in voting
                        </p>
                    </div>
                ) : (
                    <>
                        {!isRegistered && (
                            <div className="mb-8">
                                <VoterRegistration
                                    onRegistrationSuccess={() =>
                                        refetchIsRegistered()
                                    }
                                />
                            </div>
                        )}

                        {isRegistered && (
                            <div className="alert alert-success mb-6">
                                <svg
                                    xmlns="http://www.w3.org/2000/svg"
                                    className="stroke-current shrink-0 h-6 w-6"
                                    fill="none"
                                    viewBox="0 0 24 24"
                                >
                                    <path
                                        strokeLinecap="round"
                                        strokeLinejoin="round"
                                        strokeWidth="2"
                                        d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
                                    />
                                </svg>
                                <span>You are registered as a voter!</span>
                            </div>
                        )}

                        {isOwner && (
                            <div className="mb-8">
                                <CreateProposal
                                    onProposalCreated={() =>
                                        refetchProposalCount()
                                    }
                                />
                            </div>
                        )}

                        <div className="mb-8">
                            <div className="flex justify-between items-center mb-4">
                                <h2 className="text-2xl font-bold">
                                    📋 Proposals
                                    {proposalCount !== undefined && (
                                        <span className="ml-2 text-lg opacity-70">
                                            ({proposalCount.toString()})
                                        </span>
                                    )}
                                </h2>
                            </div>

                            <ProposalList
                                proposalCount={proposalCount}
                                isRegistered={!!isRegistered}
                            />
                        </div>
                    </>
                )}
            </div>
        </div>
    );
}
```

#### 📖 Code Explanation

**Main Voting Page Component:**

1. **State Management**:
    - `connectedAddress`: Gets the currently connected wallet address
    - `isRegistered`: Checks if current user is a registered voter
    - `contractOwner`: Fetches the contract owner address
    - `proposalCount`: Gets total number of proposals

2. **Smart Contract Reads** (using `useScaffoldReadContract`):
    - `is_voter_registered`: Checks registration status for connected address
    - `owner`: Gets contract owner (to show owner-only features)
    - `get_proposal_count`: Gets total proposals (to display count)
    - `enabled: !!connectedAddress`: Only fetch when wallet is connected

3. **Type Casting**:
    - `as unknown as boolean | undefined`: Converts Starknet return types to TypeScript
    - Necessary because Starknet returns arrays, we extract the actual value

4. **Owner Detection**:
    - Compares connected address with contract owner
    - Uses `.toLowerCase()` for case-insensitive comparison
    - Determines if "Create Proposal" section should show

5. **Conditional Rendering**:
    - **Not Connected**: Shows welcome message
    - **Not Registered**: Shows registration component
    - **Registered**: Shows success badge
    - **Is Owner**: Shows proposal creation form
    - **Always**: Shows proposal list

6. **Data Refetching**:
    - `refetchIsRegistered()`: Updates registration status after registration
    - `refetchProposalCount()`: Updates count after proposal creation
    - Keeps UI in sync with blockchain state

### Step 3: Create VoterRegistration Component

Create `packages/nextjs/app/voting/_components/VoterRegistration.tsx`:

```typescript
"use client";

import { useState } from "react";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-stark";

interface VoterRegistrationProps {
    onRegistrationSuccess: () => void;
}

export const VoterRegistration = ({
    onRegistrationSuccess,
}: VoterRegistrationProps) => {
    const [isRegistering, setIsRegistering] = useState(false);

    const { sendAsync: registerVoter } = useScaffoldWriteContract({
        contractName: "VotingContract",
        functionName: "register_voter",
    });

    const handleRegister = async () => {
        try {
            setIsRegistering(true);
            await registerVoter();
            onRegistrationSuccess();
        } catch (e) {
            console.error("Error registering as voter:", e);
        } finally {
            setIsRegistering(false);
        }
    };

    return (
        <div className="bg-primary/10 rounded-xl p-8 border-2 border-primary">
            <div className="flex flex-col items-center">
                <div className="text-6xl mb-4">✋</div>
                <h2 className="text-2xl font-bold mb-2">Register to Vote</h2>
                <p className="text-center mb-6 opacity-80">
                    You need to register before you can participate in voting
                </p>
                <button
                    className="btn btn-primary btn-lg"
                    onClick={handleRegister}
                    disabled={isRegistering}
                >
                    {isRegistering ? (
                        <>
                            <span className="loading loading-spinner loading-md"></span>
                            Registering...
                        </>
                    ) : (
                        "Register as Voter"
                    )}
                </button>
            </div>
        </div>
    );
};
```

#### 📖 Code Explanation

**VoterRegistration Component:**

1. **Local State**:
    - `isRegistering`: Boolean to track if registration transaction is in progress
    - Used to disable button and show loading spinner

2. **Smart Contract Write** (using `useScaffoldWriteContract`):
    - `contractName: "VotingContract"`: Targets the voting contract
    - `functionName: "register_voter"`: Calls the registration function
    - `sendAsync`: Returns a function to execute the transaction

3. **Registration Handler**:
    - `handleRegister()`: Async function to process registration
    - **Try block**: Calls `registerVoter()` and triggers success callback
    - **Catch block**: Logs errors to console
    - **Finally block**: Resets loading state regardless of outcome

4. **Callback Pattern**:
    - `onRegistrationSuccess()`: Called after successful registration
    - Triggers parent component to refetch registration status
    - Updates UI to show registered state

5. **UI/UX Features**:
    - Large emoji (✋) for visual appeal
    - Clear instructions for users
    - Loading spinner during transaction
    - Button disabled during registration to prevent double-clicks
    - Primary color theme to draw attention

### Step 4: Create CreateProposal Component

Create `packages/nextjs/app/voting/_components/CreateProposal.tsx`:

```typescript
"use client";

import { useState } from "react";
import { useScaffoldWriteContract } from "~~/hooks/scaffold-stark";

interface CreateProposalProps {
    onProposalCreated: () => void;
}

export const CreateProposal = ({ onProposalCreated }: CreateProposalProps) => {
    const [description, setDescription] = useState("");
    const [isCreating, setIsCreating] = useState(false);

    const { sendAsync: createProposal } = useScaffoldWriteContract({
        contractName: "VotingContract",
        functionName: "create_proposal",
        args: [description],
    });

    const handleCreateProposal = async () => {
        if (!description.trim()) {
            alert("Please enter a proposal description");
            return;
        }

        try {
            setIsCreating(true);
            await createProposal();
            setDescription("");
            onProposalCreated();
        } catch (e) {
            console.error("Error creating proposal:", e);
        } finally {
            setIsCreating(false);
        }
    };

    return (
        <div className="bg-warning/10 rounded-xl p-6 border-2 border-warning">
            <div className="flex items-center gap-2 mb-4">
                <span className="text-2xl">👑</span>
                <h2 className="text-2xl font-bold">
                    Create Proposal (Owner Only)
                </h2>
            </div>

            <div className="space-y-4">
                <div className="form-control">
                    <label className="label">
                        <span className="label-text font-semibold">
                            Proposal Description
                        </span>
                    </label>
                    <textarea
                        className="textarea textarea-bordered h-24 text-base"
                        placeholder="e.g., Should we upgrade the protocol to version 2.0?"
                        value={description}
                        onChange={(e) => setDescription(e.target.value)}
                        disabled={isCreating}
                    />
                </div>

                <button
                    className="btn btn-warning btn-block"
                    onClick={handleCreateProposal}
                    disabled={isCreating || !description.trim()}
                >
                    {isCreating ? (
                        <>
                            <span className="loading loading-spinner loading-md"></span>
                            Creating Proposal...
                        </>
                    ) : (
                        "Create Proposal"
                    )}
                </button>
            </div>
        </div>
    );
};
```

#### 📖 Code Explanation

**CreateProposal Component:**

1. **Local State**:
    - `description`: Stores the proposal description text
    - `isCreating`: Tracks if proposal creation is in progress

2. **Smart Contract Write**:
    - Calls `create_proposal` function on VotingContract
    - Passes `description` as argument
    - Only contract owner can successfully call this

3. **Input Validation**:
    - Checks if description is not empty (`!description.trim()`)
    - Shows alert if user tries to submit empty proposal
    - Prevents wasted gas on invalid transactions

4. **Proposal Creation Handler**:
    - **Validation**: Ensures description is provided
    - **Try block**: Calls contract, clears form, triggers callback
    - **Catch block**: Handles errors
    - **Finally block**: Resets loading state

5. **Form Reset**:
    - `setDescription("")`: Clears input after successful creation
    - Prepares form for next proposal

6. **UI Design**:
    - Warning color theme (yellow): Indicates owner-only action
    - Crown emoji (👑): Visual indicator of privileged action
    - Textarea for multi-line descriptions
    - Button disabled when creating or description empty
    - Placeholder text guides users on what to write

### Step 5: Create ProposalList Component

Create `packages/nextjs/app/voting/_components/ProposalList.tsx`:

```typescript
"use client";

import { ProposalCard } from "./ProposalCard";

interface ProposalListProps {
    proposalCount: bigint | undefined;
    isRegistered: boolean;
}

export const ProposalList = ({
    proposalCount,
    isRegistered,
}: ProposalListProps) => {
    if (proposalCount === undefined) {
        return (
            <div className="flex justify-center items-center p-12">
                <span className="loading loading-spinner loading-lg"></span>
            </div>
        );
    }

    const count = Number(proposalCount);

    if (count === 0) {
        return (
            <div className="bg-base-200 rounded-xl p-12 text-center">
                <div className="text-6xl mb-4">📭</div>
                <h3 className="text-xl font-bold mb-2">No Proposals Yet</h3>
                <p className="opacity-70">
                    Proposals created by the contract owner will appear here
                </p>
            </div>
        );
    }

    return (
        <div className="space-y-4">
            {Array.from({ length: count }, (_, index) => (
                <ProposalCard
                    key={index}
                    proposalId={BigInt(index)}
                    isRegistered={isRegistered}
                />
            ))}
        </div>
    );
};
```

#### 📖 Code Explanation

**ProposalList Component:**

1. **Props**:
    - `proposalCount`: Total number of proposals (bigint)
    - `isRegistered`: Boolean indicating if user can vote

2. **Loading State**:
    - Shows spinner while `proposalCount` is undefined
    - Fetching data from blockchain takes time
    - Provides visual feedback to user

3. **Empty State**:
    - When `count === 0`, shows empty state UI
    - Inbox emoji (📭) and helpful message
    - Better UX than showing nothing

4. **Dynamic List Rendering**:
    - `Array.from({ length: count })`: Creates array with N elements
    - `(_, index)`: Ignores value, uses index as proposal ID
    - `BigInt(index)`: Converts to bigint for contract compatibility
    - Each ProposalCard rendered with unique key

5. **Props Passing**:
    - Passes `proposalId` to each card
    - Passes `isRegistered` to enable/disable voting
    - Each card fetches its own data independently

6. **Scalability**:
    - Works with any number of proposals
    - Automatically updates when count changes
    - Each card is self-contained

### Step 6: Create ProposalCard Component

Create `packages/nextjs/app/voting/_components/ProposalCard.tsx`:

```typescript
"use client";

import { useState } from "react";
import {
    useScaffoldReadContract,
    useScaffoldWriteContract,
} from "~~/hooks/scaffold-stark";

interface ProposalCardProps {
    proposalId: bigint;
    isRegistered: boolean;
}

interface ProposalDetails {
    id: bigint;
    description: string;
    yes_votes: bigint;
    no_votes: bigint;
    ended: boolean;
}

export const ProposalCard = ({
    proposalId,
    isRegistered,
}: ProposalCardProps) => {
    const [isVoting, setIsVoting] = useState(false);

    const { data: proposalData, refetch: refetchProposal } =
        useScaffoldReadContract({
            contractName: "VotingContract",
            functionName: "get_proposal_details",
            args: [proposalId],
        });

    const proposal = proposalData as unknown as ProposalDetails | undefined;

    const { sendAsync: vote } = useScaffoldWriteContract({
        contractName: "VotingContract",
        functionName: "vote",
        args: [proposalId, true],
    });

    const handleVote = async (voteYes: boolean) => {
        try {
            setIsVoting(true);
            await vote({ args: [proposalId, voteYes] });
            await refetchProposal();
        } catch (e) {
            console.error("Error voting:", e);
        } finally {
            setIsVoting(false);
        }
    };

    if (!proposal) {
        return (
            <div className="card bg-base-200 shadow-xl">
                <div className="card-body">
                    <div className="flex justify-center">
                        <span className="loading loading-spinner loading-md"></span>
                    </div>
                </div>
            </div>
        );
    }

    const yesVotes = Number(proposal.yes_votes);
    const noVotes = Number(proposal.no_votes);
    const totalVotes = yesVotes + noVotes;
    const yesPercentage = totalVotes > 0 ? (yesVotes / totalVotes) * 100 : 0;
    const noPercentage = totalVotes > 0 ? (noVotes / totalVotes) * 100 : 0;

    return (
        <div className="card bg-base-100 shadow-xl border border-base-300">
            <div className="card-body">
                <div className="flex justify-between items-start mb-4">
                    <div>
                        <h3 className="card-title text-2xl">
                            Proposal #{proposal.id.toString()}
                        </h3>
                        {proposal.ended && (
                            <div className="badge badge-error mt-2">Ended</div>
                        )}
                    </div>
                </div>

                <div className="bg-base-200 rounded-lg p-4 mb-4">
                    <p className="text-lg">{proposal.description}</p>
                </div>

                <div className="space-y-3">
                    <div>
                        <div className="flex justify-between mb-1">
                            <span className="text-sm font-semibold flex items-center gap-2">
                                <span className="text-success">✓</span> Yes
                            </span>
                            <span className="text-sm font-semibold">
                                {yesVotes} votes ({yesPercentage.toFixed(1)}%)
                            </span>
                        </div>
                        <div className="w-full bg-base-300 rounded-full h-3">
                            <div
                                className="bg-success h-3 rounded-full transition-all duration-300"
                                style={{ width: `${yesPercentage}%` }}
                            ></div>
                        </div>
                    </div>

                    <div>
                        <div className="flex justify-between mb-1">
                            <span className="text-sm font-semibold flex items-center gap-2">
                                <span className="text-error">✗</span> No
                            </span>
                            <span className="text-sm font-semibold">
                                {noVotes} votes ({noPercentage.toFixed(1)}%)
                            </span>
                        </div>
                        <div className="w-full bg-base-300 rounded-full h-3">
                            <div
                                className="bg-error h-3 rounded-full transition-all duration-300"
                                style={{ width: `${noPercentage}%` }}
                            ></div>
                        </div>
                    </div>

                    <div className="text-sm opacity-70 text-center pt-2">
                        Total Votes: {totalVotes}
                    </div>
                </div>

                {!proposal.ended && isRegistered && (
                    <div className="card-actions justify-center mt-4 gap-4">
                        <button
                            className="btn btn-success flex-1"
                            onClick={() => handleVote(true)}
                            disabled={isVoting}
                        >
                            {isVoting ? (
                                <span className="loading loading-spinner loading-sm"></span>
                            ) : (
                                <>
                                    <span>✓</span>
                                    Vote Yes
                                </>
                            )}
                        </button>
                        <button
                            className="btn btn-error flex-1"
                            onClick={() => handleVote(false)}
                            disabled={isVoting}
                        >
                            {isVoting ? (
                                <span className="loading loading-spinner loading-sm"></span>
                            ) : (
                                <>
                                    <span>✗</span>
                                    Vote No
                                </>
                            )}
                        </button>
                    </div>
                )}

                {proposal.ended && (
                    <div className="alert alert-error mt-4">
                        <span>This proposal has ended. Voting is closed.</span>
                    </div>
                )}

                {!isRegistered && !proposal.ended && (
                    <div className="alert alert-warning mt-4">
                        <span>
                            Register as a voter to participate in this proposal
                        </span>
                    </div>
                )}
            </div>
        </div>
    );
};
```

#### 📖 Code Explanation

**ProposalCard Component (Most Complex Component):**

1. **Props & Types**:
    - `proposalId`: The ID of this specific proposal
    - `isRegistered`: Whether current user can vote
    - `ProposalDetails` interface: Defines shape of proposal data

2. **Data Fetching**:
    - `useScaffoldReadContract`: Fetches proposal details from blockchain
    - `args: [proposalId]`: Passes proposal ID to contract function
    - `refetchProposal`: Function to refresh data after voting
    - Type cast to `ProposalDetails` for TypeScript

3. **Voting Functionality**:
    - `useScaffoldWriteContract`: Prepares vote transaction
    - `args: [proposalId, true]`: Default args (overridden when called)
    - `handleVote(voteYes)`: Handles both yes and no votes
    - `vote({ args: [proposalId, voteYes] })`: Overrides args at call time

4. **Vote Calculations**:
    - `yesVotes` & `noVotes`: Convert bigint to number for display
    - `totalVotes`: Sum of yes and no votes
    - `yesPercentage`: (yes / total) \* 100
    - `noPercentage`: (no / total) \* 100
    - Handles division by zero (shows 0% when no votes)

5. **Progress Bars**:
    - Visual representation of voting results
    - Green bar for "Yes" votes
    - Red bar for "No" votes
    - Width based on percentage (`style={{ width: ${percentage}% }}`)
    - Smooth transitions (`transition-all duration-300`)

6. **Conditional Rendering**:
    - **Loading**: Shows spinner while fetching data
    - **Vote Buttons**: Only shown if proposal not ended AND user registered
    - **Ended Badge**: Shows if proposal is ended
    - **Warning**: Shown to unregistered users

7. **Vote Handler**:
    - Sets loading state during transaction
    - Calls vote function with yes/no parameter
    - Refetches proposal data after voting
    - Updates UI automatically with new vote counts
    - Handles errors gracefully

8. **UI/UX Features**:
    - Color-coded voting (green=yes, red=no)
    - Disabled buttons during voting (prevents double-voting)
    - Loading spinners for async operations
    - Percentage display for easy understanding
    - Accessibility with proper labels and states

### Step 7: Add Navigation Link

Open `packages/nextjs/components/Header.tsx` and update the `menuLinks` array:

```typescript
export const menuLinks: HeaderMenuLink[] = [
    {
        label: "Home",
        href: "/",
    },
    {
        label: "Voting",
        href: "/voting",
        icon: (
            <svg
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                strokeWidth={1.5}
                stroke="currentColor"
                className="h-4 w-4"
            >
                <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
                />
            </svg>
        ),
    },
    {
        label: "Debug Contracts",
        href: "/debug",
        icon: <BugAntIcon className="h-4 w-4" />,
    },
];
```

#### 📖 Code Explanation

**Navigation Menu Update:**

1. **Menu Structure**:
    - Array of objects defining navigation links
    - Each link has: label, href, and optional icon

2. **New "Voting" Link**:
    - `label: "Voting"`: Text shown in menu
    - `href: "/voting"`: Route to voting page
    - `icon`: SVG clipboard icon for visual identification

3. **Icon Design**:
    - Uses Heroicons SVG format
    - Clipboard/ballot icon appropriate for voting
    - Consistent size (h-4 w-4) with other menu items

4. **Menu Order**:
    - Home (landing page)
    - Voting (your new feature)
    - Debug Contracts (developer tools)

5. **Integration**:
    - Header component automatically renders all links
    - Active state highlighting included
    - Mobile-responsive menu behavior
    - Consistent styling with existing links

### Step 8: Build and Test

```bash
# Clear cache and start dev server
cd packages/nextjs
rm -rf .next
yarn dev
```

Visit `http://localhost:3000/voting`

---

## Testing the Application

### End-to-End Testing Flow

#### Test 1: Voter Registration

1. Connect your Starknet wallet
2. Click "Register as Voter"
3. Confirm transaction in wallet
4. Wait for confirmation
5. Verify success message appears

#### Test 2: Create Proposal (Owner Only)

1. Connect with owner wallet (the one that deployed)
2. Enter proposal description: "Should we upgrade to v2.0?"
3. Click "Create Proposal"
4. Confirm transaction
5. Verify proposal appears in list

#### Test 3: Vote on Proposal

1. Ensure you're registered
2. Find the proposal
3. Click "Vote Yes" or "Vote No"
4. Confirm transaction
5. Watch vote count update

#### Test 4: Multiple Voters

1. Switch to a different wallet
2. Register as voter
3. Vote on same proposal
4. Verify total votes update correctly

### Common Issues and Solutions

**Issue: "Cannot find module"**

```bash
# Clear Next.js cache
cd packages/nextjs
rm -rf .next
# Restart TS server in VS Code: Cmd+Shift+P > "TypeScript: Restart TS Server"
```

**Issue: Transaction failing**

- Ensure you have testnet ETH/STRK
- Check you're on Sepolia network
- Verify contract is deployed

**Issue: Wallet not connecting**

- Install Argent X or Braavos extension
- Switch to Sepolia testnet in wallet
- Refresh page

---

## Key Learning Points

### Smart Contract Concepts

1. **State Management**: Using storage maps for voters and proposals
2. **Access Control**: OpenZeppelin Ownable for admin functions
3. **Events**: Emitting events for transparency
4. **Validation**: Assert statements for security

### Frontend Integration

1. **Hooks**: Using Scaffold-Stark hooks for contract interaction
2. **State Management**: React hooks for UI state
3. **Type Safety**: TypeScript for type checking
4. **Real-time Updates**: Automatic refetching of data

### Security Best Practices

1. **Double Voting Prevention**: Tracking votes per user per proposal
2. **Registration Required**: Only registered voters can vote
3. **Owner Verification**: Only owner can create proposals
4. **Input Validation**: Checking all inputs before execution

---

## Next Steps

### Enhancements to Try

1. **Add Time-based Voting**
    - Add start/end timestamps to proposals
    - Implement automatic proposal ending

2. **Proposal Categories**
    - Add category field to proposals
    - Implement filtering by category

3. **Vote Delegation**
    - Allow voters to delegate their votes
    - Implement vote weight system

4. **Results Finalization**
    - Add owner function to end proposals
    - Execute actions based on results

5. **Vote History**
    - Track user's voting history
    - Display proposal archive

---

## Resources

### Documentation

- [Starknet Documentation](https://docs.starknet.io)
- [Cairo Book](https://book.cairo-lang.org)
- [Scaffold-Stark Docs](https://www.scaffoldstark.com)
- [Starknet Foundry](https://foundry-rs.github.io/starknet-foundry/)

### Tools

- [Starknet Explorer](https://sepolia.starkscan.co)
- [Argent X Wallet](https://www.argent.xyz)
- [Braavos Wallet](https://braavos.app)

### Community

- [Starknet Discord](https://discord.gg/starknet)
- [Scaffold-Stark GitHub](https://github.com/Scaffold-Stark/scaffold-stark-2)

---

## Conclusion

Congratulations! 🎉 You've built a complete decentralized voting application on Starknet. You've learned:

- ✅ Cairo smart contract development
- ✅ Writing comprehensive tests
- ✅ Deploying to Starknet testnet
- ✅ Building a React/Next.js frontend
- ✅ Integrating wallet connectivity
- ✅ Real-time blockchain data display

This foundation can be extended to build more complex dApps on Starknet. Keep building! 🚀

---

**Made with ❤️ using Scaffold-Stark 2**
