Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

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

放弃六月们
107 0 0
Ontology Wasm 自从上线测试网以来便受到了社区开发人员的极大关注。 Wasm 的上线将使得业务逻辑复杂的 dApp 合约上链成本降低,极大丰富 dApp 生态。在进行 Wasm 合约开发时,开发者不仅可以使用 Rust,还可以使用 C++ 作为合约开发语言。本期我们将通过两个简单的示例来示范如何使用 C++ 进行Wasm 合约开发。, R. N. B" a/ h3 |" |. U9 C
一、Hello World+ P9 D4 R" P; Q: M
按照惯例,我们还是从一个 Hello world 开始  M, k' q6 q2 k: b* j! f
#include#include
( A9 R( v! B/ |. G/ {% Pusing namespace ontio;class hello:public contract {: p9 b/ x7 Q( w1 `( s, ~
public:, _4 H) R, d3 A4 u) t) n
using contract::contract:
; K; G5 h3 ]$ p9 M# {- uvoid sayHello(){' ]% F9 ]; w( ~' U, H
printf("hello world!");
. w' ?& u( V) r9 Z! q, @' h}& |6 o# ?) w/ Y3 [( r2 X  F$ ]
};6 X' q3 o! s" t- D2 H& D
ONTIO_DISPATCH(hello, (sayHello));
4 i( C( N' A: t* H9 [3 Z1.1 合约入口: F5 B9 W; h5 \9 Z
Ontology Wasm CDT 编译器已经对入口和参数解析进行了封装,所以开发者不需要重新定义入口方法。接下来是定义合约的对外接口,这是智能合约对外提供服务的方法。% l) F+ Z7 j$ L2 S
ONTIO_DISPATCH(hello, (sayHello));' _* m7 U2 y. E+ X1 \5 I
在上面的例子中, 我们暂时只支持 sayHello 这个方法:. h& l0 f. c+ G2 ?
printf("hello world!");
  T( N4 f; V+ |5 ]" Z" x8 @( p这个“Hello world!”会在节点的日志中以调试信息打印出来。在实际的应用中, printf 只能用作调试的目的, 一个实际的智能合约,需要实现更多更复杂的功能。
9 W/ _4 n% N' U1.2 智能合约 API1 M' `/ H4 \' ]% N% P6 `9 y
Ontology Wasm 提供如下 API 与区块链的底层进行交互:1 K4 [3 j+ A! A! i- O. Y

* l/ q  ]6 a/ j$ V0 g  y9 @4 U二、红包合约
1 o2 j+ d9 @" \( {' y1 g下面我们通过一个更加复杂的例子来演示如何通过这些 API 来开发一个完整的 Wasm 智能合约。
  ?" c( }' B7 K$ G$ `/ g很多情况下我们都会通过各种 App,如微信等聊天工具发红包。我们可以给朋友发送红包,也可以抢其他人发送的红包,收到的钱会记入到个人微信账户中。
" r- ~$ q6 s  ^0 U类似于微信的流程,我们将尝试创建一个智能合约。用户使用该合约,可以发送 ONT,ONG 或者是标准的 OEP-4的 Token 资产红包给他的朋友们,而朋友们抢到的红包可以直接转入到他们的钱包账户中。& ], `  q2 d5 ~: A
2.1 创建合约' H' Q- d; N  J" _( v9 v
首先,我们需要新建合约的源文件,暂且命名为 redEnvelope.cpp。这个合约我们需要三个接口:
( T2 Y+ [& b1 H" z- QcreateRedEnvelope: 创建红包
5 i0 C0 R, t- fqueryEnvelope: 查询红包信息
: q4 d; }0 q: u' L% p0 V% g6 E/ _6 kclaimEnvelope: 抢红包
; @9 x9 v. C/ K, m2 v#include
/ L( }) ~9 z, n. L4 e) f$ ]using namespace ontio;+ {6 ^/ ]8 d8 {, [3 z8 |. W% M& e7 {( S
class redEnvelope: public contract{2 H! P  Y- U( v% c7 ^9 G# {, L
}
  g: c% k) U+ G6 _! OONTIO_DISPATCH(redEnvelope, (createRedEnvelope)(queryEnvelope)(claimEnvelope));+ d* A7 @  T5 x; Q  q
我们需要在存储中保存一些关键的数据。在智能合约中, 数据以 KV 的形式保存在该合约的上下文空间中,这些数据的 KEY 需要设置前缀以便于后面的查询。下面定义了三个不同的前缀供使用:
  \0 H3 p" W5 j! \0 V, @std::string rePrefix = "RE_PREFIX_";
  b6 V) S, g  Rstd::string sentPrefix = "SENT_COUNT_";  J/ m% _- t# d' M
std::string claimPrefix = "CLAIM_PREFIX_"
" \1 h7 B& q  t3 D7 p6 u因为我们的合约支持 ONT 和 ONG 这两种 Ontology 的原生资产, 我们可以预先定义好这两种资产的合约地址。不同于标准的智能合约, Ontology 原生合约(native contract)的合约地址是固定的,而不是根据合约代码的 hash 计算而来的。7 X+ ~2 U8 b: o5 j9 R' g' {7 E3 f" E
address ONTAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
' J9 Z9 X0 C; T. X4 |; Eaddress ONGAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2};7 @" Y- F5 r% ^0 t$ V$ i  A5 ]
我们需要在合约中保存红包的信息, 如红包的资产信息(token 的合约地址, 红包的总金额, 红包的个数等等)% M, R) Z- P. V* }
struct receiveRecord{/ ?9 k" S8 e1 ?) C) r
address account; //用户地址
/ U% V  u# K, R# S1 d7 R. Aasset amount; //抢到的金额" M0 `8 l( |" h1 S
ONTLIB_SERIALIZE(receiveRecord,(account)(amount))
. S2 P  \# o+ Z( }& E};
$ z  ^. T" L% K8 estruct EnvelopeStruct{. J, |9 Y0 Z3 \5 z
address tokenAddress; //资产token的地址/ r& m3 d) m; R2 e5 D+ u: N
asset totalAmount; //红包总金额. E8 B+ |' h$ J3 o  @* E
asset totalPackageCount; //红包总数
% J5 m) V8 E3 C; p; g! nasset remainAmount; //当前剩余的金额# K: x1 A) ]1 c9 C' e
asset remainPackageCount; //当前剩余的红包数
/ M. e' B7 F2 K( astd::vector records; //已经抢完的记录! z* A7 ]" W/ M6 g/ f$ E
ONTLIB_SERIALIZE( EnvelopeStruct, (tokenAddress)(totalAmount)(totalPackageCount)(remainAmount)(remainPackageCount)(records) )3 ^8 x, J9 a& t! V* \  Z4 R4 m( z
};
3 b4 J( J; p: a1 G; f其中,
; o. m* L2 J: N7 X* H, b# \ONTLIB_SERIALIZE(receiveRecord,(account)(amount))
+ D: u: P; H1 Q% S是由 Ontology Wasm CDT 定义的宏操作,用于在将 struct 存储前进行序列化的操作。/ l' `4 n" g4 j% d8 Z: y, G' v* ?* ~
2.2 创建红包
0 |& P$ K$ W) f3 {0 r0 R5 m准备工作差不多了,下面我们开始开发具体的接口逻辑。
/ u3 U% @: B7 C+ l' J" G8 v% J8 `创建红包需要指定创建者地址, 红包数量, 红包金额和资产的合约地址:8 G* _, G/ ]: e# V3 j; H/ X

) P+ y& m0 b, X& `, obool createRedEnvelope(address owner,asset packcount, asset amount,address tokenAddr ){
: A6 v$ @/ s* n  Ureturn true;9 J2 l: R0 p* g9 c
}
$ g) E% Z9 a0 Y检查是否有创建者的签名, 否则交易回滚退出:# A, s1 m+ C) P+ v" [1 q
4 f! @, q' Y/ {* L$ K( r& K( L1 X
ontio_assert(check_witness(owner),"checkwitness failed");
0 m; W3 X( V: N8 J8 l; z$ G* ^NOTE: ontio_assert(expr, errormsg):当 expr 为 false 时, 抛出异常并退出。
& n! ~* Z; N3 A如果红包资产是 ONT,由于 ONT 的不可分割性(最小为1个 ONT), 红包的金额要大于或等于红包的数量,保证每个红包最少有1个 ONT:
5 n  ^8 l' ^! ]3 i/ `$ J8 B2 g- y+ l% D( J) F
if (isONTToken(tokenAddr)){
! y5 T6 l& l: I- {+ q1 v# G7 Wontio_assert(amount >= packcount,"ont amount should greater than packcount");3 g0 B4 k2 ^0 n9 _4 U* H, ?" y
}* b( M3 ~- V9 l2 e, M) N
对于每个红包的创建者,我们需要记录一下他发送红包的总数量:* A3 b. [9 ^; v7 a+ b

3 `6 z8 M2 k3 V- W( R4 B# Hkey sentkey = make_key(sentPrefix,owner.tohexstring());" H9 Q$ c3 Z0 D( Q
asset sentcount = 0;3 a1 z/ Y% I$ e3 J/ I4 a  K& k
storage_get(sentkey,sentcount);
+ H1 U. l! \* l# @1 ~sentcount += 1;* [/ A$ Q$ y# f/ _  s0 C
storage_put(sentkey,sentcount);1 }, ~- d! F5 S7 u- i/ T
生成红包 hash, 这个 hash 就是之后标识这个红包的唯一 ID:- D& j* S% M; {
" l4 m, d, W' v) J9 j. \) ?+ M
H256 hash ;) F! e6 X8 h3 U$ O; ?/ ?" B
hash256(make_key(owner,sentcount),hash) ;
* c* G- Q0 z3 n- k" l) B# v0 {key rekey = make_key(rePrefix,hash256ToHexstring(hash));
9 K6 v9 v/ I% y. z/ i根据 token 资产的类型,将资产转入合约中,self_address()可以取得当前执行的合约地址, 我们根据用户输入的 token 类型,将指定数量的 token 转入合约:1 V/ R6 G3 z4 e4 Z4 `# J

. `2 z* B$ U: yaddress selfaddr = self_address();
( O( }0 U* O% t$ j; kif (isONTToken(tokenAddr)){9 h. j; ~7 a: @- O! K  D7 K
bool result = ont::transfer(owner,selfaddr ,amount);
9 N' v  U& l. [1 I3 u( _ontio_assert(result,"transfer native token failed!");
6 @. W' ~: q8 _' @1 n}else if (isONGToken(tokenAddr)){3 _, L- r, c- c: h8 Q
bool result = ong::transfer(owner,selfaddr ,amount);; I/ _0 f5 Z+ \+ D1 e; f
ontio_assert(result,"transfer native token failed!");
; \; v) M6 o4 z6 P+ _% f1 \2 T: U}else{+ T/ T7 q( n9 H& i" d  u
std::vector params = pack(std::string("transfer"),owner,selfaddr,amount);
% L! Z5 N* S$ j/ gbool res;
- a2 L: A& U  U7 n# V. s  Kcall_contract(tokenAddr,params, res );! ~; x( j/ ^  c! M
ontio_assert(res,"transfer oep4 token failed!");' p: e7 V7 r+ {" y
}
7 C2 a* _# h) z# F+ v& BNOTE 1:对于 ONT 和 ONG 这两种原生资产, Ontology Wasm CDT 提供了ont::transfer API 进行转账操作;而 OEP-4类的资产,需要按照普通的跨合约调用方法来转账。
/ ?' z; ?; {8 H* J0 d! k1 }NOTE 2:和普通的钱包地址一样, 合约地址也可以接受任意类型的资产。但是合约地址是由合约编译后的二进制代码 hash 产生的,所以没有对应的私钥,也就无法随意操作合约中的资产,如果你没有在合约中设置对资产的操作,就意味着你将无法控制这部分资产。1 M" _$ D4 P, \6 O
将合约的信息保存在存储中:
3 \' F# T' z3 M6 s' G# w. R1 B' G" C7 w) K  R) u8 T3 a- Q* W" a$ ]
struct EnvelopeStruct es ;/ b7 Y* d2 V* {
es.tokenAddress = tokenAddr;
( V, s) w& t/ I" V7 H5 [es.totalAmount = amount;2 c) M5 k% C3 f$ q" a
es.totalPackageCount = packcount;/ S: ~7 k' @* Z, d% E
es.remainAmount = amount;, }0 f/ Z6 C/ n4 |9 H
es.remainPackageCount = packcount;
7 X4 [) \: m. w- p) L% Des.records = {};
8 {4 o3 L: B) V+ Z& B, o  qstorage_put(rekey, es);
/ e1 q; R) Q2 q  J0 _发送创建红包的事件。对于智能合约的调用是一个异步的过程,合约会在执行成功后发送一个事件来通知客户端执行结果,这个事件的格式可以由合约的编写者来指定。
' @; o3 A1 Z! i+ c: s* O3 q: n1 K
/ ]$ U. C' V, rchar buffer [100];" P, c" C5 j& Y1 k+ t5 p
sprintf(buffer, "{\"states\":[\"%s\", \"%s\", \"%s\"]}","createEnvelope",owner.tohexstring().c_str(),hash256ToHexstring(hash).c_str());0 h/ q5 U3 [+ _( H( o0 B
notify(buffer);
# E) l8 ]% O) ^return true;* J, t% C+ v" T( e
一个简单的红包就创建完成了, 下一步我们需要实现如何查询这个红包的信息.
  J& F0 h2 s: v! a2 @* d  u( T2.3 查询红包4 _! _- v2 o; g8 w
查询红包的逻辑非常简单, 只需要将存储中的红包信息取出并格式化返回即可:
1 l2 ~! F" s3 y. Ostd::string queryEnvelope(std::string hash){
: P6 |2 h) `5 [( A0 o4 jkey rekey = make_key(rePrefix, hash);- Q* X6 q5 _; `  p8 T3 I
struct EnvelopeStruct es;5 W- [' s7 \% m" c! ^1 N
storage_get(rekey, es);
  e3 n, Z/ O6 A& D' ^- ?return formatEnvelope(es);
* s$ B# U' z9 Q& }}
2 L5 m! S! o' ~/ G3 g, ANOTE:对于智能合约的只读操作(例如查询), 可以通过预执行(pre-exec)来读取结果。不同于普通的合约调用,预执行不需要钱包的签名,同时也就无需花费 ONG。最后,其他用户可以根据 hash(红包的 ID)来领取(抢)这个红包了。
2 p  t9 F; }4 C2.4 领取红包
  g+ ~# R5 n! b我们已经把资产成功地转入到智能合约中了, 接下来就可以把这个红包的 ID 发送给你的朋友们让他们去抢红包了。
. H) X4 T3 j: e' D2 ^1 E& W9 E领取红包需要输入领取人的账户和红包的hash:
5 K0 P8 j4 j+ F2 y3 s4 n! Y
" c* A: r) e5 A6 s- Hbool claimEnvelope(address account, std::string hash){& @$ u$ r- q) A- o5 ?4 ~
return true;
+ A$ c) S: s6 n}* V& o/ W0 k" f& G& F
同样, 我们需要验证领取账户的签名, 不允许替其他人抢红包, 而且每个账户每个红包只能抢一次:8 B9 f/ c# q2 A- \# [8 a
  k1 W/ \( I7 i& @6 Q
ontio_assert(check_witness(account),"checkwitness failed");
& N7 X! E2 _' C/ u- a+ Ikey claimkey = make_key(claimPrefix,hash,account);
6 N" k  T! u0 @0 Q: p8 Aasset claimed = 0 ;3 {$ o, p: U8 o
storage_get(claimkey,claimed);1 u3 J' t) H3 g7 a' V/ X
ontio_assert(claimed == 0,"you have claimed this Envelope!");. @5 L! o1 B- t* j* V1 w4 h
按照 hash 从存储中取出红包的信息, 判断这个红包是否没有被抢完:- R6 G4 W6 [* c

1 f# o  r, o3 o& R& m5 {8 ~# Ykey rekey = make_key(rePrefix,hash);. t; D# |9 l. t
struct EnvelopeStruct es;8 D+ p5 E$ p8 h3 v
storage_get(rekey,es);
2 G/ a4 v& d" \( K2 pontio_assert(es.remainAmount > 0, "the Envelope has been claimed over!");
/ v6 r6 e6 y; A7 L( rontio_assert(es.remainPackageCount > 0, "the Envelope has been claimed over!");0 n- V. G9 K) Q1 x
新建一条领取的记录:- s% ^4 Y  U6 U3 i0 P+ t/ C/ Z! N5 l

- S( X7 Z6 p5 V% `; G4 t+ U3 {struct receiveRecord record ;
8 H  v( I' S) s7 Frecord.account = account;4 @8 G( p6 P" ?& ?
asset claimAmount = 0;  W, F* f" \) B, ^: ^
计算本次领取红包的资产数量。如果是最后一个红包, 数量为剩余的金额, 否则根据当前区块 hash 计算随机数,确定本次领取的数量, 并更新红包信息:
% r% H" r+ |9 R$ _% U* G; M) }" m/ h" k. X  X& C% P8 z$ d
if (es.remainPackageCount == 1){
3 @) r9 b7 y0 y* `0 a1 YclaimAmount = es.remainAmount;5 N6 n) l/ H* [- o+ k
record.amount = claimAmount;, o, `/ g* I; l5 E' v
}else{
. Q- h: [( `3 m" a) HH256 random = current_blockhash() ;# M- n7 i% i; y' W
char part[8];1 z- ^- L" J: e* ?1 B
memcpy(part,&random,8);- B5 i: ]0 W( R5 E3 D: L
uint64_t random_num = *(uint64_t*)part;% W' h5 ^9 L" K  U* x
uint32_t percent = random_num % 100 + 1;
' K9 D& X+ s# T2 R- gclaimAmount = es.remainAmount * percent / 100;/ g0 y" s6 i6 Y8 |( ?
//ont case; i0 S( a; P) y- b1 h% c
if (claimAmount == 0){
' n" a2 f0 K  n2 S. C6 qclaimAmount = 1;
6 t% }$ J& T  @1 }9 H}else if(isONTToken(es.tokenAddress)){
( K+ D# b& c4 g; ^/ eif ( (es.remainAmount - claimAmount) 根据计算结果, 将对应资产从合约中转到领取的账户:  x6 t2 q/ n: f3 q
/ G) k4 g& M8 u. r( D+ \
address selfaddr = self_address();
5 B, l8 ^  N3 F5 a( c& M3 R* yif (isONTToken(es.tokenAddress)){
% c# K! q6 B$ v# D6 F" Ubool result = ont::transfer(selfaddr,account ,claimAmount);
3 [9 q% J# E& x* n2 x8 L( |' \ontio_assert(result,"transfer ont token failed!");' c+ U  ^/ P2 |3 e# {7 J- O; D& U
} else if (isONGToken(es.tokenAddress)){7 e( [4 s0 L( n5 g
bool result = ong::transfer(selfaddr,account ,claimAmount);
+ K7 Q4 X. ~/ g& j" \ontio_assert(result,"transfer ong token failed!");
( X; ]* \  ~4 r4 \8 y- U$ J* ~7 @} else{
# K% f5 g9 r; w+ Z% v! {std::vector params = pack(std::string("transfer"),selfaddr,account,claimAmount);
$ U4 o/ r3 }& Q* o5 cbool res = false;
3 r% P9 I3 G+ y! Fcall_contract(es.tokenAddress,params, res );
3 v1 x6 J$ x; r' i5 gontio_assert(res,"transfer oep4 token failed!");
: g* s! X- L* g0 {, q, g5 l}
0 o2 \4 G! G9 p" ^记录领取的信息, 将更新后的红包信息写回存储并发送通知事件:: R6 A) X2 J4 x

2 z0 B! `) s! P1 ostorage_put(claimkey,claimAmount);) h9 ~3 p  R1 u, U- |
storage_put(rekey,es);, m5 @9 i7 Y* U# C- X) a. [
char buffer [100];$ J9 M' a* |6 @
std::sprintf(buffer, "{\"states\":[\"%s\",\"%s\",\"%s\",\"%lld\"]}","claimEnvelope",hash.c_str(),account.tohexstring().c_str(),claimAmount);/ V* ?% ^& w% S, F
notify(buffer);+ ^+ o" q. |( ]' D
return true;! K- w) L# @! k( P' d* b! T  ]# t% _
如前面所说,这个合约只能通过 claimEnvelope 这个接口将资产转出合约。所以,合约中的资产是安全的,任何人都无法随意的取走里面的资产。
' j$ j, o/ i; e  U& a3 M至此, 一个简单的红包合约逻辑完成, 完整的合约代码如下:https://github.com/JasonZhouPW/pubdocs/blob/master/redEnvelope.cpp
1 G8 J, a5 B0 W2.5 合约测试
5 {* t5 T) K+ ^, A2 u1 Z3 X* Y合约测试可以有两种方法:
$ q1 E# R7 D: @- P& X6 P使用 CLI6 x* `$ c' z1 e& c6 p, X3 i, D
请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/How_To_Run_ontologywasm_node.md
  f9 j. B% ~* A1 M  l使用 Golang SDK1 E* \4 F5 H( y. Z9 _
请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/example/other/main.go5 O" c# c4 C6 V1 S0 K, l
三、总结
6 o* P% |# X8 L* q  V7 W6 N本示例只是为了展示如何编写一个完整的 Wasm 智能合约, 如何通过调用 API 和底层的区块链进行交互。如果要作为正式的产品, 还需要解决红包的隐私问题: 所有人都可以通过监控合约的事件来取得红包的 hash, 意味着每个人都可以抢这个红包。一种比较简单的解决方法,就是在创建红包时指定哪些账户能够领取。如果有兴趣, 您也可以尝试修改测试一下。' D' B0 q1 q5 j, b2 ?& v2 j7 ~
我们欢迎更多的 Wasm 技术爱好者加入本体开发社区,共同打造技术生态。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

放弃六月们 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    8