80、web3学习——2020年07月22日13:27:15

80、WEB3

2020年07月22日12:51:36

一、智能合约编译原理

  1. 源代码---> solidity 编译器 ---> abi/ bytecode --->部署到某个网络。
  2. 我们从最底层,手把手实现每一个步骤,手动操作理解每一个底层工具的细节。
源代码类型 字节码类型 执行环境 调用者
.java源文件 .class字节码 jvm执行 java代码调用
.sol源文件 bytecode字节码 区块链环境执行 javascript代码调用

二、solidity开发环境搭建

  1. 编辑IDE:goland(安装插件:node.js,solidity两个)
  2. solidity编译器,编译环境
  3. mocha 抹茶测试环境
  4. 部署智能合约的脚本到指定网络

三、开发环境的目录结构

手动创建如下目录结构

1. 图示

image-20200722130612380

2. 含义

  1. 00-contracts/SimpleStorage.sol : 合约代码
  2. 01-compile.js:编译文件
  3. 02-deploy.js:部署文件
  4. 03-instance.js:获取合约实例
  5. 04-interaction.js:与合约交互

创建空工程:

初始化NPM项目,执行下面命令,创建package.json,描述当前模块属性的文件

npm init

一路yes下来即可

四、编写智能合约

在SimpleStorage.sol中添加如下代码:

pragma solidity ^0.4.24;

contract SimpleStorage {
    string str;

    constructor(string _str) public {
        str = _str;
    }

    function setValue(string _str) public {
        str = _str;
    }

    function getValue() public view returns (string) {
        return str;
    }
}

五、solidity编译

1. 安装编译器

"solc": "^0.4.25", 新版本使用方式不一样

一定要多注意版本问题

npm install --save solc@0.4.25

2. web3调用图示

image-20200722130801062

image-20200722130816818

3. 编译合约

ES6, Node.jsCore

在compile.js填入如下代码:

//导入solc编译器
let solc = require('solc') //0.4.25

let fs = require('fs')

//读取合约
let sourceCode = fs.readFileSync('./contracts/SimpleStorage.sol', 'utf-8')

// Setting 1 as second paramateractivates the optimiser
let output = solc.compile(sourceCode, 1)

 // console.log('output :', output)
//{age : 17, name : 'lily', address : 'sz'}

console.log('abi :', output['contracts'][':SimpleStorage']['interface'])

//学员对这里有些不太理解,注意解释,json访问方式,键值对形式
module.exports = output['contracts'][':SimpleStorage']

编译

node compile.js

看看编译出来的json对象,保存到compileInfo.json

  • bytecode(机器码)
  • interface(ABI)
module.exports = output['contracts'][':SimpleStorage']

六、部署合约

0. 启动Ganache UI

image-20200722130915555

设置为自己的网络:

image-20200722130932715

至此,以太坊环境已经启动,下面准备web3

1. web3初见

- 安装

npm i web3@1.0.0-beta.36 --save

- 模块划分

我们只讲eth模块utils模块

image-20200722131021887

2. 创建web3实例

在depole.js中添加如下代码:

let Web3 = require('web3')
let web3 = new Web3()
console.log('version :', web3.verison)

3. 设置区块链网络

旧版本支持:

web3.setProvider('HTTP://192.168.28.30:7545') //<<---Ganache UI配置的网络

新版本:

let web3 = new Web3('HTTP://192.168.28.30:7545')

4. 准备合约相关数据

web3手册: https://web3js.readthedocs.io/en/1.0/

这个函数有两个作用:

  1. 部署阶段, 创建合约: 只有interface,没有地址
  2. 调用阶段, 获取一个合约的实例:有interface, 有address
let {bytecode, interface} = require('./01-compile')
let contract = web3.eth.Contract(JSON.parse(interface))

image-20200722131319819

5. 部署合约

image-20200722131157338

这个函数的返回值可以使多种,我们使用send,执行真正的部署动作,整个部署动作如下:

let {bytecode, interface} = require('./01-compile')

// console.log(bytecode)
// console.log(interface)

//1. 引入web3

let Web3 = require('web3')
//2. new 一个web3实例
let web3 = new Web3()
//3. 设置网络

web3.setProvider('HTTP://192.168.28.30:7545')

//account是ganache第一个账户的地址
const account = '0xd5957914c31E1d785cCC58237d065Dd25C61c4D0'

console.log('version :', web3.version)
// console.log(web3.currentProvider)

//1. 拼接合约数据 interface, interface是string类型,可以使用typeof来查看
let contract = new web3.eth.Contract(JSON.parse(interface))

//2. 拼接bytecode
contract.deploy({
    data: bytecode, //合约的bytecode
    arguments: ['HelloWorld'] //给构造函数传递参数,使用数组
}).send({
    from: account,
    gas: '3000000', //不要用默认值,一定要写大一些, 要使用单引号
    //gasPrice: '1',
}).then(instance => {
    console.log('address :', instance.options.address)
})

这个过程是完成合约的部署,返回合约的地址

通过返回实例的options字段获取合约地址

七、获取链上合约实例

创建interaction.js,这个是与合约交互的文件。

调用前需要将链上的合约实例找到,这样才能完成交互,需要用到

  1. web3 : 指定网络
  2. ABI:二进制接口
  3. address:合约地址
//获取合约实例,导出去

//let {bytecode, interface} = require('./01-compile')

//1. 引入web3

let Web3 = require('web3')
//2. new 一个web3实例
let web3 = new Web3()
//3. 设置网络

web3.setProvider('HTTP://192.168.28.30:7545')

let abi = [{
    "constant": true,
    "inputs": [],
    "name": "getValue",
    "outputs": [{"name": "", "type": "string"}],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
}, {
    "constant": false,
    "inputs": [{"name": "_str", "type": "string"}],
    "name": "setValue",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
}, {
    "inputs": [{"name": "_str", "type": "string"}],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "constructor"
}]
let address = '0x0FE5006b70A0D58AD3c4d4BC9DAC02C970510Cf6'


//此处abi已经json对象,不需要进行parse动作
let contractInstance = new web3.eth.Contract(abi, address)

console.log('address :', contractInstance.options.address)

module.exports = contractInstance

八、调用合约

image-20200722131535108

调用代码:

先调用getValue,再调用setValue,然后再次调用getValue,查看值的变化。

//1. 导入合约实例
//2. 读取数据
//3. 写入数据
//4. 读取数据


let instance = require('./03-instance')
const from = '0xd5957914c31E1d785cCC58237d065Dd25C61c4D0'

//异步调用,返回值是一个promise
//2. 读取数据

instance.methods.getValue().call().then(data => {
    console.log('data:', data)

    //3. 写入数据
    instance.methods.setValue('Hello HangTou').send({
        from: from,
        value: 0,
    }).then(res => {
        console.log('res : ', res)

        //4. 读取数据
        instance.methods.getValue().call().then(data => {
            console.log('data2:', data)
        })
    })
})

九、使用promise改写

let instance = require('./03-contractInstance')

console.log('address :', instance.options.address)
let from = '0xd5957914c31E1d785cCC58237d065Dd25C61c4D0'

let test = async () => {
    try {
        let v1 = await instance.methods.getValue().call({
            from: from
        })
        console.log('v1:', v1)

        let res = await instance.methods.setValue('HelloHangTou').send({
            from: from,
        })

        console.log('res:', res)

        let v2 = await instance.methods.getValue().call({
            from: from
        })
        console.log('v2:', v2)
    } catch (e) {
        console.log(e)
    }
}

test()

十一、总结

a. arguments敲错

b. 调用的函数与合约中的方法不一致

十二、测试合约

1. 智能合约的安全问题

每一行代码都价值千金, 一行代码就可能搞死一个家公司,部署到真实环境之前,一定要做组测试,否则后患无穷

2. 搭建测试环境

我们在测试网络进行,在部署到测试网络之前,先使用本机的测试环境:

  • lib模式的虚拟环境(ganache-cli)(不讲)
  • 应用模式的虚拟环境(ganache GUI)

- 安装工具

我们要准备三个工具包:

  • ganache-cli(本地虚拟区块链环境)(api, 图形化,命令行)

      npm install --save ganache-cli
    
  • mocha(测试框架)

      npm install --save mocha
    
  • web3 (与区块链环境交互库,包括:部署,调用)

    若一遍安装不成功就多安装几遍,测试如果失败就把node_module删掉,重新npm i

      npm install --save web3
    

若安装报错, 注意,部分windows电脑可能要安装的工具 npm install --global --production windows-build-tools 终极大招,安装visual studio

- 引入代码

  1. 创建test文件夹(已创建)
  2. 创建SimpleStorage.test.js(规范)
  3. 引入断言库
const  asset = require('assert');
const ganache = require('ganache-cli');
const Web3 = require('web3');

3. mocha测试框架介绍

- 概述

函数 作用
it(name, callback) 跑一个测试或者断言
describe(name, callback) 对it函数分组
beforeEach(callback) 执行一些初始化代码

- demo

const  assert = require('assert');
class Person {
    say() {
        return "hello";
    }

    legs() {
        return 2;
    }
}

describe('测试Person', ()=>{ //没有参数
    it('测试Person say方法:', () => { //注意,没有参数
        const p1 = new Person();
        //console.log(p1.say());
        assert.equal(p1.say(), "hello", "say() should be hello")
    })

    it('测试Person legs方法:', () => {
        const p1 = new Person();
        //console.log(p1.legs());
        assert.equal(p1.legs(), 2, "legs() should be 2")
    })
})

package.json添加 scripts

"test":"mocha"
npm run test
  1. 抽取new Person()的过程 到beforeEach()
  2. 每个测试函数it 需要使用到dog, 声明类的全局变量 let
const assert = require('assert')

class Person {
    say() {
        return "hello"
    }


    legs() {
        return 2
    }

}

let p1
beforeEach(() => {
   p1 = new Person()
})

describe('测试Person', () => {
    it('test1', () => {
        assert.equal(p1.say(), 'hello', 'say()应该返回\'hello\'')
    })

    it('test2', () => {
        assert.equal(p1.legs(), 2, 'legs()应该返回2')
    })
})

4. mocha代码测试流程

  1. mocha start
  2. 部署智能合约 beforeEach
  3. 调用智能合约 it
  4. 进行断言 it
image-20200722131822469

5. 部署及调用测试

注意几点:

  1. 使用ganache-cli lib版写测试用例,不要启动服务
  2. ganache.provider()函数
  3. 别忘了在descripe中写it
  4. interface,bytecode同名结构,相对目录../
  5. JSON.parse解析
var assert = require('assert')
let Web3 = require('web3')
let ganache = require('ganache-cli')

let web3 = new Web3(ganache.provider())
let {interface, bytecode} = require('../compile')

let contractInsatance
let accounts
beforeEach(async () => {
    accounts = await web3.eth.getAccounts()

    contractInsatance = await
        new web3.eth.Contract(JSON.parse(interface)).deploy({
            data: bytecode,
            arguments: ['hello'],
        }).send({
            from: accounts[0],
            gas: '1000000'
        })

    console.log('address :', contractInsatance.options.address)

})

describe('部署SimpleStorage合约', () => {
    it('部署:', async () => {
        console.log('version:', web3.version)
        let msg = await contractInsatance.methods.setValue().call({
            from: accounts[0],
        })
        console.log('msg :', msg)

        assert.equal(msg, 'hello', '应该是hello')
    })

    it('测试setMessage', async () => {
        let res = await contractInsatance.methods.getValue('HELLOWORLD!').send({
            from: accounts[0],
        })
        // console.log('res :', res)

        let getRes = await contractInsatance.methods.setValue().call({
            from: accounts[0],
        })

        assert.equal(getRes, 'HELLOWORLD!', '应该是HELLOWORLD!')
    })
})

- 原型

image-20200722131948503

十三、部署到真实测试环境

部署到真实网络需要的数据:

  1. 助记词,表明花费谁的钱
    1. scout same naive genius cannon maze differ acquire penalty habit surround ice
  2. 指定一个服务商,让它帮助我们链接到真实网络
    1. https://ropsten.infura.io/v3/02cd1e3c295c425597fa105999493baa
  3. 需要使用一个npm包,接收两个参数:1,2,这个包会帮助我们链接到对应的网络。
    1. npm install truffle-hdwallet-provider@0.0.3 --save

本地网络

image-20200722132013955

以太坊测试网络

image-20200722132105444

1. 什么是Infura(科学家)

Infura是一个托管的以太坊节点集群,可以将你开发的以太坊智能合约部署到infura提供的节点上,而无需搭建自己的以太坊节点,它是MetaMask背后的以太坊供应商。

接下来我们将演示如何将truffle项目部署到测试网络。

2. 注册Infura

在使用Infura之前,需要注册Infura访问令牌 。

填写并提交表格后你就可以收到访问令牌。 相关信息将显示在屏幕上并发送到你提供的电子邮件。 需要记录下来这个访问令牌并确保它不被别人看到!

选择ropsten,然后复制链接:ropsten.infura.io/v3/0....

3. 安装HDWalletProvider

Infura的HDWalletProvider是一个独立的npm软件包,必须指定0.03版本,"truffle-hdwallet-provider": "0.0.3",其他版本有坑

npm install truffle-hdwallet-provider@0.0.3 --save

4. 完整代码

const Web3 = require('web3');
const {interface, bytecode} = require('./compile');

var HDWalletProvider = require("truffle-hdwallet-provider");

//ropsten 网络
// var mnemonic = ""
// var provider = new HDWalletProvider(mnemonic, "https://ropsten.infura.io/v3/02cd1e3c295c425597fa105999493baa");

//本地测试
//gui
var mnemonic = "scout same naive genius cannon maze differ acquire penalty habit surround ice";
//cmd
//var mnemonic = "rude feature bless puzzle drop solution hip grant gauge undo idea surprise";
var provider = new HDWalletProvider(mnemonic, "http://localhost:7545");
const web3 = new Web3(provider);

let deploy = async () => {
    const accounts = await web3.eth.getAccounts();
    console.log("async accounts :", accounts);
    const balance = await web3.eth.getBalance(accounts[0]);
    console.log("async account[0] balance :", balance);

    const contractInstance = await new web3.eth.Contract(JSON.parse(interface));
    const mycontract = await contractInstance.deploy(
        {
            data:bytecode,
            arguments:['Hi']
        }
    ).send({
        from : accounts[0],
        gas:'2000000'
    });

    console.log("contract address : ", mycontract.options.address);
    //console.log("contract address : ", mycontract);


    let message = await mycontract.methods.getValue().call();
    console.log("fist call, message :" + message);
    //assert.equal(message, 'Hi', "should be Hi");


    console.log('setMessage encodeABI: ', mycontract.methods.setValue('hello world').encodeABI())
    let resObj = await mycontract.methods.setValue('hello world').send(
        {
            from : accounts[0],
            gas: 100000
        }
    ).on('receipt', (receipt) => {
        console.log('receipt :', receipt);
    });


    message = await mycontract.methods.getValue().call();
    console.log("second call, message :" + message);
    //assert.equal(message, 'hello world', "should be hello world");
};

deploy();

代码使用方法

  1. 进入到home目录下的eth目录
  2. 进入进入01-web3-eth目录
  3. 将老师代码中的
    1. 0-contract.js,
    2. 01-compile.js ,
    3. 02-deploy.js,
    4. 03-getInstance.js,
    5. 04-interaction.js

文件拷贝到01-web3-eth目录下

  1. 执行node 02-deploy.js部署合约
  2. 其他略

编写合约

pragma solidity ^0.4.24;

contract SimpleStorage {
    string str;

    constructor(string _str) public {
        str = _str;
    }

    function setValue(string _str) public {
        str = _str;
    }

    function getValue() public view returns (string) {
        return str;
    }
}

web3编译合约

//导入solc编译器
let solc = require('solc') //0.4.25

let fs = require('fs')

//读取合约
let sourceCode = fs.readFileSync('./contracts/SimpleStorage.sol', 'utf-8')

// Setting 1 as second paramateractivates the optimiser
let output = solc.compile(sourceCode, 1)

 // console.log('output :', output)
//{age : 17, name : 'lily', address : 'sz'}
module.exports = output['contracts'][':SimpleStorage']

web3部署合约

let {bytecode, interface} = require('./01-compile')

// console.log(bytecode)
// console.log(interface)

//1. 引入web3

let Web3 = require('web3')
//2. new 一个web3实例
let web3 = new Web3()
//3. 设置网络

let web3 = new Web3('http://localhost:7545') 
//web3.setProvider('http://localhost:7545') //旧版本,新版本多了个参数,尚未研究

const account = '0xd5957914c31E1d785cCC58237d065Dd25C61c4D0'

console.log('version :', web3.version)
// console.log(web3.currentProvider)

//1. 拼接合约数据 interface
let contract = new web3.eth.Contract(JSON.parse(interface))

//2. 拼接bytecode
contract.deploy({
    data: bytecode, //合约的bytecode
    arguments: ['HelloWorld'] //给构造函数传递参数,使用数组
}).send({
    from: account,
    gas: '3000000', //一定要写
    //gasPrice: '1',
}).then(instance => {
    console.log('address :', instance.options.address) //这个字段可读可写
})

注意:

//1. JSON.parse(interface) 要使用解析,否则是string,会报错
//2. gas : '1000000', 一定要写gas,不写的话会使用默认值,会失败,值需要单引号
a. 不写gas,报错如下:
- "stack":"Error: base fee exceeds gas limit\

b. gas值过大,例如:gas : 10000000000, 则报错如下:
- message":"Exceeds block gas limit","

获取合约实例

//获取合约实例,导出去

//let {bytecode, interface} = require('./01-compile')

//1. 引入web3

let Web3 = require('web3')
//2. new 一个web3实例
let web3 = new Web3()
//3. 设置网络

web3.setProvider('http://localhost:7545')

let abi = '[{"constant":true,"inputs":[],"name":"getValue","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_str","type":"string"}],"name":"setValue","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_str","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]'

//记得加引号
let address = '0x0FE5006b70A0D58AD3c4d4BC9DAC02C970510Cf6'


//此处abi已经json对象,不需要进行parse动作
let contractInstance = new web3.eth.Contract(JSON.parse(abi), address)

console.log('address :', contractInstance.options.address)

module.exports = contractInstance

调用合约

//1. 导入合约实例

//2. 读取数据

//3. 写入数据

//4. 读取数据


let instance = require('./03-instance')
const from = '0xd5957914c31E1d785cCC58237d065Dd25C61c4D0'

//异步调用,返回值是一个promise
//2. 读取数据
instance.methods.getValue().call().then(data => {
    console.log('data:', data)

    //3. 写入数据
    instance.methods.setValue('Hello HangTou').send({
        from: from,
        value: 0,
    }).then(res => {
        console.log('res : ', res)

        //4. 读取数据
        instance.methods.getValue().call().then(data => {
            console.log('data2:', data)
        })
    })
})

使用Promise改写

//web3与区块链交互的返回值都是promise,可以直接使用async/await

let test = async () => {
    try {
        let v1 = await instance.methods.getValue().call()
        console.log('v1:', v1)

        let res = await  instance.methods.setValue('Hello HangTou').send({
            from: from,
            value: 0,
        })

        console.log('res:', res)

        let v2 = await instance.methods.getValue().call()

        console.log('v2:', v2)
    } catch (e) {
        console.log(e)
    }
}

test()

注意事项:

  1. 如果合约代码有 修改,记得要更新abi
  2. account账户要根据自己的巧克力第一个账户进行修改
  3. 部署合约时,arguments不要拼写错
  4. 不要忘记await关键字,否则失败
  5. 部署合约时,gas字段一定要写大点,否则部署失败
  6. 每次重新部署后,合约的地址都会改变,所以需要更新loadInstance.js里面的address变量

END

2020年07月22日13:22:47

posted @ 2020-07-22 13:28  一颗小苹果  阅读(191)  评论(0编辑  收藏  举报