Привет!
У меня возникла идея разработать надеюсь простое решение, для ручного тестирования смарт контрактов Ethereum. Стало интересно сделать, что-то похожее на функционал вкладки Run в Remix.
Что умеет приложение:
Получился простой всё же backend, на Golang, который умеет:
- генерировать на своих эндпоинтах статические html станицы и отдавать их в браузер;
- брать настройки из toml конфига;
- подключаться к Ethereum ноде по RPC;
- превращаться в симулятор Ethereum;
- компилировать .sol файлы;
- разворачивать контракты;
- писать в контракт и читать из контракта информацию;
- делать трансфер ETH на любой Ethereum адрес;
- получать информацию о Ethereum сети, информацию из последнего блока;
- подгружать для работы несколько контрактов из одной директории, затем можно выбрать с каким конкретно контрактом вы хотите работать;
- сохраняет в куках не зашифрованную информацию;
- раз в 15 минут запрашивает приватный ключ и от имени этого пользователя выполняются операции;
- показывать информацию о текущем сеансе: текущей адрес, текущий баланс, выбранный sol файл и контракт в нем;
- строить таблицу из всех методов контракта;
Теперь по порядку:
Выбор пал на Golang из-за того, что очень понравилась кодовая база go-ethereum, на которой строится Geth.
Для генерации статических html используется стандартный Golang пакет "html/template". Тут я расписывать ничего не буду, все шаблоны можно найти в пакете templates проекта.
Для работы с Ethereum, как я написал выше, я выбрал кодовую базу go-ethereum версии 1.7.3.
Очень хотелось использовать пакет mobile из go-ethereum, но mobile какое-то время не обновлялся и в данный момент некорректно работает с текущим форматом Abi. При обработке данных вы получите похожую ошибку:
abi: cannot unmarshal *big.Int in to []interface {}
Ошибку уже поправили, но в основную ветку исправление на момент когда я это пишу еще не добавили.
Я всё же выбрал другое решение, безобёрточное, т.к. функции в пакете mobile это по сути удобная обёртка над основным функционалом.
В итоге я забрал пакет для работы с abi (+ еще несколько пакетов которые зависят от abi) из go-ethereum себе в проект и добавил код из pull request.
Так как мне нужно было работать с любыми смарт контрактами, то утилита abigen, которая может формировать go пакет для работы с конкретным контрактом из sol файла, мне не подошла.
Я создал структуру, и методы для которых эта структура является приёмником (если не ошибаюсь в терминологии Golang):
type EthWorker struct { Container string // имя файла sol, в котором находится контракт Contract string //имя контракта Endpoint string //метод в контракте Key string // приватный ключ ContractAddress string //адрес контракта FormValues url.Values //map которая ничто иное , как POST form New bool //разворачивается ли новый контракт }
Полный интерфейс выглядит так:
type ReadWriterEth interface { Transact() (string, error) // писать в контракт Call() (string, error) // читать из контракта Deploy() (string, string, error) //развернуть контракт в сети Info() (*Info, error) // информация об аккаунте, адрес формируется из приватного ключа ParseInput() ([]interface{}, error) //парсить из POST формы входящие параметры для метода в слайс интерфейсов ParseOutput([]interface{}) (string, error) //пасить из слайса интерфейсов в стоку }
Функция для записи в контракт информации:
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 }
Функция для чтения информации из контракта:
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 }
Функция для развертывания контрактов:
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 }
Нужно было решить вопрос с тем, как из данных, введенных пользователем в форму на веб странице, получить данные, которые можно передать в функцию Call и Transact.
Я не придумал ничего лучше, как узнавать из abi метода контракта нужный тип данных для конкретного поля, и приводить к нему то, что пользователь ввел в форму на веб странице. Т.е. если какой-то тип данных я забыл, то мое решение с этим типом данных работать не будет. Нужно вносить изменения в код. Реализовал в функции 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 }
подобное преобразование я сделал для данных, которые мы получаем из Ethereum в функции 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 }
Из кодовой базы утилиты abigen, упомянутой мной ранее, я выдрал функционал по работе с Solidity компилятором. В итоге я получил abi и байткод для почти любого контракта. Реализовал в функции 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 }
В функции остался большой блок кода от экспериментов с пакетом mobile, удалять который я пока не стал, а просто сделал рефактор.
Я создал довольно большую структуру ContractContainers, в которую я поместил всю информация о текущих контрактах, в дальнейшем приложение берет всю информацию именно из неё.
Наконец расскажу как это работает:
Я запускал программу только на Linux. Других операционных систем рядом у меня нет.
Хотя собрал исполняемые файлы для Windows и Mac.
Для начала нужен Solidity компилятор для вашей платформы. Это наверное самый непростой пункт.
Можно Взять скомпилированный бинарник или исходники тут или посмотреть подробности вот тут. Версии 0.4.18 и 0.4.19 для linux и Windows я положил в директорию solc проекта. Так же можно воспользоваться уже установленным в системе компилятором. Чтобы проверить, есть ли в системе компилятор Solidity наберите в командной строке:
solc —version
Если ответ будет таким:
solc, the solidity compiler commandline interface Version: 0.4.18+commit.9cf6e910.Linux.g++
, то всё хорошо.
Если будет требовать каких-то библиотек, то просто установите их, например, если Ubuntu просит это:
./solc: error while loading shared libraries: libz3.so.4: cannot open shared object file: No such file or directory
, то ставим libz3-dev
Далее нужно решить в каком режиме мы будем работать с Ethereum. Есть два пути:
- подключаемся по RPC к ноде Ethereum и работаем через нее с той сетью с которой синхронизирована нода. Это удобно если у вас приватная сеть Ethereum или уже есть синхронизированная нода;
- эмулятор цепочки блоков Ethereum. Если работать в режиме эмулятора, то обязательно нужно положит в директорию keystore файлы формата UTC JSON Keystore File, где пароли для расшифровки этих файлов будут пустые;
Можно конечно сделать гораздо красивее, но для примера вполне подойдет существующее решение. Из этих файлов приложение берет Ethereum адреса и делает для них ненулевой баланс.
Я положил в директорию keystore 5 файлов для примера. Ими вполне можно пользоваться в тестовой среде.
Заполняем конфиг config.yaml:
- connect_url — url для подключения к rpc серверу Ethereum ноды. Если это поле оставить пустым, то приложение запустится в режиме эмуляции Ethereum, это как раз то, о чем я писал выше;
- sol_path — это папка со смарт контрактами, в которой приложение будет их искать. Приложение будет искать .sol файлы, которые будут находится в коревой директории. Поддиректории игнорируются. Но если ваши контракты с которыми вы будите работать ссылаются на контракты в поддиректориях, то ничего страшного, они тоже будут добавлены через контракты верхнего уровня;
- keystore_path — директория с файлами формата UTC JSON Keystore File. Напомню, что пароли для расшифровки должны быть пустыми;
- gaslimit — Лимит газа для транзакции или деплоя контракта;
- port — порт для локального http сервера;
- solc — путь до компилятора Solidity, если оставить пустым, то приложение возьмет компилятор установленный в системе;
Запускаем приложение. Путь до директории с конфигурационным файлом можно указать через флаг -config
./efront-v0.0.1-linux-amd64 -config $GOPATH/src/ethereum-front/
Переходим по ссылке в браузере: по умолчанию это http://localhost:8085
Нужно ввести приватный ключ. Приватные ключи для пяти тестовых адресов можно найти в keys.txt. Этот приватный ключ будет жить в куках башего браузера 15 минут. Далее будет новый запрос. Сейчас ничего не шифруется.

select-ом выбираем контейнер (.sol файл) и контракт, который приложение в нем нашло.

Далее, можно ввести адрес уже когда-то развернутого контракта или развернуть новый, отметив соответствующий checkbox. Если checkbox Deploy в положении on, то поле с адресом игнорируется.
Если всё прошло успешно то в браузере вы увидите, подобную картину.

Если будут ошибки, то они будут выведены в textarea в верхней части интерфейса.
В верхней части страницы две ссылки login и upload.
Login перенаправляет на ввод нового приватного ключа. Upload перенаправляет на выбор контракта.
Далее идет информация о текущем сеансе:

- you address: — Ethereum адрес, который соответствует текущему приватному ключу
- balance — баланс Eth на этом адресе в текущей сети. Запрашивается при каждом обновлении страницы
- file и contract — это соответственно выбранный sol файл и контракт в нём. Сохраняется в куках и берется от туда же
- Contract address — это адрес развернутого контракта в текущей сети. Сохраняется в куках и берется от туда же
Далее идут две таблицы:
Левая таблица для работы с методами текущего контракта. Она меняется динамически, в зависимости от выбранного контракта.
Правая таблица — это общие функции для работы с Ethereum:
- Balance — поверить баланс на выбранном Ethereum адресе в текущей сети;
- Gas price — текущия цена Gas в Wei;
- Last block — номер текущего блока. Не работает в симуляторе;
- Ethereum network gas limit — Лимит gas в последнем блоке. Не работает в симуляторе;
- Ethereum network time — Время майнинга последнего блока. Не работает в симуляторе;
- Ethereum network difficulty — сложность последнего блока. Не работает в симуляторе;
- Transfer — кому и сколько Wai перевести;
- Adjust time — Управление временем в симуляторе. Нужно вводить положительное число. И именно на столько секунд увеличится время в в симуляторе;
Примечание: Когда выполняете транзакции (операции на запись в блокчейн), ждите пока загрузится страница, это может длится несколько секунд. Потому что приложение ждет пока транзакция запишется в блок.
При успешном деплое контракта, приложение в поле textarea выдаст примерно такую информацию (деплой контракта это практически обычная транзакция):

- Nonce — номер транзакции
- From — адрес инициатор транзакции
- Contract Address — адрес нового контракта
- Gas price — цена Gas
- Gas Used — цена транзакции в Gas
- Cost/Fee: цена транзакции в Wai
- Status — если статус 1, то транзакция успешно записалась в блок и выполнилась успешно, если 0, то с выполнением транзакции возникли проблемы и нужно разбиратьсяв причинах.
- Transaction Hash — хеш транзакции
Вывод информации о транзакциях при работе с контрактом примерно такой же как я описал выше.
Cделал исполняемые файлы для популярных OS. Они лежат в папке bin.
Из явных минусов хочу отметить:
- на верстку html потратил минут 5. Заранее извиняюсь перед любителями прекрасного
- в пакете front использовал функционал для работы с Ethereum, хотя его нужно было бы перенести в пакет ether, в котором как раз этот функционал и находится
- не делал тесты и не писал коментарии в коде, из-за экономии времени
Исходный код
Всем спасибо.

