80、WEB3
2020年07月22日12:51:36
一、智能合约编译原理
- 源代码---> solidity 编译器 ---> abi/ bytecode --->部署到某个网络。
- 我们从最底层,手把手实现每一个步骤,手动操作理解每一个底层工具的细节。
源代码类型 | 字节码类型 | 执行环境 | 调用者 |
---|---|---|---|
.java源文件 | .class字节码 | jvm执行 | java代码调用 |
.sol源文件 | bytecode字节码 | 区块链环境执行 | javascript代码调用 |
二、solidity开发环境搭建
- 编辑IDE:goland(安装插件:node.js,solidity两个)
- solidity编译器,编译环境
- mocha 抹茶测试环境
- 部署智能合约的脚本到指定网络
三、开发环境的目录结构
手动创建如下目录结构
1. 图示
2. 含义
- 00-contracts/SimpleStorage.sol : 合约代码
- 01-compile.js:编译文件
- 02-deploy.js:部署文件
- 03-instance.js:获取合约实例
- 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调用图示
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
设置为自己的网络:
至此,以太坊环境已经启动,下面准备web3
1. web3初见
- 安装
npm i web3@1.0.0-beta.36 --save
- 模块划分
我们只讲eth模块
和utils模块
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/
这个函数有两个作用:
- 部署阶段, 创建合约: 只有interface,没有地址
- 调用阶段, 获取一个合约的实例:有interface, 有address
let {bytecode, interface} = require('./01-compile')
let contract = web3.eth.Contract(JSON.parse(interface))
5. 部署合约
这个函数的返回值可以使多种,我们使用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,这个是与合约交互的文件。
调用前需要将链上的合约实例找到,这样才能完成交互,需要用到
- web3 : 指定网络
- ABI:二进制接口
- 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
八、调用合约
调用代码:
先调用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
- 引入代码
- 创建test文件夹(已创建)
- 创建SimpleStorage.test.js(规范)
- 引入断言库
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
- 抽取new Person()的过程 到beforeEach()
- 每个测试函数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代码测试流程
- mocha start
- 部署智能合约 beforeEach
- 调用智能合约 it
- 进行断言 it
5. 部署及调用测试
注意几点:
- 使用ganache-cli lib版写测试用例,不要启动服务
- ganache.provider()函数
- 别忘了在descripe中写it
- interface,bytecode同名结构,相对目录../
- 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!')
})
})
- 原型
十三、部署到真实测试环境
部署到真实网络需要的数据:
- 助记词,表明花费谁的钱
- scout same naive genius cannon maze differ acquire penalty habit surround ice
- 指定一个服务商,让它帮助我们链接到真实网络
- 需要使用一个npm包,接收两个参数:1,2,这个包会帮助我们链接到对应的网络。
- npm install truffle-hdwallet-provider@0.0.3 --save
本地网络
以太坊测试网络
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();
代码使用方法
- 进入到home目录下的eth目录
- 进入进入01-web3-eth目录
- 将老师代码中的
- 0-contract.js,
- 01-compile.js ,
- 02-deploy.js,
- 03-getInstance.js,
- 04-interaction.js
文件拷贝到01-web3-eth
目录下
- 执行node 02-deploy.js部署合约
- 其他略
编写合约
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()
注意事项:
- 如果合约代码有 修改,记得要更新abi
- account账户要根据自己的巧克力第一个账户进行修改
- 部署合约时,arguments不要拼写错
- 不要忘记await关键字,否则失败
- 部署合约时,gas字段一定要写大点,否则部署失败
- 每次重新部署后,合约的地址都会改变,所以需要更新loadInstance.js里面的address变量
END
2020年07月22日13:22:47