Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
82dd06d
feat(sdk): restore testnet bundle dropped from squashed PR #44
iamalwaysuncomfortable Jul 10, 2026
49b5cfd
feat(facade): F1 — Aleo client, HTTPProvider, helpers, typed exceptions
iamalwaysuncomfortable Jul 10, 2026
6e9b817
fix(facade): make AleoError a true base so 'except AleoError' catches…
iamalwaysuncomfortable Jul 10, 2026
0ffb0da
feat(facade): F2 — aleo.account module (create/import/sign/sign_value)
iamalwaysuncomfortable Jul 10, 2026
e746be6
fix(facade): F2 review — repr network, tighten encrypted test, doc ex…
iamalwaysuncomfortable Jul 10, 2026
b32b3b8
feat(facade): F3 — aleo.network module (reads + submit + wait)
iamalwaysuncomfortable Jul 10, 2026
276c5d0
fix(facade): F3 review — map 404 to TransactionNotFound on tx getters
iamalwaysuncomfortable Jul 10, 2026
c425ff0
feat(facade): F4 — aleo.programs, dynamic functions, coercion, mappin…
iamalwaysuncomfortable Jul 10, 2026
65d0d60
fix(facade): F4 review — map 404 to ProgramNotFound on Mapping reads;…
iamalwaysuncomfortable Jul 10, 2026
adfc7a9
feat(facade): F5 — bound-call verb ladder (simulate/authorize/build_t…
iamalwaysuncomfortable Jul 10, 2026
d87d771
feat(facade): F6 — aleo.records + RecordProvider; close private-fee a…
iamalwaysuncomfortable Jul 10, 2026
00124bc
fix(facade): F6 review — get_unspent pulls full set when excluding no…
iamalwaysuncomfortable Jul 10, 2026
134c37f
feat(facade): F7 — AsyncAleo async facade (shared orchestration, asyn…
iamalwaysuncomfortable Jul 10, 2026
4f156bb
refactor(facade): F7 review — share OwnedFilter builder across sync/a…
iamalwaysuncomfortable Jul 10, 2026
017daad
docs(facade): lead with Aleo(provider) verb ladder + delegate; runnab…
iamalwaysuncomfortable Jul 10, 2026
1a4f887
docs(facade): F8 review — example uses facade aleo.account.sign_value…
iamalwaysuncomfortable Jul 10, 2026
d5579ab
fix(facade): final-review — async parity (abi/decode/imports/mappings…
iamalwaysuncomfortable Jul 10, 2026
edf3518
fix(facade): import aleo without testnet extension; export facade nam…
iamalwaysuncomfortable Jul 10, 2026
94917c6
test(testnet): importorskip _aleolib_testnet so mainnet-only lanes sk…
iamalwaysuncomfortable Jul 10, 2026
ce75632
ci: build both networks + run testnet tests in proving and aleo-abi l…
iamalwaysuncomfortable Jul 10, 2026
b9dfe7c
ci(wheels): chown container-written dist before testnet merge (fixes …
iamalwaysuncomfortable Jul 10, 2026
c5162a7
feat(testing): LocalRecordScanner + devnode e2e (transact + public/pr…
iamalwaysuncomfortable Jul 10, 2026
1b63698
test(e2e): live testnet delegate + hosted record scanner (env-gated A…
iamalwaysuncomfortable Jul 10, 2026
325ebc7
docs(facade): verb-ladder + async how-to; testing utilities (Devnode,…
iamalwaysuncomfortable Jul 10, 2026
ac41c86
feat(facade): base_fee override on build_transaction/transact; delega…
iamalwaysuncomfortable Jul 10, 2026
5dc332c
test(e2e): live testnet private roundtrip (delegated prover + hosted …
iamalwaysuncomfortable Jul 10, 2026
e1458e8
docs: live private-transfer usage (delegated proving + record discovery)
iamalwaysuncomfortable Jul 10, 2026
f9e7494
fix(scanner): make RecordScanner + compute_uuid network-aware (testne…
iamalwaysuncomfortable Jul 10, 2026
bd13ea5
fix(network-client): network-aware Program/Plaintext/Transaction pars…
iamalwaysuncomfortable Jul 10, 2026
1558549
docs(agents): DPS/JWT + proving endpoints live on the explorer /v2; r…
iamalwaysuncomfortable Jul 10, 2026
cd10998
test(e2e): live testnet object suite (network client + facade parsing…
iamalwaysuncomfortable Jul 10, 2026
1228589
ci: keep live (real-API) tests out of CI — standalone `live` marker, …
iamalwaysuncomfortable Jul 10, 2026
a404ac7
revert: drop consensus-heights binding + LocalRecordScanner
iamalwaysuncomfortable Jul 10, 2026
70b4713
Fix DPS prover cookie affinity: rely on the session/client cookie jar
iamalwaysuncomfortable Jul 10, 2026
c9e3dce
Harden live DPS e2e: real endpoint, self-transfer, required prover host
iamalwaysuncomfortable Jul 10, 2026
a2d8b6f
Fix DPS prover base URL: {origin}/prove/{network}
iamalwaysuncomfortable Jul 11, 2026
193b61a
Derive hosted scanner from endpoint origin; unify DPS+scanner creds; …
iamalwaysuncomfortable Jul 11, 2026
4522a69
Rename _scanner_base → scanner_base to satisfy pyright
iamalwaysuncomfortable Jul 11, 2026
88faf16
Self-heal DPS/scanner JWT on 401 (shared-consumer session rotation)
iamalwaysuncomfortable Jul 11, 2026
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
74 changes: 74 additions & 0 deletions .agents/voice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Documentation Voice — Aleo Python SDK

How docstrings and prose read in this repo. The rules bind; the examples show
them in practice. This is a **Python** SDK — docstrings, not JSDoc; types live
in annotations, not in the prose.

## Docstrings

Lead with a present-tense verb, give one or two sentences of context (including
side effects — network, fee, local-only), and describe each argument, return
value, and raised error **by its consequence**. Do not restate types the
signature already carries.

### Good

```python
def transfer_public(self, recipient: str, amount: int) -> BoundCall:
"""Build a public ``credits.aleo`` transfer call.

Returns an unexecuted call — nothing touches the network until you invoke a
verb (``.simulate``, ``.build_transaction``, ``.transact``, ``.delegate``).

Args:
recipient: The destination address (``aleo1…``).
amount: Microcredits to send; coerced to ``u64`` from the program's
declared input type.

Returns:
A bound call; ``.transact(account)`` builds, proves, and broadcasts it.

Raises:
ProgramNotFound: If ``credits.aleo`` is not loaded in the process.
"""
```

Why it works: starts with "Build"; the second sentence names the key behavior
(nothing runs until a verb — the footgun-preventing fact); each arg is described
by what it *means*, not its type; `Raises` names the concrete failure a caller
would catch.

### Bad

```python
def transfer_public(self, recipient, amount):
"""This function allows you to easily and powerfully build a transfer in a
seamless way. It takes a recipient and an amount and returns a result.

:param recipient: the recipient (string)
:param amount: the amount (int)
:return: the result
"""
```

Why it fails: filler ("easily", "powerfully", "seamless"); restates types; says
"a result" instead of what you get and what you do with it; no side-effect or
error information.

## Naming in prose and examples

- Use the Pythonic surface: properties (`key.address`, not `key.address()`),
`str(x)`/`bytes(x)` not `.to_string()`/`.to_bytes()`, `PrivateKey.random()`.
- Prefer the named algebraic method in prose where it reads clearer
(`a.add(b)`), the operator in code examples (`a + b`) — both exist.
- Examples must run as written against the documented signature. Mark any
snippet that touches the network or downloads proving parameters.
- Say "microcredits" explicitly for `u64` fee/amount values; never leave the
unit implicit.

## Privacy stance

This is a privacy chain. Do not document or add affordances that link
signatures to signer addresses (no `recover`-style verb). When a feature shares
secret material with a service (e.g. delegated record scanning shares the view
key), state that tradeoff plainly and point to the self-hosted alternative.
106 changes: 106 additions & 0 deletions .github/scripts/merge_testnet_so.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Merge the testnet extension module into the mainnet aleo wheel.
maturin builds exactly one cdylib per invocation, named by pyproject
[tool.maturin] module-name. To ship a single `aleo` wheel containing BOTH
`aleo/_aleolib_mainnet*.so` and `aleo/_aleolib_testnet*.so`, we build two wheels
(mainnet, then testnet with module-name overridden) and splice the testnet .so
into the mainnet wheel here, regenerating the RECORD so the wheel stays valid.
Usage:
merge_testnet_so.py <mainnet_dist_dir> <testnet_dist_dir>
The mainnet wheel in <mainnet_dist_dir> is rewritten in place (repacked).
Cross-platform (pure stdlib zipfile) so it also runs on the Windows CI runner.
"""
from __future__ import annotations

import base64
import csv
import hashlib
import io
import os
import sys
import zipfile


def _find_wheel(dist_dir: str) -> str:
wheels = [f for f in os.listdir(dist_dir) if f.endswith(".whl")]
if len(wheels) != 1:
raise SystemExit(
f"expected exactly one .whl in {dist_dir}, found: {wheels}"
)
return os.path.join(dist_dir, wheels[0])


def _find_testnet_so(wheel_path: str) -> tuple[str, bytes]:
with zipfile.ZipFile(wheel_path) as z:
for name in z.namelist():
base = name.rsplit("/", 1)[-1]
if base.startswith("_aleolib_testnet") and (
base.endswith(".so") or base.endswith(".pyd") or base.endswith(".dylib")
):
return name, z.read(name)
raise SystemExit(f"no _aleolib_testnet extension module found in {wheel_path}")


def _record_line(arcname: str, data: bytes) -> list[str]:
digest = hashlib.sha256(data).digest()
b64 = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
return [arcname, f"sha256={b64}", str(len(data))]


def merge(mainnet_dist: str, testnet_dist: str) -> None:
mainnet_wheel = _find_wheel(mainnet_dist)
testnet_wheel = _find_wheel(testnet_dist)

so_name, so_data = _find_testnet_so(testnet_wheel)
# Place the testnet .so alongside the mainnet one: aleo/<basename>.
so_basename = so_name.rsplit("/", 1)[-1]
target_arcname = f"aleo/{so_basename}"

with zipfile.ZipFile(mainnet_wheel) as z:
names = z.namelist()
contents = {n: z.read(n) for n in names}
infos = {n: z.getinfo(n) for n in names}

if target_arcname in contents:
print(f"note: {target_arcname} already present; overwriting")

# Locate RECORD.
record_name = next((n for n in names if n.endswith(".dist-info/RECORD")), None)
if record_name is None:
raise SystemExit("no RECORD file found in mainnet wheel")

# Parse existing RECORD, then rebuild it with the new .so line.
record_text = contents[record_name].decode("utf-8")
rows = list(csv.reader(io.StringIO(record_text)))
# Drop any prior entry for the target and the RECORD self-row (rewritten last).
rows = [r for r in rows if r and r[0] not in (target_arcname, record_name)]
rows.append(_record_line(target_arcname, so_data))
# RECORD's own row carries empty hash/size per the wheel spec.
rows.append([record_name, "", ""])

new_record = io.StringIO()
writer = csv.writer(new_record, lineterminator="\n")
writer.writerows(rows)
new_record_bytes = new_record.getvalue().encode("utf-8")

# Repack: rewrite the wheel with the added .so and updated RECORD.
tmp = mainnet_wheel + ".tmp"
with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z:
for n in names:
if n == record_name:
continue # written last
z.writestr(infos[n], contents[n])
z.writestr(target_arcname, so_data)
z.writestr(record_name, new_record_bytes)

os.replace(tmp, mainnet_wheel)
print(f"merged {target_arcname} into {os.path.basename(mainnet_wheel)}")


if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit(__doc__)
merge(sys.argv[1], sys.argv[2])
57 changes: 50 additions & 7 deletions .github/workflows/sdk-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build wheel
# Build the mainnet extension module wheel.
- name: Build mainnet wheel
uses: PyO3/maturin-action@v1
with:
# Run from sdk/ so cargo picks up sdk/.cargo/config.toml (macOS
Expand All @@ -78,18 +79,60 @@ jobs:
elif command -v yum &> /dev/null; then
yum install -y git cmake3 clang && ln -sf "$(command -v cmake3)" /usr/local/bin/cmake
fi
# maturin emits one cdylib per invocation (named by pyproject
# module-name). Build a second wheel for the testnet extension module with
# module-name overridden to aleo._aleolib_testnet.
- name: Override module-name for testnet build
shell: bash
run: |
cp sdk/pyproject.toml sdk/pyproject.toml.mainnet.bak
sed -i.bak 's/module-name = "aleo._aleolib_mainnet"/module-name = "aleo._aleolib_testnet"/' sdk/pyproject.toml
- name: Build testnet wheel
uses: PyO3/maturin-action@v1
with:
working-directory: ./sdk
target: ${{ matrix.platform.target }}
args: --release --out dist-testnet --no-default-features --features testnet
sccache: 'true'
manylinux: ${{ startsWith(matrix.platform.name, 'linux') && '2_28' || 'auto' }}
before-script-linux: |
if command -v dnf &> /dev/null; then
dnf install -y git cmake clang
elif command -v yum &> /dev/null; then
yum install -y git cmake3 clang && ln -sf "$(command -v cmake3)" /usr/local/bin/cmake
fi
# Splice the testnet .so into the mainnet wheel -> one two-network wheel.
- name: Merge testnet module into wheel
shell: bash
run: |
mv sdk/pyproject.toml.mainnet.bak sdk/pyproject.toml
# The manylinux build runs in a Docker container that writes sdk/dist
# and sdk/dist-testnet as root; this step runs on the host as the
# runner user, so reclaim ownership before repacking the wheel
# (Linux only — native macOS/Windows builds are already runner-owned).
if [ "$RUNNER_OS" = "Linux" ]; then
sudo chown -R "$(id -u):$(id -g)" sdk/dist sdk/dist-testnet
fi
python .github/scripts/merge_testnet_so.py sdk/dist sdk/dist-testnet
# Every runner here matches its wheel's platform, so install and smoke
# test the actual artifact before it can ship.
# test the actual artifact — importing BOTH networks — before it ships.
- name: Smoke test wheel
shell: bash
run: |
pip install --find-links sdk/dist aleo
python -c "
from aleo.mainnet import PrivateKey
pk = PrivateKey.random()
assert str(pk).startswith('APrivateKey1')
assert str(pk.address).startswith('aleo1')
print('wheel ok:', pk.address)
from aleo.mainnet import PrivateKey as MainnetPrivateKey
from aleo.testnet import PrivateKey as TestnetPrivateKey
import aleo.mainnet as mainnet
import aleo.testnet as testnet
mpk = MainnetPrivateKey.random()
tpk = TestnetPrivateKey.random()
assert str(mpk).startswith('APrivateKey1')
assert str(mpk.address).startswith('aleo1')
assert str(tpk.address).startswith('aleo1')
assert mainnet.Network.id() == 0
assert testnet.Network.id() == 1
print('two-network wheel ok:', mpk.address, tpk.address)
"
- name: Upload wheel
uses: actions/upload-artifact@v4
Expand Down
41 changes: 37 additions & 4 deletions .github/workflows/sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,25 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Build wheel
# Build BOTH network extension modules so aleo.mainnet AND aleo.testnet
# resolve. maturin emits one cdylib per invocation (named by pyproject
# module-name), so we build mainnet, then build testnet with the
# module-name overridden and merge its .so into the aleo package.
- name: Build both-network wheel
run: |
pip install maturin
maturin build --release --features mainnet --out dist
sed -i.bak 's/module-name = "aleo._aleolib_mainnet"/module-name = "aleo._aleolib_testnet"/' pyproject.toml
maturin build --release --no-default-features --features testnet --out dist-testnet
mv pyproject.toml.bak pyproject.toml
python ../.github/scripts/merge_testnet_so.py dist dist-testnet
- name: Install wheel + test deps
run: |
pip install --find-links dist aleo
pip install pytest pytest-xdist httpx pynacl responses pytest-asyncio
# Fast, offline tests; -n auto parallelizes across cores.
- name: pytest (fast suite)
run: python -m pytest python/tests -v -n auto -m "not slow"
run: python -m pytest python/tests -v -n auto -m "not slow and not live and not devnode"

test-proving:
name: Test (proving, network)
Expand All @@ -95,10 +103,17 @@ jobs:
with:
path: ~/.aleo
key: aleo-parameters-${{ runner.os }}-v1
- name: Build wheel
# Build BOTH network extensions so the testnet surface is exercised here
# too (not just in the OS-matrix lane). Deps are cached, so the second
# feature build is a warm recompile, not a cold one.
- name: Build both-network wheel
run: |
pip install maturin
maturin build --release --features mainnet --out dist
sed -i.bak 's/module-name = "aleo._aleolib_mainnet"/module-name = "aleo._aleolib_testnet"/' pyproject.toml
maturin build --release --no-default-features --features testnet --out dist-testnet
mv pyproject.toml.bak pyproject.toml
python ../.github/scripts/merge_testnet_so.py dist dist-testnet
- name: Install wheel + test deps
run: |
pip install --find-links dist aleo
Expand All @@ -107,6 +122,11 @@ jobs:
# would prove twice, so they run in a single worker on purpose.
- name: pytest (proving suite)
run: python -m pytest python/tests -v -m slow
# test_testnet.py is not marked slow, so `-m slow` deselects it; run the
# testnet surface/ID/derivation tests explicitly against the two-network
# wheel built above.
- name: pytest (testnet surface + KATs)
run: python -m pytest python/tests/test_testnet.py -v

test-abi:
name: Test (aleo-abi package)
Expand Down Expand Up @@ -134,10 +154,18 @@ jobs:
echo "::error::sdk/Cargo.lock references dev_skip_checks — leo build graph leaked into the main crate"
exit 1
fi
- name: Build both wheels
# Build the aleo two-network wheel (mainnet + testnet merged) plus the
# aleo-abi wheel. The testnet .so must be spliced into the aleo wheel
# BEFORE the aleo-abi wheel lands in dist, so the merge sees only the
# aleo wheel.
- name: Build wheels (aleo two-network + aleo-abi)
run: |
pip install maturin pytest requests
maturin build --release --features mainnet --manifest-path sdk/Cargo.toml --out dist
sed -i.bak 's/module-name = "aleo._aleolib_mainnet"/module-name = "aleo._aleolib_testnet"/' sdk/pyproject.toml
maturin build --release --no-default-features --features testnet --manifest-path sdk/Cargo.toml --out dist-testnet
mv sdk/pyproject.toml.bak sdk/pyproject.toml
python .github/scripts/merge_testnet_so.py dist dist-testnet
maturin build --release --manifest-path sdk-abi/Cargo.toml --out dist
- name: Install wheels
run: |
Expand All @@ -153,3 +181,8 @@ jobs:
run: |
cd sdk
python -m pytest python/tests/test_abi_hook.py -v
# Exercise the testnet extension in this lane too (it's now built above).
- name: pytest (testnet surface + KATs)
run: |
cd sdk
python -m pytest python/tests/test_testnet.py -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ tmp/

*.pkl
.idea/
.claude/worktrees/
Loading
Loading