Привет!
У меня возникла идея разработать надеюсь простое решение, для ручного тестирования смарт контрактов 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, в котором как раз этот функционал и находится
- не делал тесты и не писал коментарии в коде, из-за экономии времени
Исходный код
Всем спасибо.