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

时间: 2026-03-05 12:18 阅读数: 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/:存放 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[_prop
随机配图
osalId]; 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 网络