Skip to content

智能合约开发

🕒 Published at:

代码地址

dlsq

以太坊客户端

EVM

智能合约的运行环境,是一个虚拟机

以太坊客户端

  1. 定义:也就是 EVM 的载体,也就是区块链网络中的节点的程序,只要符合网络规范,任何语言都可以实现客户端
  2. 常见的客户端:Geth(Go 实现)、OpenEthereum(Rust 实现),通过 RPC 提供服务

账户

本质都是一个 20 个字节表示的地址

  1. 外部账户(EOA):由私钥控制(比如用户的地址)

  2. 合约账户:代码控制(比如合约代码的部署地址)

注意:交易只能从外部账号发出,合约只能被动执行。合约之间的交互称为消息,所有的 gas 都由外部账号支付

Gas

GAS 是一个工作量单位,复杂度越大,所需 gas 越多。费用=gas 数量*gas 单价(以太币计价 gwei)

单位

  • 最小单位:Wei
  • 10^9 Wei = 1 Gwei
  • 10^12 Wei = 1 Szabo
  • 10^15 Wei = 1 Finney
  • 10^18 Wei = 1 Ether

网络

  1. 主网(价值网络)
  2. 测试网
  3. 开发模拟网(本地环境,一般就是借助工具起虚拟机)

合约的编译、部署、测试

1. Remix

链接

在contracts里添加Counter.sol

solidity
// SPDX-License-Identifier:  MIT
pragma solidity ^0.8.0;

contract Counter{
    uint public counter;
    constructor(){
        counter = 0;

    }
    function count() public{
        counter+=1;
    }
}

编辑器左侧面板有文件夹区域、搜索区域、编译区域、部署区域。

编译区域可选编译器版本。

部署区域可选部署链,成功后会显示部署地址。部署合约也是一笔交易,需要在钱包确认,也需要gas费。部署成功后会显示交互面板,橙色是触发交易,蓝色仅读取。点击橙色触发交易,需要钱包确认,需要gas费。点击蓝色读就不需要。

实际操作:environment 选择 injected web3,这里连接 metamusk 钱包,钱包啥网络,就是啥网络。推荐用 goerli(eth 测试网),去goerli faucet申请一点测试币即可。部署前会连接钱包,然后就是用账号部署上去,部署完后可以去测试链查状态

部署成功后,点两次橙色触发交易,再点蓝色可以读到此时counter值的状态,控制台打印:

shell
{
	"0": "uint256: 2"
}

注意 remix 中的 environment 如果是默认的,那就是虚拟网络,在区块链浏览器是查不到的,而且不需要钱包验证,也不需要gas费

2. Truffle

Truffle:编译、部署、测试合约的一整套开发工具

ganache是开发区块链,提供本地模拟的链上环境

官方文档

中文文档

我的代码

shell
Truffle 安装  npm install -g truffle
创建工程 truffle init或者truffle unbox metacoin(我用的第二个,相当于使用metacoin这个模版,注意需要手动mkdir一个folder,再在里面执行创建,注意配置proxy)

truffle工程包含

  • contracts:智能合约目录
  • migrations:迁移文件,用来指示如何部署智能合约
  • test:智能合约测试用例文件夹
  • truffle-config.js:配置文件,配置truffle连接的网络及编译选项
  • build:编译结果目录

改造下folder

  1. 删除contracts、migrations、test下的文件

  2. contracts里添加Counter.sol

    solidity
    // SPDX-License-Identifier:  MIT
    pragma solidity ^0.8.0;
    
    contract Counter{
        uint public counter;
        constructor(){
            counter = 0;
    
        }
        function count() public{
            counter+=1;
        }
    }

合约编译

shell
//使用命令
truffle compile

在那之前需要配置truffle-config.js,不配置就是默认的0.5.16版本

js
module.exports={
  compilers:{
    solc:{
      version:"0.8.9"
    }
  }
}

编译完成会输出在build目录,同时也会在命令行输出编译使用的编译器

shell
> Compiled successfully using:
   - solc: 0.8.9+commit.e5eed63a.Emscripten.clang

编译结果是json(与合约一一对应),里面abi是和前端交互的,bytecode就是最后部署在链上的东西

接下来是合约的部署

shell
//使用命令
truffle migrate
truffle migrate --network networkname //可以在后面加network参数和network名字,部署到指定network

在那之前,需要先写好配置文件

  1. 编写部署脚本

    migrations里添加1_counter.js

    js
    const Counter = artifacts.require("Counter");
    
    module.exports = function (deployer) {
      deployer.deploy(Counter);
    };
  2. 部署到本地节点(我没有搞)

    1. 先启动一个服务模拟链上环境ganache-cli -p 7545(ganache是开发区块链,提供本地模拟的链上环境)

    2. 然后添加dev的network

      js
      //truffle-config.js development网络配置
      
      module.exports={
        networks:{
          development:{
            host:"127.0.0.1",
            port:7545,
         		network_id:"*"
          }
        }
      }
    3. truffle migrate — network development部署,本地部署,不需要验证和gas费

  3. 部署到链上

    1. 需要部署到链上的节点,所以先去infura.io创建一个project,然后copy project id(选择endpoints是goerli)

    2. 当前目录新建两个隐藏文件,.api_key和.mnemonic,.gitignore要添加这两项,分别存储上一步的id和你的钱包账户助记词(主要为了防止外泄)

    3. 初始化npm ,添加truffle-hdwallet-provider包

      shell
      npm init
      npm i truffle-hdwallet-provider -s
      .gitignore 添加node_modules
    4. config.js里的network里添加新的network goerli

      js
      const HDWalletProvider = require("truffle-hdwallet-provider");
      
      const fs = require('fs');
      const api_key = fs.readFileSync('.api_key').toString().trim();
      const mnemonic = fs.readFileSync('.mnemonic').toString().trim();
      module.exports = {
        networks: {
          goerli: {
            provider: () => {
              return new HDWalletProvider(mnemonic, 'https://goerli.infura.io/v3/' + api_key)
            },
            network_id: '5',
            gas: 4465030,
            gasPrice: 10000000000,
          },
        }
      };
    5. 执行truffle migrate — network goerli部署到goerli,这里会消耗gas费,但没有metamusk弹窗确认的过程,因为我输入了助记词,他直接在钱包扣款了

      shell
      //命令行输出
      Compiling your contracts...
      ===========================
      > Everything is up to date, there is nothing to compile.
      
      
      Migrations dry-run (simulation)
      ===============================
      > Network name:    'goerli-fork'
      > Network id:      5
      > Block gas limit: 30000000 (0x1c9c380)
      
      
      1_counter.js
      ============
      
         Deploying 'Counter'
         -------------------
         > block number:        6818292
         > block timestamp:     1651505825
         > account:             0x736D76f4C2d4b4CCced0CCA92d3dF0F0e456F35D
         > balance:             0.04813429249775099
         > gas used:            135269 (0x21065)
         > gas price:           10 gwei
         > value sent:          0 ETH
         > total cost:          0.00135269 ETH
      
         -------------------------------------
         > Total cost:          0.00135269 ETH
      
      Summary
      =======
      > Total deployments:   1
      > Final cost:          0.00135269 ETH
      
      
      
      
      Starting migrations...
      ======================
      > Network name:    'goerli'
      > Network id:      5
      > Block gas limit: 29970705 (0x1c95111)
      
      
      1_counter.js
      ============
      
         Deploying 'Counter'
         -------------------
         > transaction hash:    0x376710e125c35233b468bd232444acc2239c38e837aeecf9ef9de6762715eca8
         > Blocks: 0            Seconds: 5
         > contract address:    0xAc8dC14e7aC85556Bcc22c052FbB5Bc05D4E77D8
         > block number:        6818298
         > block timestamp:     1651505843
         > account:             0x736D76f4C2d4b4CCced0CCA92d3dF0F0e456F35D
         > balance:             0.04813429249775099
         > gas used:            135269 (0x21065)
         > gas price:           10 gwei
         > value sent:          0 ETH
         > total cost:          0.00135269 ETH
      
         > Saving artifacts
         -------------------------------------
         > Total cost:          0.00135269 ETH
      
      Summary
      =======
      > Total deployments:   1
      > Final cost:          0.00135269 ETH

补充内容

  1. Truffle console使用(可以直接在控制台调用合约)

    先把合约部署到development

    truffle console --network development开启调用模式

  2. truffle-min.sh(压缩artifacts文件)

    我们compile后,是会在build里输出一个json,但是这个json很大,所以可以借助这个脚本来对这个json进行压缩,可以从几千行压缩到几十行,然后我们后续的开发有引用这个json的需求的时候,就可以引入这个压缩的json

3. Hardhat

Hardhat:编译、部署、测试和调试以太坊应用的开发环境,围绕task(任务)和plugins(插件)概念设计

在命令行运行Hardhat时,都是在运行任务,例如:npx hardhat compile就是运行compile任务

Hardhat node:开发区块链,提供本地模拟的链上环境

官方文档,中文文档

参考代码

我的代码

创建工程

shell
mkdir hardhat-demo //创建一个folder
cd hardhat-demo
npm init //初始化npm
npm i -s hardhat //当前目录下安装hardhat
npx hardhat //在当前目录下创建项目,可以选basic-sample(注意,这一步要开启shell proxy)

contracts里新建Counter.sol,添加一下内容

solidity
// SPDX-License-Identifier:  MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";

contract Counter{
    uint public counter;
    constructor(){
        counter = 0;

    }
    function count() public{
        counter+=1;
        console.log("curr counter:",counter);
    }
    function add(uint x) public{
        counter=counter+x;
    }
}

合约编译

js
//先修改hardhat.config.js,设置编译器版本
module.exports={
  Solidity:"0.8.9"
}
//然后命令行输入
npx hardhat compile

部署

  1. 编写部署脚本

    js
    //scripts/deploy_counter.js
    const hre = require("hardhat");
    async function main() {
      //await hre.run('compile');
      const Counter = await hre.ethers.getContractFactory("Counter");
      const counter = await Counter.deploy(); //deploy里的括号可以传参,具体看合约里的construtor有没有参数而定
    
      await counter.deployed();
      console.log("Counter deployed to:", counter.address);
    }
    main()
      .then(() => process.exit(0))
      .catch((error) => {
        console.error(error);
        process.exit(1);
      });

    如果想要一次部署多个合约,比如这里有counter和greeter合约,那么可以这么写

    js
    //scripts/deploy.js
    
    const { ethers } = require("hardhat");
    
    async function main() {
      const [deployer] = await ethers.getSigners();
      console.log('Deploying contracts with the account: ' + deployer.address);
    
      // Deploy Greeter
      const Greeter = await ethers.getContractFactory("Greeter");
      const greeter = await Greeter.deploy("Hello, Hardhat!");
    
    
      // Deploy Counter
      const Counter = await hre.ethers.getContractFactory("Counter");
      const counter = await Counter.deploy();
      
      // console.log
    	console.log("Greeter deployed to:", greeter.address);
      console.log("Counter deployed to:", counter.address);
    
    }
    
    main()
        .then(() => process.exit())
        .catch(error => {
            console.error(error);
            process.exit(1);
    })
  2. 部署到本地网络

    先启动一个本地网络npx hardhat node,然后再根据端口值来修改hardhat.config.js,增加以下内容

    js
    //hardhat.config.js
    module.exports={
      networks:{
        development:{
          url:"http://127.0.0.1:8545",
          chainId:31337
        }
      }
    }

    执行部署npx hardhat run scripts/deploy_xxx.js [--network 网络],而这里的话,就是执行npx hardhat run scripts/deploy_counter.js --network development

  3. 部署到链上

    需要部署到链上的节点,所以先去infura.io创建一个project,然后copy project id(选择endpoints是goerli)

    本地添加.api_key和.mnemonic,.gitignore添加过滤这两项,然后修改hardhat.config.js,增加以下内容

    js
    //hardhat.config.js
    const fs = require('fs');
    const api_key = fs.readFileSync('.api_key').toString().trim();
    const mnemonic = fs.readFileSync('.mnemonic').toString().trim();
    module.exports={
      networks:{
        goerli:{
          url:`https://goerli.infura.io/v3/${api_key}`,
          accounts:{
            mnemonic:mnemonic,
          }
        }
      }
    }

​ 执行部署npx hardhat run scripts/deploy.js --network goerli

测试

js
const {expect} = require("chai");
const {ethers} = require("hardhat");

describe("Counter", function () {
  it("counter should be 0 when init , be added 1 after count be called, be added x after add be called with x", async function () {
    const Counter = await ethers.getContractFactory("Counter");
    const counterInstance = await Counter.deploy();
    await counterInstance.deployed();
    expect(await counterInstance.counter()).to.equal(0);

    const setCountTx = await counterInstance.count();

    // wait until the transaction is mined
    await setCountTx.wait();

    expect(await counterInstance.counter()).to.equal(1);

    const addCountTx = await counterInstance.add(10);
    await addCountTx.wait();
    expect(await counterInstance.counter()).to.equal(11);
  });
});

npx hardhat test执行测试

实战

  1. 调试利器:console.log

    solidity
    import "hardhat/console.sol";
    
    console.log(counter);
  2. 灵活参数部署,利用hardhat可以在代码中引用(这里值的是合约初始化的参数,也就是constructor需要的参数,比如Greeter合约,这里可以在部署脚本里传入参数,比如deploy_greeter.js所写。也可以用nodejs的方式,在命令行传入参数,然后在部署脚本里取出来)

    1. solidity
      // Counter.sol
      // SPDX-License-Identifier:  MIT
      pragma solidity ^0.8.0;
      import "hardhat/console.sol";
      
      contract Counter{
          uint public counter;
          constructor(uint _counter){
              counter = _counter;
      
          }
          function count() public{
              counter+=1;
              console.log("curr counter:",counter);
          }
          function add(uint x) public{
              counter=counter+x;
          }
      }
      
      // test_counter.js
      const {expect} = require("chai");
      const {ethers} = require("hardhat");
      
      describe("Counter", function () {
        it("counter should be 0 when init , be added 1 after count be called, be added x after add be called with x", async function () {
          const Counter = await ethers.getContractFactory("Counter");
          const counterInstance = await Counter.deploy(0);
          await counterInstance.deployed();
          expect(await counterInstance.counter()).to.equal(0);
      
          const setCountTx = await counterInstance.count();
      
          // wait until the transaction is mined
          await setCountTx.wait();
      
          expect(await counterInstance.counter()).to.equal(1);
      
          const addCountTx = await counterInstance.add(10);
          await addCountTx.wait();
          expect(await counterInstance.counter()).to.equal(11);
        });
      });
      
      //deploy_counter.js
      //其余相同,只需改下面这一句
       const counter = await Counter.deploy(0);
    2. 设置deploy脚本

      js
      const {ethers} = require("hardhat");
      
      const params=process.argv;
      const value=params[2];
      console.log("Counter deploy with value:",value);
      async function main() {
        //await hre.run('compile');
        const Counter = await ethers.getContractFactory("Counter");
        const counter = await Counter.deploy(value);
      
        await counter.deployed();
        console.log("Counter deployed to:", counter.address);
      
      }
      main()
        .then(() => process.exit(0))
        .catch((error) => {
          console.error(error);
          process.exit(1);
        });
    3. 执行node脚本,并传递参数

      需要两个命令,可以在命令行依次执行,也可以写成一个.sh,sh ./deploy_by_param.sh 执行这个.sh即可

      shell
      // ./deploy_by_param.sh
      export HARDHAT_NETWORK='development' //设置network的值是development;这就是node执行hardhat脚本附着hardhat参数的方式+
      node scripts/deploy_by_param.js 10 //在这里传入初始化参数是10
  3. 代码扁平:npx hardhat flatten xxx.sol > xxx.sol

    本质的意义是,当一个合约的里面有import引用的时候,可以用这个命令把引用的代码和本身的代码放在一起,方便看,这里的话就是npx hardhat flatten contracts/Counter.sol >>Counter.sol

  4. 代码验证

    当合约部署在链上后,可以通过这个命令来对代码进行验证

    shell
    1.先安装 npm i hardhat-etherscan --dev
    2.hardhat.config.js里添加require("@nomiclabs/hardhat-etherscan")
    3.添加scankey,const scankey=`${scanKey}`; //因为调用etherscan的api,所以需要去他那里申请一个key, https://etherscan.io/,这个key最好也用隐藏文件,不要暴露出去
    4.在配置里,networks的下面,添加同级项 etherscan:{apiKey:scankey}
    5.命令行执行npx hardhat verify address --network xxx // address是合约的部署地址,xxx是要验证的网络
  5. ABI导出

    当合约部署后,需要导出ABI供前端调用,默认的deploy就会导出一个总的json里面包括abi,不过比较大而已,这里是仅导出abi的json

    shell
    1.先安装 npm i hardhat-abi-exporter --dev
    2.hardhat.config.js里添加require("hardhat-abi-exporter")
    3.hardhat.config.js里配置下导出,新建./abi目录来存放
      abiExporter: {
        path: './abi',
        runOnCompile: true,
        clear: true,
        flat: true,
        spacing: 2,
        pretty: false,
      }
    4.npx hardhat export-abi
  6. 自定义Task

    在hardhat.config.js里可以写task,然后通过npx hardhat taskName完成一些想要做的事,也可以在task文件夹里写,然后在hardhat.config.js用require引入

    js
    task("accounts","Prints the list of accounts",async(taskArgs,hre)=>{
      const accounts =await hre.ethers.getSigners();
      for(const account of accounts){
        console.log(account.address)
      }
    })
    
    //命令行 npx hardhat accounts执行
  7. 书写脚本去调用合约,实现前端与合约的交互

    1. 前端想要调用合约,那就得知道合约的地址和abi,之前有用npm处理了abi的单独导出,但是并没有处理合约的地址,所以这里我们改造下deploy脚本,让它自动保存地址和abi到一个data的目录下

      js
      // scripts/deploy_counter.js
      const hre = require("hardhat");
      async function main() {
        //await hre.run('compile');
        const Counter = await hre.ethers.getContractFactory("Counter");
        const counter = await Counter.deploy(0);
      
        await counter.deployed();
        console.log("Counter deployed to:", counter.address);
        saveFrontendFiles(counter);
      }
      
      function saveFrontendFiles(counter) {
        const fs = require("fs");
        const contractsDir = "./data";
      
        if (!fs.existsSync(contractsDir)) {
          fs.mkdirSync(contractsDir);
        }
      
        fs.writeFileSync(
          contractsDir + "/contract-address.json",
          JSON.stringify({
            Counter: counter.address
          }, undefined, 2)
        );
      
        const CounterArtifact = artifacts.readArtifactSync("Counter");
      
        fs.writeFileSync(
          contractsDir + "/Counter.json",
          JSON.stringify(CounterArtifact, null, 2)
        );
      }
      main()
        .then(() => process.exit(0))
        .catch((error) => {
          console.error(error);
          process.exit(1);
        });
      
      //然后执行脚本 npx hardhat run scripts/deploy_counter.js --network development
    2. 新建action.js实现前端调用合约的逻辑

      js
      const {
        ethers
      } = require("hardhat")
      const CounterArtifact = require("../data/Counter.json")
      const contractAddress = require("../data/contract-address.json");
      
      (async () => {
        // init
        const provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545/');
      
        // Then, we initialize the contract using that provider and the token's
        // artifact. You can do this same thing with your contracts.
        const counter = new ethers.Contract(
          contractAddress.Counter,
          CounterArtifact.abi,
          provider.getSigner(0)
        );
      
        console.log("counter值是:",await counter.counter())
      
        await counter.count();
        console.log("counter值是:",await counter.counter())
      
      })()