第03课:Hello

第03课:Hello world 合约开发

绝大部分开发者学习一门语言的时候,都是从输出一个 Hello World 开始。我们也从实现一个 Hello World 合约为切入点,开始进入智能合约的世界吧。

环境准备

前面已经介绍了 Nodenpm 的安装。本系列文章依赖的环境版本:

Node:v8.9.0 Npm:5.5.1

接下来,在你的代码目录里,创建名为 smartcontract 的文件夹,并创建如下两个文件 package.jsonHello.sol

smartcontract ├── Hello.sol └── package.json

package.json 文件里,添加如下依赖包配置:

{
  "name": "smartcontract",
  "version": "0.0.1",
  "dependencies": {
    "fs": "0.0.1-security",
    "solc": "^0.4.21",
    "web3": "^0.20.0"
  }
}

fs 模块用于文件的相关操作; solc 模块是编译器; web3 模块是以太坊提供的工具包,主要用于与合约的通信。

接下来执行 npm install 下载相关的依赖包。

编写合约代码

环境准备好后,就可以开始编写合约代码了。 打开 Hello.sol 文件,编写代码如下:

//pragma关键字:版本申明。
//用来指示编译器将代码编译成特定版本,以免引起兼容性问题
//此处不支持0.4.0之前的编译器,也不支持0.5.0之后的编译器(条件为 ^)
pragma solidity ^0.4.0;

//contract关键字:合约申明
//和Java、PHP中的class类似
//此处是申明一个名为Hello的合约
contract Hello {

    //public: 函数访问属性(后续文章为详细阐述)
    //returns (string): 定义返回值类型为string
    function say(string name) public returns (string) {
        return name;
    }
}

代码很简单。就是实现了用户输入什么字符串,合约就原样返回的操作。

接下来,我们需要编写合约部署脚本。

合约部署脚本

smartcontract 目录下,新建名为 deploy.js 的文件。代码如下:

//设置web3连接
var Web3 = require('web3');
//http://localhost:7545 为Ganache提供的节点链接
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
//读取合约
var fs = require('fs');
var contractCode = fs.readFileSync('Hello.sol').toString();
//编译合约代码
var solc = require('solc');
var compileCode = solc.compile(contractCode);

console.log(compileCode);

//获取合约abi和字节码
var abi = JSON.parse(compileCode.contracts[':Hello'].interface);
var byteCode = compileCode.contracts[':Hello'].bytecode;
//创建合约对象
var VotingContract = web3.eth.contract(abi);
//部署合约,并返回部署对象
var deployedContract = VotingContract.new({
    data:byteCode,
    from:web3.eth.accounts[0],  //部署合约的外部账户地址
    gas:750000        //部署合约的矿工费
});
console.log(deployedContract);

代码里我加上了简单的注释。这里解释一下 abi 这个概念。

abi 全称是 Application Binary Interface,即应用程序二进制接口。简单的说,就是合约对外的接口描述。

需要注意的是,矿工费 Gas 为750000。以太坊上每笔交易的执行(被矿工打包)都会被收取一定数量的 Gas。Gas 的目的是限制执行交易所需的工作量,同时为执行支付费用。当 EVM 执行交易时,Gas 将按照特定规则被逐渐消耗,无论执行到什么位置,一旦 Gas 被耗尽,将会触发一个 out of gas 异常。当前调用帧所做的所有状态修改都将被回滚。如果执行结束,还有Gas剩余,这些 Gas 将会返还给发送账户。因此,如果部署时抛出 out of gas 的异常,我们可适当的提高 Gas 值。

合约部署

在当前目录下,执行 node deploy.js 命令。我们在部署脚本里将 compileCode 变量打印出来了,粗略看看就行:

{ contracts:
   { ':Hello':
      { assembly: [Object],
        bytecode: '6060604052341561000f57600080fd5b61016c8061001e6000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063d5c6130114610046575b600080fd5b341561005157600080fd5b6100a1600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190505061011c565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100e15780820151818401526020810190506100c6565b50505050905090810190601f16801561010e5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61012461012c565b819050919050565b6020604051908101604052806000815250905600a165627a7a72305820ff14cafd1df21e1edf19eff7598bc82a98940cc0fe045d6107d04bb224014f990029',
        functionHashes: [Object],
        gasEstimates: [Object],
        interface: '[{"constant":false,"inputs":[{"name":"name","type":"string"}],"name":"say","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]',
        metadata: '{"compiler":{"version":"0.4.21+commit.dfe3193c"},"language":"Solidity","output":{"abi":[{"constant":false,"inputs":[{"name":"name","type":"string"}],"name":"say","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"function"}],"devdoc":{"methods":{}},"userdoc":{"methods":{}}},"settings":{"compilationTarget":{"":"Hello"},"evmVersion":"byzantium","libraries":{},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"":{"keccak256":"0x2e3dd18fbfbd17bb4f866b1bfbb38082172a0bb58d9396b63bab04e67d9d8e08","urls":["bzzr://d1aae746dfab03e712d8a3cb76b7d4b5bf60f48fafbffa04dfa8a2d53ad5d0ca"]}},"version":1}',
        opcodes: 'PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH2 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0x16C DUP1 PUSH2 0x1E PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH2 0x41 JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0xD5C61301 EQ PUSH2 0x46 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE ISZERO PUSH2 0x51 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH2 0xA1 PUSH1 0x4 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 DUP3 ADD DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 DUP1 DUP1 PUSH1 0x1F ADD PUSH1 0x20 DUP1 SWAP2 DIV MUL PUSH1 0x20 ADD PUSH1 0x40 MLOAD SWAP1 DUP2 ADD PUSH1 0x40 MSTORE DUP1 SWAP4 SWAP3 SWAP2 SWAP1 DUP2 DUP2 MSTORE PUSH1 0x20 ADD DUP4 DUP4 DUP1 DUP3 DUP5 CALLDATACOPY DUP3 ADD SWAP2 POP POP POP POP POP POP SWAP2 SWAP1 POP POP PUSH2 0x11C JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 DUP1 PUSH1 0x20 ADD DUP3 DUP2 SUB DUP3 MSTORE DUP4 DUP2 DUP2 MLOAD DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP DUP1 MLOAD SWAP1 PUSH1 0x20 ADD SWAP1 DUP1 DUP4 DUP4 PUSH1 0x0 JUMPDEST DUP4 DUP2 LT ISZERO PUSH2 0xE1 JUMPI DUP1 DUP3 ADD MLOAD DUP2 DUP5 ADD MSTORE PUSH1 0x20 DUP2 ADD SWAP1 POP PUSH2 0xC6 JUMP JUMPDEST POP POP POP POP SWAP1 POP SWAP1 DUP2 ADD SWAP1 PUSH1 0x1F AND DUP1 ISZERO PUSH2 0x10E JUMPI DUP1 DUP3 SUB DUP1 MLOAD PUSH1 0x1 DUP4 PUSH1 0x20 SUB PUSH2 0x100 EXP SUB NOT AND DUP2 MSTORE PUSH1 0x20 ADD SWAP2 POP JUMPDEST POP SWAP3 POP POP POP PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH2 0x124 PUSH2 0x12C JUMP JUMPDEST DUP2 SWAP1 POP SWAP2 SWAP1 POP JUMP JUMPDEST PUSH1 0x20 PUSH1 0x40 MLOAD SWAP1 DUP2 ADD PUSH1 0x40 MSTORE DUP1 PUSH1 0x0 DUP2 MSTORE POP SWAP1 JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 SELFDESTRUCT EQ 0xca REVERT SAR CALLCODE 0x1e 0x1e 0xdf NOT 0xef 0xf7 MSIZE DUP12 0xc8 0x2a SWAP9 SWAP5 0xc 0xc0 INVALID DIV 0x5d PUSH2 0x7D0 0x4b 0xb2 0x24 ADD 0x4f SWAP10 STOP 0x29 ',
        runtimeBytecode: '606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063d5c6130114610046575b600080fd5b341561005157600080fd5b6100a1600480803590602001908201803590602001908080601f0160208091040260200160405190810160405280939291908181526020018383808284378201915050505050509190505061011c565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100e15780820151818401526020810190506100c6565b50505050905090810190601f16801561010e5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b61012461012c565b819050919050565b6020604051908101604052806000815250905600a165627a7a72305820ff14cafd1df21e1edf19eff7598bc82a98940cc0fe045d6107d04bb224014f990029',
        srcmap: '25:102:0:-;;;;;;;;;;;;;;;;;',
        srcmapRuntime: '25:102:0:-;;;;;;;;;;;;;;;;;;;;;;;;47:78;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;23:1:-1;8:100;33:3;30:1;27:10;8:100;;;99:1;94:3;90:11;84:18;80:1;75:3;71:11;64:39;52:2;49:1;45:10;40:15;;8:100;;;12:14;47:78:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;89:6;;:::i;:::-;114:4;107:11;;47:78;;;:::o;25:102::-;;;;;;;;;;;;;;;:::o' } },
  errors:
   [ ':5:5: Warning: Function state mutability can be restricted to pure\n    function say(string name) public returns (string) {\n    ^ (Relevant source part starts here and spans across multiple lines).\n' ],
  sourceList: [ '' ],
  sources: { '': { AST: [Object] } } }

打开 Ganache 的 LOGS 面板,可以看到部署产生的交易(Transaction)日志:

transaction

箭头所指就是部署成功后的合约地址:0xbf474d24ba8b19811db5deb51137ddccbe3ff288(每个人部署后的地址可能都不相同)。

我们记录下来,后面的合约调用代码里需要用到。同时,也可以打开 ACCOUNTS 面板,观察账户余额的变化。

合约调用

合约部署成功之后,我们写一段代码来调用合约里的 say() 方法,检测一下效果。在 smartcontract 目录下,新建名为 run.js 的文件,代码如下:

//设置web3连接
var Web3 = require('web3');
//http://localhost:7545 为Ganache提供的节点链接
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
//读取合约
var fs = require('fs');
var contractCode = fs.readFileSync('Hello.sol').toString();
//编译合约代码
var solc = require('solc');
var compileCode = solc.compile(contractCode);
//获取合约abi和字节码
var abi = JSON.parse(compileCode.contracts[':Hello'].interface);
var byteCode = compileCode.contracts[':Hello'].bytecode;
//创建合约对象
var VotingContract = web3.eth.contract(abi);
//0xbf474d24ba8b19811db5deb51137ddccbe3ff288为合约部署地址
var contractInstance = VotingContract.at("0xbf474d24ba8b19811db5deb51137ddccbe3ff288");

var result = contractInstance.say.call('Hello world');
console.log(result);

我们来执行 node run.js 命令,可以看到在终端里输出了 Hello world

字符串拼接

我们可以扩展一下。如果我们事先在合约里定义好 Hello 字符串,如何与 name 变量进行字符串拼接?

在智能合约里进行字符串的拼接可不是一件简单的事情。我们先用绝大部分编程语言都通用的连接符 +. 来尝试着修改代码:

//pragma关键字:版本申明。
//用来指示编译器将代码编译成特定版本,以免引起兼容性问题
//此处不支持0.4.0之前的编译器,也不支持0.5.0之后的编译器(条件为 ^)
pragma solidity ^0.4.0;

//contract关键字:合约申明
//和Java、PHP中的class类似
//此处是申明一个名为Hello的合约
contract Hello {

    string str="Hello ";

    //public: 函数访问属性(后续文章为详细阐述)
    //returns (string): 定义返回值类型为string
    function say(string name) public returns (string) {
        return str + name;
    }
}

运行 node deploy.js 部署脚本后,抛出了一个异常:

TypeError: Operator + not compatible with types string storage ref and string memory
return str + name;

我们将其修改为“.”并尝试部署后,也抛出一个异常:

TypeError: Member "name" not found or not visible after argument-dependent lookup in string storage ref return str . name;

说明在智能合约里,“+”和“.”都不是连接符。通过查阅 Solidity 官方文档,我们发现 Solidity 语言并不提供字符串连接符的语法:

Solidity文档

那有没有其他方式,可以实现字符串的拼接功能呢?

以太坊核心开发团队已经为其提供了字符串的扩展类。从 strings.sol 处下载字符串工具合约,保存到 smartcontract 目录中:

smartcontract
  ├── Hello.sol
  ├── deploy.js
  ├── node_modules
  ├── package.json
  ├── run.js
  └── strings.sol

修改 Hello.sol 代码:

pragma solidity ^0.4.0;

//导入 strings.sol 工具合约
import "./strings.sol"; //注意这里

contract Hello {

    //将 strings 工具合约用于所有数据类型
    using strings for *;    //注意这里

    string str="Hello ";

    function say(string name) public returns (string) {
        return str.toSlice().concat(name.toSlice());
    }
}

运行部署脚本 node deploy.js 后,抛出一个异常:

ParserError: Source "strings.sol" not found: File not supplied initially. import "./strings.sol"

提示 strings.sol 文件找不到,这并不是合约代码有问题,而是 solc 编译器 的问题。

solc 编译器 目前不支持关联文件的自动引入。我们期望编译器以更智能的方式引入关联的文件,但是这意味着它至少需要 fs 模块,而 fs 模块反过来又增加了 solc 模块的大小,同时也增加了不必要的依赖。

所以,我们需要修改部署脚本文件 deploy.js 的代码:

//设置web3连接
var Web3 = require('web3');
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
//读取strings.sol和hello.sol两个合约     
var fs = require('fs');
var contractCode = {     //注意这里的变化
    "strings.sol":fs.readFileSync('strings.sol').toString(),
    "Hello.sol":fs.readFileSync('Hello.sol').toString()
};
//编译合约代码
var solc = require('solc');
var compileCode = solc.compile({    //注意这里的变化
    sources:contractCode
},1);

console.log(compileCode);

//获取合约abi和字节码
var abi = JSON.parse(compileCode.contracts['Hello.sol:Hello'].interface);  //注意这里的变化
var byteCode = compileCode.contracts['Hello.sol:Hello'].bytecode;   //注意这里的变化

//创建合约对象
var VotingContract = web3.eth.contract(abi);
//部署合约,并返回部署对象
var deployedContract = VotingContract.new({
    data:byteCode,
    from:web3.eth.accounts[0],
    gas:750000
});
console.log(deployedContract);

读取 strings.solHello.sol 两个合约文件,并一同编译即可。再次执行 node deploy.js 命令,合约部署成功。记录下合约地址:0x58a73dea66cea789dcfb5b4a94b7247ca010b781(每个人执行的合约地址可能都不相同)。

同时,合约调用脚本 run.js 的代码修改部分和 deploy.js 相同:

//设置web3连接
var Web3 = require('web3');
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:7545'));
//读取strings.sol和hello.sol两个合约 
var fs = require('fs');
var contractCode = {
    "strings.sol":fs.readFileSync('strings.sol').toString(),     //注意这里的变化
    "Hello.sol":fs.readFileSync('Hello.sol').toString()   //注意这里的变化
};
//编译合约代码成字节码
var solc = require('solc');
var compileCode = solc.compile({   //注意这里的变化
    sources:contractCode
},1);
//获取合约abi和字节码
var abi = JSON.parse(compileCode.contracts['Hello.sol:Hello'].interface);  //注意这里的变化
var byteCode = compileCode.contracts['Hello.sol:Hello'].bytecode;   //注意这里的变化

//创建合约对象
var VotingContract = web3.eth.contract(abi);
var contractInstance = VotingContract.at("0x58a73dea66cea789dcfb5b4a94b7247ca010b781");

var result = contractInstance.say.call('world');
console.log(result);
console.log(contractInstance.say.call('Guys'));

执行脚本后,可以看到终端打印出了两行字符串:

Hello world Hello Guys

strings.sol

工具合约中还提供了字符串的比较、查找、切割等方法,大家可以动手尝试。本篇不做过多介绍。

至此,第一个智能合约 Hello world 的代码编写、编译、部署、 调用的过程就完成了。建议大家动手执行一遍,加深理解。

上一篇
下一篇
内容互动
写评论
加载更多
评论文章