📜 ⬆️ ⬇️

Easy “Frontend” on Golang for manual testing of Ethereum smart contract without JavaScript and Web3

Hello!


I had an idea to develop, I hope, a simple solution for manual testing of smart contracts Ethereum. It became interesting to do something similar to the functionality of the Run tab in Remix.


What can the application:


It turned out to be a simple, yet backend, on Golang, which can:



Now in order:


The choice fell on Golang due to the fact that I really liked the go- ethereum codebase , on which Geth is built.


To generate static html, the standard Golang package "html / template" is used. Here I will not paint anything, all templates can be found in the project templates package.
To work with Ethereum, as I wrote above, I chose the go-ethereum version 1.7.3 code base.
I really wanted to use the mobile package from go-ethereum, but mobile has not been updated for some time and is currently not working correctly with the current Abi format. When processing data, you will receive a similar error:


abi: cannot unmarshal *big.Int in to []interface {} 

The error has already been corrected , but the fix has not yet been added to the main branch at the time I am writing this.


I nevertheless chose another solution, flawless, since The functions in the mobile package are in essence a convenient wrapper over the main functionality.


In the end, I took the package to work with abi (+ some more packages that depend on abi) from go-ethereum to my project and added code from pull request.


Since I needed to work with any smart contracts, the abigen utility, which can form a go package for working with a specific contract from a sol file, did not suit me.


I created a structure, and the methods for which this structure is a receiver (if I'm not mistaken in the terminology of Golang):


 type EthWorker struct { Container string //   sol,     Contract string //  Endpoint string //   Key string //   ContractAddress string //  FormValues url.Values //map    ,  POST form New bool //    } 

The full interface looks like this:


 type ReadWriterEth interface { Transact() (string, error) //    Call() (string, error) //    Deploy() (string, string, error) //    Info() (*Info, error) //   ,      ParseInput() ([]interface{}, error) //  POST         ParseOutput([]interface{}) (string, error) //      } 

The function to write information to a contract:


Transact
 func (w *EthWorker) Transact() (string, error) { // POST ,      inputs, err := w.ParseInput() if err != nil { return "", errors.Wrap(err, "parse input") } //    EthWorker       pk := strings.TrimPrefix(w.Key, "0x") key, err := crypto.HexToECDSA(pk) if err != nil { return "", errors.Wrap(err, "hex to ECDSA") } auth := bind.NewKeyedTransactor(key) if !common.IsHexAddress(w.ContractAddress) { return "", errors.New("New Address From Hex") } addr := common.HexToAddress(w.ContractAddress) //    contract := bind.NewBoundContract( addr, Containers.Containers[w.Container].Contracts[w.Contract].Abi, Client, Client, ) //    Gas gasprice, err := Client.SuggestGasPrice(context.Background()) if err != nil { return "", errors.Wrap(err, "suggest gas price") } //      opt := &bind.TransactOpts{ From: auth.From, Signer: auth.Signer, GasPrice: gasprice, GasLimit: GasLimit, Value: auth.Value, } //   tr, err := contract.Transact(opt, w.Endpoint, inputs...) if err != nil { return "", errors.Wrap(err, "transact") } var receipt *types.Receipt //    ,       ,       switch v := Client.(type) { case *backends.SimulatedBackend: v.Commit() receipt, err = v.TransactionReceipt(context.Background(), tr.Hash()) if err != nil { return "", errors.Wrap(err, "transaction receipt") } case *ethclient.Client: receipt, err = bind.WaitMined(context.Background(), v, tr) if err != nil { return "", errors.Wrap(err, "transaction receipt") } } if err != nil { return "", errors.Errorf("error transact %s: %s", tr.Hash().String(), err.Error(), ) } //     responce := fmt.Sprintf(templates.WriteResult, tr.Nonce(), auth.From.String(), tr.To().String(), tr.Value().String(), tr.GasPrice().String(), receipt.GasUsed.String(), new(big.Int).Mul(receipt.GasUsed, tr.GasPrice()), receipt.Status, receipt.TxHash.String(), ) return responce, nil } 

The function to read information from the contract:


Call
 func (w *EthWorker) Call() (string, error) { inputs, err := w.ParseInput() if err != nil { return "", errors.Wrap(err, "parse input") } key, _ := crypto.GenerateKey() auth := bind.NewKeyedTransactor(key) contract := bind.NewBoundContract( common.HexToAddress(w.ContractAddress), Containers.Containers[w.Container].Contracts[w.Contract].Abi, Client, Client, ) opt := &bind.CallOpts{ Pending: true, From: auth.From, } outputs := Containers.Containers[w.Container].Contracts[w.Contract].OutputsInterfaces[w.Endpoint] if err := contract.Call( opt, &outputs, w.Endpoint, inputs..., ); err != nil { return "", errors.Wrap(err, "call contract") } result, err := w.ParseOutput(outputs) if err != nil { return "", errors.Wrap(err, "parse output") } return result, err } 

Function to deploy contracts:


Deploy
 func (w *EthWorker) Deploy() (string, string, error) { inputs, err := w.ParseInput() if err != nil { return "", "", errors.Wrap(err, "parse input") } pk := strings.TrimPrefix(w.Key, "0x") key, err := crypto.HexToECDSA(pk) if err != nil { return "", "", errors.Wrap(err, "hex to ECDSA") } auth := bind.NewKeyedTransactor(key) current_bytecode := Containers.Containers[w.Container].Contracts[w.Contract].Bin current_abi := Containers.Containers[w.Container].Contracts[w.Contract].Abi addr, tr, _, err := bind.DeployContract(auth, current_abi, common.FromHex(current_bytecode), Client, inputs...) if err != nil { log.Printf("error %s", err.Error()) return "", "", errors.Wrap(err, "deploy contract") } var receipt *types.Receipt switch v := Client.(type) { case *backends.SimulatedBackend: v.Commit() receipt, err = v.TransactionReceipt(context.Background(), tr.Hash()) if err != nil { return "", "", errors.Wrap(err, "transaction receipt") } case *ethclient.Client: receipt, err = bind.WaitMined(context.Background(), v, tr) if err != nil { return "", "", errors.Wrap(err, "transaction receipt") } } if err != nil { return "", "", errors.Errorf("error transact %s: %s", tr.Hash().String(), err.Error(), ) } responce := fmt.Sprintf(templates.DeployResult, tr.Nonce(), auth.From.String(), addr.String(), tr.GasPrice().String(), receipt.GasUsed.String(), new(big.Int).Mul(receipt.GasUsed, tr.GasPrice()).String(), receipt.Status, receipt.TxHash.String(), ) return responce, addr.String(), nil } 

It was necessary to solve the problem of how to obtain data from the data entered by the user in a form on a web page, which can be passed to the Call and Transact functions.


I didn’t think of anything better than to find out from the contract method abi the necessary data type for a specific field, and to bring to it what the user entered into a form on a web page. Those. If I forgot some data type, then my solution will not work with this data type. It is necessary to make changes to the code. Implemented in ParseInput function


ParseInput
 func (w *EthWorker) ParseInput() ([]interface{}, error) { //           ,        if w.New && len(Containers.Containers[w.Container].Contracts[w.Contract].Abi.Constructor.Inputs) == 0 { return nil, nil } //           ,        if !w.New && len(Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Inputs) == 0 { return nil, nil } //  Form Values inputsMap := make(map[int]string) var inputsArray []int var inputsSort []string for k, v := range w.FormValues { if k == "endpoint" { continue } if len(v) != 1 { return nil, errors.Errorf("incorrect %s field", k) } i, err := strconv.Atoi(k) if err != nil { continue //return nil, errors.Wrap(err, "incorrect inputs: strconv.Atoi") } inputsMap[i] = v[0] } //    ,   ,    if Containers.Containers[w.Container] == nil || Containers.Containers[w.Container].Contracts[w.Contract] == nil { return nil, errors.New("input values incorrect") } //  , ..  Containers  .      if !w.New && len(Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Inputs) != 0 && Containers.Containers[w.Container].Contracts[w.Contract].InputsInterfaces[w.Endpoint] == nil { return nil, errors.New("input values incorrect") } //       .     ABI var inputs_args []abi.Argument if w.New { inputs_args = Containers.Containers[w.Container].Contracts[w.Contract].Abi.Constructor.Inputs } else { inputs_args = Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Inputs } if len(inputsMap) != len(inputs_args) { return nil, errors.New("len inputs_args != inputsMap: incorrect inputs") } for k := range inputsMap { inputsArray = append(inputsArray, k) } sort.Ints(inputsArray) for k := range inputsArray { inputsSort = append(inputsSort, inputsMap[k]) } var inputs_interfaces []interface{} for i := 0; i < len(inputs_args); i++ { arg_value := inputsMap[i] switch inputs_args[i].Type.Type.String() { case "bool": var result bool result, err := strconv.ParseBool(arg_value) if err != nil { return nil, errors.New("incorrect inputs") } inputs_interfaces = append(inputs_interfaces, result) case "[]bool": var result []bool result_array := strings.Split(arg_value, ",") for _, bool_value := range result_array { item, err := strconv.ParseBool(bool_value) if err != nil { return nil, errors.Wrap(err, "incorrect inputs") } result = append(result, item) } inputs_interfaces = append(inputs_interfaces, result) case "string": inputs_interfaces = append(inputs_interfaces, arg_value) case "[]string": result_array := strings.Split(arg_value, ",") //TODO: NEED REF inputs_interfaces = append(inputs_interfaces, result_array) case "[]byte": inputs_interfaces = append(inputs_interfaces, []byte(arg_value)) case "[][]byte": var result [][]byte result_array := strings.Split(arg_value, ",") for _, byte_value := range result_array { result = append(result, []byte(byte_value)) } inputs_interfaces = append(inputs_interfaces, result) case "common.Address": if !common.IsHexAddress(arg_value) { return nil, errors.New("incorrect inputs: arg_value is not address") } inputs_interfaces = append(inputs_interfaces, common.HexToAddress(arg_value)) case "[]common.Address": var result []common.Address result_array := strings.Split(arg_value, ",") for _, addr_value := range result_array { if !common.IsHexAddress(arg_value) { return nil, errors.New("incorrect inputs: arg_value is not address") } addr := common.HexToAddress(addr_value) result = append(result, addr) } inputs_interfaces = append(inputs_interfaces, result) case "common.Hash": if !common.IsHex(arg_value) { return nil, errors.New("incorrect inputs: arg_value is not hex") } inputs_interfaces = append(inputs_interfaces, common.HexToHash(arg_value)) case "[]common.Hash": var result []common.Hash result_array := strings.Split(arg_value, ",") for _, addr_value := range result_array { if !common.IsHex(arg_value) { return nil, errors.New("incorrect inputs: arg_value is not hex") } hash := common.HexToHash(addr_value) result = append(result, hash) } inputs_interfaces = append(inputs_interfaces, result) case "int8": i, err := strconv.ParseInt(arg_value, 10, 8) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not int8") } inputs_interfaces = append(inputs_interfaces, int8(i)) case "int16": i, err := strconv.ParseInt(arg_value, 10, 16) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not int16") } inputs_interfaces = append(inputs_interfaces, int16(i)) case "int32": i, err := strconv.ParseInt(arg_value, 10, 32) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not int32") } inputs_interfaces = append(inputs_interfaces, int32(i)) case "int64": i, err := strconv.ParseInt(arg_value, 10, 64) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not int64") } inputs_interfaces = append(inputs_interfaces, int64(i)) case "uint8": i, err := strconv.ParseInt(arg_value, 10, 8) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not uint8") } inputs_interfaces = append(inputs_interfaces, big.NewInt(i)) case "uint16": i, err := strconv.ParseInt(arg_value, 10, 16) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not uint16") } inputs_interfaces = append(inputs_interfaces, big.NewInt(i)) case "uint32": i, err := strconv.ParseInt(arg_value, 10, 32) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not uint32") } inputs_interfaces = append(inputs_interfaces, big.NewInt(i)) case "uint64": i, err := strconv.ParseInt(arg_value, 10, 64) if err != nil { return nil, errors.New("incorrect inputs: arg_value is not uint64") } inputs_interfaces = append(inputs_interfaces, big.NewInt(i)) case "*big.Int": bi := new(big.Int) bi, _ = bi.SetString(arg_value, 10) if bi == nil { return nil, errors.New("incorrect inputs: " + arg_value + " not " + inputs_args[i].Type.String()) } inputs_interfaces = append(inputs_interfaces, bi) case "[]*big.Int": var result []*big.Int result_array := strings.Split(arg_value, ",") for _, big_value := range result_array { bi := new(big.Int) bi, _ = bi.SetString(big_value, 10) if bi == nil { return nil, errors.New("incorrect inputs: " + arg_value + " not " + inputs_args[i].Type.String()) } result = append(result, bi) } inputs_interfaces = append(inputs_interfaces, result) } } //    return inputs_interfaces, nil } 

I did a similar conversion for the data we get from Ethereum in the ParseOutput function


Parseoutput
 func (w *EthWorker) ParseOutput(outputs []interface{}) (string, error) { if len(Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Outputs) == 0 { return "", nil } if Containers.Containers[w.Container] == nil || Containers.Containers[w.Container].Contracts[w.Contract] == nil { return "", errors.New("input values incorrect") } if len(Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Outputs) != 0 && Containers.Containers[w.Container].Contracts[w.Contract].OutputsInterfaces[w.Endpoint] == nil { return "", errors.New("input values incorrect") } output_args := Containers.Containers[w.Container].Contracts[w.Contract].Abi.Methods[w.Endpoint].Outputs if len(outputs) != len(output_args) { return "", errors.New("incorrect inputs") } var item_array []string for i := 0; i < len(outputs); i++ { switch output_args[i].Type.Type.String() { case "bool": item := strconv.FormatBool(*outputs[i].(*bool)) item_array = append(item_array, item) case "[]bool": boolArray := *outputs[i].(*[]bool) var boolItems []string for _, bool_value := range boolArray { item := strconv.FormatBool(bool_value) boolItems = append(boolItems, item) } item := "[ " + strings.Join(boolItems, ",") + " ]" item_array = append(item_array, item) case "string": item_array = append(item_array, *outputs[i].(*string)) case "[]string": array := *outputs[i].(*[]string) var items []string for _, value := range array { items = append(items, value) } item := "[ " + strings.Join(items, ",") + " ]" item_array = append(item_array, item) case "[]byte": array := *outputs[i].(*[]byte) var items []string for _, value := range array { items = append(items, string(value)) } item := "[ " + strings.Join(items, ",") + " ]" item_array = append(item_array, item) case "[][]byte": array := *outputs[i].(*[][]byte) var items string for _, array2 := range array { var items2 []string for _, value := range array2 { items2 = append(items2, string(value)) } item2 := "[ " + strings.Join(items2, ",") + " ]" items = items + "," + item2 } item_array = append(item_array, items) case "common.Address": item := *outputs[i].(*common.Address) item_array = append(item_array, item.String()) case "[]common.Address": addrArray := *outputs[i].(*[]common.Address) var addrItems []string for _, value := range addrArray { addrItems = append(addrItems, value.String()) } item := "[ " + strings.Join(addrItems, ",") + " ]" item_array = append(item_array, item) case "common.Hash": item := *outputs[i].(*common.Hash) item_array = append(item_array, item.String()) case "[]common.Hash": hashArray := *outputs[i].(*[]common.Hash) var hashItems []string for _, value := range hashArray { hashItems = append(hashItems, value.String()) } item := "[ " + strings.Join(hashItems, ",") + " ]" item_array = append(item_array, item) case "int8": item := *outputs[i].(*int8) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "int16": item := *outputs[i].(*int16) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "int32": item := *outputs[i].(*int32) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "int64": item := *outputs[i].(*int64) str := strconv.FormatInt(item, 10) item_array = append(item_array, str) case "uint8": item := *outputs[i].(*uint8) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "uint16": item := *outputs[i].(*uint16) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "uint32": item := *outputs[i].(*uint32) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "uint64": item := *outputs[i].(*uint64) str := strconv.FormatInt(int64(item), 10) item_array = append(item_array, str) case "*big.Int": item := *outputs[i].(**big.Int) item_array = append(item_array, item.String()) case "[]*big.Int": bigArray := *outputs[i].(*[]*big.Int) var items []string for _, v := range bigArray { items = append(items, v.String()) } item := "[ " + strings.Join(items, ",") + " ]" item_array = append(item_array, item) } } return strings.Join(item_array, " , "), nil } 

From the code base of the abigen utility I mentioned earlier, I tore the functionality for working with the Solidity compiler. In the end, I got abi and bytecode for almost any contract. Implemented in a Bind function.


Bind
 func Bind(dirname, solcfile string) (*ContractContainers, error) { result := &ContractContainers{ Containers: make(map[string]*ContractContainer), } allfiles, err := ioutil.ReadDir(dirname) if err != nil { return nil, errors.Wrap(err, "error ioutil.ReadDir") } for _, v := range allfiles { if v.IsDir() { continue } if hasSuffixCaseInsensitive(v.Name(), ".sol") { contracts, err := compiler.CompileSolidity(solcfile, dirname+string(os.PathSeparator)+v.Name()) if err != nil { return nil, errors.Wrap(err, "CompileSolidity") } c := &ContractContainer{ ContainerName: v.Name(), Contracts: make(map[string]*Contract), } for name, contract := range contracts { a, _ := json.Marshal(contract.Info.AbiDefinition) ab, err := abi.JSON(strings.NewReader(string(a))) if err != nil { return nil, errors.Wrap(err, "abi.JSON") } nameParts := strings.Split(name, ":") var ab_keys []string ouputs_map := make(map[string][]interface{}) inputs_map := make(map[string][]interface{}) for key, method := range ab.Methods { ab_keys = append(ab_keys, key) var o []interface{} var i []interface{} for _, v := range method.Outputs { var ar interface{} switch v.Type.Type.String() { case "bool": ar = new(bool) case "[]bool": ar = new([]bool) case "string": ar = new(string) case "[]string": ar = new([]string) case "[]byte": ar = new([]byte) case "[][]byte": ar = new([][]byte) case "common.Address": ar = new(common.Address) case "[]common.Address": ar = new([]common.Address) case "common.Hash": ar = new(common.Hash) case "[]common.Hash": ar = new([]common.Hash) case "int8": ar = new(int8) case "int16": ar = new(int16) case "int32": ar = new(int32) case "int64": ar = new(int64) case "uint8": ar = new(uint8) case "uint16": ar = new(uint16) case "uint32": ar = new(uint32) case "uint64": ar = new(uint64) case "*big.Int": ar = new(*big.Int) case "[]*big.Int": ar = new([]*big.Int) default: return nil, errors.Errorf("unsupported type: %s", v.Type.Type.String()) } o = append(o, ar) } ouputs_map[method.Name] = o for _, v := range method.Inputs { var ar interface{} switch v.Type.Type.String() { case "bool": ar = new(bool) case "[]bool": ar = new([]bool) case "string": ar = new(string) case "[]string": ar = new([]string) case "[]byte": ar = new([]byte) case "[][]byte": ar = new([][]byte) case "common.Address": ar = new(common.Address) case "[]common.Address": ar = new([]common.Address) case "common.Hash": ar = new(common.Hash) case "[]common.Hash": ar = new([]common.Hash) case "int8": ar = new(int8) case "int16": ar = new(int16) case "int32": ar = new(int32) case "int64": ar = new(int64) case "uint8": ar = new(uint8) case "uint16": ar = new(uint16) case "uint32": ar = new(uint32) case "uint64": ar = new(uint64) case "*big.Int": ar = new(*big.Int) case "[]*big.Int": ar = new([]*big.Int) default: return nil, errors.Errorf("unsupported type: %s", v.Type.Type.String()) } i = append(i, ar) } inputs_map[method.Name] = i } sort.Strings(ab_keys) con := &Contract{ Name: nameParts[len(nameParts)-1], Abi: ab, AbiJson: string(a), Bin: contract.Code, SortKeys: ab_keys, OutputsInterfaces: ouputs_map, InputsInterfaces: inputs_map, } c.ContractNames = append(c.ContractNames, nameParts[len(nameParts)-1]) c.Contracts[nameParts[len(nameParts)-1]] = con } sort.Strings(c.ContractNames) result.ContainerNames = append(result.ContainerNames, c.ContainerName) result.Containers[c.ContainerName] = c } } sort.Strings(result.ContainerNames) return result, err } 

In the function, there remained a large block of code from experiments with the mobile package, which I have not yet removed, but simply made a refactor.


I created a fairly large ContractContainers structure in which I put all the information about current contracts, and in the future the application takes all the information from it.


Finally I will tell how it works:


I ran the program only on Linux. I have no other operating systems nearby.
Although assembled executable files for Windows and Mac.


First you need a Solidity compiler for your platform. This is probably the most difficult item.


You can take the compiled binary or source code here or see the details here . Versions 0.4.18 and 0.4.19 for linux and Windows I put in the solc directory of the project. You can also use the compiler already installed in the system. To check whether the Solidity compiler is on the system, type the following at the command prompt:


 solc —version 

If the answer is:


 solc, the solidity compiler commandline interface Version: 0.4.18+commit.9cf6e910.Linux.g++ 

then all is well.
If it will require some libraries, then just install them, for example, if Ubuntu asks for it:


 ./solc: error while loading shared libraries: libz3.so.4: cannot open shared object file: No such file or directory 

then put libz3-dev


Next you need to decide in which mode we will work with Ethereum. There are two ways:



You can certainly make it much more beautiful, but for an example the existing solution is quite suitable. The application takes Ethereum addresses from these files and makes a non-zero balance for them.
I put 5 files in the keystore directory for an example. They can be used in a test environment.


Fill in config.yaml config:



Run the application. The path to the directory with the configuration file can be specified via the -config flag


 ./efront-v0.0.1-linux-amd64 -config $GOPATH/src/ethereum-front/ 

Follow the link in the browser: the default is http: // localhost: 8085
You need to enter a private key. Private keys for five test addresses can be found in keys.txt. This private key will live in cookies of our browser for 15 minutes. Next will be a new request. Now nothing is encrypted.



select th select the container (.sol file) and the contract that the application has found in it.



Next, you can enter the address of the once-expanded contract or deploy a new one by checking the corresponding checkbox. If checkbox Deploy is on, then the address field is ignored.


If everything went well then you will see a similar picture in the browser.



If there are errors, they will be displayed in the textarea at the top of the interface.
At the top of the page are two links, login and upload.


Login redirects to enter a new private key. Upload redirects to contract selection.


Next is the current session information:




Next come two tables:
Left table for working with current contract methods. It changes dynamically, depending on the selected contract.


The right table is common functions for working with Ethereum:



Note: When performing transactions (operations on the blockchain), wait for the page to load, it can take several seconds. .


, textarea ( ):





.
C OS. bin.


:



Source
Thanks to all.


')

Source: https://habr.com/ru/post/346432/


All Articles