Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fb3ad9c
feat(codegen): fixed-length Array ABI type support
iamalwaysuncomfortable Jul 28, 2026
3d5fbd5
feat(shield-swap): regenerate wire layer from deployed shield_swap.aleo
iamalwaysuncomfortable Jul 28, 2026
af1aaca
feat(shield-swap): Q128.128 tick math + U256 helpers
iamalwaysuncomfortable Jul 28, 2026
c357657
feat(shield-swap): raw-amount swap resolution, X128 limits, freezelis…
iamalwaysuncomfortable Jul 28, 2026
beef4b1
feat(shield-swap): X128 slot price, new default program for derivations
iamalwaysuncomfortable Jul 28, 2026
dec769c
feat(shield-swap): new-stack write verbs — U256 prices, proofs, withd…
iamalwaysuncomfortable Jul 28, 2026
b073bae
feat(shield-swap): automatic router dispatch for wrapped assets
iamalwaysuncomfortable Jul 28, 2026
755e086
feat(shield-swap): mirror new-stack verbs and routing in async client
iamalwaysuncomfortable Jul 28, 2026
94fb73c
feat(shield-swap): cut API layer over to the staging Shield Swap API
iamalwaysuncomfortable Jul 28, 2026
adc1700
docs(shield-swap): re-point docs, fixtures, and helpers at the new stack
iamalwaysuncomfortable Jul 28, 2026
6f3b03b
test(shield-swap): devnode + live cutover coverage for the new stack
iamalwaysuncomfortable Jul 28, 2026
7b2a234
fix(codegen): satisfy strict pyright on Array support
iamalwaysuncomfortable Jul 28, 2026
a646c55
fix(shield-swap): pin mcp extra below 2.0 pending API port
iamalwaysuncomfortable Jul 28, 2026
c18548b
test(shield-swap): pin router ABIs as drift guards with input-count p…
iamalwaysuncomfortable Jul 28, 2026
5cf449f
fix(shield-swap): adapt auth to staging session model
iamalwaysuncomfortable Jul 28, 2026
8cc4982
fix(shield-swap): accept referral codes in redeem_code
iamalwaysuncomfortable Jul 28, 2026
ef126b2
feat(shield-swap): pasted invites are referral codes; access codes se…
iamalwaysuncomfortable Jul 28, 2026
4e4dc0d
fix(shield-swap): cookie session outranks bearer; onboarding idempote…
iamalwaysuncomfortable Jul 28, 2026
073bec1
chore(shield-swap): rehearsal takes pasted referral codes, trades hel…
iamalwaysuncomfortable Jul 28, 2026
aa71c33
fix(shield-swap): mint hints walk the on-chain tick list
iamalwaysuncomfortable Jul 28, 2026
9fbec68
chore: bump aleo-sdk, aleo-contract-abi-generator, shield-swap-sdk to…
iamalwaysuncomfortable Jul 28, 2026
18730d2
chore: bump versions to 0.3.1
iamalwaysuncomfortable Jul 29, 2026
bc96c42
docs: voice.md docstrings for the undocumented public surface (#65)
iamalwaysuncomfortable Jul 30, 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
38 changes: 37 additions & 1 deletion .agents/voice.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ 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.

### Phrasings to avoid

Beyond the filler above, these are banned outright. Each one reads like guidance
while leaving the caller with nothing to act on — replace it with the concrete
instruction, or delete it.

- **"reach for"** — never use it. Say what to call and when.
- **"defensively"**, **"as appropriate"**, **"where necessary"** — name the
actual condition instead.

```python
# Bad — sounds like advice, gives none
"""… either may be ``None``, so reach for them defensively."""

# Good — states the check and what it protects
"""… treat both as optional — guard with ``if entry.token0_info`` before
reading a field rather than assuming a default."""
```

### Plain verbs

Say what the code does, not what it is like. A service or object does not
"speak", "know", "want", or "see" — it returns, accepts, requires, stores,
decrypts. Figurative verbs read as style and cost the reader a translation step.

```python
# Bad — figurative
"""The API speaks decimal amounts; the contract speaks base units."""
"""Every pool the indexer knows."""

# Good — plain
"""The API returns decimal amounts; the contract takes base units."""
"""Every pool the DEX lists."""
```

## Naming in prose and examples

- Use the Pythonic surface: properties (`key.address`, not `key.address()`),
Expand All @@ -71,4 +106,5 @@ error information.
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.
key), state that tradeoff plainly. Where an alternative exists, name the
supported extension point (e.g. assigning a custom ``RecordProvider``).
4 changes: 2 additions & 2 deletions .claude/skills/shield-swap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use when the user wants to trade, LP, or build on the shield_swap A

Read `shield-swap-sdk/AGENTS.md` (generated from the SDK — always current)
and follow its Tier 1 lifecycle and conversation pattern. Write Python
against the SDK; don't re-implement flows the verbs already provide, and
against the SDK; don't re-implement flows the methods already provide, and
don't read SDK source unless AGENTS.md genuinely lacks the answer.
Preconditions are enforced in code — on error, read the exception message;
it names the verb that fixes it.
it names the method that fixes it.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ credits.functions.transfer_private(record, str(recipient.address), 1) \
.delegate(account)
```

`aleo.record_provider` is swappable: set it to your own object implementing the `RecordProvider` protocol (`get_unspent_credits_record` / `find`) — e.g. a self-hosted scanner — and the whole facade (including private-fee auto-sourcing) uses it, with no view-key sharing.
`aleo.record_provider` is swappable: set it to your own object implementing the `RecordProvider` protocol (`get_unspent_credits_record` / `find`), and the whole facade (including private-fee auto-sourcing) uses it, with no view-key sharing.

## Async (`AsyncAleo`)

Expand Down
2 changes: 1 addition & 1 deletion sdk-abi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk-abi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

[package]
name = "aleo-abi"
version = "0.2.2"
version = "0.3.1"
edition = "2024"
license = "GPL-3.0-or-later"
description = "Python bindings for ABI generation from Aleo bytecode (via Leo's leo-abi crate)"
Expand Down
5 changes: 3 additions & 2 deletions sdk-abi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ dependents:

```python
abi_json = aleo_abi.generate_abi(
"shield_swap_v3.aleo", amm_bytecode, "testnet",
[("test_shield_swap_multisig_core.aleo", multisig_bytecode)],
"shield_swap.aleo", amm_bytecode, "testnet",
[("shield_swap_multisig_core.aleo", multisig_bytecode),
("shield_swap_freezelist.aleo", freezelist_bytecode)],
)
```

Expand Down
2 changes: 1 addition & 1 deletion sdk-abi/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "aleo-contract-abi-generator"
version = "0.2.2"
version = "0.3.1"
description = "Python bindings for ABI generation from Aleo bytecode"
readme = "README.md"
license = {text = "GPL-3.0-or-later"}
Expand Down
2 changes: 1 addition & 1 deletion sdk/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "aleo"
authors = ["Konstantin Pandl", "Mike Turner", "Roman Proskuryakov"]
version = "0.2.2"
version = "0.3.1"
description = "A Python sdk for zero-knowledge cryptography based on Aleo"
edition = "2021"
license = "GPL-3.0-or-later"
Expand Down
2 changes: 1 addition & 1 deletion sdk/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ credits.functions.transfer_private(record, str(recipient.address), 1) \
.delegate(account)
```

`aleo.record_provider` is swappable: set it to your own object implementing the `RecordProvider` protocol (`get_unspent_credits_record` / `find`) — e.g. a self-hosted scanner — and the whole facade (including private-fee auto-sourcing) uses it, with no view-key sharing.
`aleo.record_provider` is swappable: set it to your own object implementing the `RecordProvider` protocol (`get_unspent_credits_record` / `find`), and the whole facade (including private-fee auto-sourcing) uses it, with no view-key sharing.

## Async (`AsyncAleo`)

Expand Down
4 changes: 2 additions & 2 deletions sdk/benchmarks/bench_proving.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

* ``credits.aleo/transfer_public``
* ``credits.aleo/transfer_private`` (spends a real credits record)
* ``shield_swap_v3.aleo/swap`` (spends a token record, blinded output)
* ``shield_swap_v3.aleo/mint`` (spends two token records)
* ``shield_swap.aleo/swap`` (spends a token record, blinded output)
* ``shield_swap.aleo/mint`` (spends two token records)

The first prove of each function synthesizes its proving key (and may
download SNARK parameters into ``~/.aleo/resources``) — that iteration is
Expand Down
2 changes: 1 addition & 1 deletion sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "aleo-sdk"
description = "Python SDK for building zero-knowledge apps and DeFi on the Aleo network"
version = "0.2.2"
version = "0.3.1"
readme = "Readme.md"
license = {file = "LICENSE.md"}
authors = [
Expand Down
44 changes: 44 additions & 0 deletions sdk/python/aleo/_client_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ def is_provable_host(url: str) -> bool:


def package_version() -> str:
"""Return the installed SDK version, for the telemetry headers.

Checks the current distribution name first, then the pre-0.2 name, so older
installs still report their real version.

Returns:
The version string, or ``"0.0.0"`` when the package metadata cannot be
read (a source tree that was never installed).
"""
from importlib.metadata import version

# "aleo" is the pre-0.2 distribution name; keep it as a fallback so
Expand All @@ -78,6 +87,13 @@ def user_agent() -> str:


def make_default_headers() -> dict[str, str]:
"""Build the SDK's baseline telemetry headers for a new client.

Returns:
The version and environment headers every default-transport request
carries. Callers who pass their own ``headers`` replace these outright,
and a custom transport strips them — see :func:`method_headers`.
"""
return {
"X-Aleo-SDK-Version": package_version(),
"X-Aleo-environment": "python",
Expand All @@ -94,6 +110,19 @@ def method_headers(
method: str,
has_custom_transport: bool,
) -> dict[str, str]:
"""Build the headers for one request, honouring custom-transport mode.

Args:
headers: The client's configured headers.
method: SDK method name, sent as ``X-ALEO-METHOD`` so calls can be
attributed server-side.
has_custom_transport: True when the caller owns the HTTP layer, in which
case every SDK-internal header — including the method name and
``User-Agent`` — is withheld so the caller controls the wire format.

Returns:
The headers to send.
"""
if has_custom_transport:
return user_headers(headers)
return {**headers, "X-ALEO-METHOD": method, "User-Agent": user_agent()}
Expand All @@ -106,6 +135,11 @@ def jwt_origin(host: str) -> str:


def now_ms() -> int:
"""Return the current wall-clock time in epoch milliseconds.

Matches the unit JWT expiries are stored in, so the two compare directly.
Wall-clock, not monotonic — do not use it to measure elapsed time.
"""
return int(time.time() * 1000)


Expand All @@ -118,6 +152,16 @@ def jwt_expired(jwt_data: dict[str, Any]) -> bool:


def validate_block_range(start: int, end: int) -> None:
"""Reject a block range the node would refuse, before sending it.

Args:
start: First height of the range.
end: Last height of the range.

Raises:
ValueError: If *start* is negative, exceeds *end*, or the span is wider
than the 50-block per-request cap.
"""
if start < 0:
raise ValueError("start must be >= 0")
if start > end:
Expand Down
96 changes: 82 additions & 14 deletions sdk/python/aleo/_facade_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
"""
from __future__ import annotations

from typing import Any, Protocol, runtime_checkable
from decimal import Decimal, ROUND_DOWN
from typing import Any, Protocol, Union, runtime_checkable

_MICROCREDITS_PER_CREDIT: int = 1_000_000

#: What a credits amount may be written as. ``str`` and ``Decimal`` are exact;
#: ``float`` is not (see :func:`credits_to_microcredits`).
CreditsAmount = Union[str, int, Decimal, float]


@runtime_checkable
class RecordProvider(Protocol):
Expand All @@ -17,9 +22,8 @@ class RecordProvider(Protocol):
The default implementation is :class:`~aleo.facade.records.RecordsModule`
(``aleo.records``), which wraps a delegated :class:`~aleo.record_scanner.RecordScanner`.
Any object that satisfies this Protocol can be assigned to
``aleo.record_provider`` — e.g. a self-hosted scanner wrapper — so callers
who do not want to share their view key with a hosted scanning service can
plug in their own source of records.
``aleo.record_provider``, so callers who do not want to share their view key
with a hosted scanning service can plug in their own source of records.

Implementations are consumed by :meth:`~aleo.facade.call.BoundCall.build_transaction`
(and the rest of the verb ladder) to auto-source a credits record for a
Expand All @@ -46,27 +50,91 @@ def find(self, **filters: Any) -> list[Any]:
...


def credits_to_microcredits(credits: float | int) -> int:
"""Convert a credits amount (float or int) to integer microcredits.
def _to_decimal(credits: CreditsAmount) -> Decimal:
"""Interpret a credits amount exactly.

A ``float`` has already lost the value the caller wrote — ``1.005`` is really
``1.00499999999999989...`` — so it is routed through ``str()``, which yields
the shortest representation that round-trips, i.e. the literal as typed.
``str``, ``int``, and ``Decimal`` are exact already.
"""
if isinstance(credits, Decimal):
return credits
if isinstance(credits, int):
return Decimal(credits)
return Decimal(str(credits))


def credits_to_microcredits(
credits: CreditsAmount, *, allow_rounding: bool = False
) -> int:
"""Convert a credits amount to integer microcredits, exactly.

Computed in decimal, never binary floating point: ``1.005`` credits is
1_005_000 microcredits, where a float multiply would truncate to 1_004_999
and silently lose value. Pass *credits* as ``str`` or ``Decimal`` for full
exactness; a ``float`` is interpreted as the decimal literal it prints as.

Args:
credits: Credits amount — ``"1.005"``, ``Decimal("1.005")``, ``1``, or
``1.005``.
allow_rounding: Permit input finer than one microcredit, truncating
toward zero. Off by default so lost precision is an error rather
than a silent underpayment.

Returns:
The amount in microcredits.

Raises:
ValueError: If *credits* carries sub-microcredit precision and
*allow_rounding* is False, or is not a usable number.

Examples
--------
>>> credits_to_microcredits(1)
1000000
>>> credits_to_microcredits(1.5)
1500000
>>> credits_to_microcredits("1.005")
1005000
"""
return int(credits * _MICROCREDITS_PER_CREDIT)


def microcredits_to_credits(microcredits: int) -> float:
"""Convert an integer microcredits amount to a credits float.
try:
scaled = _to_decimal(credits) * _MICROCREDITS_PER_CREDIT
except (ArithmeticError, ValueError, TypeError) as exc:
raise ValueError(f"{credits!r} is not a valid credits amount") from exc
if not scaled.is_finite():
raise ValueError(f"{credits!r} is not a finite credits amount")
rounded = scaled.to_integral_value(rounding=ROUND_DOWN)
if scaled != rounded and not allow_rounding:
raise ValueError(
f"{credits!r} credits is {scaled} microcredits — finer than the "
"chain's smallest unit. Round it yourself, or pass "
"allow_rounding=True to truncate toward zero."
)
return int(rounded)


def microcredits_to_credits(microcredits: int) -> Decimal:
"""Convert integer microcredits to credits, exactly.

Returns a :class:`~decimal.Decimal` rather than a float: microcredits are a
``u64``, and past 2**53 a float cannot even hold the integer, so a float
round-trip is not the identity. Compares equal to the obvious float
(``microcredits_to_credits(1_500_000) == 1.5``), but mixing it into float
arithmetic raises — convert deliberately with ``float(...)`` if you want
that, accepting the precision loss.

Args:
microcredits: Amount in microcredits.

Returns:
The amount in credits, exact at any ``u64`` magnitude.

Examples
--------
>>> microcredits_to_credits(1_000_000)
1.0
Decimal('1.000000')
>>> microcredits_to_credits(1_500_000)
1.5
Decimal('1.500000')
"""
return microcredits / _MICROCREDITS_PER_CREDIT
return Decimal(int(microcredits)).scaleb(-6)
Loading
Loading