Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

手把手教你Wasm合约开发(C++篇)

放弃六月们
104 0 0
Ontology Wasm 自从上线测试网以来便受到了社区开发人员的极大关注。 Wasm 的上线将使得业务逻辑复杂的 dApp 合约上链成本降低,极大丰富 dApp 生态。在进行 Wasm 合约开发时,开发者不仅可以使用 Rust,还可以使用 C++ 作为合约开发语言。本期我们将通过两个简单的示例来示范如何使用 C++ 进行Wasm 合约开发。
; ]8 F7 H+ B: H# z3 s一、Hello World# o$ j9 j4 _- V- f
按照惯例,我们还是从一个 Hello world 开始( t- k+ d0 X) a6 l9 Y. x
#include#include
& G1 \( b' c- O& c9 Jusing namespace ontio;class hello:public contract {5 ~0 E. ^/ F3 G( R1 x  t
public:
8 n1 L1 W( N/ b1 i4 i. ^using contract::contract:/ ?% a' D" s+ q" A, r4 `+ H
void sayHello(){
; r: ~) p5 k; k, ?) T. lprintf("hello world!");
* B* u# {3 L8 m; W}: j# n' q0 {4 m
};1 k1 F; y& f. z+ w0 v
ONTIO_DISPATCH(hello, (sayHello));( F  X0 Z/ ~8 Y; W8 ]
1.1 合约入口
+ G. h( |+ f# i) D/ yOntology Wasm CDT 编译器已经对入口和参数解析进行了封装,所以开发者不需要重新定义入口方法。接下来是定义合约的对外接口,这是智能合约对外提供服务的方法。0 U# O3 h: X8 Y# V4 W9 ^) i; B
ONTIO_DISPATCH(hello, (sayHello));+ F0 y/ H4 A7 {8 k7 |
在上面的例子中, 我们暂时只支持 sayHello 这个方法:5 {. |$ J) {3 s3 l2 h
printf("hello world!");
5 M( F/ K  x4 a- z' x这个“Hello world!”会在节点的日志中以调试信息打印出来。在实际的应用中, printf 只能用作调试的目的, 一个实际的智能合约,需要实现更多更复杂的功能。
# K5 ^, o8 `' V7 `# u1.2 智能合约 API' O" C2 L! X" j  X0 M* B
Ontology Wasm 提供如下 API 与区块链的底层进行交互:
9 _- k; g5 H& [) i1 s6 Z3 Y3 @5 m
  s& {5 C$ v# y% U& F0 ?+ \: q二、红包合约( J" \0 N, i, z7 `3 o# q
下面我们通过一个更加复杂的例子来演示如何通过这些 API 来开发一个完整的 Wasm 智能合约。
$ w: p, d: P* d$ ]/ [很多情况下我们都会通过各种 App,如微信等聊天工具发红包。我们可以给朋友发送红包,也可以抢其他人发送的红包,收到的钱会记入到个人微信账户中。* ?2 d. p# @; I
类似于微信的流程,我们将尝试创建一个智能合约。用户使用该合约,可以发送 ONT,ONG 或者是标准的 OEP-4的 Token 资产红包给他的朋友们,而朋友们抢到的红包可以直接转入到他们的钱包账户中。$ b6 f+ f" N! I; ^0 S7 a: ?
2.1 创建合约" r& L: Y- d) M
首先,我们需要新建合约的源文件,暂且命名为 redEnvelope.cpp。这个合约我们需要三个接口:9 F, c3 a2 K) I2 }& F
createRedEnvelope: 创建红包
& ?( `7 I8 Y7 {. [" AqueryEnvelope: 查询红包信息
7 ]/ \( g- N9 ?3 uclaimEnvelope: 抢红包
2 {, }$ b1 ]+ V. M; f#include
/ U3 {, {' w: I2 S9 uusing namespace ontio;
, D. G3 b- @+ k) l+ t3 aclass redEnvelope: public contract{6 N6 K1 W2 y0 A; E
}0 ~" n. c" s/ g: z
ONTIO_DISPATCH(redEnvelope, (createRedEnvelope)(queryEnvelope)(claimEnvelope));3 \4 v5 s3 L5 B0 d
我们需要在存储中保存一些关键的数据。在智能合约中, 数据以 KV 的形式保存在该合约的上下文空间中,这些数据的 KEY 需要设置前缀以便于后面的查询。下面定义了三个不同的前缀供使用:# L) C& N5 _0 f
std::string rePrefix = "RE_PREFIX_";
; y) F# [9 t; c& ?! U* j2 ?4 bstd::string sentPrefix = "SENT_COUNT_";
; |2 g1 [" Z4 E' Y7 n! J: t! p& G" Qstd::string claimPrefix = "CLAIM_PREFIX_"/ G, a2 \8 M( d5 U4 F% C; k3 ?7 V
因为我们的合约支持 ONT 和 ONG 这两种 Ontology 的原生资产, 我们可以预先定义好这两种资产的合约地址。不同于标准的智能合约, Ontology 原生合约(native contract)的合约地址是固定的,而不是根据合约代码的 hash 计算而来的。# m$ K: H# C  x: h6 H
address ONTAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};3 u* x! d- O- r) V" a4 |
address ONGAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2};- E( N1 n" _$ ^- h3 }# v
我们需要在合约中保存红包的信息, 如红包的资产信息(token 的合约地址, 红包的总金额, 红包的个数等等)3 @7 f( U6 O2 S: U
struct receiveRecord{# w4 @& S* N2 |8 o
address account; //用户地址
) a# B, s. _( u: }asset amount; //抢到的金额
% v5 X0 w! y6 [$ E/ Y7 A  C# L4 iONTLIB_SERIALIZE(receiveRecord,(account)(amount))
7 R2 T8 M- E) U$ i+ i};" ~' b9 O1 w( i" f: r
struct EnvelopeStruct{
+ k( J& w2 b+ l, [address tokenAddress; //资产token的地址/ s0 }# K8 d# E4 \1 _, @  P! o6 A
asset totalAmount; //红包总金额
1 L& x3 m  t- f2 B# A+ C" J# oasset totalPackageCount; //红包总数( e2 z! Q0 O, p4 ]0 \
asset remainAmount; //当前剩余的金额
  \, x1 ?( }- ~/ Z0 n7 X) Sasset remainPackageCount; //当前剩余的红包数
; u, [8 g8 a/ M6 qstd::vector records; //已经抢完的记录) |  e% @6 V3 F( s5 e, i
ONTLIB_SERIALIZE( EnvelopeStruct, (tokenAddress)(totalAmount)(totalPackageCount)(remainAmount)(remainPackageCount)(records) )) B3 M# L3 \& u5 s" \8 u' ~# d9 k2 e: h
};
- ?  q" R8 J. a+ a其中,5 ~$ ^( w. I& t! G( F2 z! }
ONTLIB_SERIALIZE(receiveRecord,(account)(amount))- x0 z0 Q9 q% y9 ]
是由 Ontology Wasm CDT 定义的宏操作,用于在将 struct 存储前进行序列化的操作。
9 f% Z8 c: _) V3 N. O2.2 创建红包0 n! e. p/ a: _& f& h! S2 |3 X7 q
准备工作差不多了,下面我们开始开发具体的接口逻辑。
  N) _$ ?" L+ q; |创建红包需要指定创建者地址, 红包数量, 红包金额和资产的合约地址:; y8 i0 C+ K" H, R, I3 p# N

( ]+ N5 {  D, l) C1 Ibool createRedEnvelope(address owner,asset packcount, asset amount,address tokenAddr ){  M  V4 S8 S. l
return true;9 M2 B' D! L! C2 m- I% W3 ]
}
, x: b1 e6 }; I" U: K6 k检查是否有创建者的签名, 否则交易回滚退出:4 i- `8 p' O" h
, L" D8 ~/ K8 I: A" o( l, {0 c/ y
ontio_assert(check_witness(owner),"checkwitness failed");
" W$ X( ~' m' O3 c1 F* nNOTE: ontio_assert(expr, errormsg):当 expr 为 false 时, 抛出异常并退出。
/ x0 ?  u+ J0 V+ B如果红包资产是 ONT,由于 ONT 的不可分割性(最小为1个 ONT), 红包的金额要大于或等于红包的数量,保证每个红包最少有1个 ONT:
# [6 y0 n( r  Z, G! x. u1 `, {$ }
3 H! Y( u  T2 m: q& Sif (isONTToken(tokenAddr)){6 u5 c4 S* C  T# A: D* `
ontio_assert(amount >= packcount,"ont amount should greater than packcount");5 |7 o' V/ c! ?  {9 [
}7 M0 _6 T3 s' @* j# {
对于每个红包的创建者,我们需要记录一下他发送红包的总数量:
6 O# w5 @2 S% E1 G" n, ]
. u2 y' Q" d: m3 v+ E7 f1 ?key sentkey = make_key(sentPrefix,owner.tohexstring());
; F, M8 u' S  |' V2 K- x. Z: iasset sentcount = 0;
5 K. ^& a& A  q, Astorage_get(sentkey,sentcount);# j! _6 o9 o- D! @: M8 K. O9 D
sentcount += 1;
. O4 Q4 r* }; V7 u7 ?storage_put(sentkey,sentcount);, Z/ H5 ^/ f9 Q1 a" _
生成红包 hash, 这个 hash 就是之后标识这个红包的唯一 ID:# K& z3 z, ^3 @
' {0 Q1 C  \  |( V" O( [* f; c  D
H256 hash ;/ w/ i: @+ _6 _( s7 ^. |5 D2 y
hash256(make_key(owner,sentcount),hash) ;
% U" k( c" _' Q9 a: ~& Ukey rekey = make_key(rePrefix,hash256ToHexstring(hash));
% p" m5 [7 ]3 x7 A; k" x* ~根据 token 资产的类型,将资产转入合约中,self_address()可以取得当前执行的合约地址, 我们根据用户输入的 token 类型,将指定数量的 token 转入合约:  A6 u! K/ \1 E
2 X3 M& J0 W" U2 x* M/ j
address selfaddr = self_address();
6 {) c( y/ G% q+ [/ b- O: zif (isONTToken(tokenAddr)){. z9 b$ {# N* ~; D8 `1 T3 W
bool result = ont::transfer(owner,selfaddr ,amount);
, _* a4 _% g* Q3 ~- M- Kontio_assert(result,"transfer native token failed!");' l9 a  u  Z) I, }& E
}else if (isONGToken(tokenAddr)){- k3 R3 c- m# p* l0 n/ V/ X
bool result = ong::transfer(owner,selfaddr ,amount);9 N& {# N0 d) V0 Q4 k
ontio_assert(result,"transfer native token failed!");7 _* h* H* p* X# R6 g/ }
}else{! R8 z' D- X! G4 |: _& j. X
std::vector params = pack(std::string("transfer"),owner,selfaddr,amount);
: D* |2 l$ `1 V0 r' t- nbool res;" U; F$ v  c/ K7 K& B: m
call_contract(tokenAddr,params, res );
! c: i2 e& y% s1 E! qontio_assert(res,"transfer oep4 token failed!");
; @) `6 r. _0 P  y& K}! `  v$ o* s; }( A! N
NOTE 1:对于 ONT 和 ONG 这两种原生资产, Ontology Wasm CDT 提供了ont::transfer API 进行转账操作;而 OEP-4类的资产,需要按照普通的跨合约调用方法来转账。9 H+ P& Y1 D7 s
NOTE 2:和普通的钱包地址一样, 合约地址也可以接受任意类型的资产。但是合约地址是由合约编译后的二进制代码 hash 产生的,所以没有对应的私钥,也就无法随意操作合约中的资产,如果你没有在合约中设置对资产的操作,就意味着你将无法控制这部分资产。9 X9 V& F: N& c( i2 j2 |3 ~
将合约的信息保存在存储中:
. c$ k  M9 _5 E) e" b& C8 l( r5 a' V  _) |$ b
struct EnvelopeStruct es ;
( E1 `) t9 Y8 y4 P# d" Nes.tokenAddress = tokenAddr;
$ m' H0 i; m/ \$ m% }  Pes.totalAmount = amount;: H6 g: t: S& j
es.totalPackageCount = packcount;$ _( @. h' V8 F0 t. D3 V
es.remainAmount = amount;8 q0 o7 x0 q) d# `. X6 |/ h% l
es.remainPackageCount = packcount;
- W0 d4 ?$ o8 O! N5 s0 e0 Qes.records = {};
* u; v3 D. e5 _+ _" Gstorage_put(rekey, es);/ Q3 {; A% [) G% @5 x% V. B
发送创建红包的事件。对于智能合约的调用是一个异步的过程,合约会在执行成功后发送一个事件来通知客户端执行结果,这个事件的格式可以由合约的编写者来指定。
- I: _% d4 f; M8 V, _. Z( L0 A: \1 q9 M& v
char buffer [100];
0 I' I0 G5 @; g) bsprintf(buffer, "{\"states\":[\"%s\", \"%s\", \"%s\"]}","createEnvelope",owner.tohexstring().c_str(),hash256ToHexstring(hash).c_str());& P; U4 l6 B  _/ A1 F3 z9 |0 X
notify(buffer);! e: V/ k) f; N$ f( j0 c0 I6 K& S
return true;6 ~; ]+ V* y7 T+ t0 @' y8 p* s
一个简单的红包就创建完成了, 下一步我们需要实现如何查询这个红包的信息.
& J8 K- \9 }. J$ \2.3 查询红包. E- o. [. p/ @+ w& @2 l, g1 l
查询红包的逻辑非常简单, 只需要将存储中的红包信息取出并格式化返回即可:
$ t% \' q2 \3 d+ ]5 {% `0 Kstd::string queryEnvelope(std::string hash){
- e% T6 o- {. {4 |* c5 v1 rkey rekey = make_key(rePrefix, hash);
4 l. j& _. N: K9 Tstruct EnvelopeStruct es;; V) A2 T$ o& u% x$ Q
storage_get(rekey, es);
; {  l( t' c! H' f5 W, o! Treturn formatEnvelope(es);
* X1 ~( n/ _9 o8 }9 F1 W3 q}
( k; N. @8 w" I1 ]& `3 p- s" ^NOTE:对于智能合约的只读操作(例如查询), 可以通过预执行(pre-exec)来读取结果。不同于普通的合约调用,预执行不需要钱包的签名,同时也就无需花费 ONG。最后,其他用户可以根据 hash(红包的 ID)来领取(抢)这个红包了。
/ K7 ?/ E  k- Q& r2.4 领取红包
) {* i9 \4 ^9 u, M/ s我们已经把资产成功地转入到智能合约中了, 接下来就可以把这个红包的 ID 发送给你的朋友们让他们去抢红包了。
! e7 a3 _; ]. t3 H领取红包需要输入领取人的账户和红包的hash:9 x! S3 w6 B7 X5 M8 X% i& P' @* T- r

9 X5 {0 X! L) ~5 _# mbool claimEnvelope(address account, std::string hash){
7 O( Y) m+ H! z' b% qreturn true;: F6 l7 {0 I1 i: x5 s5 l
}
  g. W" L) D/ R4 c% i- t同样, 我们需要验证领取账户的签名, 不允许替其他人抢红包, 而且每个账户每个红包只能抢一次:* ~5 J* i+ V; q/ q* S
1 P4 Y7 @* C; X! v* K
ontio_assert(check_witness(account),"checkwitness failed");+ E! @) x, l' a
key claimkey = make_key(claimPrefix,hash,account);
3 k8 O  I5 ~! |2 a/ Q. y# ^asset claimed = 0 ;
8 t. h1 u  h8 U+ d, W9 kstorage_get(claimkey,claimed);
4 w5 J3 M# J* k9 z6 l& |% Oontio_assert(claimed == 0,"you have claimed this Envelope!");) Y1 j0 C) s$ g* Z+ Z( z
按照 hash 从存储中取出红包的信息, 判断这个红包是否没有被抢完:: W5 G( y: G1 h0 ~& U$ t$ Y1 o

* o/ I2 u, Z* f+ c3 Y" jkey rekey = make_key(rePrefix,hash);
! l% @0 B' q5 l2 U4 K; rstruct EnvelopeStruct es;
4 x* K- g! Q2 |$ q* [3 Dstorage_get(rekey,es);
; _: E5 Y( J; o6 j5 dontio_assert(es.remainAmount > 0, "the Envelope has been claimed over!");
5 Y: f0 t6 \" N2 sontio_assert(es.remainPackageCount > 0, "the Envelope has been claimed over!");
, Z. X, \" ?% `! x3 G5 N新建一条领取的记录:
1 S- a, m0 d, {+ G
  j4 I. b; u- S* m& wstruct receiveRecord record ;) `% ^4 }8 E5 v
record.account = account;8 _! W5 {4 k; K
asset claimAmount = 0;  M4 I) c  C# X; a6 \- e7 b2 ?
计算本次领取红包的资产数量。如果是最后一个红包, 数量为剩余的金额, 否则根据当前区块 hash 计算随机数,确定本次领取的数量, 并更新红包信息:
3 i. k2 J8 M9 S# T
2 j( V2 |3 D. ]( c8 w3 S. ~if (es.remainPackageCount == 1){6 a% s! S1 ^# L
claimAmount = es.remainAmount;9 D- R0 z2 m# U$ y1 ]9 N$ l
record.amount = claimAmount;% N- N2 `5 |2 c+ X
}else{
. j$ Y/ `5 A  h5 ~H256 random = current_blockhash() ;. R0 b1 b0 ], w5 ?" S
char part[8];
. i4 _& J2 j5 T! gmemcpy(part,&random,8);
% K8 P) t3 k, X$ }4 v% x( ?uint64_t random_num = *(uint64_t*)part;1 o# _) {- D2 J1 H7 h6 m, Q
uint32_t percent = random_num % 100 + 1;! e0 J( T# {3 _; {/ \
claimAmount = es.remainAmount * percent / 100;
$ b% C* {4 Z8 ]3 o  l3 g//ont case
# p" ?, F; s* wif (claimAmount == 0){
7 Z7 m4 G" D& H8 QclaimAmount = 1;
# O0 ^- G0 E8 b9 {* ~}else if(isONTToken(es.tokenAddress)){
; m- u/ u) H- ]( }& t4 cif ( (es.remainAmount - claimAmount) 根据计算结果, 将对应资产从合约中转到领取的账户:# Y& }  [  Y% v/ N" c( a

- V5 Z$ a" G( A9 Paddress selfaddr = self_address();! n9 @. T3 H6 N* G$ R+ M( }
if (isONTToken(es.tokenAddress)){2 i$ c% Y2 q: @$ h6 Q; \
bool result = ont::transfer(selfaddr,account ,claimAmount);# `2 J# [( W5 O
ontio_assert(result,"transfer ont token failed!");% i% \0 O' R) P. X9 E
} else if (isONGToken(es.tokenAddress)){4 [: E. t* y2 b- t  \" K) t
bool result = ong::transfer(selfaddr,account ,claimAmount);
8 E. n6 g& Y: U* D# m  aontio_assert(result,"transfer ong token failed!");$ r1 A' }$ p: B8 c- M
} else{
5 d% B- P, a1 |4 j" vstd::vector params = pack(std::string("transfer"),selfaddr,account,claimAmount);4 l1 E/ d$ M8 m+ z# N
bool res = false;. a) m! y9 f# }! r) J
call_contract(es.tokenAddress,params, res );! ?& x7 B' G9 t* _4 b
ontio_assert(res,"transfer oep4 token failed!");
# A; N1 e- Z% R7 ~}
$ e0 Y9 {) j0 m% l$ H4 z记录领取的信息, 将更新后的红包信息写回存储并发送通知事件:
! S$ e( T% M3 k5 M
; d. k& B, y6 {storage_put(claimkey,claimAmount);# ?% c* t+ v5 S
storage_put(rekey,es);" H4 i* d' ]5 r2 s- K+ P( p. N
char buffer [100];
# t7 E7 y( u8 C2 v% z! y9 Jstd::sprintf(buffer, "{\"states\":[\"%s\",\"%s\",\"%s\",\"%lld\"]}","claimEnvelope",hash.c_str(),account.tohexstring().c_str(),claimAmount);1 F  [" L* E# G1 x  @
notify(buffer);% Y% z- h5 T: z& G; `! |
return true;
3 X/ h2 ^9 g0 m. @- {9 Q1 B4 E1 }( i如前面所说,这个合约只能通过 claimEnvelope 这个接口将资产转出合约。所以,合约中的资产是安全的,任何人都无法随意的取走里面的资产。
) m- Z. ~! _3 Q! s$ h, m* Y至此, 一个简单的红包合约逻辑完成, 完整的合约代码如下:https://github.com/JasonZhouPW/pubdocs/blob/master/redEnvelope.cpp# q3 h6 [' T9 N$ k# M) V
2.5 合约测试
! i3 s* A7 N/ k. P. b) d合约测试可以有两种方法:5 B3 j. m9 w5 j+ ]* ~: p2 m2 j9 i
使用 CLI
4 i2 M1 M  L/ H' M请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/How_To_Run_ontologywasm_node.md
) ]; D9 p9 {: f* O使用 Golang SDK
  V0 n  f  |( l; A" ?2 J8 T' ~请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/example/other/main.go: X# U1 t4 ]7 J
三、总结7 C5 n# d* j: p
本示例只是为了展示如何编写一个完整的 Wasm 智能合约, 如何通过调用 API 和底层的区块链进行交互。如果要作为正式的产品, 还需要解决红包的隐私问题: 所有人都可以通过监控合约的事件来取得红包的 hash, 意味着每个人都可以抢这个红包。一种比较简单的解决方法,就是在创建红包时指定哪些账户能够领取。如果有兴趣, 您也可以尝试修改测试一下。# |) }2 D9 C5 h4 m
我们欢迎更多的 Wasm 技术爱好者加入本体开发社区,共同打造技术生态。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

放弃六月们 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    8