Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions sdk/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand Down
50 changes: 43 additions & 7 deletions sdk/python/aleo/_client_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}


Expand Down
13 changes: 7 additions & 6 deletions sdk/python/aleo/async_network_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
is_provable_host,
jwt_expired,
jwt_origin,
service_root,
make_default_headers,
method_headers,
strip_quotes,
Expand Down Expand Up @@ -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 ──────────────────────────────────────────

Expand Down
31 changes: 16 additions & 15 deletions sdk/python/aleo/facade/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:
Expand All @@ -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*.
Expand Down
3 changes: 2 additions & 1 deletion sdk/python/aleo/facade/records.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Expand Down
15 changes: 9 additions & 6 deletions sdk/python/aleo/network_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
is_provable_host,
jwt_expired,
jwt_origin,
service_root,
make_default_headers,
method_headers,
strip_quotes,
Expand Down Expand Up @@ -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 ──────────────────────────────────────────

Expand Down
22 changes: 14 additions & 8 deletions sdk/python/tests/e2e/test_live_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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``.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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}.
Expand Down
4 changes: 2 additions & 2 deletions sdk/python/tests/e2e/test_testnet_objects_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
11 changes: 11 additions & 0 deletions sdk/python/tests/test_facade_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading