Перейти к содержимому
PortunaPortunaPortunaДокументацияTestnet

Go SDK

Go SDK закрывает то, что ваш бэкенд делает на своей стороне: вызовы API, перевод подписей вашей кастоди в авторизации, проверку вебхуков и проверку делегатов в сети. Зависимостей, кроме стандартной библиотеки и go-ethereum, нет.

Окно терминала
GOPRIVATE=github.com/Coddycoder go get github.com/Coddycoder/gasdelegate/sdk/go
ОбластьAPI
Клиент APINewClient с методом на каждый запрос API; ошибки — *portuna.Error
АвторизацииAuthorizationDigest, AuthorizationFromSignature, ParseSignature, NormalizeS, Authorization.Verify
Снятие делегацииRevocationDigest, RevocationFromSignature, Authorization.VerifyRevocation
СпискиSweeps, Revocations, LedgerEntries, WebhookDeliveries — по странице; AllSweeps и другие — по всем страницам
ВебхукиParseWebhook, VerifyWebhook, SignWebhook для тестов, Event.Sweep() и другие декодеры
Проверка делегатаVerifyDeployment, FactoryAddress, BatcherAddress, DelegateAddress и команда cmd/verify-delegate
client := portuna.NewClient("https://api-testnet.portuna.io", os.Getenv("PORTUNA_API_KEY"))
client.HTTPClient = &http.Client{Timeout: 30 * time.Second} // optional; http.DefaultClient otherwise

Каждый метод принимает context.Context. Методы: Balance, Tariff, Settings, UpdateSettings, HotWallets, CreateHotWallet, CreateSweep, Sweep, Sweeps, CreateRevocation, Revocation, Revocations, LedgerEntries, Usage, Webhook, SetWebhook, DeleteWebhook, WebhookDeliveries, SendTestWebhook, Health и OpenAPI. CreateSweep и CreateRevocation возвращают ещё и created: false, если ExternalID уже использовался и вернулся существующий объект.

client := portuna.NewClient(os.Getenv("PORTUNA_API_URL"), os.Getenv("PORTUNA_API_KEY"))
balance, err := client.Balance(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("gas wallet:", balance.GasWallet.Hex())
for _, c := range balance.Chains {
fmt.Printf("%s: %s wei available for gas\n", c.Name, c.Available)
}

Sweeps, Revocations, LedgerEntries и WebhookDeliveries возвращают одну страницу — Page с Items и NextCursor, пустым на последней странице. Их фильтры, например SweepFilter, не учитывают нулевые поля. Методы All — AllSweeps и другие — проходят все страницы как итератор и останавливаются на первой ошибке:

for sweep, err := range client.AllSweeps(ctx, portuna.SweepFilter{Status: portuna.SweepFailed}) {
if err != nil {
return err
}
log.Printf("sweep %s failed: %s", sweep.ID, sweep.Reason)
}

Любой ответ, кроме 2xx, — это *portuna.Error с полями StatusCode, Code и Message. portuna.ErrorCode(err) возвращает код или "", если err — не ошибка API, например сетевая. Коды и что с ними делать — в гайде Ошибки и повторы.

AuthorizationDigest даёт 32 байта, которые подписывает ваша кастоди. AuthorizationFromSignature принимает «сырую» подпись — DER или r‖s, с любым s, — нормализует её, подбирает y_parity и возвращает ошибку, если подписал не депозитный адрес. Сеть 0 не принимается.

func signAuthorization(ctx context.Context, sign Signer, keyID string, deposit common.Address,
chainID int64, delegate common.Address, nonce uint64) (portuna.Authorization, error) {
digest, err := portuna.AuthorizationDigest(chainID, delegate, nonce) // refuses chain 0
if err != nil {
return portuna.Authorization{}, err
}
sig, err := sign(ctx, keyID, digest[:])
if err != nil {
return portuna.Authorization{}, err
}
// Reads DER or r‖s, moves s to the lower half and finds y_parity. Fails unless the deposit address
// signed this very digest, so a wrong key never reaches the API.
return portuna.AuthorizationFromSignature(chainID, delegate, nonce, deposit, sig)
}

Подробнее — в гайде Подпись авторизаций.

ParseWebhook(secret, header, body) проверяет заголовок Portuna-Signature по сырому телу — в пределах пяти минут — и разбирает событие. event.Sweep(), event.Revocation(), event.HotWallet(), event.BalanceLow() и event.WebhookTest() разбирают его данные. Константы EventSweepSwept и другие называют типы событий, SignatureHeader, EventHeader и DeliveryHeader — заголовки.

http.HandleFunc("POST /hooks/sweeps", func(w http.ResponseWriter, r *http.Request) {
// Verify the body exactly as it arrived, before parsing it.
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
http.Error(w, "cannot read the body", http.StatusBadRequest)
return
}
event, err := portuna.ParseWebhook(secret, r.Header.Get(portuna.SignatureHeader), body)
if err != nil {
http.Error(w, "bad signature", http.StatusUnauthorized)
return
}
// Delivery is at least once: an event may come again, with the same id.
if handled(event.ID) {
w.WriteHeader(http.StatusNoContent)
return
}
switch event.Type {
case portuna.EventSweepSwept, portuna.EventSweepFailed, portuna.EventSweepRejected:
sweep, err := event.Sweep()
if err != nil {
http.Error(w, "bad event", http.StatusBadRequest)
return
}
// Events may come out of order: the status in the data is the current one.
log.Printf("sweep %s (%s): %s %s %s", sweep.ID, sweep.ExternalID, sweep.Status, sweep.SweptAmount, sweep.Reason)
case portuna.EventBalanceLow:
alert, err := event.BalanceLow()
if err != nil {
http.Error(w, "bad event", http.StatusBadRequest)
return
}
log.Printf("top up the gas wallet on chain %d: %s wei left", alert.ChainID, alert.Balance)
default:
// Other and future event types: accept them.
}
markHandled(event.ID)
w.WriteHeader(http.StatusNoContent)
})

VerifyDeployment считает ваш батчер и делегат горячего кошелька (котла) из байткода, встроенного в SDK, и проверяет их в сети; cmd/verify-delegate делает то же из командной строки. См. Проверка делегата.

eth, err := ethclient.DialContext(ctx, os.Getenv("RPC_URL"))
if err != nil {
log.Fatal(err)
}
report, err := portuna.VerifyDeployment(ctx, eth, portuna.Deployment{
GasWallet: common.HexToAddress(os.Getenv("GAS_WALLET")),
HotWallet: common.HexToAddress(os.Getenv("HOT_WALLET")),
ExpectDelegate: common.HexToAddress(os.Getenv("DELEGATE")),
})
if err != nil {
log.Fatal(err)
}
for _, c := range report.Checks {
fmt.Println(c.OK, c.Name, c.Detail)
}
if !report.OK() {
log.Fatal("do not sign authorizations for this delegate")
}