From c5bb091fa50cf37312f30815c7e27a5442b38431 Mon Sep 17 00:00:00 2001 From: Michael Turner Date: Fri, 11 Sep 2026 16:33:12 -0400 Subject: [PATCH] feat: edge.provable.com/api is the default hosted API; no credentials required The hosted Provable API now has an open edge at https://edge.provable.com/api: reads, delegated proving and the hosted record scanner all work with no API key, consumer id or JWT. It becomes DEFAULT_HOST. The credentialed legacy origin https://api.provable.com (LEGACY_HOST) keeps working unchanged with api_key + consumer_id. The edge keeps its services under an /api path prefix, which jwt_origin (scheme + host only) would drop. A new _client_common.service_root(url) keeps the prefix and strips a legacy /v2[/{network}] suffix; both network clients and facade.provider.scanner_base derive {root}/v2/{network}, {root}/prove and {root}/scanner from it. requires_credentials(url) is True only for the legacy host; is_provable_host recognises both. shield-swap: Profile.DEFAULT_ENDPOINT, the MCP server's ALEO_ENDPOINT default and the live-test ENDPOINT default move to the edge. The onboarding credentials stage provisions Provable consumers only when the profile's endpoint requires them; on the edge it just mints the durable DEX token. The live conftest hands out no DPS credentials on the edge. Tests default to the edge and run credential-free there; credential-gated SDK e2e tests skip only when the endpoint actually needs credentials and none are set. Unit tests pin service_root, requires_credentials and the edge URL derivation; the legacy-host derivation tests are unchanged and still pass. Verified on the edge with ALEO_E2E_API_KEY/CONSUMER_ID unset: sdk 892 unit + proving; sdk live e2e 27/27 on testnet and mainnet (delegated transfer, hosted scanner, private roundtrip); shield-swap 355 unit, live read tier 54/57 (3 environmental skips), funded testnet write tier 13/13. --- AGENTS.md | 29 +++++++---- README.md | 6 +-- sdk/Readme.md | 6 +-- sdk/python/aleo/_client_common.py | 50 ++++++++++++++++--- sdk/python/aleo/async_network_client.py | 13 ++--- sdk/python/aleo/facade/provider.py | 31 ++++++------ sdk/python/aleo/facade/records.py | 3 +- sdk/python/aleo/network_client.py | 15 +++--- sdk/python/tests/e2e/test_live_e2e.py | 22 +++++--- .../tests/e2e/test_testnet_objects_live.py | 4 +- sdk/python/tests/test_facade_records.py | 11 ++++ sdk/python/tests/test_network_client.py | 40 +++++++++++++++ shield-swap-sdk/README.md | 2 +- .../python/aleo_shield_swap/__init__.py | 2 +- .../python/aleo_shield_swap/lifecycle.py | 15 +++++- .../python/aleo_shield_swap/mcp.py | 4 +- .../python/aleo_shield_swap/profile.py | 2 +- shield-swap-sdk/tests/integration/conftest.py | 15 ++++-- shield-swap-sdk/tests/test_lifecycle.py | 34 ++++++++++++- 19 files changed, 229 insertions(+), 75 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 418942f2..7a64a6cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,15 +61,22 @@ addopts if you invoke pytest from the repo root. ## Delegated services (DPS + record scanner) -**Auth, proving, and scanning are all Provable *services* on `api.provable.com`, -hosted at the API ORIGIN — NOT under the read node's `/v2/{network}` base.** The -read/RPC endpoints live at `https://api.provable.com/v2/{network}/…`; the -services hang off the bare origin (`https://api.provable.com`) at their own path -prefixes. Each is confirmed working against live testnet (see -`tests/e2e/test_testnet_e2e.py`): - -- **JWT auth** — origin, no prefix: `POST {origin}/jwts/{consumerId}`. Derive the - origin with `jwt_origin(base_url)` (`scheme://host`, path stripped). +**Proving and scanning are Provable *services* hung off the hosted API's +SERVICE ROOT — NOT under the read node's `/v2/{network}` base.** Two hosts: + +- **`https://edge.provable.com/api` (the default, `DEFAULT_HOST`)** — open: no + API key, consumer id, or JWT for reads, `/prove`, or `/scanner`. Note the + `/api` path prefix: reads are `{root}/v2/{network}/…`, services `{root}/prove`, + `{root}/scanner`. Derive the root with `service_root(url)` (keeps the prefix; + strips a legacy `/v2[/{network}]` suffix) — NOT `jwt_origin`, which drops it. +- **`https://api.provable.com` (`LEGACY_HOST`)** — credentialed: the same + layout at the bare origin, with the prover and scanner behind + `api_key` + `consumer_id` JWTs. `requires_credentials(url)` is True only here; + everything below about JWTs applies only to this host. + +Each is confirmed working against live testnet (see `tests/e2e/`): + +- **JWT auth (legacy host only)** — `POST {root}/jwts/{consumerId}`. - **Delegated proving** — `{origin}/prove/{network}` prefix: - `GET {origin}/prove/{network}/pubkey` — ephemeral X25519 key + key id + a `Set-Cookie` **affinity** session. The ephemeral private key lives only on @@ -81,8 +88,8 @@ prefixes. Each is confirmed working against live testnet (see - `POST {origin}/prove/{network}/prove/authorization` (or `/prove/request`) — sealed-box `{key_id, ciphertext}`; JWT + the affinity cookie; SDK retries 500/503. -- **Record scanner** — `{origin}/scanner/{network}` prefix (env - `RECORD_SCANNER_URL=https://api.provable.com/scanner`). +- **Record scanner** — `{root}/scanner/{network}` prefix (env + `RECORD_SCANNER_URL=https://edge.provable.com/api/scanner`). Documented endpoints (docs describe paths *relative to the service base*; the base is the origin + service prefix above): diff --git a/README.md b/README.md index 6d93f3f5..20ef8ed8 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ python -m aleo_shield_swap.mcp # stdio server with the lifecycle tools from aleo import Aleo # Connect (construction is offline — no I/O until you make a call) -aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2")) +aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) print(aleo.network_name) # "mainnet" print(aleo.network_id) # 0 @@ -79,7 +79,7 @@ The facade follows a clean top-to-bottom narrative: **connect → account → re ```python from aleo import Aleo -aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2")) +aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) # Optional: check reachability # requires a live node if aleo.is_connected(): @@ -215,7 +215,7 @@ import asyncio from aleo import AsyncAleo async def main(): - aleo = AsyncAleo(AsyncAleo.HTTPProvider("https://api.provable.com/v2")) + aleo = AsyncAleo(AsyncAleo.HTTPProvider("https://edge.provable.com/api")) print(aleo.network_name) # sync — no I/O # Account ops are sync (purely local), even on AsyncAleo diff --git a/sdk/Readme.md b/sdk/Readme.md index 7d5ad3a1..4f73e1c4 100644 --- a/sdk/Readme.md +++ b/sdk/Readme.md @@ -13,7 +13,7 @@ It ships two layers: from aleo import Aleo # Connect (construction is offline — no I/O until you make a call) -aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2")) +aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) print(aleo.network_name) # "mainnet" print(aleo.network_id) # 0 @@ -37,7 +37,7 @@ The facade follows a clean top-to-bottom narrative: **connect → account → re ```python from aleo import Aleo -aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com/v2")) +aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) # Optional: check reachability # requires a live node if aleo.is_connected(): @@ -173,7 +173,7 @@ import asyncio from aleo import AsyncAleo async def main(): - aleo = AsyncAleo(AsyncAleo.HTTPProvider("https://api.provable.com/v2")) + aleo = AsyncAleo(AsyncAleo.HTTPProvider("https://edge.provable.com/api")) print(aleo.network_name) # sync — no I/O # Account ops are sync (purely local), even on AsyncAleo diff --git a/sdk/python/aleo/_client_common.py b/sdk/python/aleo/_client_common.py index 8c570135..afd595d7 100644 --- a/sdk/python/aleo/_client_common.py +++ b/sdk/python/aleo/_client_common.py @@ -35,20 +35,56 @@ def __init__(self, message: str, status: int | None = None) -> None: FIVE_MINUTES_MS: int = 5 * 60 * 1000 -DEFAULT_HOST: str = "https://api.provable.com" +#: The hosted Provable API's open edge: no API key, consumer id, or JWT is +#: needed for reads, the delegated prover, or the hosted scanner. +DEFAULT_HOST: str = "https://edge.provable.com/api" +#: The credentialed legacy origin; still fully supported with api_key + +#: consumer_id (JWTs minted at ``/jwts``). +LEGACY_HOST: str = "https://api.provable.com" DEFAULT_NETWORK: str = "mainnet" # The hosted Provable API splits its services across path prefixes off a single -# origin (reads at /v2, delegated proving at /prove, hosted scanner at /scanner, -# JWT auth at /jwts). We detect it by host so that EVERY other endpoint (devnode, -# local, or any custom node) is treated as a literal read base — no /v2 magic, -# and no prover/scanner wired up (those services only exist on the hosted API). -PROVABLE_API_HOSTS: frozenset[str] = frozenset({"api.provable.com"}) +# service root (reads at /v2, delegated proving at /prove, hosted scanner at +# /scanner, JWT auth at /jwts). The root is the origin on the legacy host and +# ``{origin}/api`` on the edge — see service_root(). We detect the hosted API +# by host so that EVERY other endpoint (devnode, local, or any custom node) is +# treated as a literal read base — no /v2 magic, and no prover/scanner wired up +# (those services only exist on the hosted API). +PROVABLE_API_HOSTS: frozenset[str] = frozenset({"api.provable.com", "edge.provable.com"}) +#: Hosted API hosts that gate the prover and scanner behind api_key/consumer JWTs. +CREDENTIALED_HOSTS: frozenset[str] = frozenset({"api.provable.com"}) + +_NETWORK_SEGMENTS: frozenset[str] = frozenset({"mainnet", "testnet", "canary"}) def is_provable_host(url: str) -> bool: - """True if *url* points at the hosted Provable API (api.provable.com).""" + """True if *url* points at the hosted Provable API (edge or legacy host).""" return (urlparse(url).hostname or "").lower() in PROVABLE_API_HOSTS + + +def requires_credentials(url: str) -> bool: + """True if the hosted API at *url* needs api_key/consumer_id for the prover + and scanner. The edge does not; the legacy ``api.provable.com`` does. + Off the hosted API there is nothing to authenticate to, so False.""" + return (urlparse(url).hostname or "").lower() in CREDENTIALED_HOSTS + + +def service_root(url: str) -> str: + """The root the hosted API's services hang off, path prefix included. + + ``https://edge.provable.com/api`` → itself; ``https://api.provable.com`` + → itself; a legacy ``.../v2`` or ``.../v2/{network}`` read base has that + suffix stripped so older configs keep working. Unlike :func:`jwt_origin` + this keeps a path prefix — on the edge every service lives under ``/api``. + """ + parsed = urlparse(url) + parts = [p for p in parsed.path.split("/") if p] + if len(parts) >= 2 and parts[-2] == "v2" and parts[-1] in _NETWORK_SEGMENTS: + parts = parts[:-2] + elif parts and parts[-1] == "v2": + parts = parts[:-1] + prefix = "/" + "/".join(parts) if parts else "" + return f"{parsed.scheme}://{parsed.netloc}{prefix}" SDK_HEADERS: set[str] = {"x-aleo-sdk-version", "x-aleo-environment", "x-aleo-method"} diff --git a/sdk/python/aleo/async_network_client.py b/sdk/python/aleo/async_network_client.py index 2ba8d2dd..6ee54333 100644 --- a/sdk/python/aleo/async_network_client.py +++ b/sdk/python/aleo/async_network_client.py @@ -26,6 +26,7 @@ is_provable_host, jwt_expired, jwt_origin, + service_root, make_default_headers, method_headers, strip_quotes, @@ -131,16 +132,16 @@ def _resolve_urls( and ``/consumers`` always live at the bare origin — handled in :meth:`_refresh_jwt`, not here.) Mirrors ``AleoNetworkClient._resolve_urls``. """ - origin = jwt_origin(host) network = self._network if is_provable_host(host): + root = service_root(host) # keeps the edge's /api prefix return ( - f"{origin}/v2/{network}", - origin, - f"{origin}/prove/{network}", - f"{origin}/scanner/{network}", + f"{root}/v2/{network}", + root, + f"{root}/prove/{network}", + f"{root}/scanner/{network}", ) - return (f"{host.rstrip('/')}/{network}", origin, None, None) + return (f"{host.rstrip('/')}/{network}", jwt_origin(host), None, None) # ── Network module selection ────────────────────────────────────────── diff --git a/sdk/python/aleo/facade/provider.py b/sdk/python/aleo/facade/provider.py index 58fb8414..831f9e6d 100644 --- a/sdk/python/aleo/facade/provider.py +++ b/sdk/python/aleo/facade/provider.py @@ -7,10 +7,9 @@ from __future__ import annotations from typing import Any -from urllib.parse import urlparse from ..network_client import AleoNetworkClient -from .._client_common import DEFAULT_HOST, DEFAULT_NETWORK, is_provable_host +from .._client_common import DEFAULT_HOST, DEFAULT_NETWORK, is_provable_host, service_root # AsyncAleoNetworkClient imported lazily to avoid pulling httpx at import time. @@ -33,8 +32,7 @@ def scanner_base(provider: "HTTPProvider") -> str | None: """ if not is_provable_host(provider.url): return None - parsed = urlparse(provider.url) - return f"{parsed.scheme}://{parsed.netloc}/scanner" + return f"{service_root(provider.url)}/scanner" class HTTPProvider: @@ -43,21 +41,24 @@ class HTTPProvider: Parameters ---------- url: - API origin, e.g. ``"https://api.provable.com"`` (the default). For the - hosted Provable API the SDK adds the service prefixes itself — reads at - ``/v2``, delegated proving at ``/prove``, hosted scanner at ``/scanner``, - JWT auth at ``/jwts`` — so you never spell them out. Any other host - (devnode, a local or custom node) is used as a literal read base, with no - hosted prover/scanner wired up. A legacy ``".../v2"`` value still works. + Hosted API service root, e.g. ``"https://edge.provable.com/api"`` (the + default — open, no credentials needed) or the credentialed legacy + ``"https://api.provable.com"``. For the hosted Provable API the SDK + adds the service prefixes itself — reads at ``/v2``, delegated proving + at ``/prove``, hosted scanner at ``/scanner``, JWT auth at ``/jwts`` — + so you never spell them out. Any other host (devnode, a local or + custom node) is used as a literal read base, with no hosted + prover/scanner wired up. A legacy ``".../v2"`` value still works. network: Network name — ``"mainnet"`` (default) or ``"testnet"``. api_key: - Provable API key passed through to the underlying - :class:`~aleo.network_client.AleoNetworkClient`. Shared by the - delegated prover and the hosted record scanner. + Provable API key, needed only on the legacy ``api.provable.com`` host + (see :func:`~aleo._client_common.requires_credentials`); passed through + to the underlying :class:`~aleo.network_client.AleoNetworkClient` and + shared by the delegated prover and the hosted record scanner. consumer_id: - Provable consumer id, paired with *api_key* to mint/refresh JWTs for - the delegated prover and the hosted record scanner. + Provable consumer id, paired with *api_key* to mint/refresh JWTs on the + legacy host. Leave both unset on the edge. prover_uri: Optional override for the DPS prover base (without network suffix). Defaults to ``{origin}/prove`` derived from *url*. diff --git a/sdk/python/aleo/facade/records.py b/sdk/python/aleo/facade/records.py index 4b0235ae..45b8fcf0 100644 --- a/sdk/python/aleo/facade/records.py +++ b/sdk/python/aleo/facade/records.py @@ -82,7 +82,8 @@ def _build_scanner(self) -> Any: if base is None: raise RuntimeError( "The hosted record scanner is only available on the Provable API " - f"(api.provable.com); this client points at {provider.url!r}. " + f"(edge.provable.com/api or api.provable.com); this client points at " + f"{provider.url!r}. " "Assign your own scanner (aleo.records.scanner = RecordScanner(...)) " "or a custom aleo.record_provider to scan against this endpoint." ) diff --git a/sdk/python/aleo/network_client.py b/sdk/python/aleo/network_client.py index ed8d3886..45a4263a 100644 --- a/sdk/python/aleo/network_client.py +++ b/sdk/python/aleo/network_client.py @@ -30,6 +30,7 @@ is_provable_host, jwt_expired, jwt_origin, + service_root, make_default_headers, method_headers, strip_quotes, @@ -128,16 +129,18 @@ def _resolve_urls( services do not exist off the Provable API, so we leave them unset rather than point at a bogus URL. An explicit ``prover_uri`` still works anywhere. """ - origin = jwt_origin(host) network = self._network if is_provable_host(host): + # The service root keeps the edge's /api prefix; /jwts hangs off + # it too (unused when the host needs no credentials). + root = service_root(host) return ( - f"{origin}/v2/{network}", - origin, - f"{origin}/prove/{network}", - f"{origin}/scanner/{network}", + f"{root}/v2/{network}", + root, + f"{root}/prove/{network}", + f"{root}/scanner/{network}", ) - return (f"{host.rstrip('/')}/{network}", origin, None, None) + return (f"{host.rstrip('/')}/{network}", jwt_origin(host), None, None) # ── Network module selection ────────────────────────────────────────── diff --git a/sdk/python/tests/e2e/test_live_e2e.py b/sdk/python/tests/e2e/test_live_e2e.py index f5db5a07..e8f6dd52 100644 --- a/sdk/python/tests/e2e/test_live_e2e.py +++ b/sdk/python/tests/e2e/test_live_e2e.py @@ -35,7 +35,8 @@ ``ALEO_E2E_PRIVATE_KEY_MAINNET`` take precedence for that network when set (an Aleo address is identical across networks, but funding is per-network). ``ALEO_E2E_ENDPOINT`` - API origin. Default ``https://api.provable.com`` — the SDK adds ``/v2`` for + API service root. Default ``https://edge.provable.com/api`` (open, no + credentials) — the SDK adds ``/v2`` for reads and ``/prove`` / ``/scanner`` for the services automatically. (A legacy ``.../v2`` value is still accepted.) ``ALEO_E2E_API_KEY`` / ``ALEO_E2E_CONSUMER_ID`` @@ -65,9 +66,14 @@ pytestmark = pytest.mark.live _PRIVATE_KEY = os.environ.get("ALEO_E2E_PRIVATE_KEY") -_ENDPOINT = os.environ.get("ALEO_E2E_ENDPOINT", "https://api.provable.com") +_ENDPOINT = os.environ.get("ALEO_E2E_ENDPOINT", "https://edge.provable.com/api") _API_KEY = os.environ.get("ALEO_E2E_API_KEY") _CONSUMER_ID = os.environ.get("ALEO_E2E_CONSUMER_ID") +# Only the legacy credentialed host needs DPS/scanner credentials; the default +# edge is open, so credential-gated tests run there with none. +from aleo._client_common import requires_credentials # noqa: E402 + +_MISSING_CREDS = requires_credentials(_ENDPOINT) and (_API_KEY is None or _CONSUMER_ID is None) _PROVER_URI = os.environ.get("ALEO_E2E_PROVER_URI") # Skip the ENTIRE module (collection still succeeds) when the funded key is @@ -157,8 +163,8 @@ def _client(network: str, *, with_creds: bool) -> Aleo: @pytest.mark.skipif( - _API_KEY is None or _CONSUMER_ID is None, - reason="ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID not set — DPS creds required.", + _MISSING_CREDS, + reason="legacy host needs ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID (DPS creds).", ) def test_delegate_transfer_public_live(network: str) -> None: """REAL delegated proving of a tiny ``credits.aleo/transfer_public``. @@ -193,8 +199,8 @@ def test_delegate_transfer_public_live(network: str) -> None: @pytest.mark.skipif( - _API_KEY is None or _CONSUMER_ID is None, - reason="ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID not set — hosted scanner creds required.", + _MISSING_CREDS, + reason="legacy host needs ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID (scanner creds).", ) def test_hosted_record_scanner_live(network: str) -> None: """Register with the hosted scanner and query owned credits records. @@ -232,8 +238,8 @@ def test_hosted_record_scanner_live(network: str) -> None: @pytest.mark.skipif( - _API_KEY is None or _CONSUMER_ID is None, - reason="ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID not set — DPS + scanner creds required.", + _MISSING_CREDS, + reason="legacy host needs ALEO_E2E_API_KEY / ALEO_E2E_CONSUMER_ID (DPS + scanner creds).", ) def test_private_roundtrip_live(network: str) -> None: """End-to-end private roundtrip on live {testnet, mainnet}. diff --git a/sdk/python/tests/e2e/test_testnet_objects_live.py b/sdk/python/tests/e2e/test_testnet_objects_live.py index acf00eda..38a8c00a 100644 --- a/sdk/python/tests/e2e/test_testnet_objects_live.py +++ b/sdk/python/tests/e2e/test_testnet_objects_live.py @@ -28,7 +28,7 @@ the default offline jobs don't provide.) * Env-gated + offline-safe: ``ALEO_E2E_ENDPOINT`` (default the public Provable - API origin ``https://api.provable.com``; the SDK adds ``/v2`` for reads), + API service root ``https://edge.provable.com/api``; the SDK adds ``/v2`` for reads), ``network="testnet"``. READ-ONLY — needs NO credentials and NO funded key. At import time we probe the endpoint once; if it is unreachable (offline CI) the whole module skips cleanly via @@ -59,7 +59,7 @@ pytestmark = pytest.mark.live _ENDPOINT = os.environ.get( - "ALEO_E2E_ENDPOINT", "https://api.provable.com" + "ALEO_E2E_ENDPOINT", "https://edge.provable.com/api" ) _NETWORK = "testnet" diff --git a/sdk/python/tests/test_facade_records.py b/sdk/python/tests/test_facade_records.py index f55191a9..217f4586 100644 --- a/sdk/python/tests/test_facade_records.py +++ b/sdk/python/tests/test_facade_records.py @@ -515,3 +515,14 @@ def test_private_fee_no_provider_errors() -> None: with pytest.raises(ExecutionError, match="record provider"): bc._resolve_fee_record(None, min_microcredits=5000) + + +def test_scanner_base_keeps_the_edge_api_prefix() -> None: + from aleo.facade.provider import scanner_base + assert scanner_base(HTTPProvider("https://edge.provable.com/api", network="testnet")) \ + == "https://edge.provable.com/api/scanner" + assert scanner_base(HTTPProvider("https://api.provable.com/v2", network="testnet")) \ + == "https://api.provable.com/scanner" + a = Aleo(HTTPProvider(network="testnet")) # the default: edge, no creds + assert a.records.scanner.url == "https://edge.provable.com/api/scanner/testnet" + assert a.records.scanner._api_key is None # no credentials wired diff --git a/sdk/python/tests/test_network_client.py b/sdk/python/tests/test_network_client.py index 5c25cb8f..e3d70067 100644 --- a/sdk/python/tests/test_network_client.py +++ b/sdk/python/tests/test_network_client.py @@ -875,3 +875,43 @@ def test_set_prover_uri() -> None: c = make_client() c.set_prover_uri("https://prover.example.com") assert c._prover_uri == f"https://prover.example.com/{NET}" + + +# ── Hosted API topology: open edge (default) vs credentialed legacy host ──── + +def test_default_host_is_the_open_edge() -> None: + from aleo._client_common import DEFAULT_HOST, LEGACY_HOST, requires_credentials + assert DEFAULT_HOST == "https://edge.provable.com/api" + assert LEGACY_HOST == "https://api.provable.com" + assert not requires_credentials(DEFAULT_HOST) + assert requires_credentials(LEGACY_HOST) + assert not requires_credentials("http://127.0.0.1:3030") # nothing to auth to + + +def test_service_root_keeps_the_edge_prefix_and_strips_legacy_v2() -> None: + from aleo._client_common import service_root + assert service_root("https://edge.provable.com/api") == "https://edge.provable.com/api" + assert service_root("https://edge.provable.com/api/") == "https://edge.provable.com/api" + assert service_root("https://edge.provable.com/api/v2") == "https://edge.provable.com/api" + assert service_root("https://edge.provable.com/api/v2/testnet") == "https://edge.provable.com/api" + assert service_root("https://api.provable.com") == "https://api.provable.com" + assert service_root("https://api.provable.com/v2/mainnet/") == "https://api.provable.com" + assert service_root("http://localhost:3030/v2") == "http://localhost:3030" + + +def test_edge_client_hangs_every_service_off_the_api_prefix() -> None: + # The whole point of service_root: a jwt_origin-style derivation would + # drop /api and every read, prove, and scan would 404. + c = AleoNetworkClient("https://edge.provable.com/api", network="testnet") + assert c._host == "https://edge.provable.com/api/v2/testnet" + assert c.prover_uri == "https://edge.provable.com/api/prove/testnet" + assert c.scanner_uri == "https://edge.provable.com/api/scanner/testnet" + assert c.origin == "https://edge.provable.com/api" + # No credentials → no JWT header and no /jwts round trip. + assert c._ensure_jwt(None, None, None) is None + + +def test_default_client_targets_the_edge() -> None: + c = AleoNetworkClient(network="mainnet") + assert c._host == "https://edge.provable.com/api/v2/mainnet" + assert c.prover_uri == "https://edge.provable.com/api/prove/mainnet" diff --git a/shield-swap-sdk/README.md b/shield-swap-sdk/README.md index 9687be5e..3bcb7c13 100644 --- a/shield-swap-sdk/README.md +++ b/shield-swap-sdk/README.md @@ -10,7 +10,7 @@ else. from aleo import Aleo from aleo_shield_swap import ShieldSwap -aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com")) +aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) aleo.default_account = account dex = ShieldSwap(aleo) diff --git a/shield-swap-sdk/python/aleo_shield_swap/__init__.py b/shield-swap-sdk/python/aleo_shield_swap/__init__.py index 0aede3ec..01f1cb93 100644 --- a/shield-swap-sdk/python/aleo_shield_swap/__init__.py +++ b/shield-swap-sdk/python/aleo_shield_swap/__init__.py @@ -5,7 +5,7 @@ from aleo import Aleo from aleo_shield_swap import ShieldSwap - aleo = Aleo(Aleo.HTTPProvider("https://api.provable.com")) + aleo = Aleo(Aleo.HTTPProvider("https://edge.provable.com/api")) aleo.default_account = account dex = ShieldSwap(aleo) diff --git a/shield-swap-sdk/python/aleo_shield_swap/lifecycle.py b/shield-swap-sdk/python/aleo_shield_swap/lifecycle.py index fdcbfbc7..1a1816c7 100644 --- a/shield-swap-sdk/python/aleo_shield_swap/lifecycle.py +++ b/shield-swap-sdk/python/aleo_shield_swap/lifecycle.py @@ -157,9 +157,18 @@ def provision_provable_credentials(endpoint: str, username: str) -> tuple[str, s TOKEN_IDLE_SECONDS = 24 * 3600 +def _needs_provable_credentials(ctx: _Ctx) -> bool: + """Whether the profile's node endpoint gates the prover and scanner behind + api_key/consumer JWTs. The default edge (``edge.provable.com/api``) is + open; only the legacy ``api.provable.com`` needs them.""" + from aleo._client_common import requires_credentials + return requires_credentials(ctx.profile.endpoint) + + def _creds_done(ctx: _Ctx) -> bool: c = ctx.profile.credentials - if not (c.get("dps_api_key") and c.get("dps_consumer_id")): + if _needs_provable_credentials(ctx) and not (c.get("dps_api_key") + and c.get("dps_consumer_id")): return False if c.get("dex_api_token"): return True @@ -227,7 +236,9 @@ def _creds_run(ctx: _Ctx) -> str: """ details: list[str] = [] creds = ctx.profile.credentials - if not (creds.get("dps_api_key") and creds.get("dps_consumer_id")): + if not _needs_provable_credentials(ctx): + details.append("endpoint needs no Provable credentials (open edge)") + elif not (creds.get("dps_api_key") and creds.get("dps_consumer_id")): key = os.environ.get("ALEO_E2E_API_KEY") cid = os.environ.get("ALEO_E2E_CONSUMER_ID") if key and cid: diff --git a/shield-swap-sdk/python/aleo_shield_swap/mcp.py b/shield-swap-sdk/python/aleo_shield_swap/mcp.py index 5e87adca..6d395060 100644 --- a/shield-swap-sdk/python/aleo_shield_swap/mcp.py +++ b/shield-swap-sdk/python/aleo_shield_swap/mcp.py @@ -10,7 +10,7 @@ surface) in a worker thread, keeping the event loop free. Environment: - ALEO_ENDPOINT API origin (default ``https://api.provable.com`` — + ALEO_ENDPOINT API service root (default ``https://edge.provable.com/api``, no credentials — the provider derives ``/v2`` reads, ``/prove``, and ``/scanner`` from it) ALEO_PRIVATE_KEY Explicit signer (overrides the profile); without it @@ -73,7 +73,7 @@ def _build_dex() -> Any: from .client import ShieldSwap - endpoint = os.environ.get("ALEO_ENDPOINT", "https://api.provable.com") + endpoint = os.environ.get("ALEO_ENDPOINT", "https://edge.provable.com/api") network = os.environ.get("ALEO_NETWORK", "testnet") api_key = os.environ.get("ALEO_E2E_API_KEY") aleo = Aleo(HTTPProvider(endpoint, network=network, api_key=api_key)) diff --git a/shield-swap-sdk/python/aleo_shield_swap/profile.py b/shield-swap-sdk/python/aleo_shield_swap/profile.py index 01b4bcca..94bd27b9 100644 --- a/shield-swap-sdk/python/aleo_shield_swap/profile.py +++ b/shield-swap-sdk/python/aleo_shield_swap/profile.py @@ -12,7 +12,7 @@ from pathlib import Path from typing import Any, Optional -DEFAULT_ENDPOINT = "https://api.provable.com" +DEFAULT_ENDPOINT = "https://edge.provable.com/api" _PROFILE = "profile.json" _CREDENTIALS = "credentials.json" diff --git a/shield-swap-sdk/tests/integration/conftest.py b/shield-swap-sdk/tests/integration/conftest.py index b6c2ef89..408edee7 100644 --- a/shield-swap-sdk/tests/integration/conftest.py +++ b/shield-swap-sdk/tests/integration/conftest.py @@ -20,7 +20,7 @@ import pytest -ENDPOINT = os.environ.get("ALEO_E2E_ENDPOINT", "https://api.provable.com") # origin, no /v2 +ENDPOINT = os.environ.get("ALEO_E2E_ENDPOINT", "https://edge.provable.com/api") # service root, no /v2 PRIVATE_KEY = os.environ.get("ALEO_E2E_PRIVATE_KEY") account_tier = pytest.mark.skipif(not PRIVATE_KEY, reason="ALEO_E2E_PRIVATE_KEY not set") @@ -32,14 +32,21 @@ def dps_credentials() -> tuple[str, str]: """``(api_key, consumer_id)`` for the scanner + delegated proving. - Env first; otherwise provisioned once per session and cached (a fresh - consumer per run is fine — the key/id pair is what matters, not the name). + ``(None, None)`` on the open edge (the default endpoint). On the legacy + credentialed host: env first, otherwise provisioned once per session and + cached (a fresh consumer per run is fine — the key/id pair is what + matters, not the name). """ global _CREDS if _CREDS is None: + from aleo._client_common import requires_credentials key = os.environ.get("ALEO_E2E_API_KEY") cid = os.environ.get("ALEO_E2E_CONSUMER_ID") - if not (key and cid): + if not requires_credentials(ENDPOINT): + # The default edge is open: no key, no consumer, no JWTs. Env + # creds are still honoured if someone points at the legacy host. + key, cid = key or None, cid or None + elif not (key and cid): from aleo_shield_swap.lifecycle import provision_provable_credentials key, cid = provision_provable_credentials( ENDPOINT, f"shield-swap-itest-{int(time.time())}") diff --git a/shield-swap-sdk/tests/test_lifecycle.py b/shield-swap-sdk/tests/test_lifecycle.py index ab7b38ba..0ee5f8e8 100644 --- a/shield-swap-sdk/tests/test_lifecycle.py +++ b/shield-swap-sdk/tests/test_lifecycle.py @@ -77,11 +77,16 @@ def get_private_balances(self, programs, account=None): return {p: self._balances.get(p, 0) for p in programs} +LEGACY_ENDPOINT = "https://api.provable.com" # the credentialed host + + @pytest.fixture def profile(tmp_path): # Real keygen: the authenticate stage parses the key with the native - # PrivateKey type, so a fake string won't do. - return Profile.load_or_create(tmp_path / "home") + # PrivateKey type, so a fake string won't do. Pinned to the legacy + # credentialed endpoint so the Provable-credential paths stay under test; + # the open edge default is covered by its own test below. + return Profile.load_or_create(tmp_path / "home", endpoint=LEGACY_ENDPOINT) @pytest.fixture @@ -389,3 +394,28 @@ def json(self): assert provision_provable_credentials("https://api.provable.com/v2/testnet/", "u") == ("k", "c") assert provision_provable_credentials("https://api.provable.com", "u") == ("k", "c") assert seen == ["https://api.provable.com/consumers"] * 2 + + +def test_open_edge_endpoint_needs_no_provable_credentials(tmp_path, monkeypatch): + """The default endpoint (edge.provable.com/api) gates nothing behind + api_key/consumer JWTs, so the credentials stage neither reads env + credentials nor provisions a consumer — it only mints the durable DEX + token — and it counts as done without dps_* entries.""" + from aleo_shield_swap.lifecycle import provision_provable_credentials # noqa: F401 + monkeypatch.delenv("ALEO_E2E_API_KEY", raising=False) + monkeypatch.delenv("ALEO_E2E_CONSUMER_ID", raising=False) + monkeypatch.setattr("aleo_shield_swap.lifecycle.provision_provable_credentials", + lambda endpoint, username: (_ for _ in ()).throw( + AssertionError("must not provision on the open edge"))) + edge_profile = Profile.load_or_create(tmp_path / "edge-home") # default endpoint + assert edge_profile.endpoint == "https://edge.provable.com/api" + api = _StubApi() + api._token = "jwt" + dex = _StubDex(api, {"waleo.aleo": 7}, funded_from_start=True) + report = run_onboard(dex, edge_profile) + creds = next(o for o in report.outcomes if o.name == "credentials") + assert creds.action == "ran" and "open edge" in creds.detail and "minted" in creds.detail + assert "dps_api_key" not in edge_profile.credentials + assert edge_profile.credentials["dex_api_token"].startswith("ss_minted_") + again = run_onboard(dex, edge_profile) + assert next(o for o in again.outcomes if o.name == "credentials").action == "skipped"