从零到一,构建一个去中心化投票应用—以太坊完整应用实例解析

投稿 2026-02-22 23:57 点击数: 1

以太坊作为全球领先的智能合约平台,不仅仅是一种加密货币,更是一个强大的去中心化应用(DApp)开发平台,其图灵完备的智能合约功能,使得开发者能够构建无需信任第三方、自动执行、公开透明的复杂应用,本文将以一个“去中心化投票应用”为例,详细阐述以太坊完整应用实例的构建过程,涵盖从智能合约设计、前端交互到整体部署与使用的全流程。

应用场景与需求分析

场景: 假设我们需要为一个社区选举或重要决策构建一个投票系统。

核心需求:

  1. 唯一性投票: 每个地址只能投一票。
  2. 透明性: 投票结果公开可查,且无法被篡改。
  3. 自主性: 投票过程无需中心化机构干预,由智能合约自动执行。
  4. 时限性: 投票有明确的开始和结束时间。

智能合约设计:投票合约的核心

智能合约是DApp的后端逻辑,部署在以太坊区块链上,对于投票应用,我们需要设计一个能够管理候选人、记录投票、统计结果的合约。

开发环境准备:

  • Solidity: 智能合约编程语言。
  • Remix IDE: 在线Solidity开发环境,适合初学者快速开发和测试合约。
  • MetaMask: 浏览器插件钱包,用于与以太坊网络交互和管理账户。

合约主要功能与状态变量:

  • 状态变量:

    • string public votingName; // 投票名称
    • uint256 public votingStartTime; // 投票开始时间戳
    • uint256 public votingEndTime; // 投票结束时间戳
    • address public owner; // 合约部署者,拥有某些管理权限
    • mapping(address => bool) public hasVoted; // 记录地址是否已投票
    • mapping(string => uint256) public voteCounts; // 记录每个候选人的票数
    • string[] public candidates; // 候选人列表
  • 函数:

    • constructor(string memory _votingName, string[] memory _candidates, uint256 _startTime, uint256 _endTime) public:构造函数,初始化投票名称、候选人列表、开始和结束时间,部署者自动成为owner。
    • function vote(string memory candidateName) public:投票函数,调用者需满足:投票在有效时间内、尚未投票、候选人存在,投票成功后,标记该地址已投票,并增加候选人票数。
    • function addCandidate(string memory candidateName) public onlyOwner:添加候选人函数,仅owner可调用。
    • function getVoteCount(string memory candidateName) public view returns (uint256):获取某候选人票数。
    • function getTotalVotes() public view returns (uint256):获取总投票数。
    • modifier onlyOwner() { require(msg.sender == owner, "Only owner can perform this action"); _; }:修饰符,限制仅owner可执行某些函数。

合约编写示例(简化版):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
    string public votingName;
    uint256 public votingStartTime;
    uint256 public votingEndTime;
    address public owner;
    mapping(address => bool) public hasVoted;
    mapping(string => uint256) public voteCounts;
    string[] public candidates;
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can perform this action");
        _;
    }
    modifier votingActive() {
        require(block.timestamp >= votingStartTime && block.timestamp <= votingEndTime, "Voting is not active");
        _;
    }
    constructor(string memory _votingName, string[] memory _candidates, uint256 _startTime, uint256 _endTime) {
        votingName = _votingName;
        candidates = _candidates;
        votingStartTime = _startTime;
        votingEndTime = _endTime;
        owner = msg.sender;
    }
    function vote(string memory candidateName) public votingActive {
        require(!hasVoted[msg.sender], "You have already voted&quo
随机配图
t;); bool candidateExists = false; for (uint i = 0; i < candidates.length; i++) { if (keccak256(bytes(candidates[i])) == keccak256(bytes(candidateName))) { candidateExists = true; break; } } require(candidateExists, "Candidate does not exist"); hasVoted[msg.sender] = true; voteCounts[candidateName]++; } function addCandidate(string memory candidateName) public onlyOwner { candidates.push(candidateName); } function getVoteCount(string memory candidateName) public view returns (uint256) { return voteCounts[candidateName]; } function getTotalVotes() public view returns (uint256) { uint total = 0; for (uint i = 0; i < candidates.length; i++) { total += voteCounts[candidates[i]]; } return total; } }

前端开发:用户交互界面

智能合约本身无法直接与用户交互,需要前端应用来调用合约函数并展示数据。

技术栈选择:

  • HTML/CSS/JavaScript: 基础前端技术。
  • Ethers.js: 一个流行的JavaScript库,用于与以太坊网络和智能合约进行交互。
  • Web3.js: 另一个常用的库,功能类似,本文以Ethers.js为例。

前端主要功能:

  • 连接MetaMask钱包。
  • 显示投票信息(名称、时间、候选人列表)。
  • 用户选择候选人并进行投票。
  • 实时显示投票结果。
  • 检查投票状态(是否已投票、投票是否有效)。

前端核心逻辑(伪代码/片段):

  • 连接钱包:

    let provider;
    let signer;
    let contract;
    async function connectWallet() {
        if (window.ethereum) {
            provider = new ethers.providers.Web3Provider(window.ethereum);
            await provider.send("eth_requestAccounts", []);
            signer = provider.getSigner();
            // 合约地址需要替换为实际部署后的地址
            contract = new ethers.Contract(CONTRACT_ADDRESS, VotingABI, signer);
            // 更新UI,显示钱包地址等
        } else {
            alert("Please install MetaMask!");
        }
    }
  • 获取候选人列表并显示:

    async function displayCandidates() {
        const candidates = await contract.getCandidates(); // 假设合约有此函数或通过candidates数组长度遍历
        const candidateSelect = document.getElementById('candidateSelect');
        candidates.forEach(candidate => {
            const option = document.createElement('option');
            option.value = candidate;
            option.textContent = candidate;
            candidateSelect.appendChild(option);
        });
    }
  • 投票功能:

    async function vote() {
        const candidateName = document.getElementById('candidateSelect').value;
        try {
            const tx = await contract.vote(candidateName);
            await tx.wait(); // 等待交易确认
            alert("Vote submitted successfully!");
            // 更新投票结果
            displayResults();
            // 检查并更新用户投票状态
        } catch (error) {
            console.error(error);
            alert("Vote failed: " + error.message);
        }
    }
  • 显示投票结果:

    async function displayResults() {
        const candidates = await contract.getCandidates();
        const resultsDiv = document.getElementById('results');
        resultsDiv.innerHTML = '';
        for (const candidate of candidates) {
            const voteCount = await contract.getVoteCount(candidate);
            const resultItem = document.createElement('div');
            resultItem.textContent = `${candidate}: ${voteCount} votes`;
            resultsDiv.appendChild(resultItem);
        }
    }

部署与测试

部署智能合约:

  • 测试网部署: 在以太坊测试网(如Ropsten, Goerli, Sepolia)上部署合约,避免消耗真实ETH,需要从测试网水龙头获取测试ETH。
  • Remix部署: 在Remix IDE中编译合约后,选择"Deploy & Run Transactions",选择环境(如"Injected Provider - MetaMask"),确保MetaMask连接到测试网,然后点击"Deploy"。
  • 部署后记录合约地址。

前端应用部署:

  • 将前端代码(HTML, CSS, JS文件)托管到静态网站托管服务,如:
    • GitHub Pages
    • Netlify
    • Vercel
  • 在前端代码中,将合约地址替换为实际部署在测试网上的