傳送門: 柏鏈項目學院html
咱們以前介紹了go語言調用exec處理命令行,介紹了toml配置文件的處理,以及awk處理文本文件得到ABI信息。咱們的代碼算是完成了從智能合約到go語言的自動編譯,同時也能夠自動提取到ABI信息。git
具體能夠參考:github
第二課 智能合約自動化編譯json
想要自動化生成測試代碼,首先你要知道目標代碼長什麼樣兒,也就是以前咱們寫的部署合約,調用合約的代碼。仔細思考,其實會發現分了兩層內容,第一層內容就是針對每個合約函數都要有一個調用函數生成,假設合約函數爲X,咱們把調用該函數的函數叫CallX,咱們想要完成測試,又須要在main函數內能找到調用CallX的入口。先一步一步來,先完成一個小目標,那就是生成這些個CallX,
再來梳理一下咱們以前調用合約的代碼。app
const keydata = `{"address":"791443d21a76e16cc510b6b1684344d2a5ce751c","crypto":{"cipher":"aes-128-ctr","ciphertext":"bbccbf9deb8c907d9f245767fffb57880c4cfd265dde9372d7278a8e963043bd","cipherparams":{"iv":"95e8f925fe0f3460f0ca3ccebc481b14"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"2036300fae07d954a10e70a2f87876fa198b310dc16a72f3fab3265978e7d798"},"mac":"009ce0ecd11d5f563d2f7bd41eb957f1ca3b517f31653dd18fac20e77b7feb5c"},"id":"7f42641c-580a-4398-b00c-74ef2710bcae","version":3}` func CallDeploy() error { //連接到以太坊,cli就是backend cli, err := ethclient.Dial("http://localhost:8545") if err != nil { fmt.Println("failed to dial ", err) return err } //建立身份,須要私鑰= pass+keystore文件 auth, err := bind.NewTransactor(strings.NewReader(keydata), "123") if err != nil { fmt.Println("failed to NewTransactor auth", err) return err } auth.GasLimit = 400000 addr, ts, pd, err := DeployPdbank(auth, cli, "yekai233") if err != nil { fmt.Println("failed to DeployPdbank", err) return err } bkname, _ := pd.BankName(nil) fmt.Println("addr=", addr.Hex(), "bkname=", bkname, "hash=", ts.Hash().Hex()) return err }
對於keydata來講,咱們不能寫死了,須要從keystore文件讀取這部分數據。http://localhost:8545這樣的鏈接地址最好經過配置文件來搞定,但思路要清楚的是,咱們這個代碼是未來要執行的,這個代碼它須要一個本身的配置文件,並不是是咱們當前工程的配置文件,直白一點的說就是咱們須要給將來的代碼寶寶們自動的生成一個配置文件。記得有這麼回事兒,咱們先無論這一點,先以能完成任務爲前提。框架
咱們仍是來單獨造成一個函數,用於專門造成簽名,它須要讀取keystore文件,這個文件能夠根據對應的帳戶地址到以太坊keystore目錄獲得,這樣就容易多了!ide
//設置簽名 func MakeAuth(addr, pass string) (*bind.TransactOpts, error) { keystorePath := "{{.Keydir}}" fileName, err := GetFileName(string([]rune(addr)[2:]), keystorePath) if err != nil { fmt.Println("failed to GetFileName", err) return nil, err } file, err := os.Open(keystorePath + "/" + fileName) if err != nil { fmt.Println("failed to open file ", err) return nil, err } auth, err := bind.NewTransactor(file, pass) if err != nil { fmt.Println("failed to NewTransactor ", err) return nil, err } return auth, err } func GetFileName(address, dirname string) (string, error) { data, err := ioutil.ReadDir(dirname) if err != nil { fmt.Println("read dir err", err) return "", err } for _, v := range data { if strings.Index(v.Name(), address) > 0 { //表明找到文件 return v.Name(), nil } } return "", nil }
因而,咱們將原來寫的測試代碼進行完善一下!函數
此外,鏈接這裏也規範一下,使用init函數來搞定!測試
var testclient *ethclient.Client func init() { cli, err := CreateCli("http://localhost:8545") if err != nil { log.Panic("failed to connect to eth", err) } testclient = cli }
func CallBankName() (error) { instance, err := NewPdbank(common.HexToAddress("0xD55E88D9156355C584982Db2C96dD1C2c63788C2"), testclient) if err != nil { fmt.Println("failed to get contract instance", err) return err } data,err := instance.BankName(nil) if err != nil { fmt.Println("failed to get Balances", err) return err } fmt.Println(data,err) return nil } func CallWithdraw(addr, pass string) (*types.Transaction, error) { instance, err := NewPdbank(common.HexToAddress("0xD55E88D9156355C584982Db2C96dD1C2c63788C2"), testclient) if err != nil { fmt.Println("failed to get contract instance", err) return nil, err } auth, err := MakeAuth(addr, pass) if err != nil { fmt.Println("failed to makeAuth", err) return nil, err } auth.Value = big.NewInt(0) ts,err := instance.Withdraw(auth,big.NewInt(10000)) if err != nil { fmt.Println("failed to call ", err) return nil, err } fmt.Println(ts.ChainId(), ts.Hash().Hex(), ts.Nonce()) return ts , err }
這樣代碼看上去舒服多了,接下來考慮目標代碼的生成辦法。
go語言模版編程須要用到template包,go語言的模版呢實際上也提供了兩大類模版處理。一類是文本的,一類是html的。鑑於咱們的目標代碼是go語言,因此本次使用基於文本的模版處理。
下面是一個最簡單的模版處理的例子,{{.Count}}和{{.Material}}至關因而這個模版要填的兩個空。
type Inventory struct { Material string Count uint } sweaters := Inventory{"wool", 17} tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}") if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, sweaters) if err != nil { panic(err) }
一個複雜一些的例子
// Define a template. const letter = ` Dear {{.Name}}, {{if .Attended}} It was a pleasure to see you at the wedding.{{else}} It is a shame you couldn't make it to the wedding.{{end}} {{with .Gift}}Thank you for the lovely {{.}}. {{end}} Best wishes, Josie ` // Prepare some data to insert into the template. type Recipient struct { Name, Gift string Attended bool } var recipients = []Recipient{ {"Aunt Mildred", "bone china tea set", true}, {"Uncle John", "moleskin pants", false}, {"Cousin Rodney", "", false}, } // Create a new template and parse the letter into it. t := template.Must(template.New("letter").Parse(letter)) // Execute the template for each recipient. for _, r := range recipients { err := t.Execute(os.Stdout, r) if err != nil { log.Println("executing template:", err) } }
咱們能夠發現一些特色:
因而把咱們的目標代碼,能夠製做成定製化的模版!
建立一個用於存放模版定義的文件,template_code.go
package templates const Main_tmpl = `package main import ( "fmt" "log" "os" "gosol/contracts" "io/ioutil" "math/big" "strings" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ) var testclient *ethclient.Client func init() { cli, err := CreateCli("{{.Connstr}}") if err != nil { log.Panic("failed to connect to eth", err) } testclient = cli } func GetFileName(address, dirname string) (string, error) { data, err := ioutil.ReadDir(dirname) if err != nil { fmt.Println("read dir err", err) return "", err } for _, v := range data { if strings.Index(v.Name(), address) > 0 { //表明找到文件 return v.Name(), nil } } return "", nil } //建立連接 func CreateCli(connstr string) (*ethclient.Client, error) { cli, err := ethclient.Dial(connstr) if err != nil { fmt.Println("failed to dial provide", err) return nil, err } return cli, err } //設置簽名 func MakeAuth(addr, pass string) (*bind.TransactOpts, error) { keystorePath := "{{.Keydir}}" fileName, err := GetFileName(string([]rune(addr)[2:]), keystorePath) if err != nil { fmt.Println("failed to GetFileName", err) return nil, err } file, err := os.Open(keystorePath + "/" + fileName) if err != nil { fmt.Println("failed to open file ", err) return nil, err } auth, err := bind.NewTransactor(file, pass) if err != nil { fmt.Println("failed to NewTransactor ", err) return nil, err } return auth, err } ` const Deploy_sol_tmpl = ` func Deploy{{.ContractName}}() (common.Address, error) { auth, err := MakeAuth("{{.FromAddr}}", "{{.Pass}}") if err != nil { fmt.Println("failed to makeAuth", err) return common.HexToAddress(""), err } //common.Address, *types.Transaction, *Pdbank, error contractaddr, ts, _, err := contracts.{{.CallFunc}} if err != nil { fmt.Println("failed to deloy ",err) return common.HexToAddress(""), err } fmt.Println(ts.ChainId(), ts.Hash().Hex(), ts.Nonce()) fmt.Println(contractaddr.Hex()) return contractaddr, err } `
先搞定部署合約代碼自動生成部分!
//1. 寫到哪 outfile, err := os.OpenFile("build/solcall.go", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) if err != nil { fmt.Println("failed to open file", err) return err } defer outfile.Close() //2. 寫什麼 _, err = outfile.WriteString(test_main_temp) if err != nil { fmt.Println("failed to write ", err) return err } // 讀取abi文件信息 abiInfos, err := readAbi("contracts/pdbank.abi") if err != nil { fmt.Println("failed to read abi", err) return err } //fmt.Println(infos) //3. 寫入部署合約代碼 //定義部署模版 deploy_temp, err := template.New("deploy").Parse(test_deploy_temp) if err != nil { fmt.Println("failed to template deploy", err) return err } var deploy_data DeployContractParams deploy_data.DeployName = "DeployPdbank" for _, v := range abiInfos { v.Name = strings.Title(v.Name) //標題優化,首字母大寫, hello world - > Hello World if v.Type == "constructor" { // 若是是構造函數-部署函數 deploy_data.DeployParams = "(auth,testClient" for _, vv := range v.Inputs { //須要根據輸入數據類型來判斷如何處理:string,address,uint256 if vv.Type == "address" { deploy_data.DeployParams += ",common.HexToAddress(\"0xD55E88D9156355C584982Db2C96dD1C2c63788C2\")" } else if vv.Type == "uint256" { deploy_data.DeployParams += ",big.NewInt(1000)" } else if vv.Type == "string" { deploy_data.DeployParams += ",\"yekai\"" } } deploy_data.DeployParams += ")" //模版的執行 err = deploy_temp.Execute(outfile, &deploy_data) if err != nil { fmt.Println("failed to template Execute ", err) return err } } }
接下來的代碼仍是相同的套路。
完整代碼以下;
//temp_org.go package templates const test_main_temp = ` package main import ( "fmt" "gosolkit/contracts" "io/ioutil" "log" "math/big" "os" "strings" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) var testClient *ethclient.Client func init() { cli, err := Connect("http://localhost:8545") if err != nil { log.Fatalln("failed to connect to eth", err) } testClient = cli } func Connect(connstr string) (*ethclient.Client, error) { return ethclient.Dial(connstr) } //簽名函數 func MakeAuth(addr, pass string) (*bind.TransactOpts, error) { //1. 根據addr找到keystore目錄下的文件 keyDir := "/Users/yk/ethdev/data/keystore" infos, err := ioutil.ReadDir(keyDir) if err != nil { fmt.Println("failed to readdir", err) return nil, err } //UTC--2019-03-16T13-00-48.032030904Z--791443d21a76e16cc510b6b1684344d2a5ce751c //0x791443d21a76e16cc510b6b1684344d2a5ce751c strAddr := ([]rune(addr))[2:] for _, v := range infos { strVname := []rune(v.Name()) if len(strVname) > len(strAddr) { strVname2 := strVname[len(strVname)-len(strAddr):] if strings.EqualFold(string(strAddr), string(strVname2)) { //找到了匹配的文件 //fmt.Println(addr, v.Name()) //2. 作簽名 reader, err := os.Open(keyDir + "/" + v.Name()) if err != nil { fmt.Println("failed to open file", err) return nil, err } defer reader.Close() auth, err := bind.NewTransactor(reader, pass) if err != nil { fmt.Println("failed to NewTransactor auth", err) return nil, err } return auth, err } } } return nil, nil } ` const test_deploy_temp = ` func CallDeploy() error { //建立身份,須要私鑰= pass+keystore文件 auth, err := MakeAuth("0x791443d21a76e16cc510b6b1684344d2a5ce751c", "123") if err != nil { fmt.Println("failed to MakeAuth auth", err) return err } addr, ts, _, err := contracts.{{.DeployName}}{{.DeployParams}} if err != nil { fmt.Println("failed to DeployPdbank", err) return err } fmt.Println("addr=", addr.Hex(), "hash=", ts.Hash().Hex()) return err } ` const test_nogas_temp = ` func Call{{.FuncName}}() error { //使用以前部署獲得的合約地址 instance, err := contracts.{{.NewContractName}}(common.HexToAddress("0xD55E88D9156355C584982Db2C96dD1C2c63788C2"), testClient) if err != nil { fmt.Println("failed to instance contract", err) return err } //調用合約函數 {{.OutParams}} := instance.{{.FuncName}}{{.InputParams}} fmt.Println({{.OutParams}}) return err } ` const test_gas_temp = ` func Call{{.FuncName}}() error { //2. 構造函數入口 - 合約對象 instance, err := contracts.{{.NewContractName}}(common.HexToAddress("0xD55E88D9156355C584982Db2C96dD1C2c63788C2"), testClient) if err != nil { fmt.Println("failed to contract instance", err) return err } //3. 設置簽名 auth, err := MakeAuth("0x791443d21a76e16cc510b6b1684344d2a5ce751c", "123") if err != nil { fmt.Println("failed to MakeAuth auth", err) return err } //4. 函數調用 auth.Value = big.NewInt(0) ts, err := instance.{{.FuncName}}{{.InputParams}} if err != nil { fmt.Println("failed to Deposit ", err) return err } fmt.Println(ts.Hash().Hex()) return err } `
//temp_main.go package templates const test_run_main_temp = ` package main import ( "fmt" "os" ) var funcNames = []string{%s} ` //1. 提供一個命令行幫助 const test_build_main_temp = ` func Usage() { fmt.Printf("%s 1 -- deploy\n", os.Args[0]) num := 2 for _, v := range funcNames { fmt.Printf("%s %d -- %s\n", os.Args[0], num, v) num++ } } func main() { if len(os.Args) < 2 { Usage() os.Exit(0) } if os.Args[1] == "1" { CallDeploy() }{{range.}} else if os.Args[1] == "{{.Num}}" { Call{{.FuncName}}() } {{end}} } `
//temp_impl.go package templates import ( "encoding/json" "fmt" "io/ioutil" "os" "strings" "text/template" ) type DeployContractParams struct { DeployName string DeployParams string } //無gas函數調用 type FuncNoGasParams struct { FuncName string NewContractName string OutParams string InputParams string } //有gas函數調用 type FuncGasParams struct { FuncName string NewContractName string InputParams string } type InputsOutPuts struct { Name string Type string } type FuncInfo struct { FuncName string Num int } type AbiInfo struct { Constant bool Inputs []InputsOutPuts Name string Outputs []InputsOutPuts Payable bool StateMutability string Type string } func readAbi(abifile string) ([]AbiInfo, error) { file, err := os.Open(abifile) if err != nil { fmt.Println("failed to open file ", err) return nil, err } data, err := ioutil.ReadAll(file) if err != nil { fmt.Println("failed to read abi", err) return nil, err } var abiInfos []AbiInfo strdata := strings.Replace(string(data), "\\", "", -1) err = json.Unmarshal([]byte(strdata), &abiInfos) if err != nil { fmt.Println("failed to Unmarshal abi", err) return nil, err } return abiInfos, err } func Impl_run_code() error { //1. 寫到哪 outfile, err := os.OpenFile("build/solcall.go", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) if err != nil { fmt.Println("failed to open file", err) return err } defer outfile.Close() //2. 寫什麼 _, err = outfile.WriteString(test_main_temp) if err != nil { fmt.Println("failed to write ", err) return err } // 讀取abi文件信息 abiInfos, err := readAbi("contracts/pdbank.abi") if err != nil { fmt.Println("failed to read abi", err) return err } //fmt.Println(infos) //3. 寫入部署合約代碼 //定義部署模版 deploy_temp, err := template.New("deploy").Parse(test_deploy_temp) if err != nil { fmt.Println("failed to template deploy", err) return err } var deploy_data DeployContractParams deploy_data.DeployName = "DeployPdbank" //定義nogas函數的模版 nogas_temp, err := template.New("nogas").Parse(test_nogas_temp) if err != nil { fmt.Println("failed to template nogas_temp", err) return err } var func_nogas_data FuncNoGasParams func_nogas_data.NewContractName = "NewPdbank" //定義有gas模版 hasgas_temp, err := template.New("hasgas").Parse(test_gas_temp) if err != nil { fmt.Println("failed to template hasgas_temp", err) return err } var func_gas_data FuncGasParams func_gas_data.NewContractName = "NewPdbank" //對abi進行遍歷處理 for _, v := range abiInfos { v.Name = strings.Title(v.Name) //標題優化,首字母大寫, hello world - > Hello World if v.Type == "constructor" { // 若是是構造函數-部署函數 deploy_data.DeployParams = "(auth,testClient" for _, vv := range v.Inputs { //須要根據輸入數據類型來判斷如何處理:string,address,uint256 if vv.Type == "address" { deploy_data.DeployParams += ",common.HexToAddress(\"0xD55E88D9156355C584982Db2C96dD1C2c63788C2\")" } else if vv.Type == "uint256" { deploy_data.DeployParams += ",big.NewInt(1000)" } else if vv.Type == "string" { deploy_data.DeployParams += ",\"yekai\"" } } deploy_data.DeployParams += ")" //模版的執行 err = deploy_temp.Execute(outfile, &deploy_data) if err != nil { fmt.Println("failed to template Execute ", err) return err } } else { //處理其餘函數 if len(v.Outputs) > 0 { //不須要gas函數 func_nogas_data.FuncName = v.Name func_nogas_data.InputParams = "(nil" for _, vv := range v.Inputs { //須要根據輸入數據類型來判斷如何處理:string,address,uint256 if vv.Type == "address" { func_nogas_data.InputParams += ",common.HexToAddress(\"0xD55E88D9156355C584982Db2C96dD1C2c63788C2\")" } else if vv.Type == "uint256" { func_nogas_data.InputParams += ",big.NewInt(1000)" } else if vv.Type == "string" { func_nogas_data.InputParams += ",\"yekai\"" } } func_nogas_data.InputParams += ")" //輸入參數 num := 0 strOutPuts := "" for _, _ = range v.Outputs { strOutPuts = fmt.Sprintf("%sdata%d,", strOutPuts, num) num++ } strOutPuts += "err" func_nogas_data.OutParams = strOutPuts //模版的執行 err = nogas_temp.Execute(outfile, &func_nogas_data) if err != nil { fmt.Println("failed to template nogas Execute ", err) return err } } else { //須要消耗gas func_gas_data.FuncName = v.Name func_gas_data.InputParams = "(auth" for _, vv := range v.Inputs { //須要根據輸入數據類型來判斷如何處理:string,address,uint256 if vv.Type == "address" { func_gas_data.InputParams += ",common.HexToAddress(\"0xD55E88D9156355C584982Db2C96dD1C2c63788C2\")" } else if vv.Type == "uint256" { func_gas_data.InputParams += ",big.NewInt(1000)" } else if vv.Type == "string" { func_gas_data.InputParams += ",\"yekai\"" } } func_gas_data.InputParams += ")" //模版的執行 err = hasgas_temp.Execute(outfile, &func_gas_data) if err != nil { fmt.Println("failed to template hasgas Execute ", err) return err } } } } return nil } func Impl_main_code() error { //1. 寫到哪 outfile, err := os.OpenFile("build/main.go", os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) if err != nil { fmt.Println("failed to open file", err) return err } defer outfile.Close() // 讀取abi文件信息 abiInfos, err := readAbi("contracts/pdbank.abi") if err != nil { fmt.Println("failed to read abi", err) return err } funcNames := "" //"abc","def"," num := 0 var funcInfos []FuncInfo var funcInfo FuncInfo // 2- 第一個函數 for _, v := range abiInfos { if v.Type != "constructor" { if num == 0 { //第一個 funcNames += fmt.Sprintf(`"%s"`, v.Name) } else { funcNames += fmt.Sprintf(`,"%s"`, v.Name) } num++ funcInfo.FuncName = strings.Title(v.Name) funcInfo.Num = num + 1 funcInfos = append(funcInfos, funcInfo) } } main_str1 := fmt.Sprintf(test_run_main_temp, funcNames) _, err = outfile.WriteString(main_str1) if err != nil { fmt.Println("failed to write to main.go", err) return err } //創建一個模版,輸出內容 main_temp, err := template.New("main").Parse(test_build_main_temp) if err != nil { fmt.Println("failed to template main", err) return err } err = main_temp.Execute(outfile, funcInfos) if err != nil { fmt.Println("failed to Execute main", err) return err } return err } func Run() { Impl_run_code() Impl_main_code() }
在main函數內增長此部分調用
package main import ( "fmt" "gosolkit/templates" "os" ) func Usage() { fmt.Printf("%s 1 -- compiler code\n", os.Args[0]) fmt.Printf("%s 2 -- build test code\n", os.Args[0]) } func main() { if len(os.Args) < 2 { Usage() os.Exit(0) } if os.Args[1] == "1" { CompilerRun() } else if os.Args[1] == "2" { //build test code templates.Run() } else { Usage() os.Exit(0) } }