- PVSM.RU - https://www.pvsm.ru -
Введение
Для создания мультисиг кошелька Safe [1] можно воспользоваться видеоинструкцией [2] или использовать API [3].
В этой статье рассказано, как используя Golang, вызвать метод на смарт-контракте и создать мультисиг кошелек. Дополнительная настройка этого кошелька, отправка с него транзакций и другие полезные функции планируется осветить в других статьях.
Содержание
I. Настройка окружения
II. Подключение к блокчейну
III. Создание транзакции, которая создаст мультисиг кошелек
IV. Запуск на выполнение и аналитика результатов
I. Настройка окружения
1. Развернем проект на Go, для этого выполните команду.
$ go mod init github.com/{тут название вашего аккаунта на github}/multisig
Смотрите тут [4] подробнее.
2. Получение данных для подключения к сети и смарт-контрактам
Для начала определимся с сетью, выберем тестовую сеть Ehtereum [5]. Нам нужные следующие смарт-контракты, находятся тут [6]. Будем использовать:
safe_proxy_factory: 0X4E1DCF7AD4E460CFD30791CCC4F9C8A4F820EC67
safe: 0X41675C099F32341BF84BFC5382AF534DF5C7461A
Для возможности взаимодействовать с блокчейном нам необходимо использовать подключение через провайдера, в этой статье используем alchemy [7]. Сгенерированный ключ на этой платформе (alchemy) и адреса контрактов поместим в файл .env.
rpc_url=https://eth-sepolia.g.alchemy.com/v2/{тут ваш личный ключ}
safe_proxy_factory=0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67
safe=0x41675C099F32341bf84BFc5382aF534df5C7461a
private_key={тут ваш личный приватный ключ}
Приватный ключ в создаете обычный кошелек [8].
3. Получение ABI смарт-контрактов и генерация кода на GO
Скопируем ABI смарт-контрактов в файлы (ниже указаны пути, где разместить файлы). Нам нужны json, которые находятся в разделе Contract ABI.
abi/safe_proxy_factory_abi/safe_proxy_factory.abi - ABI у safe_proxy_factory смарт-контракта смотрите по ссылке:
abi/safe_abi/safe.abi - https://sepolia.etherscan.io/address/0x41675C099F32341bf84BFc5382aF534df5C7461a#code [10]
Сгенерируем safe_proxy_factory_abi.go и safe_abi.go для этого запустим по очереди две команды с командной строки, используя abigen [11].
$ abigen --abi=abi/safe_proxy_factory_abi/safe_proxy_factory.abi --pkg=safe_proxy_factory_abi --out=abi/safe_proxy_factory_abi/safe_proxy_factory_abi.go
$ abigen --abi=abi/safe_abi/safe.abi --pkg=safe_abi --out=abi/safe_abi/safe_abi.go
На текущий момент у нас такая структура файлов:
II. Подключение к блокчейну
Файл, в котором будет основная логика, назовем multisig.go. В нем напишем код, который читает данные из .env:
package main
import (
"github.com/spf13/viper"
)
func LoadConfig() {
viper.AutomaticEnv()
viper.SetConfigFile(".env")
viper.ReadInConfig() //nolint:errcheck
}
func main() {
LoadConfig()
}
Добавим зависимости:
$ go get github.com/spf13/viper
Добавим в multisig.go функцию getProvider, которая отвечает за подключение к провайдеру. Используем URL, прописанный у нас в .env rpc_url.
func getProvider() (*ethclient.Client, error) {
rpcClient, err := rpc.DialOptions(
context.Background(),
viper.GetString("rpc_url"),
rpc.WithHTTPClient(&http.Client{ //nolint:exhaustruct
Timeout: 15 * time.Second,
}),
)
if err != nil {
return ðclient.Client{}, err
}
connection := ethclient.NewClient(rpcClient)
return connection, nil
}
Добавим зависимости:
$ go get github.com/ethereum/go-ethereum/ethclient github.com/ethereum/go-ethereum/rpc github.com/ethereum/go-ethereum/accounts/keystore@v1.15.1
III. Создание транзакции
1. Получение и обработка входящих данных
Добавим в multisig.go функцию sendDeployMultisig, в ней получим из .env данные и ABI смарт-контракта safe. Ниже приведен часть кода, в котором в разделе import внесены нужные зависимости для всего файла multisig.go на данном этапе создания файла.
import (
"context"
"net/http"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/spf13/viper"
"github.com/timofvy/multisig/abi/safe_abi"
)
//-------------------------------
func sendDeployMultisig() error {
priv := viper.GetString("private_key")
safeProxyFactoryAddress := common.HexToAddress(viper.GetString("safe_proxy_factory"))
safeAddress := common.HexToAddress(viper.GetString("safe"))
contractAbi, err := abi.JSON(strings.NewReader(safe_abi.SafeAbiABI))
if err != nil {
return err
}
return nil
}
Далее нам надо подготовить данные для вызова функции setup на смарт-контракте safe. Для понимания, какие нужно передать параметры обратимся к файлу abi/safe_abi/safe_abi.go. Ниже код из этого файла.
// Setup is a paid mutator transaction binding the contract method 0xb63e800d.
//
// Solidity: function setup(address[] _owners, uint256 _threshold, address to, bytes data, address fallbackHandler, address paymentToken, uint256 payment, address paymentReceiver) returns()
func (_SafeAbi *SafeAbiTransactor) Setup(
opts *bind.TransactOpts,
_owners []common.Address,
_threshold *big.Int,
to common.Address,
data []byte,
fallbackHandler common.Address,
paymentToken common.Address,
payment *big.Int,
paymentReceiver common.Address
) (*types.Transaction, error) {
return _SafeAbi.contract.Transact(opts,
"setup",
_owners,
_threshold,
to,
data,
fallbackHandler,
paymentToken,
payment,
paymentReceiver)
}
Данные сформируем следующим образом:
data, err := contractAbi.Pack("setup",
owners,
big.NewInt(int64(threshold)),
common.HexToAddress(zeroAddress),
[]byte{0},
common.HexToAddress(zeroAddress),
common.HexToAddress(zeroAddress),
big.NewInt(0),
)
if err != nil {
return err
}
Первым и вторым параметром передаются настройки мультисиг кошелька:
owners - массив адресов, которые смогут отдавать свои голоса (подписи)
threshold - количество положительных голосов, т.е. "ЗА", необходимых чтобы предложение (proposal) было принято.
zeroAddress - нулевой адрес, вынесем в константу.
const zeroAddress = "0x0000000000000000000000000000000000000000"
Данные owners и threshold можно прописать сразу при формировании data, на сленге это называется захаркодить [12]. Мы же передадим эти параметры из командной строки, когда будем запускать файл к исполнению. Для этого внесем некоторые правки в уже написанный код.
func main() {
LoadConfig()
owners := flag.String("owners", "", "Owners")
threshold := flag.Int("threshold", 0, "Threshold")
flag.Parse()
signerAddrs := strings.Split(*owners, ",")
var ownersAddrSlice []common.Address
for _, addr := range signerAddrs {
trimmedAddr := strings.TrimSpace(addr)
ownerAddr := common.HexToAddress(trimmedAddr)
ownersAddrSlice = append(ownersAddrSlice, ownerAddr)
}
err := sendDeployMultisig(ownersAddrSlice, *threshold)
if err != nil {
return
}
}
func sendDeployMultisig(
owners []common.Address,
threshold int,
) error {
...
Пакет flag реализует синтаксический анализ флагов командной строки.
В командной строке мы передадим нужный нам список адресов "подписантов" (адреса приведены для примера):
--owners 0x484D411062f0135585774da4ae7bAEC0560B0CB3,0xAB706bb9D041C6E3A7B1088A14b5fDCA90c7E163
и количество подписей --threshold 2.
При интеграции в вашем проекте кода из этой статьи параметры для мультисиг кошелька наверняка вы получите из другого источника, например из запроса. Функция sendDeployMultisig уже будет готова их принять и обработать.
В функцию main (см. выше) мы добавили алгоритм получения параметров из командной строки, их обработку и вызов функции создания и отправки транзакции sendDeployMultisig. Обработка параметра owners делает разбиение передаваемых данных из строки в массив и перевод формата адресов в common.Address. А также удаление пробельных символов в начале и в конце строки.
Добавим проверку в sendDeployMultisig, что данные в командной строке переданы.
func sendDeployMultisig(
owners []common.Address,
threshold int,
) error {
if threshold == 0 {
return errors.New("threshold must be greater than 0")
}
if len(owners) == 0 {
return errors.New("owners must be greater than 0")
}
...
2. Подписание и отправка транзакции в блокчейн
Получим провайдера, для этого вызовем функцию getProvider().
provider, err := getProvider()
if err != nil {
return err
}
Создадим экземпляр contractTransactor — это объект, который позволяет отправлять транзакции в смарт-контракт и получать данные из него.
contractTransactor, err := safe_proxy_factory_abi.NewSafeProxyFactoryAbiTransactor(
safeProxyFactoryAddress, provider,
)
if err != nil {
return err
}
Ниже в коде получаем:
chainID, в нашем случае это значение 11155111 [13] для https://sepolia.etherscan.io/ [5]
privateKey — ключ в формате *ecdsa.PrivateKey из вашего приватного ключа указанного в .env
Примечание: bind и crypto из github.com/ethereum/go-ethereum, автоматически добавятся в import при сохранении файла.
chainID, err := provider.ChainID(context.Background())
if err != nil {
return err
}
privateKey, err := crypto.HexToECDSA(priv)
if err != nil {
return err
}
trOpts, err := bind.NewKeyedTransactorWithChainID(
privateKey,
chainID,
)
if err != nil {
return err
}
NewKeyedTransactorWithChainID — это вспомогательный метод для простого создания подписчика транзакции из одного закрытого ключа.
Итоговым шагом отправим транзакцию в блокчейн.
transaction, err := contractTransactor.CreateProxyWithNonce(
trOpts,
safeAddress,
data,
big.NewInt(0), // saltNonce, любое случайное число,
//параметр saltNonce позволяет создавать уникальные адреса прокси-контратов
)
if err != nil {
return err
}
CreateProxyWithNonce — это метод, который представлен в abi/safe_proxy_factory_abi/safe_proxy_factory_abi.go. Ниже код для понимания какие передавать данные.
// CreateProxyWithNonce is a paid mutator transaction binding the contract method 0x1688f0b9.
//
// Solidity: function createProxyWithNonce(address _singleton, bytes initializer, uint256 saltNonce) returns(address proxy)
func (_SafeProxyFactoryAbi *SafeProxyFactoryAbiTransactor) CreateProxyWithNonce(
opts *bind.TransactOpts
, _singleton common.Address,
initializer []byte,
saltNonce *big.Int
) (*types.Transaction, error) {
return _SafeProxyFactoryAbi.contract.Transact(opts,
"createProxyWithNonce",
_singleton,
initializer,
saltNonce)
}
Для наглядного отображения факта, что транзакция успешно попала в блокчейн, выведем в лог hash этой транзакции.
log.Println("Transaction sent: ", transaction.Hash().Hex())
Сверим что у нас получилось в итоге, в файле multisig.go.
package main
import (
"context"
"errors"
"flag"
"log"
"math/big"
"net/http"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/spf13/viper"
"github.com/timofvy/multisig/abi/safe_abi"
"github.com/timofvy/multisig/abi/safe_proxy_factory_abi"
)
const zeroAddress = "0x0000000000000000000000000000000000000000"
func LoadConfig() {
viper.AutomaticEnv()
viper.SetConfigFile(".env")
viper.ReadInConfig() //nolint:errcheck
}
func getProvider() (*ethclient.Client, error) {
rpcClient, err := rpc.DialOptions(
context.Background(),
viper.GetString("rpc_url"),
rpc.WithHTTPClient(&http.Client{ //nolint:exhaustruct
Timeout: 15 * time.Second,
}),
)
if err != nil {
return ðclient.Client{}, err
}
connection := ethclient.NewClient(rpcClient)
return connection, nil
}
func main() {
LoadConfig()
owners := flag.String("owners", "", "Owners")
threshold := flag.Int("threshold", 0, "Threshold")
flag.Parse()
signerAddrs := strings.Split(*owners, ",")
var ownersAddrSlice []common.Address
for _, addr := range signerAddrs {
trimmedAddr := strings.TrimSpace(addr)
ownerAddr := common.HexToAddress(trimmedAddr)
ownersAddrSlice = append(ownersAddrSlice, ownerAddr)
}
err := sendDeployMultisig(ownersAddrSlice, *threshold)
if err != nil {
log.Println(err) // выведем ошибку в лог, если она случилась
return
}
}
func sendDeployMultisig(
owners []common.Address,
threshold int,
) error {
if threshold == 0 {
return errors.New("threshold must be greater than 0")
}
if len(owners) == 0 {
return errors.New("owners must be greater than 0")
}
priv := viper.GetString("private_key")
safeProxyFactoryAddress := common.HexToAddress(viper.GetString("safe_proxy_factory"))
safeAddress := common.HexToAddress(viper.GetString("safe"))
contractAbi, err := abi.JSON(strings.NewReader(safe_abi.SafeAbiABI))
if err != nil {
return err
}
data, err := contractAbi.Pack("setup",
owners,
big.NewInt(int64(threshold)),
common.HexToAddress(zeroAddress),
[]byte{0},
common.HexToAddress(zeroAddress),
common.HexToAddress(zeroAddress),
big.NewInt(0),
common.HexToAddress(zeroAddress),
)
if err != nil {
return err
}
provider, err := getProvider()
if err != nil {
return err
}
contractTransactor, err := safe_proxy_factory_abi.NewSafeProxyFactoryAbiTransactor(
safeProxyFactoryAddress,
provider,
)
if err != nil {
return err
}
chainID, err := provider.ChainID(context.Background())
if err != nil {
return err
}
privateKey, err := crypto.HexToECDSA(priv)
if err != nil {
return err
}
trOpts, err := bind.NewKeyedTransactorWithChainID(
privateKey,
chainID,
)
if err != nil {
return err
}
transaction, err := contractTransactor.CreateProxyWithNonce(
trOpts,
safeAddress,
data,
big.NewInt(0),
)
if err != nil {
return err
}
log.Println("Transaction sent: ", transaction.Hash().Hex())
return nil
}
IV. Запуск и аналитика результатов
В командной строке выполним команду:
$ go build
Перед следующим шагом вам надо убедиться, что на кошельке, приватный ключ, которого вы указали в .env, находятся тестовые монеты Eth. Получить их можно например здесь [14], либо на другом faucet ("кране").
Если выполнить вызов создания мультисиг кошелька кошельком на котором нет или не хватает средств на комиссию для осуществления транзакции в сети, то получим ошибку insufficient funds for transfer.
В консоле вызовем ту же команду, но после того как пополним кошелек, отправляющий транзакцию:
$ go run multisig.go --owners 0x484D411062f0135585774da4ae7bAEC0560B0CB3,0xAB706bb9D041C6E3A7B1088A14b5fDCA90c7E163 --threshold 2
Транзакция успешно прошла, в логах мы видим hash этой транзакции.
Для анализа перейдем по ссылке https://sepolia.etherscan.io/tx/0x07265d5b35c604974f4a39b055af83cfbb851902d1aa8399fb04a3f0ae36564d [15]
В поле To есть информация об адресе вновь созданного мультисиг кошелька (см. ... Created).

Проверим параметры нашего нового мультисиг кошелька, перейдем по ссылке https://app.safe.global/settings/setup?safe=sep:0xBe17c4B2dD3512046a709eF91155c8b61328e400 [16]
У нас все получилось, на языке Go мы создали новый мультисиг кошелек Safe с нужными нам параметрами, обратившись для этого к смарт-контракту.
Автор: timofvy
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/integratsiya/411759
Ссылки в тексте:
[1] кошелька Safe: https://app.safe.global/
[2] видеоинструкцией: https://dzen.ru/video/watch/64072cce54cd5e5bf6eab278
[3] API: https://docs.safe.global/core-api/api-overview
[4] тут: https://habr.com/ru/companies/mvideo/articles/744434/
[5] тестовую сеть Ehtereum: https://sepolia.etherscan.io/
[6] тут: https://docs.safe.global/advanced/smart-account-supported-networks?search=Sepolia&page=2&expand=11155111
[7] alchemy: https://www.alchemy.com/
[8] создаете обычный кошелек: https://ethereum.org/ru/guides/how-to-create-an-ethereum-account/
[9] https://sepolia.etherscan.io/address/0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67#code: https://sepolia.etherscan.io/address/0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67#code
[10] https://sepolia.etherscan.io/address/0x41675C099F32341bf84BFc5382aF534df5C7461a#code: https://sepolia.etherscan.io/address/0x41675C099F32341bf84BFc5382aF534df5C7461a#code
[11] abigen: https://geth.ethereum.org/docs/tools/abigen
[12] захаркодить: https://habr.com/ru/articles/419231/
[13] 11155111: https://chainlist.org/chain/11155111
[14] здесь: https://www.alchemy.com/faucets/ethereum-sepolia
[15] https://sepolia.etherscan.io/tx/0x07265d5b35c604974f4a39b055af83cfbb851902d1aa8399fb04a3f0ae36564d: https://sepolia.etherscan.io/tx/0x07265d5b35c604974f4a39b055af83cfbb851902d1aa8399fb04a3f0ae36564d
[16] https://app.safe.global/settings/setup?safe=sep:0xBe17c4B2dD3512046a709eF91155c8b61328e400: https://app.safe.global/settings/setup?safe=sep:0xBe17c4B2dD3512046a709eF91155c8b61328e400
[17] Источник: https://habr.com/ru/articles/885088/?utm_source=habrahabr&utm_medium=rss&utm_campaign=885088
Нажмите здесь для печати.