// Package aerecloud is the official Go client for the Aere Cloud API (Aere Network, chain 2800).
//
// Same routes and semantics as @aere/cloud (JS), aere-cloud (Python) and aere-cloud (Java) 1.5.0. Zero dependencies
// beyond the Go standard library. JSON routes return the server's JSON as json.RawMessage, so the integrator decodes it
// into whatever types they already use; the two routes that return FILES (the audit export and the check of a kept
// export) return typed results with the digest computed locally from the bytes received.
//
// Non-2xx answers are returned as *Error with the HTTP status, the server's body and the path.
package aerecloud
// Nota interna: comentariile de implementare sunt in romana fara diacritice, ca in celelalte SDK-uri ale casei;
// documentatia publica a API-ului (comentariile exportate) e in engleza.
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route).
const Version = "1.5.0"
// DefaultBaseURL is the production gateway.
const DefaultBaseURL = "https://cloud.aere.network/v1"
// Error is a non-2xx answer from Aere Cloud (Status 0 means the request never got an answer).
type Error struct {
Status int
Body []byte
Path string
}
func (e *Error) Error() string {
b := string(e.Body)
if b == "" {
b = "request failed"
}
return fmt.Sprintf("Aere Cloud %d on %s: %s", e.Status, e.Path, b)
}
// Client talks to one Aere Cloud gateway with one API key (ak2800.
.).
type Client struct {
apiKey string
baseURL string
http *http.Client
}
// New returns a client for the production gateway. The API key is required.
func New(apiKey string) (*Client, error) { return NewWithBase(apiKey, DefaultBaseURL, nil) }
// NewWithBase returns a client for another base URL (a test server) and an optional *http.Client.
func NewWithBase(apiKey, baseURL string, hc *http.Client) (*Client, error) {
if apiKey == "" {
return nil, errors.New("aerecloud: apiKey is required (ak2800..)")
}
if hc == nil {
// fara redirectari urmate: un 3xx e un raspuns, nu un drum spre alta gazda care ar primi cheia
hc = &http.Client{Timeout: 30 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
}
return &Client{apiKey: apiKey, baseURL: strings.TrimRight(baseURL, "/"), http: hc}, nil
}
type raspuns struct {
status int
body []byte
headers http.Header
}
func (c *Client) do(ctx context.Context, method, path string, body any, auth bool) (*raspuns, error) {
var rd io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
rd = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, rd)
if err != nil {
return nil, err
}
req.Header.Set("user-agent", "aere-cloud-go/"+Version)
if body != nil {
req.Header.Set("content-type", "application/json")
}
if auth {
req.Header.Set("x-api-key", c.apiKey)
}
r, err := c.http.Do(req)
if err != nil {
// mesajul de retea nu poarta cheia (e in antet, nu in URL)
return nil, &Error{Status: 0, Body: []byte(err.Error()), Path: path}
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
return nil, &Error{Status: 0, Body: []byte(err.Error()), Path: path}
}
return &raspuns{status: r.StatusCode, body: b, headers: r.Header}, nil
}
func (c *Client) jsonReq(ctx context.Context, method, path string, body any, auth bool) (json.RawMessage, error) {
r, err := c.do(ctx, method, path, body, auth)
if err != nil {
return nil, err
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: path}
}
return json.RawMessage(r.body), nil
}
func esc(s string) string { return url.PathEscape(s) }
// ---- health, account, JSON-RPC ----
func (c *Client) Health(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/health", nil, false)
}
func (c *Client) Account(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/account", nil, true)
}
// RPC sends one JSON-RPC call and returns the whole JSON-RPC envelope.
func (c *Client) RPC(ctx context.Context, method string, params []any) (json.RawMessage, error) {
if params == nil {
params = []any{}
}
return c.jsonReq(ctx, "POST", "/rpc", map[string]any{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, true)
}
// BlockNumber is eth_blockNumber as an integer.
func (c *Client) BlockNumber(ctx context.Context) (uint64, error) {
b, err := c.RPC(ctx, "eth_blockNumber", nil)
if err != nil {
return 0, err
}
var env struct {
Result string `json:"result"`
}
if err := json.Unmarshal(b, &env); err != nil || !strings.HasPrefix(env.Result, "0x") {
return 0, &Error{Status: 200, Body: b, Path: "/rpc"}
}
return strconv.ParseUint(env.Result[2:], 16, 64)
}
// ---- post-quantum verification, chain data, notarization, proofs ----
// PQVerify: payload like {"scheme":"ml-dsa-44","publicKey":"0x..","signature":"0x..","message":"0x.."}.
func (c *Client) PQVerify(ctx context.Context, payload any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/pq/verify", payload, true)
}
func (c *Client) DataHead(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/head", nil, true)
}
func (c *Client) Validators(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/validators", nil, true)
}
func (c *Client) Anchors(ctx context.Context, limit int) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/anchors?limit="+strconv.Itoa(limit), nil, true)
}
func (c *Client) Anchor(ctx context.Context, height uint64) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/anchors/"+strconv.FormatUint(height, 10), nil, true)
}
func (c *Client) Transfers(ctx context.Context, address string, limit int) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/transfers?address="+url.QueryEscape(address)+"&limit="+strconv.Itoa(limit), nil, true)
}
func (c *Client) Notarize(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/notarize", map[string]string{"hash": hash}, true)
}
func (c *Client) ProofOf(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/notarize/"+esc(hash), nil, true)
}
// Proof: the proof with its post-quantum finality (first appearance, covering anchor, verdict, verification steps).
func (c *Client) Proof(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/proof/"+esc(hash), nil, true)
}
// ---- quantum readiness ----
// Readiness scans a domain's public TLS edge. Free without a key; attest=true notarizes the report digest and sends the key.
func (c *Client) Readiness(ctx context.Context, domain string, fresh, attest bool) (json.RawMessage, error) {
b := map[string]any{"domain": domain}
if fresh {
b["fresh"] = true
}
if attest {
b["attest"] = true
}
return c.jsonReq(ctx, "POST", "/pq/readiness", b, attest)
}
func (c *Client) ReadinessReport(ctx context.Context, domain string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/pq/readiness/"+esc(domain), nil, false)
}
// PerimeterOptions: Hosts may be nil; Discover nil keeps the server default; Attest notarizes only a COMPLETE report.
type PerimeterOptions struct {
Hosts []string
Discover *bool
Attest bool
}
// ReadinessPerimeter: the whole public perimeter of a domain in one keyed report; call again while summary.complete is false.
func (c *Client) ReadinessPerimeter(ctx context.Context, domain string, o PerimeterOptions) (json.RawMessage, error) {
b := map[string]any{"domain": domain}
if o.Hosts != nil {
b["hosts"] = o.Hosts
}
if o.Discover != nil && !*o.Discover {
b["discover"] = false
}
if o.Attest {
b["attest"] = true
}
return c.jsonReq(ctx, "POST", "/pq/readiness/perimeter", b, true)
}
// PQFinality: AIP-21 per-block post-quantum finality. client: "besu", "nethermind" or "both" (with `agreement`). No key.
func (c *Client) PQFinality(ctx context.Context, block, network, client string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/pq/finality/"+esc(block)+"?network="+url.QueryEscape(network)+"&client="+url.QueryEscape(client), nil, false)
}
// ---- audit log ----
// AuditLog: the paged JSON view of one UTC day (day "" = today), with the digest of the day file.
func (c *Client) AuditLog(ctx context.Context, day string, offset, limit int) (json.RawMessage, error) {
q := "?offset=" + strconv.Itoa(offset) + "&limit=" + strconv.Itoa(limit)
if day != "" {
q += "&day=" + url.QueryEscape(day)
}
return c.jsonReq(ctx, "GET", "/account/audit"+q, nil, true)
}
// AuditExport is a day as a file. For jsonl, Verified reports whether the sha256 of the bytes RECEIVED equals the stated
// digest (computed here, not taken on trust); for cef it is nil, because the digest is the one of the source file.
type AuditExport struct {
Day, Format, Digest string
Bytes []byte
Entries int
Verified *bool
}
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return "0x" + hex.EncodeToString(h[:]) }
func (c *Client) AuditExport(ctx context.Context, day, format string) (*AuditExport, error) {
if format == "" {
format = "jsonl"
}
p := "/account/audit?format=" + url.QueryEscape(format)
if day != "" {
p += "&day=" + url.QueryEscape(day)
}
r, err := c.do(ctx, "GET", p, nil, true)
if err != nil {
return nil, err
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: "/account/audit"}
}
e := &AuditExport{Day: r.headers.Get("x-aere-day"), Format: format, Digest: r.headers.Get("x-aere-digest"), Bytes: r.body}
e.Entries, _ = strconv.Atoi(r.headers.Get("x-aere-entries"))
if format == "jsonl" {
ok := e.Digest != "" && e.Digest == sha256Hex(r.body)
e.Verified = &ok
}
return e, nil
}
// AuditRetention reads the retention of the audit log with the API key (read-only). Retention is SET with a wallet
// signature in the console, never with the key: a stolen key must not be able to shorten the trail of its own use.
func (c *Client) AuditRetention(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/account/audit/retention", nil, true)
}
// KeptCheck: does an export you KEPT still match the day we hold ("held"), or the tombstone a deleted day left ("expired")?
type KeptCheck struct {
Day, State, Digest, Local, DeletedAt string
Matches bool
}
// AuditVerifyKept hashes YOUR bytes locally and compares them with the day we hold or with the tombstone of a deleted day
// (410). Any other failure is returned as an error, never as "does not match".
func (c *Client) AuditVerifyKept(ctx context.Context, day string, kept []byte) (*KeptCheck, error) {
local := sha256Hex(kept)
r, err := c.do(ctx, "GET", "/account/audit?format=jsonl&day="+url.QueryEscape(day), nil, true)
if err != nil {
return nil, err
}
if r.status == 410 {
var t struct {
Expired *struct {
Digest string `json:"digest"`
DeletedAt string `json:"deletedAt"`
} `json:"expired"`
}
if json.Unmarshal(r.body, &t) == nil && t.Expired != nil && t.Expired.Digest != "" {
return &KeptCheck{Day: day, State: "expired", Digest: t.Expired.Digest, Local: local, Matches: local == t.Expired.Digest, DeletedAt: t.Expired.DeletedAt}, nil
}
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: "/account/audit"}
}
d := sha256Hex(r.body)
return &KeptCheck{Day: day, State: "held", Digest: d, Local: local, Matches: local == d}, nil
}
// ---- webhooks and gas sponsorship ----
func (c *Client) ListWebhooks(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/webhooks", nil, true)
}
func (c *Client) CreateWebhook(ctx context.Context, spec any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/webhooks", spec, true)
}
func (c *Client) DeleteWebhook(ctx context.Context, id string) (json.RawMessage, error) {
return c.jsonReq(ctx, "DELETE", "/webhooks/"+esc(id), nil, true)
}
func (c *Client) SponsorHealth(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/sponsor/health", nil, true)
}
func (c *Client) SponsorCreateAccount(ctx context.Context, body any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/createAccount", body, true)
}
// SponsorCreatePQAccount: a smart account with a POST-QUANTUM owner (Falcon-512, 0x09-prefixed, 897 bytes), gas sponsored.
func (c *Client) SponsorCreatePQAccount(ctx context.Context, falconPubKeyHex string, salt uint64) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/createPqAccount", map[string]any{"falconPubKey": falconPubKeyHex, "salt": salt}, true)
}
func (c *Client) SponsorExecute(ctx context.Context, body any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/execute", body, true)
}
// VerifyWebhook checks a webhook delivery's HMAC-SHA256 signature in constant time. rawBody must be the EXACT bytes
// received, not re-serialized JSON. Never act on a webhook without this.
func VerifyWebhook(rawBody []byte, signatureHeader, secret string) bool {
m := hmac.New(sha256.New, []byte(secret))
m.Write(rawBody)
want := []byte(hex.EncodeToString(m.Sum(nil)))
return subtle.ConstantTimeCompare(want, []byte(signatureHeader)) == 1
}