github.com/turingchain2020/turingchain@v1.1.21/cmd/tools/doc/gencalculator.md (about) 1 # calculator generate 2 基于gendapp自动生成合约命令,介绍合约的完整开发步骤 3 4 ### 简介 5 calculator合约支持在区块链上进行整数加减乘除交易操作,同时方便演示 6 开发,记录运算符参与运算的次数,并提供查询接口 7 8 ### 编写合约proto 9 10 ```proto 11 syntax = "proto3"; 12 13 package calculator; 14 // calculator 合约交易行为总类型 15 message CalculatorAction { 16 oneof value { 17 Add add = 1; 18 Subtract sub = 2; 19 Multiply mul = 3; 20 Divide div = 4; 21 } 22 int32 ty = 5; 23 } 24 25 message Add { 26 int32 summand = 1; //被加数 27 int32 addend = 2; //加数 28 } 29 message AddLog { 30 int32 sum = 1; //和 31 } 32 33 message Subtract { 34 int32 minuend = 1; //被减数 35 int32 subtrahend = 2; //减数 36 } 37 message SubLog { 38 int32 remainder = 1; //差 39 } 40 41 message Multiply { 42 int32 faciend = 1; //被乘数 43 int32 multiplier = 2; //乘数 44 } 45 message MultiplyLog { 46 int32 product = 1; //积 47 } 48 49 message Divide { 50 int32 dividend = 1; //被除数 51 int32 divisor = 2; //除数 52 } 53 message DivideLog { 54 int32 quotient = 1; //商 55 int32 remain = 2; //余数 56 } 57 58 message ReqQueryCalcCount { 59 string action = 1; 60 } 61 message ReplyQueryCalcCount { 62 int32 count = 1; 63 } 64 65 service calculator { 66 rpc QueryCalcCount(ReqQueryCalcCount) returns (ReplyQueryCalcCount) {} 67 } 68 ``` 69 70 主要有以下几个部分: 71 * 定义交易行为总结构,CalculatorAction,包含加减乘除 72 * 分别定义涉及的交易行为结构, Add,Sub等 73 * 定义交易涉及到的日志结构,每种运算除均有对应结果日志 74 * 如果需要grpc服务,定义service结构,如本例增加了查询次数的rpc 75 * 定义查询中涉及的request,reply结构 76 77 78 ### 代码生成 79 ##### 生成基本代码 80 >使用turingchain-tool,工具使用参考[文档](https://github.com/turingchain2020/turingchain/blob/master/cmd/tools/doc/gendapp.md) 81 ``` 82 //本例默认将calculator生成至官方plugin项目dapp目录下 83 $ cd $GOPATH/src/github.com/turingchain2020/turingchain/cmd/tools && go build -o tool 84 $ ./tool gendapp -n calculator -p doc/calculator.proto 85 $ cd $GOPATH/src/github.com/turingchain2020/plugin/plugin/dapp/calculator && ls 86 ``` 87 88 ##### 生成pb.go文件 89 pb.go文件基于protobuf提供的proto-gen-go插件生成,这里protobuf的版本必须和turingchain引用的保持一致, 90 具体可以查看turingchain项目go.mod文件,github.com/golang/protobuf库的版本 91 ``` 92 //进入生成合约的目录 93 $ cd $GOPATH/src/github.com/turingchain2020/plugin/plugin/dapp/calculator 94 //执行脚本生成calculator.pb.go 95 $ cd proto && make 96 ``` 97 98 ### 后续开发 99 以下将以模块为顺序,依次介绍 100 #### types类型模块 101 此目录统一归纳合约类型相关的代码 102 ##### 交易的action和log(types/calculator.go) 103 > 每一种交易通常有交易请求(action),交易执行回执(log), 104 目前框架要求合约开发者自定义aciton和log的id及name, 105 已经自动生成了这些常量,可以根据需要修改 106 ```go 107 // action类型id和name,可以自定义修改 108 const ( 109 TyAddAction= iota + 100 110 TySubAction 111 TyMulAction 112 TyDivAction 113 114 NameAddAction = "Add" 115 NameSubAction = "Sub" 116 NameMulAction = "Mul" 117 NameDivAction = "Div" 118 ) 119 120 // log类型id值 121 const ( 122 TyUnknownLog = iota + 100 123 TyAddLog 124 TySubLog 125 TyMulLog 126 TyDivLog 127 ) 128 ``` 129 > 开发者还需要提供name和id的映射结构,其中actionMap已自动生成, 130 交易log结构由开发者自由定义,这里logMap需要将对应结构按格式填充, 131 如本例中加减乘除都有对应的log类型(也可以采用一个通用结构对应多个交易回执),依次按照格式填入即可 132 ```go 133 134 //定义action的name和id 135 actionMap = map[string]int32{ 136 NameAddAction: TyAddAction, 137 NameSubAction: TySubAction, 138 NameMulAction: TyMulAction, 139 NameDivAction: TyDivAction, 140 } 141 //定义log的id和具体log类型及名称,填入具体自定义log类型 142 logMap = map[int64]*types.LogInfo{ 143 TyAddLog: {Ty:reflect.TypeOf(AddLog{}), Name: "AddLog"}, 144 TySubLog: {Ty:reflect.TypeOf(SubLog{}), Name: "SubLog"}, 145 TyMulLog: {Ty:reflect.TypeOf(MultiplyLog{}), Name: "MultiplyLog"}, 146 TyDivLog: {Ty:reflect.TypeOf(DivideLog{}), Name: "DivideLog"}, 147 } 148 ``` 149 150 151 #### executor执行模块 152 此目录归纳了交易执行逻辑实现代码 153 ##### 实现CheckTx接口(executor/calculator.go) 154 > CheckTx即检查交易合法性,隶属于框架Driver接口,将在交易执行前被框架调用, 155 本例简单实现除法非零检测 156 ```go 157 func (*calculator) CheckTx(tx *types.Transaction, index int) error { 158 159 action := &calculatortypes.CalculatorAction{} 160 err := types.Decode(tx.GetPayload(), action) 161 if err != nil { 162 elog.Error("CheckTx", "DecodeActionErr", err) 163 return types.ErrDecode 164 } 165 //这里只做除法除数零值检查 166 if action.Ty == calculatortypes.TyDivAction { 167 div, ok := action.Value.(*calculatortypes.CalculatorAction_Div) 168 if !ok { 169 return types.ErrTypeAsset 170 } 171 if div.Div.Divisor == 0 { //除数不能为零 172 elog.Error("CheckTx", "Err", "ZeroDivisor") 173 return types.ErrInvalidParam 174 } 175 } 176 return nil 177 } 178 ``` 179 ##### KV常量(executor/kv.go) 180 >目前合约进行存取框架KV数据库(stateDB或localDB)时, 181 其Key的前缀必须满足框架要求规范,已经以常量形式自动生成在代码中, 182 开发者在构造数据key时,需要以此为前缀 183 ``` 184 var ( 185 //KeyPrefixStateDB state db key必须前缀 186 KeyPrefixStateDB = "mavl-calculator-" 187 //KeyPrefixLocalDB local db的key必须前缀 188 KeyPrefixLocalDB = "LODB-calculator-" 189 ) 190 ``` 191 ##### 实现Exec类接口(executor/exec.go) 192 >Exec类接口是交易链上执行的函数,实现交易执行的业务逻辑, 193 数据上链也是此部分完成(生成stateDB KV对),以及生成交易日志,以Add交易为例 194 ```go 195 func (c *calculator) Exec_Add(payload *ptypes.Add, tx *types.Transaction, index int) (*types.Receipt, error) { 196 var receipt *types.Receipt 197 sum := payload.Addend + payload.Summand 198 addLog := &ptypes.AddLog{Sum: sum} 199 logs := []*types.ReceiptLog{{Ty:ptypes.TyAddLog, Log: types.Encode(addLog)}} 200 key := fmt.Sprintf("%s-%s-formula", KeyPrefixStateDB, tx.Hash()) 201 val := fmt.Sprintf("%d+%d=%d", payload.Summand, payload.Addend, sum) 202 receipt = &types.Receipt{ 203 Ty: types.ExecOk, 204 KV: []*types.KeyValue{{Key:[]byte(key), Value:[]byte(val)}}, 205 Logs: logs, 206 } 207 return receipt, nil 208 } 209 ``` 210 ##### 实现ExecLocal类接口(executor/exec_local.go) 211 >ExecLocal类接口是交易执行成功后本地执行, 212 主要目的是将辅助性数据进行localDB存取,方便前端查询, 213 以Add为例,在localDB中存入加法运算的次数,在函数最后需要调用addAutoRollBack接口,以适配框架localdb自动回滚功能 214 ```go 215 func (c *calculator) ExecLocal_Add(payload *ptypes.Add, tx *types.Transaction, receiptData *types.ReceiptData, index int) (*types.LocalDBSet, error) { 216 var dbSet *types.LocalDBSet 217 var countInfo calculatortypes.ReplyQueryCalcCount 218 localKey := []byte(fmt.Sprintf("%s-CalcCount-Add", KeyPrefixLocalDB)) 219 oldVal, err := c.GetLocalDB().Get(localKey) 220 //此处需要注意,目前db接口,获取key未找到记录,返回空时候也带一个notFound错误,需要特殊处理,而不是直接返回错误 221 if err != nil && err != types.ErrNotFound{ 222 return nil, err 223 } 224 err = types.Decode(oldVal, &countInfo) 225 if err != nil { 226 elog.Error("execLocalAdd", "DecodeErr", err) 227 return nil, types.ErrDecode 228 } 229 countInfo.Count++ 230 dbSet = &types.LocalDBSet{KV: []*types.KeyValue{{Key:localKey, Value:types.Encode(&countInfo)}}} 231 //封装kv,适配框架自动回滚,这部分代码已经自动生成 232 return c.addAutoRollBack(tx, dbSet.KV), nil 233 } 234 ``` 235 236 ##### 实现ExecDelLocal类接口(executor/exec_del_local.go) 237 >ExecDelLocal类接口可以理解为ExecLocal的逆过程,在区块回退时候被调用,生成代码已支持自动回滚,无需实现 238 239 ##### 实现Query类接口(executor/query.go) 240 > Query类接口主要实现查询相关业务逻辑,如访问合约数据库, 241 Query类接口需要满足框架规范(固定格式函数名称和签名),才能被框架注册和使用, 242 具体调用方法将在rpc模块介绍,本例实现查询运算符计算次数的接口 243 ```go 244 //函数名称,Query_+实际方法名格式,返回值为protobuf Message结构 245 func (c *calculator) Query_CalcCount(in *ptypes.ReqQueryCalcCount) (types.Message, error) { 246 247 var countInfo ptypes.ReplyQueryCalcCount 248 localKey := []byte(fmt.Sprintf("%s-CalcCount-%s", KeyPrefixLocalDB, in.Action)) 249 oldVal, err := c.GetLocalDB().Get(localKey) 250 if err != nil && err != types.ErrNotFound{ 251 return nil, err 252 } 253 err = types.Decode(oldVal, &countInfo) 254 if err != nil { 255 elog.Error("execLocalAdd", "DecodeErr", err) 256 return nil, err 257 } 258 return &countInfo, nil 259 } 260 ``` 261 #### rpc模块 262 此目录归纳了rpc相关类型和具体调用服务端实现的代码 263 ##### 类型(rpc/types.go) 264 >定义了rpc相关结构和初始化,此部分代码已经自动生成 265 ```go 266 // 实现grpc的service接口 267 type channelClient struct { //实现grpc接口的类 268 rpctypes.ChannelClient 269 } 270 // Jrpc 实现json rpc调用实例 271 type Jrpc struct { //实现json rpc接口的类 272 cli *channelClient 273 } 274 ``` 275 ##### grpc接口(rpc/rpc.go) 276 >grpc即实现proto文件中service声明的rpc接口,本例中即查询计算次数的rpc。 277 此处通过框架Query接口,间接调用之前实现的Query_CalcCount接口 278 ```go 279 func (c *channelClient)QueryCalcCount(ctx context.Context, in *ptypes.ReqQueryCalcCount) (*ptypes.ReplyQueryCalcCount, error) { 280 281 msg, err := c.Query(ptypes.CalculatorX, "CalcCount", in) 282 if err != nil { 283 return nil, err 284 } 285 if reply, ok := msg.(*ptypes.ReplyQueryCalcCount); ok { 286 return reply, nil 287 } 288 return nil, types.ErrTypeAsset 289 } 290 ``` 291 292 ##### json rpc接口 293 >json rpc主要给前端相关平台产品调用,本例为查询计算次数接口 294 ```go 295 func (j *Jrpc)QueryCalcCount(in *ptypes.ReqQueryCalcCount, result *interface{}) error { 296 297 //此处直接调用内部的grpc接口 298 reply, err := j.cli.QueryCalcCount(context.Background(), in) 299 if err != nil { 300 return err 301 } 302 *result = *reply 303 return nil 304 } 305 ``` 306 307 ##### rpc说明 308 >对于构造交易和query类接口可以通过turingchain框架的rpc去调用, 309 分别是Turingchain.CreateTransaction和Turingchain.Query,上述代码只是示例如何开发rpc接口, 310 实际使用中,只需要实现query接口,并通过框架rpc调用,也可以根据需求封装rpc接口,在commands模块将会介绍如何调用框架rpc 311 312 #### commands命令行模块 313 如果需要支持命令行交互式访问区块节点,开发者需要实现具体合约的命令, 314 框架的命令行基于cobra开源库 315 ##### import路径(commands/commands.go) 316 >涉及框架基础库使用,包括相关类型和网络组件 317 ```go 318 import ( 319 "github.com/turingchain2020/turingchain/rpc/jsonclient" 320 "github.com/turingchain2020/turingchain/types" 321 "github.com/spf13/cobra" 322 323 rpctypes "github.com/turingchain2020/turingchain/rpc/types" 324 calculatortypes "github.com/turingchain2020/plugin/plugin/dapp/calculator/types" 325 ) 326 ``` 327 ##### 创建交易命令(commands/commands.go) 328 >前端输入相关参数,调用rpc实现创建原始交易的功能 329 ```go 330 func createAddCmd() *cobra.Command { 331 cmd := &cobra.Command{ 332 Use: "add", 333 Short:"create add calc tx", 334 Run: createAdd, 335 } 336 cmd.Flags().Int32P("summand", "s", 0, "summand integer number") 337 cmd.Flags().Int32P("addend", "a", 0, "addend integer number") 338 cmd.MarkFlagRequired("summand") 339 cmd.MarkFlagRequired("addend") 340 return cmd 341 } 342 343 344 func createAdd(cmd *cobra.Command, args []string) { 345 rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr") 346 summand, _ := cmd.Flags().GetInt32("summand") 347 addend, _ := cmd.Flags().GetInt32("addend") 348 349 req := ptypes.Add{ 350 Summand: summand, 351 Addend: addend, 352 } 353 turingchainReq := rpctypes.CreateTxIn{ 354 Execer: ptypes.CalculatorX, 355 ActionName: ptypes.NameAddAction, 356 Payload: types.MustPBToJSON(&req), 357 } 358 var res string 359 //调用框架CreateTransaction接口构建原始交易 360 ctx := jsonclient.NewRPCCtx(rpcLaddr, "Turingchain.CreateTransaction", turingchainReq, &res) 361 ctx.RunWithoutMarshal() 362 } 363 ``` 364 365 ##### 查询计算次数(commands/commands.go) 366 ```go 367 func queryCalcCountCmd() *cobra.Command { 368 369 cmd := &cobra.Command{ 370 Use: "query_count", 371 Short: "query calculator count", 372 Run: queryCalcCount, 373 } 374 cmd.Flags().StringP("action", "a", "", "calc action name[Add | Sub | Mul | Div]") 375 cmd.MarkFlagRequired("action") 376 377 return cmd 378 } 379 380 func queryCalcCount(cmd *cobra.Command, args []string) { 381 382 rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr") 383 action, _ := cmd.Flags().GetString("action") 384 req := ptypes.ReqQueryCalcCount{ 385 Action: action, 386 } 387 turingchainReq := &rpctypes.Query4Jrpc{ 388 Execer: ptypes.CalculatorX, 389 FuncName: "CalcCount", 390 Payload: types.MustPBToJSON(&req), 391 } 392 var res interface{} 393 res = &calculatortypes.ReplyQueryCalcCount{} 394 //调用框架Query rpc接口, 通过框架调用,需要指定query对应的函数名称,具体参数见Query4Jrpc结构 395 ctx := jsonclient.NewRPCCtx(rpcLaddr, "Turingchain.Query", turingchainReq, &res) 396 //调用合约内部rpc接口, 注意合约自定义的rpc接口是以合约名称作为rpc服务,这里为calculator 397 //ctx := jsonclient.NewRPCCtx(rpcLaddr, "calculator.QueryCalcCount", req, &res) 398 ctx.Run() 399 } 400 ``` 401 ##### 添加到主命令(commands/commands.go) 402 ```go 403 func Cmd() *cobra.Command { 404 cmd := &cobra.Command{ 405 Use: "calculator", 406 Short: "calculator command", 407 Args: cobra.MinimumNArgs(1), 408 } 409 cmd.AddCommand( 410 //add sub command 411 createAddCmd(), 412 queryCalcCountCmd(), 413 ) 414 return cmd 415 } 416 ``` 417 #### 合约集成 418 新增合约需要显示初始化 419 ##### 初始化(dapp/init/init.go) 420 >需要在此文件import目录,新增calculator包导入 421 ```go 422 import ( 423 _ "github.com/turingchain2020/plugin/plugin/dapp/calculator" //init calculator 424 ) 425 ``` 426 427 ##### 编译 428 >直接通过官方makefile文件 429 ``` 430 $ cd $GOPATH/src/github.com/turingchain2020/plugin && make 431 ``` 432 433 #### 测试 434 ##### 单元测试 435 为合约代码增加必要的单元测试,提高测试覆盖 436 ##### 集成测试 437 编译后可以运行节点,进行钱包相关配置,即可发送合约交易进行功能性测试,本例相关命令行 438 ```bash 439 # 通过curl方式调用rpc接口构建Add原始交易 440 curl -kd '{"method":"Turingchain.CreateTransaction", "params":[{"execer":"calculator", "actionName":"Add", "payload":{"summand":1,"addend":1}}]}' http://localhost:9671 441 # 通过turingchain-cli构建Add原始交易 442 ./turingchain-cli calculator add -a 1 -s 1 443 444 # queryCount接口类似 445 curl -kd '{"method":"calculator.QueryCalcCount", "params":[{"action":"Add"}]}' http://localhost:9671 446 ./turingchain-cli calculator query_count -a Add 447 ``` 448 449 #### 进阶 450 ##### 计算器 451 基于 [本例代码](https://github.com/bysomeone/plugin/tree/dapp-example-calculator) 实现减法等交易行为 452 ##### 其他例子 453 官方 [plugin项目](https://github.com/turingchain2020/plugin) 提供了丰富的插件,可以参考学习 454 455 456 457 458 459