以太坊DApp开发——从无知到找到门

杜鸿彩
2023-12-01

目录

零、相关工具

本文中直接用到的工具:Remix, truffle(v5.1.0), ganache(v2.1.2), MetaMask等

0.1 本地测试节点

  • testrpc: nodejs包,命令行工具,不过对新版的solidity支持度不高,部署合约时会报VM Exception等异常
  • ganache-cli: nodejs包,命令行工具
  • ganache:界面工具,比较方便

0.2 MetaMask

Chrome插件。可选择连接不同的节点;导入用户私钥后还可查看用户的以太币数量;关联账户后部分交易产生时可弹窗让用户确认交易。

0.3 Remix的使用

Remix: http://remix.ethereum.org

File explorers里可进行新建合约,编辑合约等操作。

Solidity compiler里选择Solidity编译器版本,先编译验证语法是否有问题,再在Deploy & run transactions中选择环境:Environment选择Injected Web3会弹出netmask插件进行账号绑定(Environment也可选Web3 Provider,会提示连接到以太坊节点,可先连接localhost:8545进行测试。)

Deploy & run transactions面板中合约选择下拉框的下方还有Deploy和At Address,前一个可通过构造函数创建合约,后一个通过输入一个合约的地址获取(连接?)合约,创建(/连接)成功后,下方会显示出合约中可调用的方法,可在这个面板上操作调用合约的函数,(通过输入地址连接上的合约,需要配置账户等信息才会执行交易)。

合约编写窗口下方为控制台,可在其中通过web3的API执行命令,如web3.eth.getBalance(“0x04555018d100bd9ad75544de623ec8e3692e423a”);

一、模板项目测试

1.1 初始化项目

# truffle unbox react
✔ Preparing to download
✔ Downloading
✔ Cleaning up temporary files
......
✔ Setting up box

Unbox successful. Sweet!

Commands:

  Compile:              truffle compile
  Migrate:              truffle migrate
  Test contracts:       truffle test
  Test dapp:            cd client && npm test
  Run dev server:       cd client && npm run start
  Build for production: cd client && npm run build

(直接下载比较慢,考虑使用代理下载) 执行命令 truffle unbox react下载一个模板项目。其中client目录下是一个React项目,当执行到Setting up box这一步时,耗时较长,其实是在client/node_modules目录下下载所需依赖(100+MB)。

也可在TRUFFLE BOXES搜索box的名字下载,如react在https://www.trufflesuite.com/boxes/react,可在其中点击下载按钮下载,但这样下载client目录中没有node_modules,需要自己解决依赖问题。

直接在truffle网站上下载的项目目录结构如下:

.
├── box-img-lg.png
├── box-img-sm.png
├── client
│   ├── package.json
│   ├── package-lock.json
│   ├── public
│   │   ├── favicon.ico
│   │   ├── index.html
│   │   ├── logo192.png
│   │   ├── logo512.png
│   │   ├── manifest.json
│   │   └── robots.txt
│   ├── README.md
│   ├── src
│   │   ├── App.css
│   │   ├── App.js
│   │   ├── App.test.js
│   │   ├── getWeb3.js
│   │   ├── index.css
│   │   ├── index.js
│   │   ├── logo.svg
│   │   └── serviceWorker.js
│   └── yarn.lock
├── contracts
│   ├── Migrations.sol
│   └── SimpleStorage.sol
├── LICENSE
├── migrations
│   ├── 1_initial_migration.js
│   └── 2_deploy_contracts.js
├── README.md
├── test
│   ├── simplestorage.js
│   └── TestSimpleStorage.sol
├── truffle-box.json
└── truffle-config.js

1.2 编译合约

root@kali:/BlockChain/react-box-master# truffle compile

Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol
> Compiling ./contracts/SimpleStorage.sol
> Artifacts written to /BlockChain/react-box-master/client/src/contracts
> Compiled successfully using:
   - solc: 0.5.12+commit.7709ece9.Emscripten.clang

项目目录下执行truffle compile进行编译,编译后生成了client/src/contracts目录,该目录下有两个文件:Migrations.json SimpleStorage.json

1.3 部署合约

1.3.1 启动ganache

1.3.2 修改truffle-config.js

配置节点信息,修改后的文件内容如下:

const path = require("path");

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  contracts_build_directory: path.join(__dirname, "client/src/contracts"),
  networks: {
    development: {
      host: "localhost",
      port: 8545,
      network_id: "*"
    }
  }
};

1.3.3 部署

使用truffle migrate进行部署:

root@kali:/BlockChain/react-box-master# truffle migrate

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.



Starting migrations...
======================
> Network name:    'development'
> Network id:      5777
> Block gas limit: 0x6691b7


1_initial_migration.js
======================

   Deploying 'Migrations'
   ----------------------
   > transaction hash:    0x872c9be81fc25005dc3535821e9fe52375b0ccb409cbdf4b85e2483d76bbcfe0
   > Blocks: 0            Seconds: 0
   > contract address:    0xe7536a28473dFF0875b81bDdb7dCe10CA1e95cDe
   > block number:        1
   > block timestamp:     1574910360
   > account:             0xA5D2948e8fd43D6d0C8eD7bCe0fEba3039A8D009
   > balance:             99.99472518
   > gas used:            263741
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00527482 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00527482 ETH


2_deploy_contracts.js
=====================

   Deploying 'SimpleStorage'
   -------------------------
   > transaction hash:    0xba46904f988290f25d36918c7f731f9d4bdb3d2b6b3c2520f2b79f90f0df5173
   > Blocks: 0            Seconds: 0
   > contract address:    0x0D7C49CFfD2e82B5C124F455fCEaDC27d1cdC41D
   > block number:        3
   > block timestamp:     1574910361
   > account:             0xA5D2948e8fd43D6d0C8eD7bCe0fEba3039A8D009
   > balance:             99.99173734
   > gas used:            107369
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00214738 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00214738 ETH


Summary
=======
> Total deployments:   2
> Final cost:          0.0074222 ETH

1.4 测试

1.4.1 在Remix中测试

在在线IDE中测试合约,先将MetaMask连接到ganache启动的节点,导入其中的一个测试账号。将SimpleStorage.sol拷贝到Remix Solidity IDE中,编译,然后在Deploy & run transactions窗口中选中合约,在At Address按钮后的输入框中输入上面部署得到的合约地址0x0D7C49CFfD2e82B5C124F455fCEaDC27d1cdC41D,再点击按钮,获取到合约实例。

在下方显示出的合约方法列表中,输入参数并点击显示set方法名的按钮调用方法,弹出MetaMask窗口,确认交易,完成后在ganache中可看到新增了一个块和一个类型为CONTRACT CALL的交易。再在在线IDE的页面上点击get方法按,可看到返回的就是刚才存入的值。

1.4.2 启动React项目进行测试

client/src/App.js中定义了一个React组件,当生命周期方法componentDidMount被调用时,获取web3、账号、合约实例,更新状态,并调用runExample方法。runExample方法中先调用合约的set方法写了一个值5,再通过合约的get方法获取刚写入的值,再将获取的值更新到组件的状态中,然后组件的render方法被调用,该值以及一段HTML被渲染到DOM中。App.js代码如下:

import React, { Component } from "react";
import SimpleStorageContract from "./contracts/SimpleStorage.json";
import getWeb3 from "./getWeb3";

import "./App.css";

class App extends Component {
  state = { storageValue: 0, web3: null, accounts: null, contract: null };

  componentDidMount = async () => {
    try {
      // Get network provider and web3 instance.
      const web3 = await getWeb3();

      // Use web3 to get the user's accounts.
      const accounts = await web3.eth.getAccounts();

      // Get the contract instance.
      const networkId = await web3.eth.net.getId();
      const deployedNetwork = SimpleStorageContract.networks[networkId];
      const instance = new web3.eth.Contract(
        SimpleStorageContract.abi,
        deployedNetwork && deployedNetwork.address,
      );

      // Set web3, accounts, and contract to the state, and then proceed with an
      // example of interacting with the contract's methods.
      this.setState({ web3, accounts, contract: instance }, this.runExample);
    } catch (error) {
      // Catch any errors for any of the above operations.
      alert(
        `Failed to load web3, accounts, or contract. Check console for details.`,
      );
      console.error(error);
    }
  };

  runExample = async () => {
    const { accounts, contract } = this.state;

    // Stores a given value, 5 by default.
    await contract.methods.set(5).send({ from: accounts[0] });

    // Get the value from the contract to prove it worked.
    const response = await contract.methods.get().call();

    // Update state with the result.
    this.setState({ storageValue: response });
  };

  render() {
    if (!this.state.web3) {
      return <div>Loading Web3, accounts, and contract...</div>;
    }
    return (
      <div className="App">
        <h1>Good to Go!</h1>
        <p>Your Truffle Box is installed and ready.</p>
        <h2>Smart Contract Example</h2>
        <p>
          If your contracts compiled and migrated successfully, below will show
          a stored value of 5 (by default).
        </p>
        <p>
          Try changing the value stored on <strong>line 40</strong> of App.js.
        </p>
        <div>The stored value is: {this.state.storageValue}</div>
      </div>
    );
  }
}

export default App;

可将await contract.methods.set(5).send({ from: accounts[0] });这行代码注释,然后在client目录下运行npm start启动服务,浏览器访问localhost:3000,页面上可看到上一步从在线IDE中写入的值。

二、DIY

2.1 修改项目&编译合约

就在模板项目上进行修改,先在项目目录下contracts目录下新建合约Voting.sol(从某教程拷贝而来,一个简单的投票合约,根据solidity版本稍作修改):

pragma solidity ^0.5.0;

contract Voting {

    bytes32[] candidates;
    
    mapping(bytes32 => uint) candidatesVotingCount;
    
    // https://stackoverflow.com/questions/53460851/typeerror-data-location-must-be-memory-for-parameter-in-function-but-none-wa
    // google浏览器下remix智能合约中 bytes32[] 类型的输入:https://blog.csdn.net/github_38575699/article/details/101269929
    // 新版remix还是solidity本身的问题?Deploy的输入框中不能直接输入字符串数组,要转成十六进制形式,且bytes32类型要写成32个字节的十六进制形式
    // ["A", "B"]应写成下面的形式:
    /* ["0x4100000000000000000000000000000000000000000000000000000000000000", 
        "0x4200000000000000000000000000000000000000000000000000000000000000"]
    */
    constructor(bytes32[] memory _candidates) public {
        candidates = _candidates;
    }
    
    function votingToPerson(bytes32  person) public {
        assert(isValidToPerson(person));
        candidatesVotingCount[person] += 1;
    }
    
    function votingTotalToPerson(bytes32  person) view public returns(uint) {
        require(isValidToPerson(person));
        return candidatesVotingCount[person];
    }
    
    function isValidToPerson(bytes32  person) view public returns(bool) {
        for (uint i = 0; i < candidates.length; i++) {
            if (candidates[i] == person) {
                return true;
            }
        }
        return false;
    }
}

然后执行truffle compile编译合约,编译完成后client/src/contracts比之前多了一个Voting.json

2.2 部署合约

修改migrations目录下的2_deploy_contracts.js,将其中的模板项目合约名SimpleStorage全部换成Voting,再执行truffle migrate进部署,报错:

......
Error: Error: Error:  *** Deployment Failed ***

"Voting" -- Invalid number of parameters for "undefined". Got 0 expected 1!.

    at Object.run (/node-v12.13.0-linux-x64/lib/node_modules/truffle/build/webpack:/packages/migrate/index.js:92:1)
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
Truffle v5.1.0 (core: 5.1.0)
Node v12.13.0

根据一些解决方法(如https://stackoverflow.com/questions/54304214/truffle-smart-contract-error-invalid-number-of-parameter),可知2_deploy_contracts.js中的deployer.deploy可以传第2个参数,试了几次之后发现第2个参数是合约的构造函数的参数,于是修改2_deploy_contracts.js内容如下:

var Voting = artifacts.require("./Voting.sol");

module.exports = function(deployer) {
  deployer.deploy(Voting, ["0x4100000000000000000000000000000000000000000000000000000000000000", "0x4200000000000000000000000000000000000000000000000000000000000000"]);
};

然后使用truffle migrate进行部署,部署后已经可以根据输出的合约地址在在线IDE中进行测试。

2.3 修改React项目

先另存App.js作为备份,然后修改App.js的内容如下:

import React, { Component } from "react";
import Voting from "./contracts/Voting.json";
import getWeb3 from "./getWeb3";

import "./App.css";

class App extends Component {
  state = {voting: [], web3: null, accounts: null, contract: null};

  componentDidMount = async () => {
    try {
      // Get network provider and web3 instance.
      const web3 = await getWeb3();

      // Use web3 to get the user's accounts.
      const accounts = await web3.eth.getAccounts();

      // Get the contract instance.
      const networkId = await web3.eth.net.getId();
      const deployedNetwork = Voting.networks[networkId];
      const instance = new web3.eth.Contract(
        Voting.abi,
        deployedNetwork && deployedNetwork.address,
      );

      // Set web3, accounts, and contract to the state, and then proceed with an
      // example of interacting with the contract's methods.
      this.setState({ web3, accounts, contract: instance}, this.getVoting);
    } catch (error) {
      // Catch any errors for any of the above operations.
      alert(
        `Failed to load web3, accounts, or contract. Check console for details.`,
      );
      console.error(error);
    }
  };

  getVoting = async () => {
    const { contract } = this.state;
    let candidates = ["0x4100000000000000000000000000000000000000000000000000000000000000", "0x4200000000000000000000000000000000000000000000000000000000000000"];
    let voting = new Array();
    await Promise.all(candidates.map(async (elem, index) => {
      voting.push({
        name: elem,
        // 获取票数
        count: await contract.methods.votingTotalToPerson(elem).call(),
        key: index
      })
    }));
    this.setState({ voting});
  };

  render() {
    if (!this.state.web3) {
      return <div>Loading Web3, accounts, and contract...</div>;
    }
    return (
      <div className="App">
        <h1>Voting</h1>
        {
          this.state.voting.map((candidate) => {
          return (<h2 key={candidate.key}>{candidate.name}获得{candidate.count}票</h2>)
          })
        }
        <input ref="candidate"></input><button onClick = { async () => {
          // 投票
          const { contract, accounts } = this.state;
          console.log(contract);
          await contract.methods.votingToPerson(this.refs.candidate.value).send({from: accounts[0]});
          await this.getVoting();
        }
          }>投票</button>
      </div>
    );
  }
}

export default App;

在client目录下用命令npm start启动服务,再从浏览器访问localhost:3000可看到列出的投票数,也可通过页面上的输入框和按钮进行投票。

相关连接

 类似资料: