手把手教你Wasm合约开发(C++篇)
放弃六月们
发表于 2023-1-9 00:14:24
105
0
0
一、Hello World
按照惯例,我们还是从一个 Hello world 开始
#include#include
using namespace ontio;class hello:public contract {
public:) c$ m/ W8 _" }* A) Q- A
using contract::contract:& k8 {6 q8 V6 D2 {9 M, S/ B% N8 D
void sayHello(){' `2 o" v/ A V1 n; {
printf("hello world!");
}
};2 |+ R" |5 T. u! ^( L8 h1 `4 K
ONTIO_DISPATCH(hello, (sayHello));
1.1 合约入口
Ontology Wasm CDT 编译器已经对入口和参数解析进行了封装,所以开发者不需要重新定义入口方法。接下来是定义合约的对外接口,这是智能合约对外提供服务的方法。% X/ F2 r: }* Z9 F6 w e1 L8 L
ONTIO_DISPATCH(hello, (sayHello));
在上面的例子中, 我们暂时只支持 sayHello 这个方法:
printf("hello world!");7 |, G" s& q( R5 z ]+ Y H; ~
这个“Hello world!”会在节点的日志中以调试信息打印出来。在实际的应用中, printf 只能用作调试的目的, 一个实际的智能合约,需要实现更多更复杂的功能。3 r, J& k2 ?; J6 o8 m
1.2 智能合约 API: `5 E# B& S4 m1 d0 a3 H# m& O; x1 l
Ontology Wasm 提供如下 API 与区块链的底层进行交互:: a5 ^+ T+ @! K- F
二、红包合约8 M' Z* f$ {: M7 H6 I: ?3 s% v7 N
下面我们通过一个更加复杂的例子来演示如何通过这些 API 来开发一个完整的 Wasm 智能合约。
很多情况下我们都会通过各种 App,如微信等聊天工具发红包。我们可以给朋友发送红包,也可以抢其他人发送的红包,收到的钱会记入到个人微信账户中。$ ]1 T7 k% h0 q
类似于微信的流程,我们将尝试创建一个智能合约。用户使用该合约,可以发送 ONT,ONG 或者是标准的 OEP-4的 Token 资产红包给他的朋友们,而朋友们抢到的红包可以直接转入到他们的钱包账户中。
2.1 创建合约
首先,我们需要新建合约的源文件,暂且命名为 redEnvelope.cpp。这个合约我们需要三个接口:
createRedEnvelope: 创建红包
queryEnvelope: 查询红包信息: j9 w! @0 T* {5 ?6 R0 B$ m. ^
claimEnvelope: 抢红包; L* E. e) l; U. Z9 t
#include
using namespace ontio;
class redEnvelope: public contract{
}
ONTIO_DISPATCH(redEnvelope, (createRedEnvelope)(queryEnvelope)(claimEnvelope));
我们需要在存储中保存一些关键的数据。在智能合约中, 数据以 KV 的形式保存在该合约的上下文空间中,这些数据的 KEY 需要设置前缀以便于后面的查询。下面定义了三个不同的前缀供使用:
std::string rePrefix = "RE_PREFIX_";, @; y. s! L, [& X2 [
std::string sentPrefix = "SENT_COUNT_";
std::string claimPrefix = "CLAIM_PREFIX_"8 U: u" d' M2 B8 o8 u6 |/ b$ u
因为我们的合约支持 ONT 和 ONG 这两种 Ontology 的原生资产, 我们可以预先定义好这两种资产的合约地址。不同于标准的智能合约, Ontology 原生合约(native contract)的合约地址是固定的,而不是根据合约代码的 hash 计算而来的。9 T! V1 e7 Q- [
address ONTAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};6 ^1 h- }: [* c- ?) X( `% f
address ONGAddress = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2};
我们需要在合约中保存红包的信息, 如红包的资产信息(token 的合约地址, 红包的总金额, 红包的个数等等)1 u2 h3 w1 G4 V5 J( v |! |
struct receiveRecord{
address account; //用户地址
asset amount; //抢到的金额% z0 v. v( j: ?7 h
ONTLIB_SERIALIZE(receiveRecord,(account)(amount))( k8 W- v+ U# @3 ~+ ?7 o8 o
};
struct EnvelopeStruct{+ e6 Q) h% h, M) W3 L
address tokenAddress; //资产token的地址
asset totalAmount; //红包总金额
asset totalPackageCount; //红包总数) F1 O& b1 h' o7 r; f
asset remainAmount; //当前剩余的金额
asset remainPackageCount; //当前剩余的红包数
std::vector records; //已经抢完的记录5 [8 o1 i+ O& {. C
ONTLIB_SERIALIZE( EnvelopeStruct, (tokenAddress)(totalAmount)(totalPackageCount)(remainAmount)(remainPackageCount)(records) )
};
其中,) q4 J, m$ j; \# ?7 h. z( d$ t. [
ONTLIB_SERIALIZE(receiveRecord,(account)(amount))
是由 Ontology Wasm CDT 定义的宏操作,用于在将 struct 存储前进行序列化的操作。6 U4 U! `: R. Z2 p* k& J! ?+ a4 l$ A
2.2 创建红包, T- m2 b, h5 [1 b
准备工作差不多了,下面我们开始开发具体的接口逻辑。
创建红包需要指定创建者地址, 红包数量, 红包金额和资产的合约地址:
' v* n/ K8 f* y4 }' N: O+ K5 n
bool createRedEnvelope(address owner,asset packcount, asset amount,address tokenAddr ){
return true;
}
检查是否有创建者的签名, 否则交易回滚退出:
ontio_assert(check_witness(owner),"checkwitness failed");
NOTE: ontio_assert(expr, errormsg):当 expr 为 false 时, 抛出异常并退出。
如果红包资产是 ONT,由于 ONT 的不可分割性(最小为1个 ONT), 红包的金额要大于或等于红包的数量,保证每个红包最少有1个 ONT:
, g/ |* f, I; I7 x7 v
if (isONTToken(tokenAddr)){
ontio_assert(amount >= packcount,"ont amount should greater than packcount");
}( l' V( p8 h6 U; f
对于每个红包的创建者,我们需要记录一下他发送红包的总数量:
0 J* f7 B- `9 Y! w" {& R8 k! o
key sentkey = make_key(sentPrefix,owner.tohexstring());! r0 J+ q/ {9 Q3 G5 Q
asset sentcount = 0;6 o9 K& B! L! E/ e5 \
storage_get(sentkey,sentcount);' Z0 F% B/ `! d7 W% a/ Y/ a5 V
sentcount += 1;
storage_put(sentkey,sentcount);( N* h( e( l( f8 ^0 D$ R7 G* O
生成红包 hash, 这个 hash 就是之后标识这个红包的唯一 ID:8 d U! a- ?# S- V" p" w/ Z. W
) [% L- B0 G W4 W8 S- a9 d
H256 hash ;" M+ Y- B8 L6 ]& ?" j
hash256(make_key(owner,sentcount),hash) ;
key rekey = make_key(rePrefix,hash256ToHexstring(hash));! @' _6 z& w& k* Z5 `% q
根据 token 资产的类型,将资产转入合约中,self_address()可以取得当前执行的合约地址, 我们根据用户输入的 token 类型,将指定数量的 token 转入合约:5 N$ P& {1 f! y
]3 h& T9 {" \8 \
address selfaddr = self_address();
if (isONTToken(tokenAddr)){
bool result = ont::transfer(owner,selfaddr ,amount);1 S" V* z. m. z5 ^; _8 g0 n
ontio_assert(result,"transfer native token failed!");
}else if (isONGToken(tokenAddr)){
bool result = ong::transfer(owner,selfaddr ,amount);( h/ }. {( F* z/ p0 \3 N- t3 Q
ontio_assert(result,"transfer native token failed!");4 a L* A% f2 ?& z* g9 g
}else{
std::vector params = pack(std::string("transfer"),owner,selfaddr,amount);; S9 v/ G. d4 R: y7 ]) U
bool res;. Q8 s. H! G' u. L9 b
call_contract(tokenAddr,params, res );
ontio_assert(res,"transfer oep4 token failed!");4 Y: `/ i5 N0 q4 z/ S
}
NOTE 1:对于 ONT 和 ONG 这两种原生资产, Ontology Wasm CDT 提供了ont::transfer API 进行转账操作;而 OEP-4类的资产,需要按照普通的跨合约调用方法来转账。& U" p8 F' C8 Q& [, @
NOTE 2:和普通的钱包地址一样, 合约地址也可以接受任意类型的资产。但是合约地址是由合约编译后的二进制代码 hash 产生的,所以没有对应的私钥,也就无法随意操作合约中的资产,如果你没有在合约中设置对资产的操作,就意味着你将无法控制这部分资产。& ^& u' q- J. m' L6 k7 V
将合约的信息保存在存储中:
struct EnvelopeStruct es ;
es.tokenAddress = tokenAddr;
es.totalAmount = amount;
es.totalPackageCount = packcount;
es.remainAmount = amount;
es.remainPackageCount = packcount;
es.records = {};
storage_put(rekey, es);! t/ P |3 q' a" X- @: G0 k
发送创建红包的事件。对于智能合约的调用是一个异步的过程,合约会在执行成功后发送一个事件来通知客户端执行结果,这个事件的格式可以由合约的编写者来指定。
6 n/ j H8 i4 w
char buffer [100];
sprintf(buffer, "{\"states\":[\"%s\", \"%s\", \"%s\"]}","createEnvelope",owner.tohexstring().c_str(),hash256ToHexstring(hash).c_str());
notify(buffer);
return true;
一个简单的红包就创建完成了, 下一步我们需要实现如何查询这个红包的信息.3 v7 r/ a) K1 D
2.3 查询红包
查询红包的逻辑非常简单, 只需要将存储中的红包信息取出并格式化返回即可:
std::string queryEnvelope(std::string hash){; J7 A' ?- S) ^% c
key rekey = make_key(rePrefix, hash);
struct EnvelopeStruct es;
storage_get(rekey, es);
return formatEnvelope(es);
}' Y' ?# m8 l) T. B9 m* K! b3 n
NOTE:对于智能合约的只读操作(例如查询), 可以通过预执行(pre-exec)来读取结果。不同于普通的合约调用,预执行不需要钱包的签名,同时也就无需花费 ONG。最后,其他用户可以根据 hash(红包的 ID)来领取(抢)这个红包了。
2.4 领取红包" }) w# W! }8 G% v& Z
我们已经把资产成功地转入到智能合约中了, 接下来就可以把这个红包的 ID 发送给你的朋友们让他们去抢红包了。9 i/ ^, l- `/ Q2 b6 i/ Z; J2 U
领取红包需要输入领取人的账户和红包的hash:6 p) z, @* n* e+ J& ]" P
0 |0 z0 {/ i( Z, ]. j2 K
bool claimEnvelope(address account, std::string hash){
return true;
}; `% O0 M7 e+ ~" n) t
同样, 我们需要验证领取账户的签名, 不允许替其他人抢红包, 而且每个账户每个红包只能抢一次:+ p% l- C% D' o* K- a2 d& V, I% j
ontio_assert(check_witness(account),"checkwitness failed");
key claimkey = make_key(claimPrefix,hash,account);: y3 E) [, m' Z+ p
asset claimed = 0 ;( u% ~0 { ]$ f) c
storage_get(claimkey,claimed);& o9 N$ j7 r% R" h& p5 f7 C
ontio_assert(claimed == 0,"you have claimed this Envelope!");
按照 hash 从存储中取出红包的信息, 判断这个红包是否没有被抢完:. m" H1 B1 w) E0 K8 v5 c
. W; w$ @( y* F4 Y* q. o
key rekey = make_key(rePrefix,hash);% B E. o5 {( }0 E2 N/ R
struct EnvelopeStruct es;
storage_get(rekey,es);/ i4 q5 H' p' K" L+ \% x" H" E
ontio_assert(es.remainAmount > 0, "the Envelope has been claimed over!");1 h* j4 }$ I% R3 G+ x+ J/ f+ ?
ontio_assert(es.remainPackageCount > 0, "the Envelope has been claimed over!");5 ]6 @6 p" h: S& _8 `1 |
新建一条领取的记录:( {' v- u! N3 o: A6 [
struct receiveRecord record ; \% u3 ^6 N# R% s6 h! q8 U s! K, a
record.account = account;8 Q! ^! v* W6 s' {/ t* S
asset claimAmount = 0;6 A" X# ]8 X7 L' r- K
计算本次领取红包的资产数量。如果是最后一个红包, 数量为剩余的金额, 否则根据当前区块 hash 计算随机数,确定本次领取的数量, 并更新红包信息:
; A- Y# w: \6 h7 H. @
if (es.remainPackageCount == 1){
claimAmount = es.remainAmount;
record.amount = claimAmount;
}else{+ r( E: ~$ t/ d R
H256 random = current_blockhash() ;' U8 y O5 D1 k4 J6 V
char part[8];# ?8 a* p T: K, [* p: _ }& l6 g
memcpy(part,&random,8);% C3 H! I c* s9 L5 |
uint64_t random_num = *(uint64_t*)part;
uint32_t percent = random_num % 100 + 1;7 r9 a# a8 J0 Y( J
claimAmount = es.remainAmount * percent / 100;9 F3 V2 T! b9 X# L$ e
//ont case% C, u# J7 m( y) Y! L
if (claimAmount == 0){
claimAmount = 1;! z. P3 b6 g1 Z& X3 G) k K! t
}else if(isONTToken(es.tokenAddress)){. ` t' o0 }8 z$ [8 s$ Y3 ~
if ( (es.remainAmount - claimAmount) 根据计算结果, 将对应资产从合约中转到领取的账户:7 w; C7 ~& o5 o; N T1 L
" ~ Q0 c1 q: z9 J4 H/ o
address selfaddr = self_address();
if (isONTToken(es.tokenAddress)){$ l, |+ h0 b$ P1 d
bool result = ont::transfer(selfaddr,account ,claimAmount);- l2 Y* S: @; f
ontio_assert(result,"transfer ont token failed!");0 `5 ]) V, G, Y0 i8 ^ |& v
} else if (isONGToken(es.tokenAddress)){& m7 V! [4 i; _3 {* X& r
bool result = ong::transfer(selfaddr,account ,claimAmount);7 H& J( `% S" D. h" I
ontio_assert(result,"transfer ong token failed!");! P7 ]/ T9 w& A% g _
} else{
std::vector params = pack(std::string("transfer"),selfaddr,account,claimAmount);
bool res = false;8 H" w4 k9 Y) B; Q7 K
call_contract(es.tokenAddress,params, res );
ontio_assert(res,"transfer oep4 token failed!");
}
记录领取的信息, 将更新后的红包信息写回存储并发送通知事件:
0 v6 I' ]* k6 l0 \
storage_put(claimkey,claimAmount);, j! D" b( o# y5 x% Z3 c' M& v9 T
storage_put(rekey,es);
char buffer [100];
std::sprintf(buffer, "{\"states\":[\"%s\",\"%s\",\"%s\",\"%lld\"]}","claimEnvelope",hash.c_str(),account.tohexstring().c_str(),claimAmount);7 e+ K8 O4 o$ H! W2 U
notify(buffer);
return true;
如前面所说,这个合约只能通过 claimEnvelope 这个接口将资产转出合约。所以,合约中的资产是安全的,任何人都无法随意的取走里面的资产。
至此, 一个简单的红包合约逻辑完成, 完整的合约代码如下:https://github.com/JasonZhouPW/pubdocs/blob/master/redEnvelope.cpp3 G0 {% c2 H6 ?% m6 r" a/ Y0 N
2.5 合约测试3 H* C# s( _8 N0 O3 Q
合约测试可以有两种方法:
使用 CLI
请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/How_To_Run_ontologywasm_node.md4 {; w7 v0 f$ h& b1 n
使用 Golang SDK
请参考:https://github.com/ontio/ontology-wasm-cdt-cpp/blob/master/example/other/main.go: y6 N+ G0 ^; c* Q2 ~
三、总结1 o' T8 @- D7 r* D. ]
本示例只是为了展示如何编写一个完整的 Wasm 智能合约, 如何通过调用 API 和底层的区块链进行交互。如果要作为正式的产品, 还需要解决红包的隐私问题: 所有人都可以通过监控合约的事件来取得红包的 hash, 意味着每个人都可以抢这个红包。一种比较简单的解决方法,就是在创建红包时指定哪些账户能够领取。如果有兴趣, 您也可以尝试修改测试一下。
我们欢迎更多的 Wasm 技术爱好者加入本体开发社区,共同打造技术生态。
成为第一个吐槽的人