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