从零开始,以太坊开发实例—构建一个简单的投票DApp

时间: 2026-02-25 11:09 阅读数: 1人阅读

以太坊,作为全球领先的智能合约平台,不仅加密货币的基石,更催生了去中心化应用(DApps)的蓬勃发展,对于许多开发者而言,踏入以太坊开发的世界,最有效的方式莫过于通过一个具体的实例来学习,本文将以一个简单但功能完整的“投票DApp”为例,带你走过以太坊开发的核心流程,包括环境搭建、智能合约编写、测试、部署以及与前端交互的初步概念。

开发环境准备:工欲善其事,必先利其器

在开始编码之前,我们需要准备以下开发环境:

  1. Node.js 和 npm:JavaScript 运行时环境和包管理器,建议从官网下载并安装 LTS 版本。
  2. Truffle Suite:一套强大的以太坊开发框架,包含智能合约编译、测试、部署等功能。
    npm install -g truffle
  3. Ganache:一个个人区块链,用于本地开发和测试,它会为你提供一个模拟的以太坊网络,并分配 10 个测试账户,每个账户都有 100 个模拟 ETH。
    • 可以从 Ganache 官网下载桌面版,或通过 npm install -g ganache 命令安装命令行版本。
  4. MetaMask:浏览器钱包插件,用于与以太坊网络交互(包括本地测试网络和主网),从 Chrome 等浏览器的应用商店安装。
  5. 代码编辑器:推荐使用 Visual Studio Code,并安装 Solidity 插件,以获得更好的代码提示和高亮。

创建项目结构

  1. 创建一个新的项目文件夹,voting-dapp
    mkdir voting-dapp
    cd voting-dapp
  2. 使用 Truffle 初始化项目:
    truffle init

    这会创建以下标准目录结构:

    • contracts/随机配图
code>:存放 Solidity 智能合约文件。
  • migrations/:存放部署脚本文件。
  • test/:存放测试文件。
  • truffle-config.js:Truffle 配置文件。
  • 编写智能合约:投票DApp的核心

    我们的投票DApp将允许创建投票,并让参与者对特定选项进行投票。

    1. contracts 目录下创建一个新的 Solidity 文件,命名为 Voting.sol
    2. 编写合约代码:
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    contract Voting {
        // 定义投票选项的结构体
        struct Option {
            string name;
            uint voteCount;
        }
        // 定义提案的结构体
        struct Proposal {
            string description;
            mapping(address => bool) hasVoted; // 记录用户是否已投票
            Option[] options; // 投票选项数组
        }
        // 存储所有提案的映射,key为提案ID,value为提案
        mapping(uint => Proposal) public proposals;
        uint public proposalCount;
        // 提案创建者
        address public owner;
        // 构造函数,设置合约所有者
        constructor() {
            owner = msg.sender;
        }
        // 创建新提案
        function createProposal(string memory _description, string[] memory _optionNames) public {
            require(msg.sender == owner, "Only owner can create proposals");
            proposalCount++;
            Proposal storage proposal = proposals[proposalCount];
            proposal.description = _description;
            for (uint i = 0; i < _optionNames.length; i++) {
                proposal.options.push(Option({
                    name: _optionNames[i],
                    voteCount: 0
                }));
            }
        }
        // 对提案的某个选项进行投票
        function vote(uint _proposalId, uint _optionIndex) public {
            Proposal storage proposal = proposals[_proposalId];
            require(!proposal.hasVoted[msg.sender], "You have already voted for this proposal");
            require(_proposalId > 0 && _proposalId <= proposalCount, "Proposal does not exist");
            require(_optionIndex < proposal.options.length, "Option does not exist");
            proposal.hasVoted[msg.sender] = true;
            proposal.options[_optionIndex].voteCount++;
        }
        // 获取提案的选项数量
        function getProposalOptionsCount(uint _proposalId) public view returns (uint) {
            return proposals[_proposalId].options.length;
        }
        // 获取提案的某个选项信息
        function getOption(uint _proposalId, uint _optionIndex) public view returns (string memory, uint) {
            return (proposals[_proposalId].options[_optionIndex].name, proposals[_proposalId].options[_optionIndex].voteCount);
        }
    }

    合约解析

    • Option 结构体:存储投票选项名称和票数。
    • Proposal 结构体:存储提案描述、已投票用户记录和选项数组。
    • proposals:映射,用于存储所有提案。
    • proposalCount:提案总数,也用作新提案的ID。
    • owner:提案创建者地址,只有合约所有者可以创建提案。
    • createProposal:创建新提案,传入提案描述和选项名称数组。
    • vote:对指定提案的指定选项进行投票,并确保每个地址只能投一次票。
    • getProposalOptionsCountgetOption:视图函数,用于获取提案的选项信息和投票结果。

    编写迁移(部署)脚本

    为了让 Truffle 知道如何部署我们的合约,我们需要在 migrations 目录下创建一个新的迁移脚本。

    1. migrations 目录下创建文件 2_deploy_contracts.js(数字前缀表示部署顺序)。
    2. 编写脚本:
    const Voting = artifacts.require("Voting");
    module.exports = function (deployer) {
      // 部署 Voting 合约
      // 可以在这里传入初始参数,如果构造函数有的话
      // deployer.deploy(Voting, "Initial Proposal Description", ["Option1", "Option2"]);
      // 但我们的合约构造函数没有参数,所以直接部署
      deployer.deploy(Voting);
    };

    编写测试

    Truffle 使用 Mocha 和 Chai 进行测试,在 test 目录下创建 voting.test.js 文件:

    const Voting = artifacts.require("Voting");
    contract("Voting", (accounts) => {
        let votingInstance;
        const owner = accounts[0];
        const voter1 = accounts[1];
        const voter2 = accounts[2];
        beforeEach(async () => {
            votingInstance = await Voting.new();
        });
        it("should have an owner", async () => {
            const contractOwner = await votingInstance.owner();
            assert.equal(contractOwner, owner, "The owner is not correct");
        });
        it("should allow the owner to create a proposal", async () => {
            const description = "What should we have for lunch?";
            const optionNames = ["Pizza", "Sushi", "Burger"];
            await votingInstance.createProposal(description, optionNames, { from: owner });
            const proposalCount = await votingInstance.proposalCount();
            assert.equal(proposalCount.toNumber(), 1, "Proposal count should be 1");
            const retrievedDescription = await votingInstance.proposals(1).description;
            assert.equal(retrievedDescription, description, "Proposal description is incorrect");
            const optionsCount = await votingInstance.getProposalOptionsCount(1);
            assert.equal(optionsCount.toNumber(), 3, "Options count is incorrect");
        });
        it("should allow a voter to vote", async () => {
            await votingInstance.createProposal("Favorite color?", ["Red", "Blue", "Green"], { from: owner });
            await votingInstance.vote(1, 0, { from: voter1 }); // 投给第一个选项 "Red"
            const redVotes = await votingInstance.getOption(1, 0);
            assert.equal(redVotes[1].toNumber(), 1, "Red vote count should be 1");
            const hasVoted = await votingInstance.proposals(1).hasVoted(voter1);
            assert.equal(hasVoted, true, "Voter should have voted");
        });
        it("should not allow a voter to vote twice", async () => {
            await votingInstance.createProposal("Favorite color?", ["Red", "Blue", "Green"], { from: owner });
            await votingInstance.vote(1, 0, { from: voter1 });
            try {
                await votingInstance.vote(1, 1, { from: voter1 });
                assert.fail("Expected revert but none was received");
            } catch (error) {
                assert.include(error.message, "You have already voted for this proposal");
            }
        });
    });

    运行测试: 在项目根目录下运行:

    truffle test

    Truffle 会自动启动本地测试网络(如果未启动),运行测试并输出结果。

    部署到本地 Ganache 网络