web3是一个专门与以太坊交互的node.js库。我们先回顾一下使用remix部署合约的步骤:
第一步:编写合约。
第二步:编译合约(之前我们设置了自动编译)。
第三步:部署合约,部署成功后返回合约地址。
第四步:调用合约。
remix底层就是使用了web3实现了编译、部署、调用合约的功能。那么web3是如何实现这些功能呢?看完这篇文章就一清二楚了!!!
首先,使用web3编译合约之后,会生成bytecode和abi接口。从上图可以看出,部署合约的时候需要用到这两个东西。
如果要调用合约中的方法,那么先要找到合约实例。通过abi接口和合约地址可以找到合约实例。
所以,无论部署合约还是查找合约实例都需要使用abi。
(1)创建项目,配置nodejs环境。
参考“Goland工具开发NodeJs”的Node环境配置。
(2)安装编译器
在命令窗口中进入项目所在路径,然后执行下面命令安装solc和web3编译器。
>> npm init -y
>> npm install --save [email protected]
>> npm install --save [email protected]
(3)创建项目目录结构
创建一个contract文件夹,专门保存sol合约文件。另外再创建四个js文件。分别如下所示:
01_compile.js 编译合约
02_deploy.js 部署合约
03_instance.js 获取合约实例
04_interact.js 调用合约
创建后的目录结构如下图所示:
(4)创建合约
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;
}
}
为了防止编写合约时候出现的人为错误,建议先在remix上创建合约并测试通过后,再拷贝到项目的contracts目录下。
solc官方提供了编译智能合约的示例,具体可以参考:https://www.npmjs.com/package/solc。
(1)打开01_compile.js文件,导入solc和fs
let solc = require('solc')
let fs = require('fs')
(2)读取合约代码
let contractCode = fs.readFileSync('./contracts/SimpleStorage.sol', 'utf-8')
(3)编译合约代码
let output = solc.compile(contractCode, 1)
output是一个json对象,其格式如下所示:
{ contracts:
{ ':SimpleStorage':
{ assembly: [Object],
bytecode:
'608060405234801561001057600080fd5b5060405161039238038061039283398101604052805101805161003a906000906020840190610041565b50506100dc565b82805460018160011
6156101000203166002900490600052602060002090601f016020900481019282601f1061008257805160ff19168380011785556100af565b828001600101855582156100af579182015b8281111561
00af578251825591602001919060010190610094565b506100bb9291506100bf565b5090565b6100d991905b808211156100bb57600081556001016100c5565b90565b6102a7806100eb6000396000f
30060806040526004361061004b5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166320965255811461005057806393a09352146100da575b6000
80fd5b34801561005c57600080fd5b50610065610135565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561009f57818101518382015260200161008
7565b50505050905090810190601f1680156100cc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156100e657600080fd5b506040805160
206004803580820135601f81018490048402850184019095528484526101339436949293602493928401919081908401838280828437509497506101cc9650505050505050565b005b6000805460408
0516020601f60026000196101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156101c15780601f1061019657610100808354040283
5291602001916101c1565b820191906000526020600020905b8154815290600101906020018083116101a457829003601f168201915b505050505090505b90565b80516101df9060009060208401906
101e3565b5050565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061022457805160ff1916838001178555610251565b828001600101
85558215610251579182015b82811115610251578251825591602001919060010190610236565b5061025d929150610261565b5090565b6101c991905b8082111561025d57600081556001016102675
600a165627a7a723058209a431495f0223bccfd15255ce1400f96f38db198124404ee19016ee697bf93d90029',
functionHashes: [Object],
gasEstimates: [Object],
interface:
'[{"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"}]',
...
} } }
从上面可以看到,output中包含了bytecode和interface(abi)的信息。
(4)执行导出
module.exports = output['contracts'][':SimpleStorage']
console.log('bytecode : ', output['contracts'][':SimpleStorage']['bytecode'])
console.log('abi : ', output['contracts'][':SimpleStorage']['interface'])
(1)从“01_compile”中导入bytecode和interface
let {bytecode, interface} = require('./01_compile')
(2)创建Web3实例
// 1.引入web3
let Web3 = require('web3')
// 2.创建web3实例
let web3 = new Web3()
// 3.设置网络
web3.setProvider('http://localhost:7545')
http://localhost:7545是Ganache-cli客户端配置的地址。当部署合约后,可以在客户端上查询到具体的交易。
(3)创建Contract对象
let contract = new web3.eth.Contract(JSON.parse(interface))
因为interface是一个字符串,所以需要通过JSON.parse(interface)把它转换成JSON对象。
(4)部署合约
// 合约拥有者的帐号
const account = '0x4B249c138A04FaE2b48441cAAbde22bF32cB9613'
// 部署合约
contract.deploy({
data : bytecode,
arguments : ['helloworld']
}).send({
from : account,
gas : '3000000',
}).then(instance => {
console.log("contract address : ", instance.options.address)
})
使用deploy方法部署合约的时候需要指定两个参数。data表示bytecode,arguments是合约构造函数的参数,它是一个数组。
部署完成后,记得调用send方法发起一个创建合约的交易。同样它也指定了两个参数。from代表创建合约的帐号,gas是油的数量。因为默认的gas比较少,为了让交易创建成功,建议设置gas的值高一点。
部署成功后,可以在Ganache-cli客户端查看到新创建的合约交易。
(1)创建Web3实例
// 引入web3
let Web3 = require('web3')
// 创建web3实例
let web3 = new Web3()
// 设置网络
web3.setProvider('http://localhost:7545')
(2)指定abi和合约地址
// 从01_compile.js中复制过来
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"}]
// 在02_deploy.js中生成的合约地址
let address = '0xfC005a1aA55432E72A427383d3F5405A6D04287b'
abi和address可以查询之前步骤获取到。
(3)获取合约实例
let contractInstance = new web3.eth.Contract(abi, address)
(4)导出合约实例
// 导出合约实例
module.exports = contractInstance
导出合约实例是为了后面调用合约时候使用。
(1)导入合约实例
let contractInstance = require('./03_instance')
(2)调用合约方法
调用合约需要使用到两个方法:call()和send()。它们都可以发送一个交易并调用方法,但是call不会改变合约状态,而send会改变合约状态。因此,调用getValue方法时候使用call,调用setValue时候,由于会改变合约数据,所以使用send。
contractInstance.methods.getValue().call().then(data => {
console.log("data : ", data)
// 测试合约的setValue方法
contractInstance.methods.setValue("hello jj").send({
from : from,
value : 0,
}).then(res => {
console.log("res : ", res)
contractInstance.methods.getValue().call().then(data => {
console.log("data : ", data)
})
})
})
上面代码的可读性较差,如果使用async和await,代码改为:
let test = async() => {
try {
let v1 = await contractInstance.methods.getValue().call()
console.log('v1 : ', v1)
let res = contractInstance.methods.setValue("hello jj").send({
from: from,
value: 0,
})
console.log('res : ', res)
let v2 = await contractInstance.methods.getValue().call()
console.log('v2 : ', v2)
} catch (e) {
console.log(e)
}
}
call()和send()方法都是返回一个Promise,因此不需要自己new Promise。
上图描述了如何通过Web3去连接以太坊的测试网络。从图上可以看出,如果要部署到测试网络,需要提供一个服务商(Provider)和账户助记词(Account Mnemonic)。
Infura是一个托管的以太坊节点集群,可以将你开发的以太坊智能合约部署到infura提供的节点上,而无需搭建自己的以太坊节点。
其实MetaMask背后就是使用了Infura作为以太坊供应商。
(1)注册供应商帐号
第一步:访问infura网站(https://infura.io),先注册一个帐号。注册完成后登录网站。
第二步:登录成功后,创建一个Project。
第三步:点击View Project按钮,选择网络为Ropsten,然后复制服务商的地址,后面需要用到。
(2)在命令窗口切换到项目路径下,然后执行下面命令安装truffle-hdwallet-provider包。
>> npm install [email protected] --save
(3)修改部署合约代码
第一步:打开02_deploy.js文件,引入infura包。
let hdWalletProvider = require('truffle-hdwallet-provider')
第二步:创建HDWalletProvider对象,传入两个参数:助记词和Provider的地址。
// 助记词,从metamask中复制过来
let mnemonic = 'letter debris ready dog window mountain front truth project bottom merit valley'
// 提供商地址,从infura.io网站复制过来
let providerUrl = 'https://ropsten.infura.io/v3/3d30b778a38b41df8f502d8b8e3ee37b'
// 创建HDWalletProvider对象
let provider = new hdWalletProvider(mnemonic, providerUrl)
第三步:设置Provider。
// web3.setProvider('http://localhost:7545')
web3.setProvider(provider)
第四步:修改帐号地址,这里改为动态获取帐号地址。
web3提供getAccounts方法获取帐号的api。由于该方法是一个异步方法,所以最好把它放入Promise中进行同步。
// 5.部署合约
/*contract.deploy({
data : bytecode,
arguments : ['helloworld'] // 传入合约构造函数参数
}).send({
from : account,
gas : '3000000',
}).then(instance => {
console.log("contract address : ", instance.options.address)
})*/
let deploy = async () => {
// 获取账户,调用该方法每次只会返回一个帐号
let accounts = await web3.eth.getAccounts()
// 创建合约实例
let contractInstance = await contract.deploy({
data : bytecode,
arguments : ['helloworld']
}).send({
from : accounts[0],
gas : '3000000',
})
console.log("contract instance address : ", contractInstance.options.address)
}
deploy()
(4)测试部署
首先在浏览器打开Metamask,并切换到Ropsten测试网络。
接着,找到View on Etherscan按钮,点击后在https://ropsten.etherscan.io中查看当前帐号详情。
找到最后一笔交易,点击交易ID查看交易详情。
至此部署完成。