11 January 2018

Лёгкий «Frontend» на Golang для ручного тестирования Ethereum смарт контракта без JavaScript и Web3

Go
Sandbox

Привет!


У меня возникла идея разработать надеюсь простое решение, для ручного тестирования смарт контрактов 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) //пасить из слайса интерфейсов в стоку
}

Функция для записи в контракт информации:


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
}

Функция для чтения информации из контракта:


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
}

Функция для развертывания контрактов:


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
}

Нужно было решить вопрос с тем, как из данных, введенных пользователем в форму на веб странице, получить данные, которые можно передать в функцию Call и Transact.


Я не придумал ничего лучше, как узнавать из abi метода контракта нужный тип данных для конкретного поля, и приводить к нему то, что пользователь ввел в форму на веб странице. Т.е. если какой-то тип данных я забыл, то мое решение с этим типом данных работать не будет. Нужно вносить изменения в код. Реализовал в функции ParseInput


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


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.


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, в котором как раз этот функционал и находится
  • не делал тесты и не писал коментарии в коде, из-за экономии времени

Исходный код
Всем спасибо.

Tags:golangethereum
Hubs: Go
+9
5.3k 35
Leave a comment
Popular right now
Старший/Ведущий Go (Golang) разработчик
from 200,000 to 300,000 ₽Positive TechnologiesМоскваRemote job
Ведущий блокчейн разработчик
from 5,400 to 6,000 €LUNU Solutions GmbHRemote job
Разработчик
from 230,000 ₽Ассоциация ФинтехМоскваRemote job
Разработчик Go, PHP
from 130,000 ₽ВсеИнструменты.руRemote job
Golang Developer
from 150,000 to 200,000 ₽QuadcodeСанкт-Петербург