第13章 踏上区块链开发之旅-开发简单投票应用

1,510 阅读7分钟

知识讲解

Ethereum

以太坊是一个开源的、去中心化的区块链平台,它允许开发者构建和部署智能合约及去中心化应用程序(DApps)。与比特币主要作为数字货币不同,以太坊旨在提供一个更广泛的应用程序开发平台。以下是关于以太坊的一些关键点:

核心特性:

  • 智能合约:以太坊引入了图灵完备的编程语言,使开发者能够编写复杂的合约逻辑,这些合约一旦部署到以太坊网络上就可以自动执行。
  • 去中心化应用(DApps):基于以太坊的DApps不受任何单一实体控制,它们可以用于各种场景,如金融、游戏、供应链管理等。
  • 以太币(Ether, ETH):以太坊的原生加密货币,主要用于支付矿工费用、参与智能合约以及在DApps中作为价值转移的手段。

技术架构:

  • 以太坊虚拟机(EVM):每个以太坊节点都运行EVM来执行智能合约代码。EVM是一个沙盒环境,确保所有操作都在安全且隔离的状态下进行,Solidity 智能合同将部署到虚拟环境中并在虚拟环境中运行。
  • 共识机制:最初使用工作量证明(Proof of Work, PoW),但在2022年9月通过“合并”升级转向权益证明(Proof of Stake, PoS),大幅降低了能源消耗。
  • Gas费:Gas是区块链网络中用于衡量执行智能合约所需计算量和资源消耗的单位。‌在以太坊网络中,Gas的作用尤为重要,它不仅防止网络滥用,还补偿矿工的计算资源付出,从而控制网络拥堵。‌用户需要支付Gas费来执行交易或智能合约。

任务描述

创建一个简单的区块链应用,投票应用。

  • 智能合约: 编写一个投票合约,允许用户对候选人进行投票。 合约逻辑包括:注册候选人、投票、获取投票结果等。
  • 前端界面: 使用 HTML/CSS/JavaScript 创建投票应用界面。 用户可以输入候选人信息、投票和查看结果。 image.png

任务实施

编写前端代码:vote.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>投票应用</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            color: #333;
            margin: 0;
            padding: 20px;
        }

        h1 {
            text-align: center;
            color: #5a5a5a;
        }

        .container {
            max-width: 600px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }

        h2 {
            border-bottom: 2px solid #007bff;
            color: #007bff;
            padding-bottom: 10px;
        }

        .candidate-list {
            margin-bottom: 20px;
        }

        .candidate {
            padding: 10px;
            border: 1px solid #e0e0e0;
            border-radius: 5px;
            margin-bottom: 10px;
            display: flex;
            justify-content: space-between;
        }

        input {
            padding: 10px;
            border-radius: 5px;
            border: 1px solid #007bff;
            width: calc(100% - 22px);
            margin-bottom: 10px;
        }

        button {
            padding: 10px 15px;
            background-color: #007bff;
            border: none;
            border-radius: 5px;
            color: white;
            cursor: pointer;
            transition: background 0.3s;
            width: 100%;
        }

        button:hover {
            background-color: #0056b3;
        }

        .result {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #007bff;
            border-radius: 5px;
            background-color: #e7f3fe;
            color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>投票应用</h1>
        <div>
            <h2>候选人</h2>
            <div id="candidates" class="candidate-list"></div>
        </div>
        <div>
            <h2>投票</h2>
            <input type="number" id="candidateId" placeholder="输入候选人的ID">
            <button onclick="vote()">投票</button>
        </div>
        <br>
        <button onclick="getResults()">查看结果</button>
        <div id="result" class="result"></div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/web3/dist/web3.min.js"></script>
    <script>
        const contractAddress = 'YOUR_CONTRACT_ADDRESS';
        const abi = [ /* 合约的 ABI */ ];
        const web3 = new Web3(Web3.givenProvider || "http://localhost:8545");
        const contract = new web3.eth.Contract(abi, contractAddress);

        async function loadCandidates() {
            const candidatesCount = await contract.methods.candidatesCount().call();
            const candidatesDiv = document.getElementById('candidates');
            candidatesDiv.innerHTML = '';

            for (let i = 1; i <= candidatesCount; i++) {
                const candidate = await contract.methods.candidates(i).call();
                candidatesDiv.innerHTML += `<div class="candidate"><span>ID: ${candidate.id}, 姓名: ${candidate.name}</span><span>得票数: ${candidate.voteCount}</span></div>`;
            }
        }

        async function vote() {
            const candidateId = document.getElementById('candidateId').value;
            const accounts = await web3.eth.getAccounts();
            await contract.methods.vote(candidateId).send({ from: accounts[0] });
            alert('投票成功!');
            loadCandidates();
        }

        async function getResults() {
            const result = await contract.methods.getResult().call();
            const resultDiv = document.getElementById('result');
            resultDiv.innerHTML = `赢家: ${result[0]}, 得票数: ${result[1]}`;
        }

        window.onload = loadCandidates;
    </script>
</body>
</html>

编写一个 Solidity 合约来处理投票逻辑。Voting.sol

  • 添加候选人;
  • 允许用户进行投票并防止重复投票;
  • 提供查询投票结果的功能。
// SPDX-License-Identifier: MIT
// 包含了许可证声明以及指定Solidity版本为0.7.4。
pragma solidity ^0.7.4;

contract Voting {
    struct Candidate {
        // 数据结构:包含候选人的ID、姓名和得票数。
        uint id;
        string name;
        uint voteCount;
    }
/* 状态变量:candidates: 用于存储候选人的映射(ID到候选人对象)。
voters: 用于跟踪哪些地址已投票,避免重复投票。
candidatesCount: 用于记录候选人数量。
*/
    mapping(uint => Candidate) public candidates;
    mapping(address => bool) public voters;
    uint public candidatesCount;
// 定义了两个事件,用于在候选人添加和投票时触发,便于前端监听这些事件。
    event CandidateAdded(uint id, string name);
    event Voted(uint indexed candidateId);
// 构造函数:在合约部署时自动添加两个候选人(Alice和Bob)。
    constructor() {
        addCandidate("Alice");
        addCandidate("Bob");
    }
/* 添加候选人函数:该函数用于添加候选人(私有访问修饰符,只能在合约内部调用)。
每添加一个候选人,candidatesCount 自增,并创建新的候选人对象。
触发CandidateAdded事件。
*/
    function addCandidate(string memory _name) private {
        candidatesCount++;
        candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
        emit CandidateAdded(candidatesCount, _name);
    }
/* 投票函数:该函数允许用户为候选人投票。
使用require语句确保用户尚未投票,且提供的候选人ID有效。
更新用户的投票状态和候选人的得票数,并触发Voted事件。
*/
    function vote(uint _candidateId) public {
        require(!voters[msg.sender], "You have already voted.");
        require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");

        voters[msg.sender] = true;
        candidates[_candidateId].voteCount++;
        emit Voted(_candidateId);
    }
/*结果查询函数:该函数用于查询当前投票结果。
遍历所有候选人,找到得票最多的候选人及其票数,并返回。*/
    function getResult() public view returns (string memory winnerName, uint winnerVoteCount) {
        uint winningVoteCount = 0;
        for (uint i = 1; i <= candidatesCount; i++) {
            if (candidates[i].voteCount > winningVoteCount) {
                winningVoteCount = candidates[i].voteCount;
                winnerName = candidates[i].name;
            }
        }
        winnerVoteCount = winningVoteCount;
    }
}

编译

image.png

部署

image.png 备注:Remix IDE 中的功能界面:

  • Compiler:编译器

  • Auto compile:自动编译

  • File:文件

    • study-frontend/Voting.sol
  • Compile:编译

  • Account:账户

    • 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4(目前选中的账户地址)
  • Compiled contracts:已编译的合约

    • Voting - study-frontend/Voting.sol
  • Deploy:部署

  • Deployed Contracts:已部署的合约

    • Voting - study-frontend/Voting.sol

视图(Views)

  • candidatesuint256(返回候选人的详细信息)
  • candidatesCount:候选人数
  • getResult:获取结果
  • votersaddress(检查投票者是否已投票)

功能(Functions)

  • vote:投票功能

    • uint256: _candidateId(候选人ID)

这个界面用于编译和部署智能合约,并与合约进行交互。

实验实训

运行投票应用项目,需要按以下步骤操作:

步骤 1: 启动以太坊节点

如果您还没有运行以太坊节点,可以使用开发工具,如 Ganache 或者硬分叉(Hardhat、Truffle等)来创建本地的以太坊环境。

  • 使用 Ganache:

    1. 下载并安装 Ganache(可以访问其官方网站)。
    2. 启动 Ganache,记下提供的 RPC 端口(通常是 7545)。
  • 使用 Hardhat/Truffle:

    1. 安装 Node(16+).js 和 npm。
    2. 使用命令 npm install -g hardhat 或 npm install -g truffle 来安装所需的框架。
    3. 创建新的项目并使用 npx hardhat 或 truffle init 初始化项目。

image.png

步骤 2: 部署智能合约

确保您已经写好了 vote.sol 合约,并编译与部署它。

  • 使用 Truffle:

    1. 编写部署脚本。
    2. 使用命令 truffle migrate 部署合约并记录合约的地址。
  • 使用 Hardhat:

    1. 编写部署脚本。
    2. 使用命令 npx hardhat run scripts/deploy.js --network localhost 部署合约。

步骤 3: 更新 HTML 文件

在您的 vote.html 文件中,将 YOUR_CONTRACT_ADDRESS 替换为您刚才部署合约得到的地址,确保 ABI 也已正确放入。

const contractAddress = '部署后的合约地址';
const abi = [ /* 放置合约的 ABI */ ];

步骤 4: 启动本地服务器

为了能够从浏览器访问您的 HTML 文件,您需要启动一个本地服务器。

  • 使用 live-server:

    1. 如果您没有安装 live-server,可以使用命令 npm install -g live-server 进行安装。
    2. 在项目文件夹中打开终端,运行 live-server。该命令会启动一个本地服务器并自动打开浏览器。
  • 使用 Python:

    1. 如果您有 Python 安装,可以在项目目录中运行:

      • Python 2: python -m SimpleHTTPServer 8000
      • Python 3: python -m http.server 8000
    2. 然后打开浏览器访问 http://localhost:8000

步骤 5: 测试应用

在浏览器中打开应用,确保能够加载候选人、进行投票以及查看结果。

确保您的钱包(例如 MetaMask)已连接并选择与您部署合约的相同网络。如果一切顺利,您就能开始使用您的投票应用了!