Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
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: 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.3.1"
version = "0.4.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.3.1"
version = "0.4.0"
description = "Python bindings for ABI generation from Aleo bytecode"
readme = "README.md"
license = {text = "GPL-3.0-or-later"}
Expand Down
9 changes: 8 additions & 1 deletion sdk-abi/python/aleo_abi/_aleo_abi.pyi
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
def generate_abi(program_name: str, bytecode: str, network: str) -> str: ...
from typing import Optional, Sequence, Tuple

def generate_abi(
program_name: str,
bytecode: str,
network: str,
imports: Optional[Sequence[Tuple[str, str]]] = None,
) -> str: ...
def check_compatibility(candidate_abi_json: str, standard_abi_json: str) -> list[str]: ...
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.3.1"
version = "0.4.0"
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/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.3.1"
version = "0.4.0"
readme = "Readme.md"
license = {file = "LICENSE.md"}
authors = [
Expand Down
28 changes: 24 additions & 4 deletions shield-swap-sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,26 @@ report = dex.swap_many(pool_key=pools[0].key, token_in_id=pools[0].token0,
dex.collect_all() # any session, any time
```

### `from_profile(home: 'Any' = None) -> "'ShieldSwap'"`
### `from_profile(home: 'Any' = None, *, network: 'Optional[str]' = None, endpoint: 'Optional[str]' = None) -> "'ShieldSwap'"`

The client for the local participant profile (created on first use).

Wires endpoint, network, signer, and (when present) delegated-proving
credentials from ``$SHIELD_SWAP_HOME``/``~/.shield-swap``. Run
``onboard()`` next on a fresh profile.

*network* and *endpoint* apply only when the profile is being created —
an existing one keeps what it was created with, because its derived pool
keys and blinded identities are network-scoped and would not transfer.
Give each network its own home directory.

Args:
home: Profile directory; defaults to ``$SHIELD_SWAP_HOME`` or
``~/.shield-swap``.
network: ``"mainnet"`` or ``"testnet"`` for a NEW profile; defaults
to testnet.
endpoint: Node API origin for a NEW profile.

### `onboard(self, invite_code: 'Optional[str]' = None) -> 'OnboardReport'`

Register this profile end to end — safe to re-run any time.
Expand Down Expand Up @@ -323,7 +335,7 @@ fast when something is systematically wrong (e.g. wrong program).

### Chain methods

### `swap(self, *, pool_key: 'str', token_in_id: 'str', amount_in: 'int', slippage_bps: 'int' = 50, expected_out: 'Optional[int]' = None, sqrt_price_limit: 'Optional[int]' = None, deadline_offset_blocks: 'int' = 10000, nonce: 'Optional[int]' = None, identity: 'Optional[BlindedIdentity]' = None, token_in_program: 'Optional[str]' = None, token_record: 'Optional[str]' = None, wrapper_proofs: 'Optional[str]' = None, imports: 'Optional[dict[str, str]]' = None, account: 'Any' = None) -> 'DexCall[SwapHandle]'`
### `swap(self, *, pool_key: 'str', token_in_id: 'str', amount_in: 'int', slippage_bps: 'int' = 50, expected_out: 'Optional[int]' = None, sqrt_price_limit: 'Optional[int]' = None, deadline_offset_blocks: 'int' = 10000, nonce: 'Optional[int]' = None, identity: 'Optional[BlindedIdentity]' = None, token_in_program: 'Optional[str]' = None, token_record: 'Optional[str]' = None, wrapper_proofs: 'Optional[str]' = None, track: 'bool' = True, imports: 'Optional[dict[str, str]]' = None, account: 'Any' = None) -> 'DexCall[SwapHandle]'`

Request a private swap — phase one of the two-transaction flow.

Expand All @@ -339,8 +351,16 @@ process might die before the claim.

Quote first (``dex.api.get_route``) and pass *expected_out*: without
it a spot estimate is used, which ignores fees and price impact.
Pass *identity* (from journal-reserved counters) to skip the
on-chain probe — required for concurrent swaps. The default
**Building is not free with a journal.** The blinded address is a
transition input, so a counter is reserved *here*, not at the terminal
method — discarding the call, or only simulating, still spends it. That
reservation is what makes concurrent swaps safe: it serializes under a
file lock where the probe it replaces could hand two callers the same
counter. The handle is journaled once the broadcast is accepted, so a
crash before the claim keeps the blinding factor. ``track=False`` builds
on the racing probe instead; *identity* supplies your own.

The default
*deadline_offset_blocks* (~8h at ~3s blocks) absorbs delegated-
proving latency; a tight deadline aborts at finalize when proving
outlives it.
Expand Down
72 changes: 70 additions & 2 deletions shield-swap-sdk/codegen/amm_api.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@
"airdrop"
],
"summary": "POST /airdrop",
"description": "Delivers ~$10 worth each of ALEO, USDCx, and ETH to the given address as\nprivate records via `transfer_public_to_private`, spent from the\ntreasury's public balance in each target's underlying program. One claim\nper recipient address per 15 minutes (429 after that).\n\nProving each token takes ~15-30s, so this returns immediately with\n`status: \"running\"` + a `job_id`. The transfers run in a detached worker\n(capped concurrency, per-token timeout); poll `GET /airdrop/{job_id}` until\n`status == \"complete\"` to read per-token results.\n\nRequired env: `DEPLOYER_PRIVATE_KEY` (the treasury key whose public balances\nfund every airdrop; `AIRDROP_PRIVATE_KEY` honored as a fallback),\n`RPC_URLS` (first entry used).",
"description": "Delivers configured amounts of ALEO, USDCx, and ETH to the given address as\nprivate records via `transfer_public_to_private`, spent from the\ntreasury's public balance in each target's underlying program. One claim\nper recipient address per 15 minutes (429 after that).\n\nProving each token takes ~15-30s, so this returns immediately with\n`status: \"running\"` + a `job_id`. The transfers run in a detached worker\n(capped concurrency, per-token timeout); poll `GET /airdrop/{job_id}` until\n`status == \"complete\"` to read per-token results.\n\nRequired env: `DEPLOYER_PRIVATE_KEY` (the treasury key whose public balances\nfund every airdrop; `AIRDROP_PRIVATE_KEY` honored as a fallback),\n`RPC_URLS` (first entry used).",
"operationId": "airdrop",
"requestBody": {
"content": {
Expand Down Expand Up @@ -1848,6 +1848,15 @@
"type": "integer",
"format": "int64"
}
},
{
"name": "include_valuation",
"in": "query",
"description": "Include the server USDC/USD valuation",
"required": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
Expand Down Expand Up @@ -3170,6 +3179,15 @@
"schema": {
"type": "string"
}
},
{
"name": "pool_key",
"in": "query",
"description": "Optional pool key. The route uses only this pool, as a single hop, with no fallback.",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
Expand Down Expand Up @@ -5266,6 +5284,16 @@
},
"pagination": {
"$ref": "#/components/schemas/PaginationMeta"
},
"valuation": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/UsdcUsdQuote"
}
]
}
}
},
Expand Down Expand Up @@ -7237,6 +7265,46 @@
}
}
},
"UsdcUsdQuote": {
"type": "object",
"required": [
"price",
"decimals",
"publishTime",
"validUntil",
"serverTime",
"source",
"corroborated"
],
"properties": {
"corroborated": {
"type": "boolean"
},
"decimals": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"price": {
"type": "string"
},
"publishTime": {
"type": "integer",
"format": "int64"
},
"serverTime": {
"type": "integer",
"format": "int64"
},
"source": {
"type": "string"
},
"validUntil": {
"type": "integer",
"format": "int64"
}
}
},
"VerifyRequestDoc": {
"type": "object",
"required": [
Expand Down Expand Up @@ -7325,7 +7393,7 @@
},
{
"name": "airdrop",
"description": "Faucet \u2014 sends ~$10 each of ALEO, USDCx, and ETH to a user address as private records, once per address per 15 min"
"description": "Faucet \u2014 sends ALEO, USDCx, and ETH to a user address as private records, once per address per 15 min"
},
{
"name": "unclaimed",
Expand Down
13 changes: 10 additions & 3 deletions shield-swap-sdk/codegen/regen-openapi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
# Refetch the DEX API's OpenAPI spec and regenerate the response models.
set -euo pipefail
cd "$(dirname "$0")"
# Staging serves the migrated shield_swap.aleo stack; the old dev host still
# serves the pre-migration deployment — never mix the two.
BASE="${1:-https://amm-api-staging.dev.provable.com}"
# The API is deployed per network on separate hosts; pass the network (or a
# full base URL) as $1. Never point this at amm-api.dev.provable.com — that
# host indexes the pre-migration shield_swap_v3.aleo.
TARGET="${1:-testnet}"
case "$TARGET" in
mainnet) BASE="https://api.swap.shield.fi" ;;
testnet) BASE="https://api.testnet.swap.shield.fi" ;;
*) BASE="$TARGET" ;;
esac
echo "fetching spec from $BASE"
PYTHON="${PYTHON:-python3}"
curl -sf "${BASE}/openapi.json" | "$PYTHON" -m json.tool > amm_api.openapi.json
"$PYTHON" -m datamodel_code_generator \
Expand Down
2 changes: 1 addition & 1 deletion shield-swap-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "shield-swap-sdk"
version = "0.3.1"
version = "0.4.0"
description = "Python SDK for the shield swap AMM Dex on Aleo"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
28 changes: 24 additions & 4 deletions shield-swap-sdk/python/aleo_shield_swap/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,26 @@ report = dex.swap_many(pool_key=pools[0].key, token_in_id=pools[0].token0,
dex.collect_all() # any session, any time
```

### `from_profile(home: 'Any' = None) -> "'ShieldSwap'"`
### `from_profile(home: 'Any' = None, *, network: 'Optional[str]' = None, endpoint: 'Optional[str]' = None) -> "'ShieldSwap'"`

The client for the local participant profile (created on first use).

Wires endpoint, network, signer, and (when present) delegated-proving
credentials from ``$SHIELD_SWAP_HOME``/``~/.shield-swap``. Run
``onboard()`` next on a fresh profile.

*network* and *endpoint* apply only when the profile is being created —
an existing one keeps what it was created with, because its derived pool
keys and blinded identities are network-scoped and would not transfer.
Give each network its own home directory.

Args:
home: Profile directory; defaults to ``$SHIELD_SWAP_HOME`` or
``~/.shield-swap``.
network: ``"mainnet"`` or ``"testnet"`` for a NEW profile; defaults
to testnet.
endpoint: Node API origin for a NEW profile.

### `onboard(self, invite_code: 'Optional[str]' = None) -> 'OnboardReport'`

Register this profile end to end — safe to re-run any time.
Expand Down Expand Up @@ -323,7 +335,7 @@ fast when something is systematically wrong (e.g. wrong program).

### Chain methods

### `swap(self, *, pool_key: 'str', token_in_id: 'str', amount_in: 'int', slippage_bps: 'int' = 50, expected_out: 'Optional[int]' = None, sqrt_price_limit: 'Optional[int]' = None, deadline_offset_blocks: 'int' = 10000, nonce: 'Optional[int]' = None, identity: 'Optional[BlindedIdentity]' = None, token_in_program: 'Optional[str]' = None, token_record: 'Optional[str]' = None, wrapper_proofs: 'Optional[str]' = None, imports: 'Optional[dict[str, str]]' = None, account: 'Any' = None) -> 'DexCall[SwapHandle]'`
### `swap(self, *, pool_key: 'str', token_in_id: 'str', amount_in: 'int', slippage_bps: 'int' = 50, expected_out: 'Optional[int]' = None, sqrt_price_limit: 'Optional[int]' = None, deadline_offset_blocks: 'int' = 10000, nonce: 'Optional[int]' = None, identity: 'Optional[BlindedIdentity]' = None, token_in_program: 'Optional[str]' = None, token_record: 'Optional[str]' = None, wrapper_proofs: 'Optional[str]' = None, track: 'bool' = True, imports: 'Optional[dict[str, str]]' = None, account: 'Any' = None) -> 'DexCall[SwapHandle]'`

Request a private swap — phase one of the two-transaction flow.

Expand All @@ -339,8 +351,16 @@ process might die before the claim.

Quote first (``dex.api.get_route``) and pass *expected_out*: without
it a spot estimate is used, which ignores fees and price impact.
Pass *identity* (from journal-reserved counters) to skip the
on-chain probe — required for concurrent swaps. The default
**Building is not free with a journal.** The blinded address is a
transition input, so a counter is reserved *here*, not at the terminal
method — discarding the call, or only simulating, still spends it. That
reservation is what makes concurrent swaps safe: it serializes under a
file lock where the probe it replaces could hand two callers the same
counter. The handle is journaled once the broadcast is accepted, so a
crash before the claim keeps the blinding factor. ``track=False`` builds
on the racing probe instead; *identity* supplies your own.

The default
*deadline_offset_blocks* (~8h at ~3s blocks) absorbs delegated-
proving latency; a tight deadline aborts at finalize when proving
outlives it.
Expand Down
5 changes: 4 additions & 1 deletion shield-swap-sdk/python/aleo_shield_swap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from .async_client import AsyncShieldSwap as AsyncShieldSwap
from .api import ApiClient as ApiClient, AsyncApiClient as AsyncApiClient
from .types import (
OwnedPosition as OwnedPosition,
OwnedPositionState as OwnedPositionState,
ClaimResult as ClaimResult,
CollectReport as CollectReport,
MintResult as MintResult,
Expand Down Expand Up @@ -70,7 +72,7 @@ def agent_guide() -> str:
return files(__name__).joinpath("AGENTS.md").read_text()


__version__ = "0.3.1"
__version__ = "0.4.0"

__all__ = [
"ShieldSwap", "AsyncShieldSwap", "ApiClient", "AsyncApiClient",
Expand All @@ -83,6 +85,7 @@ def agent_guide() -> str:
"NotAuthenticatedError", "NotRedeemedError", "NotFundedError",
"AirdropPendingError", "AirdropRateLimitedError",
"CredentialsMissingError",
"OwnedPosition", "OwnedPositionState",
"Profile", "Journal", "REGISTRATION_STAGES",
"OnboardReport", "StageOutcome", "SessionStatus", "PositionView",
"SwapBatchReport", "CollectReport", "blinded_identity_at",
Expand Down
12 changes: 12 additions & 0 deletions shield-swap-sdk/python/aleo_shield_swap/_api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,17 @@ class TokenResponseDoc:
data: TokenDoc


@dataclass
class UsdcUsdQuote:
corroborated: bool
decimals: int
price: str
publishTime: int
serverTime: int
source: str
validUntil: int


@dataclass
class VerifyRequestDoc:
address: str
Expand Down Expand Up @@ -1039,6 +1050,7 @@ class LiveCompatibility:
class PoolListResponseDoc:
data: list[PoolResponseDoc]
pagination: PaginationMeta
valuation: UsdcUsdQuote | None = None


@dataclass
Expand Down
22 changes: 22 additions & 0 deletions shield-swap-sdk/python/aleo_shield_swap/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,28 @@ def record_plaintext(rec: Any) -> Optional[str]:
return getattr(rec, "record_plaintext", None)


#: Fields a PositionNFT record carries. Checked as a set, so a future record
#: type sharing one of them is not mistaken for a position.
POSITION_RECORD_FIELDS = ("token_id", "pool", "tick_lower", "tick_upper",
"token0_id", "token1_id", "withdrawal")


def decode_position_record(plaintext: str) -> Optional[dict[str, Any]]:
"""A PositionNFT record's fields, or None if *plaintext* is not one.

Shared by both clients so the record shape is defined once. Returns None
rather than raising for a record of any other type, letting a mixed record
set be filtered in one pass.
"""
try:
decoded = parse_plaintext(plaintext)
except (ValueError, TypeError):
return None
if not isinstance(decoded, dict):
return None
return decoded if all(f in decoded for f in POSITION_RECORD_FIELDS) else None


def find_position_plaintext(records: Any, pool_key: str) -> Optional[str]:
"""First unspent PositionNFT plaintext whose ``pool`` matches, or None."""
for rec in records:
Expand Down
Loading
Loading