Go SDK
The Go SDK covers what your backend does on its side: calling the API, turning your custody’s signatures into authorizations, verifying webhooks and checking delegates on chain. It depends on nothing but the standard library and go-ethereum.
GOPRIVATE=github.com/Coddycoder go get github.com/Coddycoder/gasdelegate/sdk/goWhat is inside
Section titled “What is inside”| Area | API |
|---|---|
| API client | NewClient, with a method for each request of the API; errors are *portuna.Error |
| Authorizations | AuthorizationDigest, AuthorizationFromSignature, ParseSignature, NormalizeS, Authorization.Verify |
| Revocations | RevocationDigest, RevocationFromSignature, Authorization.VerifyRevocation |
| Lists | Sweeps, Revocations, LedgerEntries, WebhookDeliveries a page at a time; AllSweeps and the like through every page |
| Webhooks | ParseWebhook, VerifyWebhook, SignWebhook for tests, Event.Sweep() and the other decoders |
| Delegate check | VerifyDeployment, FactoryAddress, BatcherAddress, DelegateAddress, and the cmd/verify-delegate command |
Client
Section titled “Client”client := portuna.NewClient("https://api-testnet.portuna.io", os.Getenv("PORTUNA_API_KEY"))client.HTTPClient = &http.Client{Timeout: 30 * time.Second} // optional; http.DefaultClient otherwiseEvery method takes a context.Context. Methods: Balance, Tariff, Settings, UpdateSettings, HotWallets,
CreateHotWallet, CreateSweep, Sweep, Sweeps, CreateRevocation, Revocation, Revocations,
LedgerEntries, Usage, Webhook, SetWebhook, DeleteWebhook, WebhookDeliveries, SendTestWebhook, Health
and OpenAPI. CreateSweep and CreateRevocation also return created: false when the ExternalID was used
before and the existing object came back.
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 and WebhookDeliveries return one page, a Page with Items and
NextCursor, empty on the last page. Their filters, such as SweepFilter, leave out the fields left zero. The All
methods, AllSweeps and the like, go through every page as an iterator and stop at the first error:
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)}Errors
Section titled “Errors”An answer other than 2xx is a *portuna.Error with StatusCode, Code and Message. portuna.ErrorCode(err)
returns the code, or "" when err is not an API error, for example a network failure. Codes and what to do:
Errors and retries.
Authorizations
Section titled “Authorizations”AuthorizationDigest gives the 32 bytes your custody signs. AuthorizationFromSignature takes the raw signature, DER
or r‖s, with any s, normalizes it, finds y_parity and fails unless the deposit address signed. Chain 0 is refused.
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)}More in Signing authorizations.
Webhooks
Section titled “Webhooks”ParseWebhook(secret, header, body) verifies the Portuna-Signature header against the raw body, within five
minutes, and decodes the event. event.Sweep(), event.Revocation(), event.HotWallet(), event.BalanceLow() and
event.WebhookTest() decode its data. The constants EventSweepSwept and so on name the event types,
SignatureHeader, EventHeader and DeliveryHeader the headers.
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)})Delegate check
Section titled “Delegate check”VerifyDeployment computes your batcher and the hot wallet’s delegate from the bytecode built into the SDK and checks
them on chain; cmd/verify-delegate does the same from the command line. See
Verifying the 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")}