Go SDK
Go SDK закрывает то, что ваш бэкенд делает на своей стороне: вызовы API, перевод подписей вашей кастоди в авторизации, проверку вебхуков и проверку делегатов в сети. Зависимостей, кроме стандартной библиотеки и go-ethereum, нет.
GOPRIVATE=github.com/Coddycoder go get github.com/Coddycoder/gasdelegate/sdk/goЧто внутри
Заголовок раздела «Что внутри»| Область | API |
|---|---|
| Клиент API | NewClient с методом на каждый запрос 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")}