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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ tmp/
.superpowers/
docs/superpowers/
.worktrees/
# Shield Swap journals (claim secrets) written by the examples when run from the repo root
.shield-swap/
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,83 @@ already held by the application, or assign a custom `RecordProvider` to
`aleo.record_provider`. Choose local proving separately if transaction contents
must also stay out of the proving service.

## Manage private records

A private balance is a set of records, and one transaction spends whole
records. A large payment needs one record that covers it; several payments
submitted at the same time need several records, one each. `aleo.inventory`
reads an asset's spendable records, plans the joins and splits that reach a
chosen layout, executes that plan one confirmed transaction at a time, and
keeps writers that share a journal from spending the same record.

Build the client with record management enabled and the two halves of a
write each do their part of the journal work, the way snarkVM separates
building a transaction from broadcasting it: `build_transaction` reserves the
call's record inputs before proving and saves the proved transaction, and
`submit_transaction` records the broadcast. `transact` is the two in sequence;
`delegate` does the same through the prover. A built transaction that was never
sent is released with `result.discard()`; once a broadcast has been attempted,
even one that timed out, the reservation stays until
`reconcile_record_inventory()` settles it. The example below reshapes the
account's private credits into four equal records of at least one credit each:

```python
from aleo import Aleo, HTTPProvider
from aleo.inventory import InventoryTarget, RecordAsset, RecordManagementConfig

aleo = Aleo(HTTPProvider("https://edge.provable.com/api", network="testnet"),
record_management=RecordManagementConfig(enabled=True))
aleo.default_account = account
aleo.records.register(account) # The scanner can decrypt the account's records.

credits = RecordAsset("credits.aleo")
snapshot = aleo.inventory.get_record_inventory(credits)
print(snapshot.balance, len(snapshot.available), len(snapshot.reserved))

plan = aleo.inventory.plan_record_inventory(
credits, InventoryTarget(records=4, min_record_amount=1_000_000, distribution="balanced"))
print(plan.deduction) # Microcredits the native splits remove; network fees are separate.
result = aleo.inventory.rebalance_record_inventory(plan, max_fee_microcredits=1_000_000)
```

Planning reads the scanner and the deployed program and submits nothing.
Execution requires record management to be enabled. Each step reserves its
inputs, proves, broadcasts, waits for confirmation, and waits until the scanner
shows the exact outputs before the next step spends them. No accepted step is
rolled back. An `interrupted` result names the reason and every submitted
transaction. After an interruption, reconcile, rescan, and plan again; the old
plan describes records that the completed steps have already spent.

More than two records join through deployed helper programs in one
transaction where one exists (up to 16 credits or dedicated stablecoin records,
15 for other ARC-20 and ARC-22 tokens). `join_records`, `split_record`, and
`auto_join` perform single operations without a plan.

The journal is kept by a store chosen with `RecordManagementConfig(store=...)`.
Three ship with the SDK; the choice decides what survives a restart and which
processes coordinate.

| Store | Survives a restart | Coordinates | Choose it when |
| --- | --- | --- | --- |
| `MemoryRecordInventoryStore` (default) | No | Threads in one process | A script runs to completion and nothing else trades for the account. |
| `JsonFileRecordInventoryStore(path)` | Yes | Processes on one machine | The journal should be readable in an editor; one JSON file under a file lock. |
| `SqliteRecordInventoryStore(path)` | Yes | Processes on one machine | A long-running service, or a journal shared with a Veil client; same layout as Veil's store. |

Both file-backed stores release a reservation left by an SDK process on the
same machine that exited before it proved anything; an entry a Veil wallet
client wrote is kept, since the wallet may have broadcast before its answer
arrived. Any object with `list`,
`acquire`, and `update` methods works as a store for other backends, such as a
database shared across machines; `acquire` must reserve all of an entry's
records or none. After an interruption, `reconcile_record_inventory()` checks
each pending entry against the chain and, with `rebroadcast=True`, resubmits a
saved proved transaction with its original id. A write that finds its record
already reserved raises `RecordReservationConflictError` before any proof is
built, so the caller can choose another record.

The [Shield Swap guide](shield-swap-sdk/README.md#concurrent-swaps) shows the
same mechanism preparing records for several trades submitted at once.

## Bridge assets

Bring assets from Ethereum or Solana to Aleo for use in Aleo applications, or
Expand Down
2 changes: 1 addition & 1 deletion bridge-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "aleo-bridge-sdk"
version = "0.7.0"
version = "0.8.0"
description = "Bridge Aleo, Ethereum, Arc, Base, Arbitrum and Solana assets with Hyperlane, Circle xReserve and CCTP"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion bridge-sdk/python/aleo_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from pathlib import Path

__version__ = "0.7.0"
__version__ = "0.8.0"
_ALEO_SDK_MINIMUM = "0.7.0"


Expand Down
2 changes: 1 addition & 1 deletion bridge-sdk/tests/test_client_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ def test_public_exports_pin_pre_existing_set_and_add_lifecycle_names():
assert hasattr(aleo_bridge, name), name
assert set(PRE_EXISTING_EXPORTS) <= set(aleo_bridge.__all__)
assert set(NEW_EXPORTS) <= set(aleo_bridge.__all__)
assert aleo_bridge.__version__ == "0.7.0"
assert aleo_bridge.__version__ == "0.8.0"
assert aleo_bridge.lifecycle is lifecycle
assert aleo_bridge.prepare is lifecycle.prepare
assert aleo_bridge.bridge_tools is agent.bridge_tools
Expand Down
4 changes: 2 additions & 2 deletions bridge-sdk/tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

def test_version_is_pinned_in_lockstep():
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
assert pyproject["project"]["version"] == "0.7.0" == aleo_bridge.__version__
assert pyproject["project"]["version"] == "0.8.0" == aleo_bridge.__version__
assert pyproject["project"]["name"] == "aleo-bridge-sdk"
deps = pyproject["project"]["dependencies"]
for base in ("aleo-sdk", "pynacl", "web3", "eth-account", "solders", "solana", "privy-client", "dynamic-wallet-sdk"):
Expand Down Expand Up @@ -105,7 +105,7 @@ def test_import_without_optional_extras(monkeypatch):
monkeypatch.delitem(sys.modules, name) # reverted at teardown — a fresh reimport below must not
# leak new module/class objects into tests that run after this one
pkg = importlib.import_module("aleo_bridge")
assert pkg.__version__ == "0.7.0"
assert pkg.__version__ == "0.8.0"
assert issubclass(pkg.RouteNotFoundError, pkg.BridgeError)


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.7.0"
version = "0.8.0"
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
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.7.0"
version = "0.8.0"
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.7.0"
version = "0.8.0"
description = "A Python sdk for zero-knowledge cryptography based on Aleo"
edition = "2021"
license = "GPL-3.0-or-later"
Expand Down
17 changes: 17 additions & 0 deletions sdk/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ credits.functions.transfer_private(record, str(recipient.address), 1) \

`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.

### Record inventory — join, split, plan, reserve, recover

One transaction spends whole records, so a large payment needs one record that covers it and several payments submitted together need one record each. `aleo.inventory` reads an asset's spendable records, plans the joins and splits that reach a target layout, and executes that plan one confirmed transaction at a time. Build the client with `record_management` enabled and the journal follows the two halves of a write: `build_transaction` reserves the record inputs before proving and saves the proved transaction, `submit_transaction` records the broadcast (`transact` is both in sequence, `delegate` does the same through the prover), and `result.discard()` releases a built transaction that was never sent (after a broadcast attempt the reservation stays until reconciliation). Writers sharing the same store never spend the same record.

```python
from aleo.inventory import InventoryTarget, RecordAsset, RecordManagementConfig

aleo = Aleo(HTTPProvider(ENDPOINT, network="testnet"),
record_management=RecordManagementConfig(enabled=True))
credits = RecordAsset("credits.aleo")
plan = aleo.inventory.plan_record_inventory(
credits, InventoryTarget(records=4, min_record_amount=1_000_000, distribution="balanced"))
result = aleo.inventory.rebalance_record_inventory(plan, max_fee_microcredits=1_000_000)
```

`get_record_inventory` reads; `plan_record_inventory` plans without signing; `join_records`, `split_record`, and `auto_join` run single operations; `reconcile_record_inventory` settles pending journal entries against the chain and can rebroadcast a saved proved transaction. The journal is in memory by default; `JsonFileRecordInventoryStore(path)` and `SqliteRecordInventoryStore(path)` keep it across restarts and coordinate processes on one machine, and any object with `list`, `acquire`, and `update` works for another backend. The top-level [README](../README.md#manage-private-records) compares the stores and explains the recovery steps.

## Async (`AsyncAleo`)

The async facade mirrors the sync surface. Construction and the local, CPU-bound steps stay synchronous; everything that touches the network is awaitable.
Expand Down
2 changes: 1 addition & 1 deletion sdk/docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
copyright = '2019-2026, Provable Inc.'
author = 'kpp'

version = '0.7.0'
version = '0.8.0'

# -- General configuration

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.7.0"
version = "0.8.0"
readme = "Readme.md"
license = {file = "LICENSE.md"}
authors = [
Expand Down
21 changes: 21 additions & 0 deletions sdk/python/aleo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,27 @@
ExecutionError as ExecutionError,
TransactionConfirmationTimeout as TransactionConfirmationTimeout,
)
from .inventory import (
AsyncRecordInventory as AsyncRecordInventory,
InventoryError as InventoryError,
InventoryPlan as InventoryPlan,
InventoryPlanError as InventoryPlanError,
InventoryRecord as InventoryRecord,
InventoryResult as InventoryResult,
InventoryTarget as InventoryTarget,
JsonFileRecordInventoryStore as JsonFileRecordInventoryStore,
MemoryRecordInventoryStore as MemoryRecordInventoryStore,
RecordAsset as RecordAsset,
RecordInventory as RecordInventory,
RecordInventorySnapshot as RecordInventorySnapshot,
RecordInventoryStore as RecordInventoryStore,
RecordJoinProgram as RecordJoinProgram,
RecordManagementConfig as RecordManagementConfig,
RecordReservationConflictError as RecordReservationConflictError,
RecordSpend as RecordSpend,
SqliteRecordInventoryStore as SqliteRecordInventoryStore,
TokenJoinRouter as TokenJoinRouter,
)


def __getattr__(name: str) -> object:
Expand Down
21 changes: 21 additions & 0 deletions sdk/python/aleo/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ from .facade.errors import (
ExecutionError as ExecutionError,
TransactionConfirmationTimeout as TransactionConfirmationTimeout,
)
from .inventory import (
AsyncRecordInventory as AsyncRecordInventory,
InventoryError as InventoryError,
InventoryPlan as InventoryPlan,
InventoryPlanError as InventoryPlanError,
InventoryRecord as InventoryRecord,
InventoryResult as InventoryResult,
InventoryTarget as InventoryTarget,
JsonFileRecordInventoryStore as JsonFileRecordInventoryStore,
MemoryRecordInventoryStore as MemoryRecordInventoryStore,
RecordAsset as RecordAsset,
RecordInventory as RecordInventory,
RecordInventorySnapshot as RecordInventorySnapshot,
RecordInventoryStore as RecordInventoryStore,
RecordJoinProgram as RecordJoinProgram,
RecordManagementConfig as RecordManagementConfig,
RecordReservationConflictError as RecordReservationConflictError,
RecordSpend as RecordSpend,
SqliteRecordInventoryStore as SqliteRecordInventoryStore,
TokenJoinRouter as TokenJoinRouter,
)


def consensus_version(block_height: Optional[int] = None) -> int: ...
Expand Down
11 changes: 8 additions & 3 deletions sdk/python/aleo/async_network_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ async def _post(
body: str,
method_name: str,
extra_headers: dict[str, str] | None = None,
*,
retry: bool = True,
) -> Any:
hdrs = {
**self._request_headers(method_name),
Expand All @@ -327,7 +329,7 @@ async def _do() -> Any:
)
return resp

return await async_retry_with_backoff(_do)
return await async_retry_with_backoff(_do) if retry else await _do()

# ── JWT refresh ───────────────────────────────────────────────────────

Expand Down Expand Up @@ -903,7 +905,7 @@ async def get_deployment_transaction_for_program(self, program_id: str) -> Any:

# ── POST endpoints ────────────────────────────────────────────────────

async def submit_transaction(self, transaction: Any) -> str:
async def submit_transaction(self, transaction: Any, *, retry: bool = True) -> str:
"""Broadcast a proven transaction to the network.

This spends the transaction's fee and is not reversible. Returning
Expand All @@ -917,6 +919,9 @@ async def submit_transaction(self, transaction: Any) -> str:
Args:
transaction: The proven transaction; stringified before broadcast, so
either a ``Transaction`` or its serialized form works.
retry: Retry transient node errors (5xx) with backoff. Pass
``False`` to make exactly one attempt, when the caller needs
to see each outcome itself.

Returns:
The broadcast transaction ID (``at1…``).
Expand All @@ -931,7 +936,7 @@ async def submit_transaction(self, transaction: Any) -> str:
if self._verbose_errors
else f"{self._host}/transaction/broadcast"
)
resp = await self._post(endpoint, tx_str, "submitTransaction")
resp = await self._post(endpoint, tx_str, "submitTransaction", retry=retry)
return json.loads(resp.text)

async def submit_solution(self, solution: str) -> str:
Expand Down
Loading
Loading