ZBLOG

从零开始,以太坊智能合约开发实例详解

以太坊作为全球领先的区块链平台,其核心魅力在于智能合约——一种运行在区块链上、自动执行合约条款的计算机程序,它允许开发者在去中心化的环境中构建各种应用,如去中心化金融(DeFi)、非同质化代币(NFT)、去中心化自治组织(DAO)等,本文将通过一个简单而实用的实例,带您一步步了解以太坊智能合约的开发流程。

开发环境准备

在开始编写智能合约之前,我们需要准备以下开发环境:

  1. Node.js 和 npm:JavaScript 运行时环境和包管理器,从 Node.js 官网 下载并安装 LTS 版本。
  2. Truffle Suite:一套流行的以太坊开发框架,包括 Truffle(开发环境、测试框架和构建工具)、Ganache(个人区块链,用于本地测试)和 Drizzle(与前端交互的库)。
    • 安装:npm install -g truffle
  3. MetaMask:一款浏览器插件钱包,用于与以太坊网络交互(测试网和主网),从 MetaMask 官网 下载并安装。
  4. 代码编辑器:如 VS Code,并安装 Solidity 插件以提供语法高亮和智能提示。

智能合约实例:一个简单的投票合约

我们将开发一个简单的投票合约,允许创建投票提案,并对提案进行投票,合约功能包括:

  • 创建者可以创建新的投票提案。
  • 地址可以投票,且每个地址只能投一次票。
  • 查询提案的描述、得票数以及投票状态。

创建项目结构

  1. 创建一个新的项目文件夹,voting-dapp
  2. 在终端中进入该文件夹,并初始化 Truffle 项目:
    truffle init

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

    • contracts/:存放 Solidity 智能合约文件。
    • migrations/:存放部署脚本文件。
    • test/:存放测试脚本文件。
    • truffle-config.js:Truffle 配置文件。

编写智能合约

  1. contracts 目录下创建一个新的 Solidity 文件,命名为 Voting.sol
  2. 编写合约代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
    // 定义提案结构体
    struct Proposal {
        string description; // 描述
        uint voteCount;    // 得票数
    }
    // 存储提案的映射,键为提案名称,值为 Proposal 结构体
    mapping(string => Proposal) public proposals;
    // 存储已投票地址的集合,防止重复投票
    mapping(address => bool) public voters;
    // 提案名称数组
    string[] public proposalNames;
    // 投票是否已结束
    bool public votingEnded;
    // 事件:当新提案创建时触发
    event ProposalCreated(string proposalName);
    // 事件:当有人投票时触发
    event Voted(address voter, string proposalName);
    // 创建提案的函数,仅限合约创建者调用
    function createProposal(string memory _proposalName) public {
        require(!votingEnded, "Voting has ended");
        require(bytes(proposals[_proposalName].description).length == 0, "Proposal already exists");
        proposals[_proposalName] = Proposal({
            description: _proposalName,
            voteCount: 0
        });
        proposalNames.push(_proposalName);
        emit ProposalCreated(_proposalName);
    }
    // 投票函数
    function vote(string memory _proposalName) public {
        require(!votingEnded, "Voting has ended");
        require(bytes(proposals[_proposalName].description).length != 0, "Proposal does not exist");
        require(!voters[msg.sender], "You have already voted");
        voters[msg.sender] = true;
        proposals[_proposalName].voteCount++;
        emit Voted(msg.sender, _proposalName);
    }
    // 结束投票函数,仅限合约创建者调用
    function endVoting() public {
        // 在实际应用中,可能需要添加更复杂的权限控制,比如只有特定角色可以结束投票
        // 这里为了简化,假设任何调用者都可以结束,或者可以添加一个owner变量
        require(!votingEnded, "Voting has already ended");
        votingEnded = true;
    }
    // 重新开始投票函数(可选)
    function restartVoting() public {
        // 同样,这里简化权限控制
        require(votingEnded, "Voting has not ended yet");
        for (uint i = 0; i < proposalNames.length; i++) {
            proposals[proposalNames[i]].voteCount = 0;
        }
        delete voters;
        votingEnded = false;
    }
    // 获取所有提案名称
    function getProposalNames() public view returns (string[] memory) {
        return proposalNames;
    }
    // 获取特定提案的得票数
    function getVoteCount(string memory _proposalName) public view returns (uint) {
        return proposals[_proposalName].voteCount;
    }
}

代码解释:

  • SPDX-License-Identifierpragma solidity:Solidity 合约的标准开头,指定许可证和编译器版本。
  • struct Proposal:定义提案的数据结构,包含描述和得票数。
  • mapping(string => Proposal) public proposals:一个映射,用于通过提案名称快速查找提案信息。
  • mapping(address => bool) public voters:一个映射,记录地址是否已投票,防止重复投票。
  • string[] public proposalNames:存储所有提案名称的数组,方便遍历。
  • bool public votingEnded:标记投票是否结束。
  • event:定义事件,用于前端监听合约状态变化。
  • createProposal:创建新提案,要求投票未结束且提案名称唯一。
  • vote:为指定提案投票,要求投票未结束、提案存在且当前地址未投票。
  • endVoting:结束投票,设置 votingEndedtrue
  • restartVoting(可选):重新开始投票,重置所有票数和投票记录。
  • getProposalNamesgetVoteCount:视图函数,用于查询提案信息,不消耗 gas。

编写部署脚本 (Migration Script)

Truffle 使用 migrations 目录下的脚本来部署合约。

  1. migrations 目录下创建一个新的文件,命名为 2_deploy_voting.js(数字前缀表示部署顺序)。
const Voting = artifacts.require("Voting");
module.exports = function(deployer) {
  // 部署 Voting 合约
  deployer.deploy(Voting);
};

编译与测试

  1. 编译合约: 在终端中运行:

    truffle compile

    如果成功,build/contracts 目录下会生成 Voting.json 文件,这是合约的 ABI(应用程序二进制接口)和字节码。

  2. 本地测试

    • 启动 Ganache:打开 Ganache,选择 "QUICKSTART"(它会创建一个本地区块链,并提供 10 个测试账户,每个账户有 100 个 ETH)。
    • 配置 Truffle:打开 truffle-config.js,确保 networks 部分配置了 Ganache 的网络信息(默认情况下,Ganache 监听 HTTP://127.0.0.1:7545,端口可能需要根据 Ganache 启动信息调整)。
      // 示例配置,确保与 Ganache 一致
      module.exports = {
        networks: {
          development: {
            host: "127.0.0.1",
            port: 7545, // Ganache 默认端口
            network_id: "*", // 匹配任何网络 id
          },
        },
        compilers: {
          solc: {
            version: "0.8.0", // 必须与合约中 pragma solidity 版本匹配或兼容
          },
        },
      };
    • 运行迁移脚本
      truffle migrate --network development

      这会执行 migrations 目录下的脚本,将合约部署到 Ganache 创建的本地区块链上,成功后,终端会显示合约的部署地址。

与合约交互

  1. 使用 Truffle Console

    truffle console --network development

    进入控制台后,可以与部署的合约实例交互:

    // 获取合约实例
    let votingInstance = await Voting.deployed();
    // 创建提案
分享:
扫描分享到社交APP