The post-quantum finality point of the chain (finalized/safe = the parent of the most recent anchor whose certificate reaches the threshold in force). Same surface in every client; tests updated (JS 12, Python 12, Go ok, Java 7). Synced byte for byte from the source tree (aerenew/sdk) on 2026-09-26. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
261 lines
12 KiB
Python
261 lines
12 KiB
Python
"""aere-cloud: clientul Python oficial al Aere Cloud (chain 2800).
|
|
|
|
Oglinda fidela a @aere/cloud (sdk/index.mjs): fiecare ruta a gateway-ului e o metoda,
|
|
erorile se ridica drept AereCloudError (cod HTTP + corpul serverului), si verify_webhook
|
|
e inclus ca primitorul de webhook-uri sa nu-si scrie singur verificarea HMAC. Zero
|
|
dependinte in afara bibliotecii standard (urllib, hmac, hashlib, json); Python 3.9+.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
import hashlib
|
|
import json as _json
|
|
import urllib.request
|
|
import urllib.error
|
|
from typing import Any, Optional
|
|
|
|
__version__ = "1.5.2"
|
|
__all__ = ["AereCloud", "AereCloudError", "verify_webhook"]
|
|
|
|
_BAZA = "https://cloud.aere.network/v1"
|
|
|
|
|
|
class _FaraRedirectari(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
return None
|
|
|
|
|
|
_FARA_REDIRECTARI = urllib.request.build_opener(_FaraRedirectari)
|
|
|
|
|
|
class AereCloudError(Exception):
|
|
"""Raspuns non-2xx (sau eroare JSON-RPC) de la Aere Cloud.
|
|
|
|
Poarta .status (int), .body (dict sau None, corpul serverului) si .path, ca
|
|
integratorul sa poata actiona pe eroarea exacta, nu pe un mesaj generic.
|
|
"""
|
|
|
|
def __init__(self, status: int, body: Any, path: str):
|
|
mesaj = body.get("error") if isinstance(body, dict) else None
|
|
super().__init__(f"Aere Cloud {status} on {path}: {mesaj or 'request failed'}")
|
|
self.status = status
|
|
self.body = body
|
|
self.path = path
|
|
|
|
|
|
class AereCloud:
|
|
"""Client pentru https://cloud.aere.network/v1. Cheia: ak2800.<adresa>.<secret>."""
|
|
|
|
def __init__(self, api_key: str, base_url: str = _BAZA, timeout: float = 30.0, opener=None):
|
|
if not api_key:
|
|
raise ValueError("AereCloud: api_key is required (ak2800.<address>.<secret>)")
|
|
self.api_key = api_key
|
|
self.base_url = base_url.rstrip("/")
|
|
self.timeout = timeout
|
|
# opener injectabil pentru teste: primeste (urllib.request.Request, timeout) si
|
|
# intoarce (status:int, corp:bytes). Implicit, urllib.
|
|
self._opener = opener or self._urllib_opener
|
|
|
|
@staticmethod
|
|
def _urllib_opener(cerere: urllib.request.Request, timeout: float):
|
|
try:
|
|
# 1.5.1: fara redirectari urmate (urllib le urmeaza implicit si copiaza x-api-key spre orice gazda din Location);
|
|
# un 3xx ajunge aici ca HTTPError si se intoarce drept raspuns, deci ca AereCloudError cu statusul lui
|
|
with _FARA_REDIRECTARI.open(cerere, timeout=timeout) as r:
|
|
return r.status, r.read()
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read()
|
|
|
|
def _req(self, metoda: str, cale: str, body: Any = None, auth: bool = True) -> Any:
|
|
# User-Agent-ul explicit nu e cosmetic: marginea (Cloudflare) raspunde 403 la
|
|
# "Python-urllib/3.x" pentru ORICE ruta, inclusiv /health fara cheie. Masurat
|
|
# 2026-08-27 chiar la prima rulare vie a acestui SDK.
|
|
anteturi = {"content-type": "application/json", "user-agent": f"aere-cloud-python/{__version__}"}
|
|
if auth:
|
|
anteturi["x-api-key"] = self.api_key
|
|
date = None if body is None else _json.dumps(body).encode("utf-8")
|
|
cerere = urllib.request.Request(self.base_url + cale, data=date, headers=anteturi, method=metoda)
|
|
status, corp = self._opener(cerere, self.timeout)
|
|
try:
|
|
d = _json.loads(corp.decode("utf-8")) if corp else None
|
|
except ValueError:
|
|
d = None
|
|
if not (200 <= status < 300):
|
|
raise AereCloudError(status, d, cale)
|
|
return d
|
|
|
|
# ---- health & account ----
|
|
def health(self) -> dict:
|
|
return self._req("GET", "/health", auth=False)
|
|
|
|
def account(self) -> dict:
|
|
return self._req("GET", "/account")
|
|
|
|
# ---- JSON-RPC ----
|
|
def rpc(self, method: str, params: Optional[list] = None) -> Any:
|
|
d = self._req("POST", "/rpc", {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []})
|
|
if isinstance(d, dict) and d.get("error"):
|
|
raise AereCloudError(200, d, "/rpc")
|
|
return d.get("result") if isinstance(d, dict) else None
|
|
|
|
def block_number(self) -> int:
|
|
return int(self.rpc("eth_blockNumber"), 16)
|
|
|
|
# ---- post-quantum verification ----
|
|
def pq_verify(self, payload: dict) -> dict:
|
|
"""{scheme, publicKey, signature?, message?, signedMessage?} (0x-hex) -> {valid, ...}"""
|
|
return self._req("POST", "/pq/verify", payload)
|
|
|
|
# ---- chain data ----
|
|
def data_head(self) -> dict:
|
|
return self._req("GET", "/data/head")
|
|
|
|
def validators(self) -> dict:
|
|
return self._req("GET", "/data/validators")
|
|
|
|
def anchors(self, limit: int = 10) -> dict:
|
|
return self._req("GET", f"/data/anchors?limit={int(limit)}")
|
|
|
|
def anchor(self, height: int) -> dict:
|
|
return self._req("GET", f"/data/anchors/{int(height)}")
|
|
|
|
def finality(self) -> dict:
|
|
"""2026-09-26: the post-quantum finality point (finalized/safe = the parent of the last certified anchor)."""
|
|
return self._req("GET", "/data/finality")
|
|
|
|
def transfers(self, address: str, limit: int = 50) -> dict:
|
|
return self._req("GET", f"/data/transfers?address={address}&limit={int(limit)}")
|
|
|
|
# ---- notarization ----
|
|
def notarize(self, digest_hex: str) -> dict:
|
|
"""Notarizeaza un digest de 32 de octeti (0x + 64 hex); Aere plateste gazul."""
|
|
return self._req("POST", "/notarize", {"hash": digest_hex})
|
|
|
|
def proof_of(self, digest_hex: str) -> dict:
|
|
return self._req("GET", f"/notarize/{digest_hex}")
|
|
|
|
def proof(self, digest_hex: str) -> dict:
|
|
"""2026-09-17: dovada cu finalitatea ei post-cuantica (prima aparitie, ancora care o acopera, verdict, pasii de verificare)."""
|
|
return self._req("GET", f"/proof/{digest_hex}")
|
|
|
|
# ---- quantum readiness ----
|
|
def readiness(self, domain: str, attest: bool = False, fresh: bool = False) -> dict:
|
|
"""Scanarea marginii TLS publice a unui domeniu: libera, fara cheie; attest=True notarizeaza digestul raportului pe lant (cere cheia)."""
|
|
body: dict = {"domain": domain}
|
|
if fresh:
|
|
body["fresh"] = True
|
|
if attest:
|
|
body["attest"] = True
|
|
return self._req("POST", "/pq/readiness", body, auth=attest)
|
|
|
|
def readiness_report(self, domain: str) -> dict:
|
|
return self._req("GET", f"/pq/readiness/{domain}", auth=False)
|
|
|
|
def readiness_perimeter(self, domain: str, hosts: list | None = None, discover: bool = True, attest: bool = False) -> dict:
|
|
"""Tot perimetrul public al unui domeniu intr-un raport (cu cheie): gazdele numite (etichete sau nume de sub domeniu) plus
|
|
prefixele obisnuite gasite prin DNS, fiecare masurata cu aceleasi strangeri de mana. Cat summary['complete'] e False se cere
|
|
din nou (scanarile terminate stau in cache); attest=True notarizeaza digestul unui raport COMPLET pe lantul 2800."""
|
|
body: dict = {"domain": domain}
|
|
if hosts:
|
|
body["hosts"] = list(hosts)
|
|
if discover is False:
|
|
body["discover"] = False
|
|
if attest:
|
|
body["attest"] = True
|
|
return self._req("POST", "/pq/readiness/perimeter", body)
|
|
|
|
def pq_finality(self, block: str = "latest", network: str = "testnet", client: str = "besu") -> dict:
|
|
"""AIP-21: finalitatea post-cuantica a unui bloc (sigiliile auzite de un validator, re-verificate, verdict de cvorum). Libera.
|
|
client: "besu" (implicit), "nethermind" sau "both" (doua implementari independente, cu `agreement` calculat de gateway)."""
|
|
return self._req("GET", f"/pq/finality/{block}?network={network}&client={client}", auth=False)
|
|
|
|
# ---- audit log ----
|
|
def audit_log(self, day: Optional[str] = None, offset: int = 0, limit: int = 1000) -> dict:
|
|
"""Jurnalul cererilor cu cheie ale contului pentru o zi UTC (paginat), cu sha256-ul fisierului zilei."""
|
|
q = f"offset={int(offset)}&limit={int(limit)}" + (f"&day={day}" if day else "")
|
|
return self._req("GET", f"/account/audit?{q}")
|
|
|
|
def audit_export(self, day: Optional[str] = None, fmt: str = "jsonl") -> dict:
|
|
"""Ziua ca FISIER (1.4.0): fmt='jsonl' = octetii exacti ai fisierului zilei, al caror sha256 e digestul pe care il notarizezi
|
|
(calculat AICI, din ce ai primit, nu luat pe cuvant); fmt='cef' = un rand ArcSight CEF pe cerere, pentru un SIEM."""
|
|
import hashlib
|
|
q = f"format={fmt}" + (f"&day={day}" if day else "")
|
|
anteturi = {"user-agent": f"aere-cloud-python/{__version__}", "x-api-key": self.api_key}
|
|
cerere = urllib.request.Request(self.base_url + f"/account/audit?{q}", headers=anteturi, method="GET")
|
|
status, corp = self._opener(cerere, self.timeout)
|
|
if not (200 <= status < 300):
|
|
try:
|
|
d = _json.loads(corp.decode("utf-8")) if corp else None
|
|
except ValueError:
|
|
d = None
|
|
raise AereCloudError(status, d, "/account/audit")
|
|
corp = corp or b""
|
|
return {"format": fmt, "bytes": corp, "text": corp.decode("utf-8"), "entries": sum(1 for l in corp.split(b"\n") if l),
|
|
"sha256": "0x" + hashlib.sha256(corp).hexdigest()}
|
|
|
|
def audit_retention(self) -> dict:
|
|
"""Retentia jurnalului de audit (1.5.0), DOAR-CITIRE cu cheia API: ce e in vigoare, ce asteapta si pana cand, ce tinem, si
|
|
registrul fiecarei schimbari si al fiecarei zile sterse (cu digestul ei). Se SETEAZA cu semnatura portofelului, in consola."""
|
|
return self._req("GET", "/account/audit/retention")
|
|
|
|
def audit_verify_kept(self, day: str, data) -> dict:
|
|
"""Exportul pe care l-ai PASTRAT se mai potriveste cu ziua de la noi, chiar dupa ce am sters-o prin retentia ta? sha256 al
|
|
octetilor TAI, calculat aici, fata de digestul zilei tinute ('held') sau de piatra lasata de ziua stearsa ('expired', 410)."""
|
|
import hashlib
|
|
octeti = data.encode("utf-8") if isinstance(data, str) else bytes(data)
|
|
local = "0x" + hashlib.sha256(octeti).hexdigest()
|
|
anteturi = {"user-agent": f"aere-cloud-python/{__version__}", "x-api-key": self.api_key}
|
|
cerere = urllib.request.Request(self.base_url + f"/account/audit?format=jsonl&day={day}", headers=anteturi, method="GET")
|
|
status, corp = self._opener(cerere, self.timeout)
|
|
corp = corp or b""
|
|
if not (200 <= status < 300):
|
|
try:
|
|
d = _json.loads(corp.decode("utf-8")) if corp else None
|
|
except ValueError:
|
|
d = None
|
|
if status == 410 and isinstance(d, dict) and isinstance(d.get("expired"), dict):
|
|
e = d["expired"]
|
|
return {"day": day, "state": "expired", "digest": e.get("digest"), "local": local, "matches": local == e.get("digest"), "deleted_at": e.get("deletedAt")}
|
|
raise AereCloudError(status, d, "/account/audit")
|
|
digest = "0x" + hashlib.sha256(corp).hexdigest()
|
|
return {"day": day, "state": "held", "digest": digest, "local": local, "matches": local == digest, "deleted_at": None}
|
|
|
|
# ---- webhooks ----
|
|
def list_webhooks(self) -> dict:
|
|
return self._req("GET", "/webhooks")
|
|
|
|
def create_webhook(self, spec: dict) -> dict:
|
|
return self._req("POST", "/webhooks", spec)
|
|
|
|
def delete_webhook(self, webhook_id: str) -> dict:
|
|
return self._req("DELETE", f"/webhooks/{webhook_id}")
|
|
|
|
# ---- gas sponsorship ----
|
|
def sponsor_health(self) -> dict:
|
|
return self._req("GET", "/sponsor/health")
|
|
|
|
def sponsor_create_account(self, body: dict) -> dict:
|
|
return self._req("POST", "/sponsor/createAccount", body)
|
|
|
|
def sponsor_create_pq_account(self, falcon_pub_key: str, salt: int = 0) -> dict:
|
|
"""Cont inteligent cu proprietar POST-CUANTIC (Falcon-512), gazul platit de releu.
|
|
|
|
falcon_pub_key: 0x-hex, 897 octeti, antetul 0x09 (cheia publica Falcon-512).
|
|
Idempotent pe (cheie, salt): a doua chemare intoarce contul existent, fara tx.
|
|
"""
|
|
return self._req("POST", "/sponsor/createPqAccount", {"falconPubKey": falcon_pub_key, "salt": salt})
|
|
|
|
def sponsor_execute(self, body: dict) -> dict:
|
|
return self._req("POST", "/sponsor/execute", body)
|
|
|
|
|
|
def verify_webhook(raw_body, signature_header: str, secret: str) -> bool:
|
|
"""Verifica semnatura unei livrari de webhook. A NU crede un webhook fara asta.
|
|
|
|
raw_body: octetii EXACTI ai cererii (nu re-serializati). Comparatia e in timp constant.
|
|
"""
|
|
if isinstance(raw_body, str):
|
|
raw_body = raw_body.encode("utf-8")
|
|
asteptat = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
|
|
return hmac.compare_digest(asteptat, str(signature_header or ""))
|