Skip to content

Web3.py-style facade (Aleo/AsyncAleo) + restore dropped testnet#45

Open
iamalwaysuncomfortable wants to merge 37 commits into
masterfrom
feat/web3py-facade-clean
Open

Web3.py-style facade (Aleo/AsyncAleo) + restore dropped testnet#45
iamalwaysuncomfortable wants to merge 37 commits into
masterfrom
feat/web3py-facade-clean

Conversation

@iamalwaysuncomfortable

Copy link
Copy Markdown
Member

Web3.py-style facade for the Aleo Python SDK (+ restore dropped testnet)

Adds a high-level, Web3.py-shaped facade over the existing PyO3 bindings so
Python developers get an ergonomic Aleo(provider) / AsyncAleo(provider)
client instead of driving the low-level Process/authorize/execute/prove
sequence by hand. Built phase-by-phase (F1–F8) with a review gate after each
phase and a final whole-branch review.

Also restores testnet

The squash-merge of #44 landed on master without the testnet bundle
(testnet.py, _aleolib_testnet.pyi, build-both.sh, merge_testnet_so.py,
AGENTS.md, the network-aware client changes). The first commit here restores
it, so the two-build (mainnet and testnet) packaging ships and the facade's
network-aware dispatch works.

What's in the facade

  • Aleo(provider) clientHTTPProvider, is_connected, get_balance,
    unit helpers, is_valid_address, network_id/network_name, lazy process,
    escape hatches (.network_client, .process, .raw).
  • Typed exceptions — everything under a single AleoError
    (TransactionNotFound, ProgramNotFound, ExecutionError,
    TransactionConfirmationTimeout, …); one except AleoError catches all.
  • aleo.accountcreate/from_private_key/from_seed, encrypted
    import/export, sign/verify (bytes) and sign_value/verify_value
    (structured Aleo values). No signer-recovery verb (privacy stance).
  • aleo.network — typed pass-throughs over the network client;
    submit_transaction + send_raw_transaction alias; wait_for_transaction.
  • aleo.programsget(id) → bound Program with a dynamic
    .functions namespace
    built from the program's real function set
    (web3.py's ABI-driven contract.functions analog), input coercion against
    declared types, mapping reads, and ABI generation (local + from the web).
  • Bound-call verb laddersimulate/call/authorizeAuthorization;
    build_transactionTransaction; transact → tx id; delegate
    delegated-proving result. delegate defaults to the prover's fee master
    paying (no fee funding needed) — the frictionless flagship path. Result
    inspection via .outputs/.decoded(); aleo.decode_transition.
  • aleo.records + RecordProvider — record scanning/discovery over the
    RecordScanner; auto-sources a private-fee record; aleo.record_provider is
    swappable (self-hosted scanner escape hatch). Docstrings note the
    view-key-sharing tradeoff of delegated scanning.
  • AsyncAleo — async mirror over the async network client + record scanner;
    shares all pure orchestration with the sync path; blocking Process work runs
    in asyncio.to_thread.
  • Docs lead with the facade; runnable sdk/python/examples/facade_quickstart.py.

Testing

  • 812 offline tests pass; pyright strict = 0. Network/proving paths are
    behind @pytest.mark.slow (deselected by default); the delegated-proving path
    was validated live against the real prover earlier in development.
  • Final whole-branch review verdict: READY — money path (execution-id↔fee
    binding, fee-master default) verified correct on both sync and async rungs.

No Rust changes — pure Python over the existing bindings.

iamalwaysuncomfortable and others added 21 commits July 10, 2026 08:24
PR #44 was squash-merged to master without the testnet work (testnet.py,
_aleolib_testnet.pyi, build-both.sh, merge_testnet_so.py, AGENTS.md, the
network-aware client changes). This restores that bundle on top of master so
the two-build (mainnet+testnet) packaging ships, matching the migration
branch tip e897fd1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the F1 skeleton for the Web3.py-style facade:
- sdk/python/aleo/facade/{__init__,client,provider,errors}.py
- sdk/python/aleo/_facade_common.py (shared unit-conversion helpers)
- sdk/python/tests/test_facade_client.py (43 mocked tests, all green)
- Exports Aleo, HTTPProvider, AleoError + subclasses from aleo/__init__.py

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… all

Move AleoError into _client_common (lowest layer); AleoNetworkError,
AleoProvingError, and the RecordScanner errors now subclass it. Existing
'except AleoNetworkError' sites are unaffected (still subclasses). Corrects
the F1 review finding that the internal errors were siblings, not subclasses,
silently breaking the documented single-except contract; fixes the false
'conversion helpers' comment. is_connected keeps a broad catch (reachability
probe, web3.py semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds AccountModule attached to the Aleo client as aleo.account, with:
- create() / from_private_key(s) / from_seed(field) account factories
- export_encrypted / import_encrypted via PrivateKeyCiphertext
- sign(msg, acct) / verify(addr, msg, sig) — raw-bytes signing
- sign_value(value, acct) / verify_value(addr, value, sig) — structured Aleo Value signing
- default_account fallback on sign/sign_value when account arg is omitted
- No recover/signer-from-signature verb (privacy stance)

33 new offline tests in test_facade_account.py; suite 648 → 681 passed;
pyright strict 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port network-agnostic

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…g reads, ABI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… dedup 404 mapping; bool-rejection regression test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ransaction/transact/delegate) + result inspection

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uto-sourcing seam

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nces; document single-fee-record path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sync; make input_type_name public

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le quickstart example; snarkvm 4.8.1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/verify_value

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…), route decode-by-id through network (404→TransactionNotFound), get_transaction_object passthrough
…es in __init__.pyi; type functions.<fn>() as BoundCall

- Guard the eager `from . import testnet` so mainnet-only builds (CI
  proving/network + aleo-abi lanes) can `import aleo` (was
  ModuleNotFoundError: aleo._aleolib_testnet).
- Mirror __init__.py's re-export block into __init__.pyi so pyright
  resolves `from aleo import Aleo/AsyncAleo/HTTPProvider/AleoError/...`
  (fixes lint lane on examples/facade_quickstart.py).
- Annotate _FunctionCaller.__call__ -> BoundCall (via TYPE_CHECKING) so the
  verb ladder (simulate/transact/delegate) resolves without isinstance narrowing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ip cleanly

test_testnet.py does a direct `import aleo.testnet`, which bypasses the
__init__.py guard and hard-fails collection on mainnet-only builds
(proving/network lane runs -m slow but pytest imports the module at collection
regardless of markers). Skip the module when the extension isn't built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anes

Previously only the OS-matrix lanes built the testnet extension; the
proving/network and aleo-abi lanes were mainnet-only. Build the merged
two-network wheel in those lanes too (deps are cached, so the second feature
build is a warm recompile) and run test_testnet.py explicitly — in the proving
lane it's otherwise deselected by -m slow. Now testnet is exercised in every
test lane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…linux build)

The manylinux build runs maturin in a Docker container that writes sdk/dist as
root; the host-side merge_testnet_so.py step then hit PermissionError creating
the .whl.tmp. Reclaim ownership (Linux only) before repacking. macOS/Windows
build natively and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iamalwaysuncomfortable and others added 8 commits July 10, 2026 16:31
…ivate roundtrip)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…LEO_E2E_*)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… LocalRecordScanner); ALEO_E2E_* vars; snarkvm 4.8.1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…te gets priority_fee only

The SDK's execution_cost can undershoot a node's required base (storage) fee
(devnode fee-schedule skew). Add a base_fee override to the sync+async verb
ladder + _authorize_fee so callers can overpay; devnode e2e now overpay via
DEVNODE_OVERPAY_BASE_FEE and the transact tests pass against a real devnode.
delegate never overrides base_fee — only priority_fee (sync+async).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…scanner)

transfer_public_to_private (delegate) -> hosted-scanner discovery (polled) ->
transfer_private (delegate). Env-gated on ALEO_E2E_* (DPS + scanner creds);
skips cleanly without them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t was using mainnet types)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ing (sync+async)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iamalwaysuncomfortable and others added 8 commits July 10, 2026 18:48
…ecord correct paths + SDK drift

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, network-correctness guard)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fast lane excludes slow/live/devnode

The live object suite + creds-gated testnet e2e are marked `live` (not `slow`),
so no CI lane runs them (fast lanes filter not-slow-and-not-live-and-not-devnode;
the proving lane runs -m slow). They run only locally via -m live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Back to snarkvm v4.8.1 (the consensus-heights/set_consensus_version_heights
approach didn't unblock the devnode private roundtrip — the vanishing-polynomial
is deeper — and pinned an unmerged snarkvm branch). Remove the stale
set_consensus_version_heights .pyi stubs. Delete LocalRecordScanner: the private
roundtrip lives on testnet with the hosted scanner (aleo.records), so the
client-side scanner is unused. Scrub docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /pubkey handshake mints an ephemeral X25519 key held ONLY on the prover
backend that served it, so the follow-up /prove POST must stick to that same
backend via its LB affinity cookie. Both clients hand-built a Cookie header
from `set-cookie`, which (a) comma-joins multiple Set-Cookie values, (b) carries
attributes (Path/Secure/...), and (c) makes requests/httpx SKIP the cookie jar —
silently dropping affinity when the LB sets more than one cookie.

Rely on the persistent jar instead: sync forwards the parsed `pk_resp.cookies`
via the `cookies=` kwarg (covers the custom-transport path that bypasses
`self._session`); async relies on the AsyncClient jar (which wraps custom
transports at the client level). Adds an async cookie-affinity test mirroring
the sync one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Default ALEO_E2E_ENDPOINT to https://api.provable.com/v2 (JWT origin derives
  correctly from it; reads resolve under /v2/{network}).
- Transfer 1 microcredit to self (a valid recipient) instead of a fabricated
  burn address whose checksum failed Value.parse.
- Gate the delegated-proving tests on ALEO_E2E_PROVER_URI: the prover is a
  distinct service host from the read/JWT API, so there is no sensible
  fallback — the pubkey/prove handshake 404s against the read node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The delegated-proving handshake is a Provable *service* at the API origin under
the /prove/{network} prefix (sibling to /scanner) — e.g.
https://api.provable.com/prove/testnet/pubkey — NOT the read node's
/v2/{network} base (which 404s) nor the bare origin. Default the prover base to
{jwt_origin(base_url)}/prove/{network} so the handshake resolves to
{base}/pubkey and {base}/prove/authorization. An explicit HTTPProvider(prover_uri=)
override still wins.

Verified end-to-end: test_delegate_transfer_public_live now PASSES against live
testnet (real JWT + /pubkey affinity cookie + delegated proof). Also drops the
ALEO_E2E_PROVER_URI gate (the default now derives correctly) and corrects the
AGENTS.md DPS section (wrong host + stale drift note).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dual-network live e2e

Scanner base now derives from the endpoint origin under /scanner (a Provable
service sibling to /prove and /jwts) — {origin}/scanner/{network} — instead of
the read node's /v2/{network} base (which 404s). HTTPProvider gains a
consumer_id field; api_key + consumer_id flow through the one provider to BOTH
the delegated prover and the hosted scanner, so a user configures only
https://api.provable.com — reads, proving, and scanning all derive from its
origin. The scanner's own /pubkey→register/encrypted handshake keeps affinity
via its requests.Session / httpx.AsyncClient cookie jar.

Live e2e (tests/e2e/test_live_e2e.py, renamed from test_testnet_e2e.py) is now
parametrised over testnet AND mainnet (one at a time). Verified live:
delegate_transfer_public and hosted_record_scanner PASS on both networks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants