以太坊DApp开发初探

炒币这么火,咱们来玩玩里面的技术吧

Posted by Forrest Lam on 2018-01-24

关于“以太猫”的流行,相信不少人都有所耳闻,甚至入手养过几只。从游戏性来说,其本质就是一个简单的收集交换类游戏,然鹅,是区块链赋予了它魅力,让用户每一只猫永远不会消失、不被篡改,更重要的是可以炒(滑稽脸),于是今天借此机会一探以太坊应用DApp的开发过程以及开发中遇到的坑。

以太坊DApp介绍

以太坊是一个区块链公有链平台,和比特币类似,以太坊也有其代币–以太币,可在挖矿、交易中获得,然而,说到以太坊和比特币的区别就是其支持智能合约,一个智能合约由代码和数据组成,和其他编程语言中的类类似,一个以太坊分布式应用DApp由众多智能合约组成,每个智能合约都有其独特的地址,可以看做以太坊上的一个账户,可以存取以太币,作用就像一个裁判、中间人。一个简单但不是很恰当的例子就是赌博,我和小明打赌明天会下雨,输的人给赢的人一百块,这种情况我们在现实中一般会以下面两种方法实现:

  • 依靠朋友间的信任。等明天到了,根据下雨与否进行交易。但这种方法一般不可行,因为毕竟是朋友,输的人会自动把昨天的打赌作为玩笑话,而赢的人也碍于面子不好意思要钱,所以交易无法达成。
  • 依靠公证的第三方。OK,我们这次认真点,找一个彼此都认识的朋友小方作为公证,把我和小明的赌注一百块都先存着,等明天到了再给赢的人两百块。这种方法确实比第一种要好,但还是害怕就是第三方拿着两个人的赌注夹带私逃了,这对交易双方的损失更大。

OK,智能合约就是为了解决以上的信任问题而诞生的,由于智能合约存放于区块链,而区块链具有的不可抵赖和不可篡改性,使得智能合约比现实中任意一个机构的公信力都强。其实,区块链去中心化思想最大的优势就是解决了信任问题,而现实中最常见需要解决信任问题的场景莫过于涉及货币交易,从以太坊的众多DApp列表https://www.stateofthedapps.com/ 中看到,大多数都是关于交易、赌博性质的应用,可以说“以太猫”的横空出世刷新了人们对于区块链应用的固有认知。

开发准备

开发以太坊DApp需要安装以下环境或工具,以Mac OS X为例

  • $ brew install node
    以太坊DApp其他开发工具都是通过npm安装的,node.js大法好,mac用户可通过homebrew安装。
  • $ npm install ethereumjs-testrpc
    以太坊提供的区块链测试环境,所有节点都是虚拟的存在内存中,启动后默认创建10个账户。读者也可以选择安装geth搭建私有链,使用真实节点存储,具体可参考http://km.oa.com/group/18297/articles/show/330349
  • $ npm install web3
    以太坊提供读写区块链数据的JavaScript接口,源码地址:https://github.com/ethereum/web3.js/ ,通过web3.js我们可以访问各个账户、部署智能合约、调用合约方法、发起交易等等。
  • $ npm install truffle
    第三方提供的开源以太坊DApp集成工具,源码地址:https://github.com/trufflesuite/truffle ,truffle工具会帮助我们编译、测试、打包和部署DApp项目中的所有合约,类似的还有Meteor(官方推荐工具,但实用下来感觉没有truffle方便,而且文档也较少)。

以下是非必需工具

  • $ npm install truffle-contract
    基于web3.js封装的JavaScript与智能合约交互接口,通过链式调用将对合约的各个操作串联在一起,具体API参考源码地址:https://github.com/trufflesuite/truffle-contract
  • $ npm install express
    node.js社区中基于connect流行的服务器开发框架,本文使用该框架搭建后台服务器,读者可自行选择其他框架。

编程语言

编写一个DApp可以说是包括两部分,合约部分和业务逻辑部分。

智能合约

  • Solidity,类JavaScript,这是以太坊推荐的旗舰语言,也是最流行的智能合约语言,具体用法参考http://solidity.readthedocs.io/en/latest/ ,本文所有合约都使用该语言编写,另外测试、调试Solidity有一个非常好的在线IDE–Remix https://remix.ethereum.org/,由以太坊团队推出的。
  • Serpent,类Python。
  • LLL,类Lisp。

业务逻辑

业务逻辑部分即提供客户端与智能合约交互的接口,相当于目前BS结构中的后台逻辑,因此业务逻辑部分可部署在中心服务器中,而且在以太坊中每个智能合约函数的每一行代码都有固定的gas费用以及延时的,一些简单的逻辑应该交由业务逻辑处理,编写业务逻辑目前提供有以下几种语言:

  • JavaScript,主要是基于Web3.js这个库调用智能合约,本文例子也是使用JavaScript编写的。
  • Go,上述提到的以太坊私链搭建工具geth就是使用Go编写的。
  • Python
  • Java
  • Ruby
  • Haskell
  • Rust

DApp实践

废话不多说,下面我们通过一个DApp例子来窥探一下区块链智能合约的魅力,demo源码地址:http://git.code.oa.com/forrestlin/QzoneBlockPet

Demo功能介绍

该demo是一个卡片收集类游戏,业务场景为每个用户都拥有一只随机的宠物,用户通过收集卡片作用于宠物身上进行装扮,而卡片的收集来源分三种:

  • 系统定期为随机用户生成卡片
  • 与其他用户交换卡片
  • 在卡片商城中购买卡片

Demo目录结构

我们通过$ truffle init命令创建一个DApp项目,truffle会帮我们组织好一个DApp的目录结构,如下所示,其中app目录为笔者添加的,用于存放业务逻辑代码。

  • app
    业务逻辑代码,后面再展开讨论

  • build
    合约编译生成目录,不要手动修改

  • contracts
    合约目录,后面展开讨论

  • migrations
    truffle部署配置文件,新的合约需要部署需要修改里面的配置文件1_initial_migration.js,该demo包含两个合约,加上truffle部署时需要使用的合约,一共三个合约,代码如下所示,当添加一个合约时需要在该文件中添加合约变量而且需要通过deployer部署到区块链,需要注意的是这里当前目录是contracts目录。

    1
    2
    3
    4
    5
    6
    7
    8
    var Migrations = artifacts.require("./Migrations.sol");
    var PetCard = artifacts.require("./PetCard.sol");
    var UserCenter = artifacts.require("./UserCenter.sol");
    module.exports = function(deployer) {
    deployer.deploy(Migrations);
    deployer.deploy(PetCard);
    deployer.deploy(UserCenter);
    };
  • test
    合约的测试文件,我们可以在该目录中存放各个合约的测试代码,类似于其他编程语言中的单元测试,该文章不展开讨论。

  • truffle.js
    区块链网络配置文件,在truffle部署合约时会使用该文件定义的地址,目前配的是testrpc默认测试环境,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    module.exports = {
    networks: {
    development: {
    host: "localhost",
    port: 8545,
    network_id: "*" // Match any network id
    }
    }
    };

Demo运行方式

  1. 安装上述提到的依赖(包括非必需)
  2. $ testrpc
    启动区块链测试环境,可以看到testrpc在内存中为我们创建了10个虚拟账户以及对应的私钥。
  3. $ truffle compile
    编译智能合约,底层调用的是solc编译器,该编译方式是增量的,如果要全量编译,可加上--all参数。
  4. $ truffle migrate --reset
    部署所有智能合约,部署的环境由truffle.js定义,和compile类似,migrate也是增量部署,如果要重新部署所有合约,可加上--reset参数。
  5. $ cd app
  6. $ npm start
    启动服务器
  7. 浏览器访问localhost:8080,目前提供的接口详见INTERFACE.md文件,下面展示其中两个接口。
  • 生成卡片
接口名称 方法 路由参数
createRandomCard GET
例子 返回
/createRandomCard {“cardId”:”2”,”code”:”0x616161666”,”owner”:”0x5727b589bca4500e896ffc82e3fedf56cae7017f”,”value”:”52”}
  • 获取用户所有卡片
接口名称 方法 路由参数
getAllCardsForUser GET /:address
例子 返回
/getAllCardsForUser/0xc3d9b7ea1e42b04dddf3475b464bb1abd5f8451f [{“cardId”:”0”,”code”:”0x616161666”,”value”:”4”}]
需要注意的是上面两个方法调用前都需要设置gas(以太坊交易手续费),不过由于demo运行在testrpc中所有账户的balance都是虚拟的,业务逻辑直接从接口调用方账户扣除了gas,对其屏蔽了该过程,但如果正式部署到生产环境我们需要先询问用户是否愿意付该笔gas然后再真正调用合约接口,因此,以太坊的web3.js提供了estimateGas方法来预估合约函数执行所需的gas。

编写智能合约

智能合约使用Solidity语言编写,语法有点类似于JavaScript,文件名以.sol结尾,通常来说一个.sol文件定义一个合约,相当于Java中一个文件定义一个public class。一个合约通常包含两部分,成员变量和成员函数。
进入本demo的contracts目录,可以看见里面包含了以下文件:

  • Migrations.sol:truffle创建目录时创建的合约,用于部署DApp
  • PetCard.sol:本demo核心合约,定义了宠物卡片合约
  • strings.sol:第三方定义的字符串类库,本demo主要使用了其分割字符串的函数
  • UserCenter.sol:用户中心合约,用于注册用户和查询用户

下面展示的是宠物卡片合约的部分代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
pragma solidity ^0.4.17;

contract PetCard {
struct Card {
bytes32 code; //卡片代码,决定卡片的功能
uint256 value;
address owner;
bool isSelling;
uint sellingPrice;
uint cardId;
}
enum ErrorCode {ERROR_NO_ERROR, ERROR_INDEX_OUT_OF_RANGE, ERROR_WRONG_OWNER, ERROR_CARD_IS_SELLING, ERROR_CARD_IS_NOT_SELLING, ERROR_PRICE_NOT_ENOUGH}
Card[] cards;
address CEO;
function PetCard() public payable {
CEO = msg.sender;
}

// 匿名函数,当外部调用找不到时调用该函数
event FallbackTrigged(bytes data);
function() public payable {
FallbackTrigged(msg.data);
}

event BuyCardEvent(uint cardId, bool isSuccess, ErrorCode errorCode);
// 从卡片商城中购买卡片
function buyCard(uint cardId) public payable {
address buyer = msg.sender;
// 判断card下标是否合法,不合法时退款给买家
if (cardId >= cards.length || cardId < 0) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_INDEX_OUT_OF_RANGE);
return;
}
Card storage card = cards[cardId];
// 判断消费金额是否小于card价格
if (msg.value < card.sellingPrice) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_PRICE_NOT_ENOUGH);
return;
}
// 判断卡片是否正在销售
if (!card.isSelling) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_CARD_IS_NOT_SELLING);
return;
}
// 将卡片卖的钱还给卖家
if (this.balance >= card.sellingPrice) {
card.owner.transfer(card.sellingPrice);
}
card.owner = buyer;
card.isSelling = false;
card.sellingPrice = 0;
BuyCardEvent(cardId, true, ErrorCode.ERROR_NO_ERROR);
}

// 获取用户所有卡片
function getAllCardsForUser() public constant returns (uint[] cardIds, bytes32[] codes, uint[] values, uint len) {
cardIds = new uint[](cards.length);
codes = new bytes32[](cards.length);
values = new uint[](cards.length);
// codes = new string[](cards.length);
len = 0;
for (uint i = 0; i < cards.length; i++) {
if (cards[i].owner == msg.sender) {
cardIds[len] = cards[i].cardId;
codes[len] = cards[i].code;
values[len] = cards[i].value;
len++;
}
}
}

event CreateNewCardEvent(uint cardId, bytes32 code, address owner, uint value);
// 给用户掉落新卡片
function createNewCardForUser(bytes32 code, uint value) public {
Card memory card = Card({code: code, value: value, owner: msg.sender, isSelling: false, cardId: cards.length, sellingPrice: 0});
cards.push(card);
CreateNewCardEvent(card.cardId, card.code, card.owner, card.value);
}

}

定义卡片结构与成员变量

合约内部可以定义多个结构体,关键字为struct,结构体内部也可定义成员变量,允许的类型和合约一样。此外,合约支持数据类型包括以下几种:

  • 整型,uintx / intx,其中x代表整型所占用的位数,从8到256,步长为8,如果我们直接使用uint / int,则与uint256 / int256等价。
  • 布尔型,bool,有true/false两个值。
  • 浮点型,fixedMxN / ufixedMxN,浮点数在Solidity中支持得不是很好,它与其他语言中的浮点数并不一样,Solidity中浮点数在声明时就必须确定长度,而其他语言是可变的,M代表的是浮点数占用的总位数,从8到256,步长为8,N代表小数部分的长度,范围是0-80。
  • 定长字节型,bytesx,其中x代表变量所占字节长度,范围是1-32,当变量打印出来时,显示的是十六进制。
  • 变长字节型,bytes或string,两者区别在于bytes使用十六进制标识,string是用UTF-8表示。
  • 地址,address, 等价于bytes20,而且Solidity为地址变量预设了几个方法,例如,balance方法获取地址对应账户的余额,transfer方法转账以太币到地址对应的账户中,转账者为调用者,收款者为address,另一个方法send类似于transfer也是转账,但值得注意的是,当transfer失败时,会回滚交易并抛出异常,而send方法则不会。
  • 枚举,enum,和其他语言一样,Solidity也支持枚举值,语法也类似,可参考代码中错误码枚举值的定义。

根据上述的数据类型,我们定义卡片的结构体,包括卡片代码、卡片价值、卡片拥有者、卡片是否正在出售、卡片出售价格以及卡片id。然后,定义了函数执行可能会发生的错误码,还有一个卡片的集合以及合约的创建者CEO。

1
2
3
4
5
6
7
8
9
10
11
struct Card {
bytes32 code; //卡片代码,决定卡片的功能
uint256 value;
address owner;
bool isSelling;
uint sellingPrice;
uint cardId;
}
enum ErrorCode {ERROR_NO_ERROR, ERROR_INDEX_OUT_OF_RANGE, ERROR_WRONG_OWNER, ERROR_CARD_IS_SELLING, ERROR_CARD_IS_NOT_SELLING, ERROR_PRICE_NOT_ENOUGH}
Card[] cards;
address CEO;

函数

在Solidity中函数的定义语法是

function 函数名(参数列表) 修饰符 returns (返回值列表)

这里值得注意的是,在函数生命中返回值列表我们可以声明返回值的名字,类似于形参,当在函数体中给返回值变量赋值后,我们可以不用写return,但如果写了还是以return为主,同时,一个函数返回值支持多个,调用者拿到的将是一个返回值数组,和python有点像。

另外,EVM会给每个合约的函数传入一个名为msg的对象,该对象包含几个属性,如sender是调用者账户地址、value是调用者执行该函数支付的以太币(单位是wei)、data是函数调用的描述。除了data外,其他属性的值是由调用者传入,详见业务逻辑代码的介绍。

构建函数和匿名函数

和大部分语言一样,Solidity中每个合约也有构建函数,在构建函数中我们可以做一些初始化的操作,在下面的代码中我们注意到函数后有两个修饰符,分别是publicpayable,其中public说明该函数外部合约也可见,对应的还有externalprivateinternal,要说到这四者的区别,需要查看函数的调用方式和可见性,本文就不展开了。然后payable说明该函数会涉及货币交易,同时当我们在一个合约的其他函数中调用了转账操作,那么构建函数必须也得声明为payable
匿名函数,也就是没有名字的函数,每个合约中最多可定义一个,当其他地方调用该合约不存在的函数或者出现异常时,EVM(以太坊智能合约执行虚拟机)会自动调用合约的匿名函数,同样地,当合约内其他函数有转账操作时匿名函数也需要加上payable修饰。

1
2
3
4
5
6
7
8
9
function PetCard() public payable {
CEO = msg.sender;
}

// 匿名函数,当外部调用找不到时调用该函数
event FallbackTrigged(bytes data);
function() public payable {
FallbackTrigged(msg.data);
}

事件

代码中我们定义了多个event,每个event只需要定义其名字和参数列表即可以,其作用相当于其他语言中的log,在函数中传入实参即可记录,虽说event的作用和log一样,但在Solidity中作用却非同小可,因为当一个函数是以transaction的形式被调用,调用者是无法拿到返回值的,因为transaction的调用是异步的,EVM无法立刻执行给出返回值,所以调用者只能通过event的记录取得函数执行后的数据,具体操作流程见业务逻辑代码的介绍。

购买卡片

定义购买卡片的函数,函数一开始我们写了三个是否合法的判断,这里可以使用require关键字对这些条件进行限定,但由于笔者希望调用者可以接收到错误信息,这里就使用了四个if判断,并且使用了事件通知调用者,同时当条件不满足时我们需要做一些回滚操作,例如将金额退还给调用者账户。而当条件满足后,我们将卡片定价转给卖家,转移卡片拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
event BuyCardEvent(uint cardId, bool isSuccess, ErrorCode errorCode);
// 从卡片商城中购买卡片
function buyCard(uint cardId) public payable {
address buyer = msg.sender;
// 判断card下标是否合法,不合法时退款给买家
if (cardId >= cards.length || cardId < 0) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_INDEX_OUT_OF_RANGE);
return;
}
Card storage card = cards[cardId];
// 判断消费金额是否小于card价格
if (msg.value < card.sellingPrice) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_PRICE_NOT_ENOUGH);
return;
}
// 判断卡片是否正在销售
if (!card.isSelling) {
buyer.transfer(msg.value);
BuyCardEvent(cardId, false, ErrorCode.ERROR_CARD_IS_NOT_SELLING);
return;
}
// 将卡片卖的钱还给卖家
if (this.balance >= card.sellingPrice) {
card.owner.transfer(card.sellingPrice);
}
card.owner = buyer;
card.isSelling = false;
card.sellingPrice = 0;
BuyCardEvent(cardId, true, ErrorCode.ERROR_NO_ERROR);
}

遍历卡片

该函数的作用是获取所有属于调用者账户的卡片,值得注意的是,该函数在EVM中是一个昂贵的操作,首先我们声明了三个定长数组(定长是和临时变量存储的地方有关),每个长度都等于所有卡片数组的大小,因此每个数组都已经开销了不少gas,然后遍历又是一个耗时操作,又需要花费gas,而且函数在编译时并不知道cards的长度,所以即使调用者使用estimategas函数预估该函数所需gas也是不准确的,这对于调用者是危险的,随时都可能因为gas不够而执行失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getAllCardsForUser() public constant returns (uint[] cardIds, bytes32[] codes, uint[] values, uint len) {
cardIds = new uint[](cards.length);
codes = new bytes32[](cards.length); //这里不能用string,solidity不支持定长的变长数组
values = new uint[](cards.length);
// codes = new string[](cards.length);
len = 0;
for (uint i = 0; i < cards.length; i++) {
if (cards[i].owner == msg.sender) {
cardIds[len] = cards[i].cardId;
codes[len] = cards[i].code;
values[len] = cards[i].value;
len++;
}
}
}

生成卡片

这里生成卡片的逻辑交给业务层,合约只负责根据参数创建一个新的卡片,最后通知调用者即业务层。

1
2
3
4
5
6
7
event CreateNewCardEvent(uint cardId, bytes32 code, address owner, uint value);
// 给用户掉落新卡片
function createNewCardForUser(bytes32 code, uint value) public {
Card memory card = Card({code: code, value: value, owner: msg.sender, isSelling: false, cardId: cards.length, sellingPrice: 0});
cards.push(card);
CreateNewCardEvent(card.cardId, card.code, card.owner, card.value);
}

编写业务逻辑

合约编写完成后,可先到Remix上测试,测试通过后再使用truffle编译和部署到区块链上。之后,便是业务逻辑的编写了。
由于truffleweb3等都是依赖于node.js,为了一致性与方便性,本demo也是使用node.js构建业务服务器,主要依赖的模块是expresstruffle-contract,前者用于更方便的业务路由和模块化,后者用于更方便调用合约。
打开app目录,我们会看到一下的文件结构:

  • PetCard.js:宠物卡片业务路由处理以及合约交互
  • UserCenter.js:用户中心,负责用户注册和获取所有用户的上层调用
  • UserCenterCore.js:用户中心核心,负责业务层与合约层交互
  • Web3Provider.js:定义Web3连接的是区块链地址
  • package.json:定义npm运行所需要的命令和依赖
  • server.js:业务层总入口,负责默认页面、404页面处理,以及各业务模块的中转路由,还有定义服务器绑定的端口

下面我们主要看PetCard.js中业务层是如何与合约层进行交互的。

获取合约示例

这一步我们首先获取宠物卡片合约和用户中心合约的实例,便于下面调用合约,这里我们需要依赖truffle-contract还有本地的Web3Provider模块。而truffle-contract的用法都是链式调用,通过then函数连接起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract = require('truffle-contract');
provider = require('./Web3Provider.js');
express = require("express");

const PetCard = contract(require('../../build/contracts/PetCard.json'));
PetCard.setProvider(provider);
var petCard;
PetCard.deployed().then(function(instance){
petCard = instance;
});

var userCenter;
require('./UserCenterCore.js').then(function(instance) {
userCenter = instance;
});
var app = module.exports = express();

购买卡片

从下面代码中可以看到,业务层接受客户端传递的路由参数,再传入合约层,这里合约层函数的参数分两种,一种是自定义参数,另一种就是EVM预设参数,而预设参数是一个对象,需要在最后传入,正如上面Solidity函数介绍,预设参数对象需要包括from为调用者地址,value为传入合约的以太币。最后,由于这是直接通过合约实例调用函数,是一个transaction操作,因此如上面Solidity事件介绍,我们需要从返回值的日志中获取合约执行后的数据。由于日志拿到的事件参数是一个对象,所以我们直接以json形式返回给客户端即可,例如下面的返回就表示卡片购买失败,原因是卡片当前不在销售:{“cardId”:”1”,”isSuccess”:false,”errorCode”:”4”}。

1
2
3
4
5
6
7
8
app.get('/buyCard/:address/:cardId/:price', function(req, res) {
petCard.buyCard(req.params.cardId, {from: req.params.address, value: req.params.price}).then(function(result) {
if (result.logs.length > 0) {
var eventObj = result.logs[0].args;
res.send(JSON.stringify(eventObj));
}
});
});

遍历所有卡片

遍历卡片的操作并不涉及永久写入合约数据的操作,因此遍历卡片这里我们不使用transaction,而使用call的形式,因此我们可以直接拿到函数的返回值,然后由于函数返回多个值,因此result是一个数组。这里需要注意的是,上面我们说到遍历卡片时合约需要创建三个未知长度的数组,而且遍历的次数也是未知的,因此,estimategas函数预估的gas会不准确,我们这里直接给一个比较大的gas值。该接口返回的例子如:[{“cardId”:”0”,”code”:”0x616161666”,”value”:”4”}]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/getAllCardsForUser/:address', function(req, res) {
// 因为这需要创建未知长度数组,estimate 估计的gas会不准确,该方法慎调
petCard.getAllCardsForUser.call({from: req.params.address, gas: 3000000}).then(function(result) {
if (result.length >= 4) {
var cardIds = result[0], codes = result[1], values = result[2];
var len = result[3];
var cards = [];
for (var i = 0; i < len; i ++) {
cards.push({cardId: cardIds[i], code: codes[i], value: values[i]});
}
res.send(JSON.stringify(cards));
}
});
});

生成卡片

生成卡片的逻辑是在所有用户随机挑选一个用户作为卡片的拥有者,然后卡片的code这里先简单地写死了一串,后续可以想更好玩的code生成逻辑,接着就是调用estimateGas函数估计所需的gas,最后才是真正调用合约函数,传入预估的gas,其实比较好的交互应该像以太猫那样,在进行真正的调用之前告知用户交易所需的gas,并可以让用户调整,用户确认后再执行合约函数。下面是生成卡片调用后返回的一个例子:{“cardId”:”2”,”code”:”0x616161666”,”owner”:”0x5727b589bca4500e896ffc82e3fedf56cae7017f”,”value”:”52”}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
app.get('/createRandomCard', function(req, res) {
var allUsers,
randomUser;
userCenter.showAllPlayers.call().then(function(result){
allUsers = result;
randomIdx = Math.floor(Math.random() * allUsers.length);
randomUser = allUsers[randomIdx];
if (randomUser != undefined) {
var cardCode = "aaaforestlinbbb";
var cardValue = Math.floor(Math.random() * 100 + 1);
petCard.createNewCardForUser.estimateGas(cardCode, cardValue).then(function(esti_gas) {
return petCard.createNewCardForUser(cardCode, cardValue, {from: randomUser, gas: esti_gas});
}).then(function(rest) {
if (rest.logs.length > 0) {
var eventObj = rest.logs[0].args;
res.send(JSON.stringify(eventObj));
}
});
} else {
res.send("random user is undefined");
}
});
});

总结DApp开发中遇到的坑

一个DApp开发流程介绍到此结束,下面总结一下开发中值得注意的地方:

  • Solidity这个语言目前还不是很完善,版本还是0.4.x,而且文档相对其他语言较少,这里除了官网,还推荐两个论坛区块链技术博客以太坊爱好者供大家参考。
  • 合约函数中慎用未知长度的数组以及遍历操作,比较耗费gas,而且对于调用者极不友好,无法预估gas。
  • 对于不需要写操作的函数,我们可以加上constant修饰符或者调用时使用call的方法而非直接调用,不产生transaction,也就不需要写入区块链。
  • 对于不需要的数组我们可以使用delete操作删除整个数组或者某个元素,可以归还一些gas,但是最好复用,使用指示器标记当前使用的长度,因为delete操作本身也是需要耗费gas的。
  • 合约内不适合做业务过重的操作,如上面的生成卡片操作,应该将逻辑放在业务层,毕竟在EVM中没执行一行代码都是需要gas的,合约应该只有读写区块链的操作。